工程化 微前端 主流的微前端框架生命周期/路由/资源系统 左杰 2025-12-17 2025-12-17
生命周期 qiankun生命周期 微应用的生命周期boostrap、mount、unmount,在注册微应用只需要导出上面三个方法即可,分别对应微应用的注册、挂载、卸载
基本使用 vue2使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import './public-path' ;import ElementUI from 'element-ui' ;import 'element-ui/lib/theme-chalk/index.css' ;import Vue from 'vue' ;import VueRouter from 'vue-router' ;import App from './App.vue' ;import routes from './router' ;import store from './store' ;Vue .config .productionTip = false ;Vue .use (ElementUI );let router = null ;let instance = null ;function render (props = {} ) { const { container } = props; router = new VueRouter ({ base : window .__POWERED_BY_QIANKUN__ ? '/vue' : '/' , mode : 'history' , routes, }); instance = new Vue ({ router, store, render : h => h (App ), }).$mount(container ? container.querySelector ('#app' ) : '#app' ); } if (!window .__POWERED_BY_QIANKUN__ ) { render (); } function storeTest (props ) { props.onGlobalStateChange && props.onGlobalStateChange ( (value, prev ) => console .log (`[onGlobalStateChange - ${props.name} ]:` , value, prev), true , ); props.setGlobalState && props.setGlobalState ({ ignore : props.name , user : { name : props.name , }, }); } export async function bootstrap ( ) { console .log ('[vue] vue app bootstraped' ); } export async function mount (props ) { console .log ('[vue] props from main framework' , props); storeTest (props); render (props); } export async function unmount ( ) { instance.$destroy(); instance.$el .innerHTML = '' ; instance = null ; router = null ; }
vue3使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import './public-path' ;import { createApp } from 'vue' ;import { createRouter, createWebHistory } from 'vue-router' ;import App from './App.vue' ;import routes from './router' ;import store from './store' ;let router = null ;let instance = null ;let history = null ;function render (props = {} ) { const { container } = props; history = createWebHistory (window .__POWERED_BY_QIANKUN__ ? '/vue3' : '/' ); router = createRouter ({ history, routes, }); instance = createApp (App ); instance.use (router); instance.use (store); instance.mount (container ? container.querySelector ('#app' ) : '#app' ); } if (!window .__POWERED_BY_QIANKUN__ ) { render (); } export async function bootstrap ( ) { console .log ('%c%s' , 'color: green;' , 'vue3.0 app bootstraped' ); } function storeTest (props ) { props.onGlobalStateChange && props.onGlobalStateChange ( (value, prev ) => console .log (`[onGlobalStateChange - ${props.name} ]:` , value, prev), true , ); props.setGlobalState && props.setGlobalState ({ ignore : props.name , user : { name : props.name , }, }); } export async function mount (props ) { storeTest (props); render (props); instance.config .globalProperties .$onGlobalStateChange = props.onGlobalStateChange ; instance.config .globalProperties .$setGlobalState = props.setGlobalState ; } export async function unmount ( ) { instance.unmount (); instance._container .innerHTML = '' ; instance = null ; router = null ; history.destroy (); }
原理 qiankun的生命周期大部分都是继承的single-spa的生命周期
micro app生命周期 基本使用1 micro app通过自定义事件CustomEvent实现生命周期,主要有:
● created<micro-app>标签初始化后,资源加载前触发 ● beforeMount 资源加载后,开始渲染前触发 ● mounted 子应用渲染完成后触发 ● unmount 子应用卸载时触发 ● error 子应用发生错误时触发
vue使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template> <micro-app name ='xx' url ='xx' @created ='created' @beforemount ='beforemount' @mounted ='mounted' @unmount ='unmount' @error ='error' /> </template> <script > export default { methods : { created () { console .log ('micro-app元素被创建' ) }, beforemount () { console .log ('即将被渲染' ) }, mounted () { console .log ('已经渲染完成' ) }, unmount () { console .log ('已经卸载' ) }, error () { console .log ('渲染出错' ) } } } </script >
react使用示例
1 2 3 4 5 6 7 8 9 10 11 12 import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event' <micro-app name='xx' url='xx' onCreated={() => console .log ('micro-app元素被创建' )} onBeforemount={() => console .log ('即将被渲染' )} onMounted={() => console .log ('已经渲染完成' )} onUnmount={() => console .log ('已经卸载' )} onError={() => console .log ('渲染出错' )} />
原理1 定义了lifeCyclesType接口(这些生命周期钩子都是CustomEvent自定义事件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 interface lifeCyclesType { created?(e : CustomEvent ): void beforemount?(e : CustomEvent ): void mounted?(e : CustomEvent ): void unmount?(e : CustomEvent ): void error?(e : CustomEvent ): void beforeshow?(e : CustomEvent ): void aftershow?(e : CustomEvent ): void afterhidden?(e : CustomEvent ): void }
dispatchLifecyclesEvent
在主应用中触发生命周期事件,接收四个参数:
element 元素,ShadowDOM元素或者html元素
appName 微应用名称(唯一标识)
lifecycleName 生命周期名称(符合上面的lifeCyclesType类定义)
error 可选 错误处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 dispatchLifecyclesEvent ( element : HTMLElement | ShadowRoot , appName : string, lifecycleName : LifecycleEventName , error?: Error , ): void { if (!element) { return logError (`element does not exist in lifecycle ${lifecycleName} ` , appName) } element = getRootContainer (element) removeDomScope ()const detail = assign ({ name : appName, container : element, }, error && { error }) const event = new CustomEvent (lifecycleName, { detail, }) formatEventInfo (event, element)if (isFunction (microApp.options .lifeCycles ?.[lifecycleName])) { microApp.options .lifeCycles ![lifecycleName]!(event) } element.dispatchEvent (event) }
dispatchCustomEventToMicroApp
微应用触发自定义事件,接收三个参数:
app app实例
eventName 事件名
detail 上面dispatchLifecyclesEvent方法定义的detail信息
1 2 3 4 5 6 7 8 9 10 11 export function dispatchCustomEventToMicroApp ( app: AppInterface, eventName: string, detail: Record<string, any> = {}, ): void { const event = new CustomEvent (eventName, { detail, }) app.sandBox ?.microAppWindow .dispatchEvent (event) }
挂载阶段 回到micro app的初始化函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 renderApp (options : RenderAppOptions ){ ... for (const attr in options) { ... else if (attr === 'lifeCycles' ) { const lifeCycleConfig = options[attr] if (isPlainObject (lifeCycleConfig)) { for (const lifeName in lifeCycleConfig) { if (lifeName.toUpperCase () in lifeCycles && isFunction (lifeCycleConfig[lifeName])) { } } } } } }
挂载阶段只不过是对mount和error生命周期进行了监听,执行完毕后并移除相应监听
卸载阶段 unmountApp
目前只关注对生命周期的处理,其他逻辑忽略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 export function unmountApp (appName: string, options?: unmountAppOptions ): Promise <boolean> { const app = appInstanceMap.get (formatAppName (appName)) return new Promise ((resolve ) => { if (app) { if (app.isUnmounted () || app.isPrefetch ) { if (app.isPrerender ) { ... } else if (app.isHidden ()) { ... } else if (options?.clearAliveState ) { ... } else { resolve (true ) } } else { const container = getRootContainer (app.container !) const unmountHandler = ( ) => { container.removeEventListener (lifeCycles.UNMOUNT , unmountHandler) resolve (true ) } const afterhiddenHandler = ( ) => { container.removeEventListener (lifeCycles.UNMOUNT , unmountHandler) container.removeEventListener (lifeCycles.AFTERHIDDEN , afterhiddenHandler) resolve (true ) } container.addEventListener (lifeCycles.UNMOUNT , unmountHandler) container.addEventListener (lifeCycles.AFTERHIDDEN , afterhiddenHandler) ... } } else { logWarn (`app ${appName} does not exist` ) resolve (false ) } }) }
可以看到卸载阶段只绑定了对unmount和afterhidden的事件监听,执行完成后移除相应监听
icestark生命周期 icestark有两个生命周期:
● 微应用挂载到主应用 ● 微应用从主应用中卸载
挂载阶段2 registerMicroApps用于自动注册微应用,createMicroApp用于手动注册微应用;微应用注册成功后,路由劫持,根据路由判断微应用是否激活,如果是激活的,就会进入挂载阶段
start
挂载执行的核心方法,主要做的是: ● 配置name防重复处理 ● 更新全局配置,判断是否执行资源预加载 ● 劫持history和eventListener ● 根据loadScriptMode确定js使用哪种方式加载,加载css/js/图片等微应用资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function start (options?: StartConfiguration ) { if (options?.shouldAssetsRemove && !temporaryState.shouldAssetsRemoveConfigured ) { temporaryState.shouldAssetsRemoveConfigured = true ; } if (started) { console .log ('icestark has been already started' ); return ; } started = true ; recordAssets (); globalConfiguration.reroute = reroute; Object .keys (options || {}).forEach ((configKey ) => { globalConfiguration[configKey] = options[configKey]; }); const { prefetch, fetch } = globalConfiguration; if (prefetch) { doPrefetch (getMicroApps (), prefetch, fetch); } hijackHistory (); hijackEventListener (); globalConfiguration.reroute (location.href , 'init' ); }
loadScriptByImport
loadScriptMode为import时使用这个方法
根据js类型执行不同逻辑: ● js类型为INLINE时,创建<script>标签动态添加到根节点的<head>中 ● js类型为EXTERNAL,即为外部引入,使用动态引入的方法加载js资源
最终这个方法会返回mount(挂载阶段)和unmount(卸载阶段)的生命周期钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 export async function loadScriptByImport (jsList: Asset[] ): Promise <null | ModuleLifeCycle > { let mount = null ; let unmount = null ;await asyncForEach (jsList, async (js, index) => { if (js.type === AssetTypeEnum .INLINE ) { await appendExternalScript (js, { id : `${PREFIX} -js-module-${index} ` , }); } else { let dynamicImport = null ; try { dynamicImport = new Function ('url' , 'return import(url)' ); } catch (e) { return Promise .reject ( new Error ( formatErrMessage ( ErrorCode .UNSUPPORTED_IMPORT_BROWSER , isDev && 'You can not use loadScriptMode = import where dynamic import is not supported by browsers.' , ), ), ); } try { if (dynamicImport) { const { mount : maybeMount, unmount : maybeUnmount } = await dynamicImport (js.content ); if (maybeMount && maybeUnmount) { mount = maybeMount; unmount = maybeUnmount; } } } catch (e) { return Promise .reject (e); } } }); if (mount && unmount) { return { mount, unmount, }; } return null ;}
loadScriptByFetch
loadScriptMode为fetch时使用这个方法,实现功能:通过加载fetch请求获取js资源
处理js bundle的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export function loadScriptByFetch (jsList: Asset[], sandbox?: Sandbox, fetch = window .fetch ) { return fetchScripts (jsList, fetch) .then ((scriptTexts ) => { const globalwindow = getGobalWindow (sandbox); const libraryExport = executeScripts (scriptTexts, sandbox, globalwindow); let moduleInfo = getLifecyleByLibrary () || getLifecyleByRegister (); if (!moduleInfo) { moduleInfo = (libraryExport ? globalwindow[libraryExport] : {}) as ModuleLifeCycle ; if (globalwindow[libraryExport]) { delete globalwindow[libraryExport]; } } return moduleInfo; }); }
卸载阶段2 这个阶段主要功能: ● 解绑history和eventListener ● 如果开启了沙箱,变量location还原 ● 清除微应用的所有资源 ● 将微应用缓存清空(如果调用了removeMicroApps)
1 2 3 4 5 6 7 8 9 10 11 function unload ( ) { unHijackEventListener (); unHijackHistory (); started = false ; emptyAssets (globalConfiguration.shouldAssetsRemove , true ); clearMicroApps (); }
wujie生命周期 基本使用 wujie提供的生命周期有: ● beforeLoad 子应用加载静态资源前触发 ● beforeMount 子应用渲染前触发 ● aferMount 子应用渲染后触发 ● beforeUnmount 子应用卸载前触发 ● afterUnmount 子应用卸载后触发 ● activated 子应用保活模式下激活触发(路由进入),类似于keep-alive,在保活模式下子应用实例和web component都不会被销毁,子应用的状态和路由也不会丢失,适用于不同的子应用切换时不想做生命周期改造或者需要减少白屏时间的情况 ● deactivated 子应用保活模式下失活触发(路由离开) ● loadError 子应用加载资源失败后触发
原理2 挂载 在核心API startApp 中有处理生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 export async function startApp (startOptions: startOptions ): Promise <Function | void > { const sandbox = getWujieById (startOptions.name ); const cacheOptions = getOptionsById (startOptions.name );const options = mergeOptions (startOptions, cacheOptions);const { name, url, html, replace, fetch, props, attrs, degradeAttrs, fiber, alive, degrade, sync, prefix, el, loading, plugins, lifecycles, } = options; } if (sandbox) { sandbox.plugins = getPlugins (plugins); sandbox.lifecycles = lifecycles; const iframeWindow = sandbox.iframe .contentWindow ; if (sandbox.preload ) { await sandbox.preload ; } if (alive) { await sandbox.active ({ url, sync, prefix, el, props, alive, fetch, replace }); if (!sandbox.execFlag ) { sandbox.lifecycles ?.beforeLoad ?.(sandbox.iframe .contentWindow ); const { getExternalScripts } = await importHTML ({ url, html, opts : { fetch : fetch || window .fetch , plugins : sandbox.plugins , loadError : sandbox.lifecycles .loadError , fiber, }, }); await sandbox.start (getExternalScripts); } sandbox.lifecycles ?.activated ?.(sandbox.iframe .contentWindow ); return sandbox.destroy ; } else if (isFunction (iframeWindow.__WUJIE_MOUNT )) { sandbox.unmount (); await sandbox.active ({ url, sync, prefix, el, props, alive, fetch, replace }); sandbox.rebuildStyleSheets (); sandbox.lifecycles ?.beforeMount ?.(sandbox.iframe .contentWindow ); iframeWindow.__WUJIE_MOUNT (); sandbox.lifecycles ?.afterMount ?.(sandbox.iframe .contentWindow ); sandbox.mountFlag = true ; return sandbox.destroy ; } else { sandbox.destroy (); } }
从这一大段代码中,可以梳理出:
当微应用预加载但是没执行时,就会触发beforeLoad生命周期钩子,加载css和js资源,如果资源加载失败会触发loadError
配置了alive属性为true时,进入保活模式,步骤1执行后资源加载完成,进入activated阶段
子应用切换时,如果是保活模式就恢复样式,触发beforeMount,新的子应用挂载后触发afterMount
卸载 在unmount方法会执行卸载相关的生命周期钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public unmount (): void { this .activeFlag = false ; clearInactiveAppUrl ();if (this .alive ) { this .lifecycles ?.deactivated ?.(this .iframe .contentWindow ); } if (!this .mountFlag ) return ;if (isFunction (this .iframe .contentWindow .__WUJIE_UNMOUNT ) && !this .alive && !this .hrefFlag ) { this .lifecycles ?.beforeUnmount ?.(this .iframe .contentWindow ); this .iframe .contentWindow .__WUJIE_UNMOUNT (); this .lifecycles ?.afterUnmount ?.(this .iframe .contentWindow ); this .mountFlag = false ; this .bus .$clear(); if (!this .degrade ) { clearChild (this .shadowRoot ); removeEventListener (this .head ); removeEventListener (this .body ); } clearChild (this .head ); clearChild (this .body ); } }
主要流程:
unmount执行子应用卸载,如果配置了保活模式,执行deactivated生命周期钩子
执行beforeUnmount钩子,卸载子应用皇后执行afterUnmount钩子
路由系统 vue的路由系统分为history模式和hash模式
history 模式
history模式允许我们通过JavaScript操作浏览器的历史记录,所有浏览过的页面会记录在一个栈中,可以通过浏览器的前进、后退、刷新导航
原理是通过监听HTML5的History API的popchange事件
1 2 3 4 5 6 7 8 window .addEventListener ('popchange' ,(event )=> { console .log ('event' ,event) }) window .history .pushState (state,title,url) window .history .replaceState (state,title,url) window .history .back () window .history .forward () window .history .go ()
pushState 接收三个参数:
● state state对象用于传值 ● title 跳转页面的标题 ● url 跳转地址
比如当前页面是localhost:3000/home,执行
1 2 3 window .history .pushState ({ title :"测试" },'null' ,'test' )
页面将会跳转到localhost:3000/test
replaceState
参数同pushState,当前页面时localhost:3000/test,执行
1 2 3 window .history .replaceState ({ title :"测试2" },'null' ,'test2' )
页面将会被替换为localhost:3000/test2,history.length不会发生变化
onpopstatechange 事件
pushState和replaceState不会触发onpopstatechange事件,其他方法会触发,比如调用histo的forward()/back()/go()方法
hash 模式
原理是监听window.onhashchange事件 掌握了路由模式,就掌握了微前端的路由跳转原理,Vue/React的路由跳转原理也是一样的
qiankun路由 qiankun的路由继承的single-spa
基本使用3 主应用跳转子应用 可以直接通过主应用的路由系统跳转,在主应用中注册路由暴露微应用
子应用使用的hash模式: 使用location.hash区分不同子应用
1 2 3 4 5 6 7 8 9 10 const getActiveRule = (hash ) => (location ) => location.hash .startsWith (hash);registerMicroApps ([ { name : 'microApp1' , entry : 'http://localhost:8080' , container : '#container' , activeRule : getActiveRule ('#/microApp1' ), }, ]);
子应用使用的history模式:使用location.pathname区分不同子应用,子应用设置路由base
1 2 3 4 5 6 7 8 registerMicroApps ([ { name : 'microApp1' , entry : 'http://localhost:8080' , container : '#container' , activeRule : '/microApp1' , }, ]);
子应用跳转主应用 支持以下方法:
● history.pushState() 只适用于history模式,切换时只有url改变了,不会触发页面刷新 ● window.location.href跳转 这种方式可能会出现页面加载时间长,白屏的问题 ● 将主应用实例暴露给子应用
1 2 3 4 5 6 7 8 9 10 11 const parentRoute = ['/main' ]; const isParentRoute = (path ) => parentRoute.some (item => path.startsWith (item));const rawAppendChild = HTMLHeadElement .prototype .appendChild ;const rawAddEventListener = window .addEventListener ;router.beforeEach ((to, from , next ) => { if (!isParentRoute (from .path ) && isParentRoute (to.path )) { HTMLHeadElement .prototype .appendChild = rawAppendChild; window .addEventListener = rawAddEventListener; })
这种方式跳转会出现主应用css未加载的情况,原因是qiankun的沙箱隔离机制,在子应用中移除了主应用的css和事件监听方法,只有在子应用卸载完成才会还原主应用的css和事件监听方法,因此需要额外处理
子应用互相跳转 支持三种方法: ● history.pushState() ● 使用a链接跳转,拼接完整地址<a href="http://localhost:8080/microApp1">微应用1</a> ● 修改location href跳转 window.location.href=’http://localhost:8080/microApp1 ‘
原理3 通过监听hashchange和popstate事件获取路由的变化
1 2 3 4 5 6 7 8 const capturedEventListeners = { hashchange : [], popstate : [], }; export const routingEventsListeningTo = ["hashchange" , "popstate" ];window .addEventListener ("hashchange" , urlReroute);window .addEventListener ("popstate" , urlReroute);
为了兼容IE浏览器,还定义了navigateToUrl处理路由跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export function navigateToUrl (obj ) { let url; if (typeof obj === "string" ) { url = obj; } else if (this && this .href ) { url = this .href ; } else if ( obj && obj.currentTarget && obj.currentTarget .href && obj.preventDefault ) { url = obj.currentTarget .href ; obj.preventDefault (); } else { throw Error ( formatErrorMessage ( 14 , **DEV ** && `singleSpaNavigate/navigateToUrl must be either called with a string url, with an <a> tag as its context, or with an event whose currentTarget is an <a> tag` ) ); } }
在single-spa中查看start方法
1 2 3 4 5 6 7 8 9 10 11 12 let started = false ;export function start (opts ) { started = true ; if (isInBrowser) { patchHistoryApi (opts); reroute (); } }
怎么判断是否在浏览器环境呢 ?很简单,只要window属性存在即可(node环境判断global属性存在即可)
1 const isInBrowser = typeof window !== "undefined" ;
上述的start主要执行了两个方法: ● patchHistoryApi ● reroute
patchHistoryApi 这个方法主要是劫持hashchange/popstate/addEventListener/removeEventListener事件 reroute方法主要逻辑:
监听路由的变化触发路由导航
根据不同生命周期执行不同逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 export function reroute ( pendingPromises = [], eventArguments, silentNavigation = false ) { if (appChangeUnderway) { return new Promise ((resolve, reject ) => { peopleWaitingOnAppChange.push ({ resolve, reject, eventArguments, }); }); } ... const { appsToUnload, appsToUnmount, appsToLoad, appsToMount } = getAppChanges (); let appsThatChanged, cancelPromises = [], oldUrl = currentUrl, newUrl = (currentUrl = window .location .href ); if (isStarted ()) { appChangeUnderway = true ; appsThatChanged = appsToUnload.concat ( appsToLoad, appsToUnmount, appsToMount ); return performAppChanges (); } else { appsThatChanged = appsToLoad; return loadApps (); } function cancelNavigation (val = true ) { const promise = typeof val?.then === "function" ? val : Promise .resolve (val); cancelPromises.push ( promise.catch ((err ) => { ... console .warn (err); return false ; }) ); } function loadApps ( ) { return Promise .resolve ().then (() => { const loadPromises = appsToLoad.map (toLoadPromise); let succeeded; return ( Promise .all (loadPromises) .then (callAllEventListeners) .then (() => { if (__PROFILE__) { succeeded = true ; } return []; }) .catch ((err ) => { if (__PROFILE__) { succeeded = false ; } callAllEventListeners (); throw err; }) ... ); }); } function performAppChanges ( ) { return Promise .resolve ().then (() => { fireSingleSpaEvent ( appsThatChanged.length === 0 ? "before-no-app-change" : "before-app-change" , getCustomEventDetail (true ) ); fireSingleSpaEvent ( "before-routing-event" , getCustomEventDetail (true , { cancelNavigation }) ); return Promise .all (cancelPromises).then ((cancelValues ) => { const navigationIsCanceled = cancelValues.some ((v ) => v); if (navigationIsCanceled) { originalReplaceState.call ( window .history , history.state , "" , oldUrl.substring (location.origin .length ) ); currentUrl = location.href ; appChangeUnderway = false ; ... return reroute (pendingPromises, eventArguments, true ); } const unloadPromises = appsToUnload.map (toUnloadPromise); const unmountUnloadPromises = appsToUnmount .map (toUnmountPromise) .map ((unmountPromise ) => unmountPromise.then (toUnloadPromise)); const allUnmountPromises = unmountUnloadPromises.concat (unloadPromises); const unmountAllPromise = Promise .all (allUnmountPromises); let unmountFinishedTime; unmountAllPromise.then ( () => { if (__PROFILE__) { unmountFinishedTime = performance.now (); addProfileEntry ( "routing" , "unmountAndUnload" , profilerKind, startTime, performance.now (), true ); } fireSingleSpaEvent ( "before-mount-routing-event" , getCustomEventDetail (true ) ); }, ... const loadThenMountPromises = appsToLoad.map ((app ) => { return toLoadPromise (app).then ((app ) => tryToBootstrapAndMount (app, unmountAllPromise) ); }); const mountPromises = appsToMount .filter ((appToMount ) => appsToLoad.indexOf (appToMount) < 0 ) .map ((appToMount ) => { return tryToBootstrapAndMount (appToMount, unmountAllPromise); }); return unmountAllPromise .catch ((err ) => { callAllEventListeners (); throw err; }) .then (() => { callAllEventListeners (); return Promise .all (loadThenMountPromises.concat (mountPromises)) .catch ((err ) => { pendingPromises.forEach ((promise ) => promise.reject (err)); throw err; }) .then (finishUpAndReturn) .then ( () => { if (__PROFILE__) { addProfileEntry ( "routing" , "loadAndMount" , profilerKind, unmountFinishedTime, performance.now (), true ); } }, (err ) => { if (__PROFILE__) { addProfileEntry ( "routing" , "loadAndMount" , profilerKind, unmountFinishedTime, performance.now (), false ); } throw err; } ); }); }); }); } function finishUpAndReturn ( ) { const returnValue = getMountedApps (); pendingPromises.forEach ((promise ) => promise.resolve (returnValue)); try { const appChangeEventName = appsThatChanged.length === 0 ? "no-app-change" : "app-change" ; fireSingleSpaEvent (appChangeEventName, getCustomEventDetail ()); fireSingleSpaEvent ("routing-event" , getCustomEventDetail ()); } catch (err) { setTimeout (() => { throw err; }); } appChangeUnderway = false ; if (peopleWaitingOnAppChange.length > 0 ) { const nextPendingPromises = peopleWaitingOnAppChange; peopleWaitingOnAppChange = []; reroute (nextPendingPromises); } return returnValue; } ... }
micro app路由 基本使用7 详见micro-zoe.github.io/micro-app/d…
原理4
updateMicroLocation
更新微应用路由,触发更新的条件:从路由守卫中拿到from和to,from和to的fullPath不同,最终执行的是executeNavigationGuard 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export function updateMicroLocation ( appName : string , path : string , microLocation : MicroLocation , type ?: string , ): void { const from = createGuardLocation (appName, microLocation) const newLocation = createURL (path, microLocation.href ) if (isIframeSandbox (appName)) { const microAppWindow = appInstanceMap.get (appName)!.sandBox .microAppWindow microAppWindow.rawReplaceState ?.call (microAppWindow.history , getMicroState (appName), '' , newLocation.href ) } else { for (const key of locationKeys) { microLocation.self [key] = newLocation[key] } } const to = createGuardLocation (appName, microLocation) if (type === 'auto' || (from .fullPath !== to.fullPath && type !== 'prevent' )) { executeNavigationGuard (appName, to, from ) } }
executeNavigationGuard 这个方法主要用于更新路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function executeNavigationGuard ( appName : string , to : GuardLocation , from : GuardLocation , ): void { router.current .set (appName, to) runGuards (appName, to, from , beforeGuards.list ()) requestIdleCallback (() => { runGuards (appName, to, from , afterGuards.list ()) }) }
icestark路由 基本使用 主应用技术栈:vue3/vite/vue-router4 微应用技术栈:vue3/vite/vue-router4
主应用
基座、通用组件放在主应用中处理,在主应用中定义路由用于挂载微应用,主应用中路由定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /router/index.ts import { createRouter,createWebHashHistory,type RouteRecordRaw } from "vue-router" ;import Home from "@/views/Home/index" ;import Entrust from "@/views/Entrust/index" import Lowcode from "@/views/Lowcode/index" const routes :Array <RouteRecordRaw > = [ { path :'/' , name :'Home' , component :Home }, { path :'/entrust' , name :'Entrust' , component :Entrust }, { path :'/lowcode' , name :'Lowcode' , component :Lowcode } ];
Lowcode 主要逻辑如下:
定义一个节点用于挂载微应用
在vue组件挂载阶段手动引入微应用,通过start启动
在vue组件卸载阶段手动移除微应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <template> <div id ="lc-container" > </div > </template> <script setup lang ="ts" > import {onMounted, onUnmounted} from 'vue' ; import {useRoute} from 'vue-router' ;import start from '@ice/stark/lib/start' ;import { createMicroApp, unmountMicroApp } from '@ice/stark/lib/apps' ; onMounted (()=> { let container :HTMLElement |null = document .getElementById ('lc-container' ) const route = useRoute () createMicroApp ({ name :'lowcode-micro' , title :'微应用vue3' , entry :'http://localhost:3000/' , loadScriptMode :'import' , container, props :{ name :'from icestark' , route } }) start ({ onAppEnter ( ){ console .log ('micro enter' ) }, onAppLeave ( ){ console .log ('micro leave' ) }, onRouteChange (_,pathname ){} }) }) onUnmounted (()=> { unmountMicroApp ('lowcode-micro' ).then (()=> { console .log ('低代码已移除' ) }) }) </script >
主应用跳转子应用路由
方法一: 可以使用vue-router的API实现 // 在主应用跳转到子应用时,调用
1 2 3 4 5 import {useRouter} from "vue-router" const router = useRouter ()const goMicro = ( ) => { router.push ('/lowcode' ) }
点击对应微应用菜单时,通过路由劫持触发微应用挂载
子应用跳转主应用路由
使用appHistory跳转
1 appHistory.push ('/home' )
原理 appHistory是如何实现的? 实质上通过路由劫持全局的popchange事件,在路由发生了前进/后退/刷新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 export enum CapturedEventNameEnum { POPSTATE = 'popstate' , HASHCHANGE = 'hashchange' , } const capturedEventListeners = { [CapturedEventNameEnum .POPSTATE ]: [], [CapturedEventNameEnum .HASHCHANGE ]: [], }; export function callCapturedEventListeners ( ) { if (historyEvent) { Object .keys (capturedEventListeners).forEach ((eventName ) => { const capturedListeners = capturedEventListeners[eventName]; if (capturedListeners.length ) { capturedListeners.forEach ((listener ) => { listener.call (this , historyEvent); }); } }); historyEvent = null ; } } const appHistory : AppHistory = { push : (path: string ) => { window .history .pushState ({}, null , path); }, replace : (path: string ) => { window .history .replaceState ({}, null , path); }, };
wujie路由 基本使用5 具体使用查看官网示例,不再赘述 wujie-micro.github.io/doc/guide/j…
原理5 同步子应用到主应用
syncUrlToWindow 使用的history api的replaceState实现的路由跳转,触发条件: 需要跳转的路由不等于当前路由(window.location.href)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function syncUrlToWindow (iframeWindow: Window ): void { const { sync, id, prefix } = iframeWindow.__WUJIE ; let winUrlElement = anchorElementGenerator (window .location .href ); const queryMap = getAnchorElementQueryMap (winUrlElement); if (!sync && !queryMap[id]) return (winUrlElement = null ); const curUrl = iframeWindow.location .pathname + iframeWindow.location .search + iframeWindow.location .hash ; let validShortPath = "" ; if (prefix) { Object .keys (prefix).forEach ((shortPath ) => { const longPath = prefix[shortPath]; if (curUrl.startsWith (longPath) && (!validShortPath || longPath.length > prefix[validShortPath].length )) { validShortPath = shortPath; } }); } if (sync) { queryMap[id] = window .encodeURIComponent ( validShortPath ? curUrl.replace (prefix[validShortPath], `{${validShortPath} }` ) : curUrl ); } else { delete queryMap[id]; } const newQuery = "?" + Object .keys (queryMap) .map ((key ) => key + "=" + queryMap[key]) .join ("&" ); winUrlElement.search = newQuery; if (winUrlElement.href !== window .location .href ) { window .history .replaceState (null , "" , winUrlElement.href ); } winUrlElement = null ; }
同步主应用跳转到子应用
syncUrlToIframe 跟上面一样,使用history api的replaceState实现路由跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export function syncUrlToIframe (iframeWindow: Window ): void { const { pathname, search, hash } = iframeWindow.location ; const { id, url, sync, execFlag, prefix, inject } = iframeWindow.__WUJIE ; const idUrl = sync && !execFlag ? getSyncUrl (id, prefix) : url; const syncUrl = (/^http/ .test (idUrl) ? null : idUrl) || url; const { appRoutePath } = appRouteParse (syncUrl); const preAppRoutePath = pathname + search + hash; if (preAppRoutePath !== appRoutePath) { iframeWindow.history .replaceState (null , "" , inject.mainHostPath + appRoutePath); } }
location.href情况路由处理
processAppForHrefJump
监听popstate的变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 export function processAppForHrefJump ( ): void { window .addEventListener ("popstate" , () => { let winUrlElement = anchorElementGenerator (window .location .href ); const queryMap = getAnchorElementQueryMap (winUrlElement); winUrlElement = null ; Object .keys (queryMap) .map ((id ) => getWujieById (id)) .filter ((sandbox ) => sandbox) .forEach ((sandbox ) => { const url = queryMap[sandbox.id ]; const iframeBody = rawDocumentQuerySelector.call (sandbox.iframe .contentDocument , "body" ); if (/http/ .test (url)) { if (sandbox.degrade ) { renderElementToContainer (sandbox.document .documentElement , iframeBody); renderIframeReplaceApp ( window .decodeURIComponent (url), getDegradeIframe (sandbox.id ).parentElement , sandbox.degradeAttrs ); } else renderIframeReplaceApp ( window .decodeURIComponent (url), sandbox.shadowRoot .host .parentElement , sandbox.degradeAttrs ); sandbox.hrefFlag = true ; } else if (sandbox.hrefFlag ) { if (sandbox.degrade ) { const { iframe } = initRenderIframeAndContainer (sandbox.id , sandbox.el , sandbox.degradeAttrs ); patchEventTimeStamp (iframe.contentWindow , sandbox.iframe .contentWindow ); iframe.contentWindow .onunload = () => { sandbox.unmount (); }; iframe.contentDocument .appendChild (iframeBody.firstElementChild ); sandbox.document = iframe.contentDocument ; } else renderElementToContainer (sandbox.shadowRoot .host , sandbox.el ); sandbox.hrefFlag = false ; } }); }); }
资源系统与预加载 预加载是指在子应用尚未渲染时提前加载静态资源,从而提升子应用的首次渲染速度。 为了不影响主应用的性能,预加载会在浏览器空闲时间执行。
qiankun qiankun通过import-html-entry 实现资源加载(import-html-entry是一个三方库)
资源预加载
首先使用requestIdleCallback,不支持上述方法就使用MessageChannel降级处理,不支持MesageChannel才会用setTimeout实现,传入的是一个任务队列(先进先出)
requestIdlleCallback指在浏览器空闲的时候执行,适合执行耗时长但优先级不高的任务
由此引出问题,MessageChannel为什么比setTimeout实现要好?
setTimeout(callback,timeout)接收两个参数,第一个是定时器到期后要执行的回调函数,第二个是执行回调前要等待的时间(单位:毫秒),但是它实际延迟的时间可能会比预期时间长。
当浏览器执行其他优先级更高的任务时,超时会比预期时间长,setTimeout会被加入宏任务队列,每调用一次的最少时延为4毫秒,而MessageChannel不会有时延的问题,因此我们优先使用MessageChannel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function prefetch (entry: Entry, opts?: ImportEntryOpts ): void { if (!navigator.onLine || isSlowNetwork) { return ; } requestIdleCallback (async () => { const { getExternalScripts, getExternalStyleSheets } = await importEntry (entry, opts); requestIdleCallback (getExternalStyleSheets); requestIdleCallback (getExternalScripts); }); }
从这里我们可以学到怎么判断弱网环境
1 2 3 4 5 6 const isSlowNetwork = navigator.connection ? navigator.connection .saveData || (navigator.connection .type !== 'wifi' && navigator.connection .type !== 'ethernet' && /([23])g/ .test (navigator.connection .effectiveType )) : false ;
navigator.connection.effectiveType 用于检测用户的网络连接质量,返回值有slow-2g/2g/3g/4g,上面写的判断条件是2g/3g时属于弱网
navigator.connection.saveData 返回一个boolean值,当用户在user agent中设置了保存数据就会返回true navigator.onchange用于检测网络的变化
navigator.connection.type 返回联网的设备类型,返回值有: ● “bluetooth” 蓝牙连接 ● “cellular” 手机 ● “ethernet” 以太网 ● “none” 没有联网 ● “wifi” wifi ● “wimax” 和wifi不同的其他无线网 ● “other” 其他 ● “unknown” 未知
核心方法importEntry importEntry接收两个参数:
● entry 需要解析的html模板路径 ● opts 配置项,默认为空对象
其中opts 配置项包含: ● fetch window.fetch方法,支持自定义,用于获取远程的脚本/样式资源 ● getPublicPath 用于路径转换,将相对路径转换为绝对路径 ● getDomain getPublicPath不存在时使用 ● getTemplate 获取模板字符串 ● postProcessTemplate 处理模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export function importEntry (entry, opts = {} ) { const { fetch = defaultFetch, getTemplate = defaultGetTemplate, postProcessTemplate } = opts; const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath; if (!entry) { throw new SyntaxError ('entry should not be empty!' ); } if (typeof entry === 'string' ) { return importHTML (entry, { fetch, getPublicPath, getTemplate, postProcessTemplate, }); } ...
importHTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 export default function importHTML (url, opts = {} ) { if (typeof opts === 'function' ) { fetch = opts; } else { if (opts.fetch ) { if (typeof opts.fetch === 'function' ) { fetch = opts.fetch ; } else { fetch = opts.fetch .fn || defaultFetch; autoDecodeResponse = !!opts.fetch .autoDecodeResponse ; } } getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath; getTemplate = opts.getTemplate || defaultGetTemplate; } return embedHTMLCache[url] || (embedHTMLCache[url] = fetch (url) .then (response => readResAsString (response, autoDecodeResponse)) .then (html => { const assetPublicPath = getPublicPath (url); const { template, scripts, entry, styles } = processTpl (getTemplate (html), assetPublicPath, postProcessTemplate); return getEmbedHTML (template, styles, { fetch }).then (embedHTML => ({ template : embedHTML, assetPublicPath, getExternalScripts : () => getExternalScripts (scripts, fetch), getExternalStyleSheets : () => getExternalStyleSheets (styles, fetch), execScripts : (proxy, strictGlobal, opts = {} ) => { if (!scripts.length ) { return Promise .resolve (); } return execScripts (entry, scripts, proxy, { fetch, strictGlobal, ...opts, }); }, })); })); }
getExternalScripts 遍历处理<script>标签内容getExternalStyleSheets 遍历处理<link>和<style>标签的内容execScripts 执行script中的代码,返回为html入口脚本链接entry指向的模块导出对象
micro app micro app提供了API执行预加载 microApp.preFetch
1 microApp.preFetch (apps : app[] | () => app[], delay?: number)
● apps 微应用列表,支持的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 microApp.preFetch ([ { name :'microApp' , url :'localhost:8080/microApp/' , iframe :false , inlinne :false , 'disable-scopecss' : false , level :1 , 'default-page' : '/microApp' , 'disable-patch-request' : false , } ])
● delay 延迟执行的时间,单位为毫秒
prefetch源码 micro App的预加载也是由requestIDLECallback实现的,在不支持 requestIDLECallback时用setTimeout降级处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default function preFetch (apps: prefetchParamList, delay?: number ): void { if (!isBrowser) { return logError ('preFetch is only supported in browser environment' ) } requestIdleCallback (() => { const delayTime = isNumber (delay) ? delay : microApp.options .prefetchDelay setTimeout (() => { preFetchInSerial (apps) }, isNumber (delayTime) ? delayTime : 3000 ) }) }
fetch拿到js列表和css列表,处理后分别添加到<script>标签和<style>标签中
1 2 3 4 5 6 7 8 9 10 11 12 export function getGlobalAssets (assets: globalAssetsType ): void { if (isPlainObject (assets)) { requestIdleCallback (() => { fetchGlobalResources (assets.js , 'js' , sourceCenter.script ) fetchGlobalResources (assets.css , 'css' , sourceCenter.link ) }) } }
icestark icestark的预加载通过在registerMicroApps或createMicroApp中配置prefetch为true时开启,它会在requestIdleCallback浏览器空闲时执行资源的加载,从而加快首屏加载速度(icestark性能优化的重要手段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export function doPrefetch ( apps: MicroApp[], prefetchStrategy: Prefetch, fetch: Fetch, ) { const executeAllPrefetchTasks = (strategy: (app: MicroApp) => boolean ) => { getPrefetchingApps (apps)(strategy) .forEach (prefetchIdleTask (fetch)); }; if (Array .isArray (prefetchStrategy)) { return ; } if (typeof prefetchStrategy === 'function' ) { executeAllPrefetchTasks (prefetchStrategy); return ; } if (prefetchStrategy) { executeAllPrefetchTasks ((app ) => app.status === NOT_LOADED || !app.status ); } }
在不支持requestIdleCallback的情况下,使用setTimeout降级处理
wujie 预加载时主要处理css和js getExternalStyleSheets
处理样式的核心方法,主要处理流程:
遍历样式列表,根据样式类型选择不同的处理逻辑
内联样式,将内容返回Promise
行内样式,调用getInlineCode返回结果
外部样式调用fetchAssets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export function getExternalStyleSheets ( styles: StyleObject[], fetch: (input: RequestInfo, init?: RequestInit) => Promise <Response> = defaultFetch, loadError: loadErrorHandler ): StyleResultList { return styles.map (({ src, content, ignore } ) => { if (content) { return { src : "" , contentPromise : Promise .resolve (content) }; } else if (isInlineCode (src)) { return { src : "" , contentPromise : Promise .resolve (getInlineCode (src)) }; } else { return { src, ignore, contentPromise : ignore ? Promise .resolve ("" ) : fetchAssets (src, styleCache, fetch, true , loadError), }; } }); }
getExternalScripts 处理js脚本的核心方法,主要流程:
遍历js列表,配置了async异步加载或defer延迟加载,src属性存在且不在module中,fiber存在执行requestIDLECallback,不存在就执行fetchAssets
配置了igore属性或者module、src属性同时存在,返回Promise.resolve(“”)
src属性不存在,返回script.content作为Promise.resolve的结果(处理行内js情况)
不是上述情况则为外部js脚本,执行fetchAssets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 export function getExternalScripts ( scripts: ScriptObject[], fetch: (input: RequestInfo, init?: RequestInit) => Promise <Response> = defaultFetch, loadError: loadErrorHandler, fiber: boolean ): ScriptResultList { return scripts.map ((script ) => { const { src, async , defer, module , ignore } = script; let contentPromise = null ; if ((async || defer) && src && !module ) { contentPromise = new Promise ((resolve, reject ) => fiber ? requestIdleCallback (() => fetchAssets (src, scriptCache, fetch, false , loadError).then (resolve, reject)) : fetchAssets (src, scriptCache, fetch, false , loadError).then (resolve, reject) ); } else if ((module && src) || ignore) { contentPromise = Promise .resolve ("" ); } else if (!src) { contentPromise = Promise .resolve (script.content ); } else { contentPromise = fetchAssets (src, scriptCache, fetch, false , loadError); } if (module && !async ) script.defer = true ; return { ...script, contentPromise }; }); }
fetchAssets 处理资源的核心方法,实际上就是执行fetch请求,拿到对应的资源列表处理结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const fetchAssets = ( src : string , cache : Object , fetch : (input: RequestInfo, init?: RequestInit) => Promise <Response >, cssFlag ?: boolean , loadError ?: loadErrorHandler ) => cache[src] || (cache[src] = fetch (src) .then ((response ) => { if (response.status >= 400 ) { cache[src] = null ; if (cssFlag) { error (WUJIE_TIPS_CSS_ERROR_REQUESTED , { src, response }); loadError?.(src, new Error (WUJIE_TIPS_CSS_ERROR_REQUESTED )); return "" ; } else { error (WUJIE_TIPS_SCRIPT_ERROR_REQUESTED , { src, response }); loadError?.(src, new Error (WUJIE_TIPS_SCRIPT_ERROR_REQUESTED )); throw new Error (WUJIE_TIPS_SCRIPT_ERROR_REQUESTED ); } } return response.text (); }) .catch ((e ) => { cache[src] = null ; if (cssFlag) { error (WUJIE_TIPS_CSS_ERROR_REQUESTED , src); loadError?.(src, e); return "" ; } else { error (WUJIE_TIPS_SCRIPT_ERROR_REQUESTED , src); loadError?.(src, e); return "" ; } }));
如何防止资源重复加载 设置外部external qiankun/micro app/icestark/wujie都支持通过打包工具配置提取公共依赖库 webpack配置示例
1 2 3 4 5 6 { externals : { 'vue' : 'Vue' , 'vue-router' : 'vueRouter' } }
资源缓存 icestark样式资源缓存可以通过微应用配置实现:
开启沙箱sandbox:true
loadScriptMode 为 ‘fetch’、’import’
umd为true(兼容场景)
如果脚本资源可以通过fetch加载,基本不会存在样式资源跨域问题
多实例激活 这个目前只有qiankun能做到