游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

手把手教你写一个简易的微前端框架

  最近看了几个微前端框架的源码(singlespa〔1〕、qiankun〔2〕、microapp〔3〕),感觉收获良多。所以打算造一个迷你版的轮子,来加深自己对所学知识的了解。
  这个轮子将分为五个版本,逐步的实现一个最小可用的微前端框架:
  1。支持不同框架的子应用(v1〔4〕分支)2。支持子应用HTML入口(v2〔5〕分支)3。支持沙箱功能,子应用window作用域隔离、元素隔离(v3〔6〕分支)4。支持子应用样式隔离(v4〔7〕分支)5。支持各应用之间的数据通信(main〔8〕分支)
  每一个版本的代码都是在上一个版本的基础上修改的,所以V5版本的代码是最终代码。
  Github项目地址:https:github。comwoai3cminisinglespaV1版本
  V1版本打算实现一个最简单的微前端框架,只要它能够正常加载、卸载子应用就行。如果将V1版本细分一下的话,它主要由以下两个功能组成:
  1。监听页面URL变化,切换子应用2。根据当前URL、子应用的触发规则来判断是否要加载、卸载子应用监听页面URL变化,切换子应用
  一个SPA应用必不可少的功能就是监听页面URL的变化,然后根据不同的路由规则来渲染不同的路由组件。因此,微前端框架也可以根据页面URL的变化,来切换到不同的子应用:当location。pathname以vue为前缀时切换到vue子应用https:www。example。comvuexxx当location。pathname以react为前缀时切换到react子应用https:www。example。comreactxxx
  这可以通过重写两个API和监听两个事件来完成:
  1。重写window。history。pushState()〔9〕2。重写window。history。replaceState()〔10〕3。监听popstate〔11〕事件4。监听hashchange〔12〕事件
  其中pushState()、replaceState()方法可以修改浏览器的历史记录栈,所以我们可以重写这两个API。当这两个API被SPA应用调用时,说明URL发生了变化,这时就可以根据当前已改变的URL判断是否要加载、卸载子应用。执行下面代码后,浏览器的URL将从https:www。xxx。com变为https:www。xxx。comvuewindow。history。pushState(null,,vue)
  当用户手动点击浏览器上的前进后退按钮时,会触发popstate事件,所以需要对这个事件进行监听。同理,也需要监听hashchange事件。
  这一段逻辑的代码如下所示:import{loadApps}from。。applicationappsconstoriginalPushStatewindow。history。pushStateconstoriginalReplaceStatewindow。history。replaceStateexportdefaultfunctionoverwriteEventsAndHistory(){window。history。pushStatefunction(state:any,title:string,url:string){constresultoriginalPushState。call(this,state,title,url)根据当前url加载或卸载apploadApps()returnresult}window。history。replaceStatefunction(state:any,title:string,url:string){constresultoriginalReplaceState。call(this,state,title,url)loadApps()returnresult}window。addEventListener(popstate,(){loadApps()},true)window。addEventListener(hashchange,(){loadApps()},true)}
  从上面的代码可以看出来,每次URL改变时,都会调用loadApps()方法,这个方法的作用就是根据当前的URL、子应用的触发规则去切换子应用的状态:exportasyncfunctionloadApps(){先卸载所有失活的子应用consttoUnMountAppgetAppsWithStatus(AppStatus。MOUNTED)awaitPromise。all(toUnMountApp。map(unMountApp))初始化所有刚注册的子应用consttoLoadAppgetAppsWithStatus(AppStatus。BEFOREBOOTSTRAP)awaitPromise。all(toLoadApp。map(bootstrapApp))consttoMountApp〔。。。getAppsWithStatus(AppStatus。BOOTSTRAPPED),。。。getAppsWithStatus(AppStatus。UNMOUNTED),〕加载所有符合条件的子应用awaittoMountApp。map(mountApp)}
  这段代码的逻辑也比较简单:
  1。卸载所有已失活的子应用2。初始化所有刚注册的子应用3。加载所有符合条件的子应用根据当前URL、子应用的触发规则来判断是否要加载、卸载子应用
  为了支持不同框架的子应用,所以规定了子应用必须向外暴露bootstrap()mount()unmount()这三个方法。bootstrap()方法在第一次加载子应用时触发,并且只会触发一次,另外两个方法在每次加载、卸载子应用时都会触发。
  不管注册的是什么子应用,在URL符合加载条件时就调用子应用的mount()方法,能不能正常渲染交给子应用负责。在符合卸载条件时则调用子应用的unmount()方法。registerApplication({name:vue,初始化子应用时执行该方法loadApp(){return{mount(){这里进行挂载子应用的操作app。mount(app)},unmount(){这里进行卸载子应用的操作app。unmount()},}},如果传入一个字符串会被转为一个参数为location的函数activeRule:vue会被转为(location)location。pathnamevueactiveRule:(location)location。hashvue})
  上面是一个简单的子应用注册示例,其中activeRule()方法用来判断该子应用是否激活(返回true表示激活)。每当页面URL发生变化,微前端框架就会调用loadApps()判断每个子应用是否激活,然后触发加载、卸载子应用的操作。何时加载、卸载子应用
  首先我们将子应用的状态分为三种:
  bootstrap,调用registerApplication()注册一个子应用后,它的状态默认为bootstrap,下一个转换状态为mount。mount,子应用挂载成功后的状态,它的下一个转换状态为unmount。unmount,子应用卸载成功后的状态,它的下一个转换状态为mount,即卸载后的应用可再次加载。
  现在我们来看看什么时候会加载一个子应用,当页面URL改变后,如果子应用满足以下两个条件,则需要加载该子应用:
  1。activeRule()的返回值为true,例如URL从变为vue,这时子应用vue为激活状态(假设它的激活规则为vue)。2。子应用状态必须为bootstrap或unmount,这样才能向mount状态转换。如果已经处于mount状态并且activeRule()返回值为true,则不作任何处理。
  如果页面的URL改变后,子应用满足以下两个条件,则需要卸载该子应用:
  1。activeRule()的返回值为false,例如URL从vue变为,这时子应用vue为失活状态(假设它的激活规则为vue)。2。子应用状态必须为mount,也就是当前子应用必须处于加载状态(如果是其他状态,则不作任何处理)。然后URL改变导致失活了,所以需要卸载它,状态也从mount变为unmount。API介绍
  V1版本主要向外暴露了两个API:
  1。registerApplication(),注册子应用。2。start(),注册完所有的子应用后调用,在它的内部会执行loadApps()去加载子应用。
  registerApplication(Application)接收的参数如下:interfaceApplication{子应用名称name:string激活规则,例如传入vue,当url的路径变为vue时,激活当前子应用。如果activeRule为函数,则会传入location作为参数,activeRule(location)返回true时,激活当前子应用。activeRule:Functionstring传给子应用的自定义参数props:AnyObjectloadApp()必须返回一个Promise,resolve()后得到一个对象:{bootstrap:()Promisemount:(props:AnyObject)Promiseunmount:(props:AnyObject)Promise}loadApp:()Promise}一个完整的示例
  现在我们来看一个比较完整的示例(代码在V1分支的examples目录):letvueAppregisterApplication({name:vue,loadApp(){returnPromise。resolve({bootstrap(){console。log(vuebootstrap)},mount(){console。log(vuemount)vueAppVue。createApp({data(){return{text:VueApp}},render(){returnVue。h(p,标签名称this。text标签内容)},})vueApp。mount(app)},unmount(){console。log(vueunmount)vueApp。unmount()},})},activeRule:(location)location。hashvue,})registerApplication({name:react,loadApp(){returnPromise。resolve({bootstrap(){console。log(reactbootstrap)},mount(){console。log(reactmount)ReactDOM。render(React。createElement(LikeButton),(app));},unmount(){console。log(reactunmount)ReactDOM。unmountComponentAtNode((app));},})},activeRule:(location)location。hashreact})start()
  演示效果如下:
  小结
  V1版本的代码打包后才100多行,如果只是想了解微前端的最核心原理,只看V1版本的源码就可以了。V2版本
  V1版本的实现还是非常简陋的,能够适用的业务场景有限。从V1版本的示例可以看出,它要求子应用提前把资源都加载好(或者把整个子应用打包成一个NPM包,直接引入),这样才能在执行子应用的mount()方法时,能够正常渲染。
  举个例子,假设我们在开发环境启动了一个vue应用。那么如何在主应用引入这个vue子应用的资源呢?首先排除掉NPM包的形式,因为每次修改代码都得打包,不现实。第二种方式就是手动在主应用引入子应用的资源。例如vue子应用的入口资源为:
  那么我们可以在注册子应用时这样引入:registerApplication({name:vue,loadApp(){returnPromise。resolve({bootstrap(){import(http:localhost:8001jschunkvendors。js)import(http:localhost:8001jsapp。js)},mount(){。。。},unmount(){。。。},})},activeRule:(location)location。hashvue})
  这种方式也不靠谱,每次子应用的入口资源文件变了,主应用的代码也得跟着变。还好,我们有第三种方式,那就是在注册子应用的时候,把子应用的入口URL写上,由微前端来负责加载资源文件。registerApplication({子应用入口URLpageEntry:http:localhost:8081。。。})自动加载资源文件
  现在我们来看一下如何自动加载子应用的入口文件(只在第一次加载子应用时执行):exportdefaultfunctionparseHTMLandLoadSources(app:Application){returnnewPromisevoid(async(resolve,reject){constpageEntryapp。pageEntryloadhtmlconsthtmlawaitloadSourceText(pageEntry)constdomparsernewDOMParser()constdocdomparser。parseFromString(html,texthtml)const{scripts,styles}extractScriptsAndStyles(docasunknownasElement,app)提取了scriptstyle后剩下的body部分的html内容app。pageBodydoc。body。innerHTMLletisStylesDonefalse,isScriptsDonefalse加载stylescript的内容Promise。all(loadStyles(styles))。then(data{isStylesDonetrue将style样式添加到document。head标签addStyles(dataasstring〔〕)if(isScriptsDoneisStylesDone)resolve()})。catch(errreject(err))Promise。all(loadScripts(scripts))。then(data{isScriptsDonetrue执行script内容executeScripts(dataasstring〔〕)if(isScriptsDoneisStylesDone)resolve()})。catch(errreject(err))})}
  上面代码的逻辑:
  1。利用ajax请求子应用入口URL的内容,得到子应用的HTML2。提取HTML中scriptstyle的内容或URL,如果是URL,则再次使用ajax拉取内容。最后得到入口页面所有的scriptstyle的内容3。将所有style添加到document。head下,script代码直接执行4。将剩下的body部分的HTML内容赋值给子应用要挂载的DOM下。
  下面再详细描述一下这四步是怎么做的。一、拉取HTML内容exportfunctionloadSourceText(url:string){returnnewPromisestring((resolve,reject){constxhrnewXMLHttpRequest()xhr。onload(res:any){resolve(res。target。response)}xhr。onerrorrejectxhr。onabortrejectxhr。open(get,url)xhr。send()})}
  代码逻辑很简单,使用ajax发起一个请求,得到HTML内容。
  上图就是一个vue子应用的HTML内容,箭头所指的是要提取的资源,方框标记的内容要赋值给子应用所挂载的DOM。二、解析HTML并提取stylescript标签内容
  这需要使用一个APIDOMParser〔13〕,它可以直接解析一个HTML字符串,并且不需要挂到document对象上。constdomparsernewDOMParser()constdocdomparser。parseFromString(html,texthtml)
  提取标签的函数extractScriptsAndStyles(node:Element,app:Application)代码比较多,这里就不贴代码了。这个函数主要的功能就是递归遍历上面生成的DOM树,提取里面所有的stylescript标签。三、添加style标签,执行script脚本内容
  这一步比较简单,将所有提取的style标签添加到document。head下:exportfunctionaddStyles(styles:string〔〕HTMLStyleElement〔〕){styles。forEach(item{if(typeofitemstring){constnodecreateElement(style,{type:textcss,textContent:item,})head。appendChild(node)}else{head。appendChild(item)}})}
  js脚本代码则直接包在一个匿名函数内执行:exportfunctionexecuteScripts(scripts:string〔〕){try{scripts。forEach(code{newFunction(window,code)。call(window,window)})}catch(error){throwerror}}四、将剩下的body部分的HTML内容赋值给子应用要挂载的DOM下
  为了保证子应用正常执行,需要将这部分的内容保存起来。然后每次在子应用mount()前,赋值到所挂载的DOM下。保存HTML代码app。pageBodydoc。body。innerHTML加载子应用前赋值给挂载的DOMapp。container。innerHTMLapp。pageBodyapp。mount()
  现在我们已经可以非常方便的加载子应用了,但是子应用还有一些东西需要修改一下。子应用需要做的事情
  在V1版本里,注册子应用的时候有一个loadApp()方法。微前端框架在第一次加载子应用时会执行这个方法,从而拿到子应用暴露的三个方法。现在实现了pageEntry功能,我们就不用把这个方法写在主应用里了,因为不再需要在主应用里引入子应用。
  但是又得让微前端框架拿到子应用暴露出来的方法,所以我们可以换一种方式暴露子应用的方法:每个子应用都需要这样暴露三个API,该属性格式为minisinglespa{appName}window〔minisinglespavue〕{bootstrap,mount,unmount}
  这样微前端也能拿到每个子应用暴露的方法,从而实现加载、卸载子应用的功能。
  另外,子应用还得做两件事:
  1。配置cors,防止出现跨域问题(由于主应用和子应用的域名不同,会出现跨域问题)2。配置资源发布路径
  如果子应用是基于webpack进行开发的,可以这样配置:module。exports{devServer:{port:8001,子应用访问端口headers:{AccessControlAllowOrigin:}},publicPath:localhost:8001,}一个完整的示例
  示例代码在examples目录。registerApplication({name:vue,pageEntry:http:localhost:8001,activeRule:pathPrefix(vue),container:(subappviewport)})registerApplication({name:react,pageEntry:http:localhost:8002,activeRule:pathPrefix(react),container:(subappviewport)})start()
  V3版本
  V3版本主要添加以下两个功能:
  1。隔离子应用window作用域2。隔离子应用元素作用域隔离子应用window作用域
  在V2版本下,主应用及所有的子应用都共用一个window对象,这就导致了互相覆盖数据的问题:先加载a子应用window。namea后加载b子应用window。nameb这时再切换回a子应用,读取window。name得到的值却是bconsole。log(window。name)b
  为了避免这种情况发生,我们可以使用Proxy〔14〕来代理对子应用window对象的访问:app。windownewProxy({},{get(target,key){if(Reflect。has(target,key)){returnReflect。get(target,key)}constresultoriginalWindow〔key〕window原生方法的this指向必须绑在window上运行,否则会报错TypeError:Illegalinvocatione。g:constobj{};obj。alertalert;obj。alert();return(isFunction(result)needToBindOriginalWindow(result))?result。bind(window):result},set:(target,key,value){this。injectKeySet。add(key)returnReflect。set(target,key,value)}})
  从上述代码可以看出,用Proxy对一个空对象做了代理,然后把这个代理对象作为子应用的window对象:
  1。当子应用里的代码访问window。xxx属性时,就会被这个代理对象拦截。它会先看看子应用的代理window对象有没有这个属性,如果找不到,就会从父应用里找,也就是在真正的window对象里找。2。当子应用里的代码修改window属性时,会直接在子应用的代理window对象上修改。
  那么问题来了,怎么让子应用里的代码读取修改window时候,让它们访问的是子应用的代理window对象?
  刚才V2版本介绍过,微前端框架会代替子应用拉取js资源,然后直接执行。我们可以在执行代码的时候使用with〔15〕语句将代码包一下,让子应用的window指向代理对象:exportfunctionexecuteScripts(scripts:string〔〕,app:Application){try{scripts。forEach(code{ts使用with会报错,所以需要这样包一下将子应用的js代码全局window环境指向代理环境proxyWindowconstwarpCode;(function(proxyWindow){with(proxyWindow){(function(window){{code}})。call(proxyWindow,proxyWindow)}})(this);newFunction(warpCode)。call(app。sandbox。proxyWindow)})}catch(error){throwerror}}卸载时清除子应用window作用域
  当子应用卸载时,需要对它的window代理对象进行清除。否则下一次子应用重新加载时,它的window代理对象会存有上一次加载的数据。刚才创建Proxy的代码中有一行代码this。injectKeySet。add(key),这个injectKeySet是一个Set对象,存着每一个window代理对象的新增属性。所以在卸载时只需要遍历这个Set,将window代理对象上对应的key删除即可:for(constkeyofinjectKeySet){Reflect。deleteProperty(microAppWindow,keyas(stringsymbol))}记录绑定的全局事件、定时器,卸载时清除
  通常情况下,一个子应用除了会修改window上的属性,还会在window上绑定一些全局事件。所以我们要把这些事件记录起来,在卸载子应用时清除这些事件。同理,各种定时器也一样,卸载时需要清除未执行的定时器。
  下面的代码是记录事件、定时器的部分关键代码:部分关键代码microAppWindow。setTimeoutfunctionsetTimeout(callback:Function,timeout?:numberundefined,。。。args:any〔〕):number{consttimeroriginalWindow。setTimeout(callback,timeout,。。。args)timeoutSet。add(timer)returntimer}microAppWindow。clearTimeoutfunctionclearTimeout(timer?:number):void{if(timerundefined)returnoriginalWindow。clearTimeout(timer)timeoutSet。delete(timer)}microAppWindow。addEventListenerfunctionaddEventListener(type:string,listener:EventListenerOrEventListenerObject,options?:booleanAddEventListenerOptionsundefined,){if(!windowEventMap。get(type)){windowEventMap。set(type,〔〕)}windowEventMap。get(type)?。push({listener,options})returnoriginalWindowAddEventListener。call(originalWindow,type,listener,options)}microAppWindow。removeEventListenerfunctionremoveEventListener(type:string,listener:EventListenerOrEventListenerObject,options?:booleanAddEventListenerOptionsundefined,){constarrwindowEventMap。get(type)〔〕for(leti0,lenarr。length;ilen;i){if(arr〔i〕。listenerlistener){arr。splice(i,1)break}}returnoriginalWindowRemoveEventListener。call(originalWindow,type,listener,options)}
  下面这段是清除事件、定时器的关键代码:for(consttimeroftimeoutSet){originalWindow。clearTimeout(timer)}for(const〔type,arr〕ofwindowEventMap){for(constitemofarr){originalWindowRemoveEventListener。call(originalWindow,typeasstring,item。listener,item。options)}}缓存子应用快照
  之前提到过子应用每次加载的时候会都执行mount()方法,由于每个js文件只会执行一次,所以在执行mount()方法之前的代码在下一次重新加载时不会再次执行。
  举个例子:window。nametestfunctionbootstrap(){。。。}functionmount(){。。。}functionunmount(){。。。}
  上面是子应用入口文件的代码,在第一次执行js代码时,子应用可以读取window。name这个属性的值。但是子应用卸载时会把name这个属性清除掉。所以子应用下一次加载的时候,就读取不到这个属性了。
  为了解决这个问题,我们可以在子应用初始化时(拉取了所有入口js文件并执行后)将当前的子应用window代理对象的属性、事件缓存起来,生成快照。下一次子应用重新加载时,将快照恢复回子应用上。
  生成快照的部分代码:const{windowSnapshot,microAppWindow}thisconstrecordAttrswindowSnapshot。get(attrs)!constrecordWindowEventswindowSnapshot。get(windowEvents)!缓存window属性this。injectKeySet。forEach(key{recordAttrs。set(key,deepCopy(microAppWindow〔key〕))})缓存window事件this。windowEventMap。forEach((arr,type){recordWindowEvents。set(type,deepCopy(arr))})
  恢复快照的部分代码:const{windowSnapshot,injectKeySet,microAppWindow,windowEventMap,onWindowEventMap,}thisconstrecordAttrswindowSnapshot。get(attrs)!constrecordWindowEventswindowSnapshot。get(windowEvents)!recordAttrs。forEach((value,key){injectKeySet。add(key)microAppWindow〔key〕deepCopy(value)})recordWindowEvents。forEach((arr,type){windowEventMap。set(type,deepCopy(arr))for(constitemofarr){originalWindowAddEventListener。call(originalWindow,typeasstring,item。listener,item。options)}})隔离子应用元素作用域
  我们在使用document。querySelector()或者其他查询DOM的API时,都会在整个页面的document对象上查询。如果在子应用上也这样查询,很有可能会查询到子应用范围外的DOM元素。为了解决这个问题,我们需要重写一下查询类的DOMAPI:将所有查询dom的范围限制在子应用挂载的dom容器上Document。prototype。querySelectorfunctionquerySelector(this:Document,selector:string){constappgetCurrentApp()if(!app!selectorisUniqueElement(selector)){returnoriginalQuerySelector。call(this,selector)}将查询范围限定在子应用挂载容器的DOM下returnapp。container。querySelector(selector)}Document。prototype。getElementByIdfunctiongetElementById(id:string){。。。}
  将查询范围限定在子应用挂载容器的DOM下。另外,子应用卸载时也需要恢复重写的API:Document。prototype。querySelectororiginalQuerySelectorDocument。prototype。querySelectorAlloriginalQuerySelectorAll。。。
  除了查询DOM要限制子应用的范围,样式也要限制范围。假设在vue应用上有这样一个样式:body{color:red;}
  当它作为一个子应用被加载时,这个样式需要被修改为:body被替换为子应用挂载DOM的id选择符app{color:red;}
  实现代码也比较简单,需要遍历每一条css规则,然后替换里面的body、html字符串:constre(s,)?(bodyhtml)bg将bodyhtml标签替换为子应用挂载容器的idcssText。replace(re,{app。container。id})V4版本
  V3版本实现了window作用域隔离、元素隔离,在V4版本上我们将实现子应用样式隔离。第一版
  我们都知道创建DOM元素时使用的是document。createElement()API,所以我们可以在创建DOM元素时,把当前子应用的名称当成属性写到DOM上:Document。prototype。createElementfunctioncreateElement(tagName:string,options?:ElementCreationOptions,):HTMLElement{constappNamegetCurrentAppName()constelementoriginalCreateElement。call(this,tagName,options)appNameelement。setAttribute(singlespaname,appName)returnelement}
  这样所有的style标签在创建时都会有当前子应用的名称属性。我们可以在子应用卸载时将当前子应用所有的style标签进行移除,再次挂载时将这些标签重新添加到document。head下。这样就实现了不同子应用之间的样式隔离。
  移除子应用所有style标签的代码:exportfunctionremoveStyles(name:string){conststylesdocument。querySelectorAll(style〔singlespaname{name}〕)styles。forEach(style{removeNode(style)})returnstylesasunknownasHTMLStyleElement〔〕}
  第一版的样式作用域隔离完成后,它只能对每次只加载一个子应用的场景有效。例如先加载a子应用,卸载后再加载b子应用这种场景。在卸载a子应用时会把它的样式也卸载。如果同时加载多个子应用,第一版的样式隔离就不起作用了。第二版
  由于每个子应用下的DOM元素都有以自己名称作为值的singlespaname属性(如果不知道这个名称是哪来的,请往上翻一下第一版的描述)。
  所以我们可以给子应用的每个样式加上子应用名称,也就是将这样的样式:p{color:red;}
  改成:p〔singlespanamevue〕{color:red;}
  这样一来,就把样式作用域范围限制在对应的子应用所挂载的DOM下。给样式添加作用域范围
  现在我们来看看具体要怎么添加作用域:给每一条css选择符添加对应的子应用作用域1。a{}a〔singlespaname{app。name}〕{}2。abc{}a〔singlespaname{app。name}〕bc{}3。a,b{}a〔singlespaname{app。name}〕,b〔singlespaname{app。name}〕{}4。body{}{子应用挂载容器的id}〔singlespaname{app。name}〕{}5。mediasupports特殊处理,其他规则直接返回cssText
  主要有以上五种情况。
  通常情况下,每一条css选择符都是一个css规则,这可以通过style。sheet。cssRules获取:
  拿到了每一条css规则之后,我们就可以对它们进行重写,然后再把它们重写挂载到document。head下:functionhandleCSSRules(cssRules:CSSRuleList,app:Application){letresultArray。from(cssRules)。forEach(cssRule{constcssTextcssRule。cssTextconstselectorText(cssRuleasCSSStyleRule)。selectorTextresultcssRule。cssText。replace(selectorText,getNewSelectorText(selectorText,app),)})returnresult}letcount0constre(s,)?(bodyhtml)bgfunctiongetNewSelectorText(selectorText:string,app:Application){constarrselectorText。split(,)。map(text{constitemstext。trim()。split()items〔0〕{items〔0〕}〔singlespaname{app。name}〕returnitems。join()})如果子应用挂载的容器没有id,则随机生成一个idletidapp。container。idif(!id){idsinglespaidcountapp。container。idid}将bodyhtml标签替换为子应用挂载容器的idreturnarr。join(,)。replace(re,{id})}
  核心代码在getNewSelectorText()上,这个函数给每一个css规则都加上了〔singlespaname{app。name}〕。这样就把样式作用域限制在了对应的子应用内了。效果演示
  大家可以对比一下下面的两张图,这个示例同时加载了vue、react两个子应用。第一张图里的vue子应用部分字体被react子应用的样式影响了。第二张图是添加了样式作用域隔离的效果图,可以看到vue子应用的样式是正常的,没有被影响。
  V5版本
  V5版本主要添加了一个全局数据通信的功能,设计思路如下:
  1。所有应用共享一个全局对象window。spaGlobalState,所有应用都可以对这个全局对象进行监听,每当有应用对它进行修改时,会触发change事件。2。可以使用这个全局对象进行事件订阅发布,各应用之间可以自由的收发事件。
  下面是实现了第一点要求的部分关键代码:exportdefaultclassGlobalStateextendsEventBus{privatestate:AnyObject{}privatestateChangeCallbacksMap:Mapstring,ArrayCallbacknewMap()set(key:string,value:any){this。state〔key〕valuethis。emitChange(set,key)}get(key:string){returnthis。state〔key〕}onChange(callback:Callback){constappNamegetCurrentAppName()if(!appName)returnconst{stateChangeCallbacksMap}thisif(!stateChangeCallbacksMap。get(appName)){stateChangeCallbacksMap。set(appName,〔〕)}stateChangeCallbacksMap。get(appName)?。push(callback)}emitChange(operator:string,key?:string){this。stateChangeCallbacksMap。forEach((callbacks,appName){如果是点击其他子应用或父应用触发全局数据变更,则当前打开的子应用获取到的app为null所以需要改成用activeRule来判断当前子应用是否运行constappgetApp(appName)asApplicationif(!(isActive(app)app。statusAppStatus。MOUNTED))returncallbacks。forEach(callbackcallback(this。state,operator,key))})}}
  下面是实现了第二点要求的部分关键代码:exportdefaultclassEventBus{privateeventsMap:Mapstring,Recordstring,ArrayCallbacknewMap()on(event:string,callback:Callback){if(!isFunction(callback)){throwError(Thesecondparam{typeofcallback}isnotafunction)}constappNamegetCurrentAppName()parentconst{eventsMap}thisif(!eventsMap。get(appName)){eventsMap。set(appName,{})}consteventseventsMap。get(appName)!if(!events〔event〕){events〔event〕〔〕}events〔event〕。push(callback)}emit(event:string,。。。args:any){this。eventsMap。forEach((events,appName){如果是点击其他子应用或父应用触发全局数据变更,则当前打开的子应用获取到的app为null所以需要改成用activeRule来判断当前子应用是否运行constappgetApp(appName)asApplicationif(appNameparent(isActive(app)app。statusAppStatus。MOUNTED)){if(events〔event〕?。length){for(constcallbackofevents〔event〕){callback。call(this,。。。args)}}}})}}
  以上两段代码都有一个相同的地方,就是在保存监听回调函数的时候需要和对应的子应用关联起来。当某个子应用卸载时,需要把它关联的回调函数也清除掉。
  全局数据修改示例代码:父应用window。spaGlobalState。set(msg,父应用在spa全局状态上新增了一个msg属性)子应用window。spaGlobalState。onChange((state,operator,key){alert(vue子应用监听到spa全局状态发生了变化:{JSON。stringify(state)},操作:{operator},变化的属性:{key})})
  全局事件示例代码:父应用window。spaGlobalState。emit(testEvent,父应用发送了一个全局事件:testEvent)子应用window。spaGlobalState。on(testEvent,()alert(vue子应用监听到父应用发送了一个全局事件:testEvent))
  总结
  至此,一个简易微前端框架的技术要点已经讲解完毕。强烈建议大家在看文档的同时,把demo运行起来跑一跑,这样能帮助你更好的理解代码。
  如果你觉得我的文章写得不错,也可以看看我的其他一些技术文章或项目:
  带你入门前端工程〔16〕可视化拖拽组件库一些技术要点原理分析〔17〕前端性能优化24条建议(2020)〔18〕前端监控SDK的一些技术要点原理分析〔19〕手把手教你写一个脚手架〔20〕计算机系统要素从零开始构建现代计算机〔21〕References
  〔1〕singlespa:https:github。comsinglespasinglespa
  〔2〕qiankun:https:github。comumijsqiankun
  〔3〕microapp:https:github。commicrozoemicroapp
  〔4〕v1:https:github。comwoai3cminisinglespatreev1
  〔5〕v2:https:github。comwoai3cminisinglespatreev2
  〔6〕v3:https:github。comwoai3cminisinglespatreev3
  〔7〕v4:https:github。comwoai3cminisinglespatreev4
  〔8〕main:https:github。comwoai3cminisinglespa
  〔9〕window。history。pushState():https:developer。mozilla。orgzhCNdocsWebAPIHistorypushState
  〔10〕window。history。replaceState():https:developer。mozilla。orgzhCNdocsWebAPIHistoryreplaceState
  〔11〕popstate:https:developer。mozilla。orgzhCNdocsWebAPIWindowpopstateevent
  〔12〕hashchange:https:developer。mozilla。orgzhCNdocsWebAPIWindowhashchangeevent
  〔13〕DOMParser:https:developer。mozilla。orgzhCNdocsWebAPIDOMParser
  〔14〕Proxy:https:developer。mozilla。orgzhCNdocsWebJavaScriptReferenceGlobalObjectsProxy
  〔15〕with:https:developer。mozilla。orgzhCNdocsWebJavaScriptReferenceStatementswith
  〔16〕带你入门前端工程:https:woai3c。gitee。iointroductiontofrontendengineering
  〔17〕可视化拖拽组件库一些技术要点原理分析:https:github。comwoai3cFrontendarticlesissues19
  〔18〕前端性能优化24条建议(2020):https:github。comwoai3cFrontendarticlesblobmasterperformance。md
  〔19〕前端监控SDK的一些技术要点原理分析:https:github。comwoai3cFrontendarticlesissues26
  〔20〕手把手教你写一个脚手架:https:github。comwoai3cFrontendarticlesissues22
  〔21〕计算机系统要素从零开始构建现代计算机:https:github。comwoai3cnand2tetris

韩国第一,中国第二近年来,生育率持续走低一直是困扰世界上许多国家的大问题,尤其是越发达的国家和地区,人们生孩子的意愿就越少,这其中的原因十分复杂且多样,有社会进步、受教育水平提高导致的不愿生观念……斯坦福教授3岁起,用这15句话夸孩子,决定他们一生思维高度〔转发收藏〕〔比心〕你相信夸奖孩子的方式,可以改变他的思维方式吗?美国斯坦福教授实验证实:长期被夸你真聪明的孩子,在面对挑战任务时大部分会选择拒绝,而被夸奖你真努力的孩子……拒绝宋丹丹得罪王家卫,穷到卡里只剩100万,徐峥不敢与他合作他删掉600万粉丝的社交账号拒绝高片酬的爱情公寓5整整一年消失在观众的视野等再次出现已是这副模样亲手杀死了关谷神奇做回了属于自己的王传君从一……实在太惨烈!孙颖莎被闺蜜打得呆坐含泪,陈梦输到双眼紧闭11月10日,全锦赛女单14决赛结束了争夺,王曼昱、王艺迪、陈幸同、孙铭阳击败各自对手,继续前进,而陈梦吃了败仗,苦战五局,23不敌陈幸同,甚至被打到闭眼睛。真是太惨了!……探访卡塔尔世界杯房车营地200美元一晚值不值?卡塔尔世界杯所面临的最棘手的问题之一,就是如何解决上百万蜂拥而至的各国球迷住宿。除了尽可能地新建酒店和公寓,卡塔尔政府还想了不少办法,房车营地就是应对方案之一。俗话说,物……两大新品齐发!石头科技助力扫地机器人行业加速复苏【CNMO新闻】石头科技成立于2014年,是一家专注于智能清洁机器人及其他智能产品的品牌。在清洁机器人方面,石头科技目前已推出G、P和T等多个系列,涵盖不同的消费人群和差异化的……神探狄仁杰出现了三名内卫大阁领三名阁领,其中有四人死于非命在《神探狄仁杰》中,梅花内卫是女皇武则天设立的一个特务机构,负责刺探各种情报,处置不合武则天心意的大臣,成员众多,让人闻风丧胆。即使是朝廷重臣狄仁杰、张柬之,乃至太子、梁王这些……世界上最爱我的人去了纪念母亲去世十周年后记母亲去世后的几年时间,每当我们带公婆去餐馆吃饭、过年过节置办东西时,我不禁就会想到我的母亲:多少次在打麻将的人群中,在遛弯的人流中,在想似的背影里,在无数个梦里,幻想……锡安二代正式发售!球鞋待遇力压威少和东契奇,胖虎凭什么?目前JordanBrand旗下有三位球员拥有个人签名鞋,大家不可避免地会将这三双球鞋进行横向比较。发售定位1099元的JordanWhyNot0。5,中底配置为全掌Phy……杠上了!任泽平9天内6次炮轰苹果割高端韭菜价格歧视知名经济学家任泽平接连发文吐槽iPhone14,炮轰苹果公司通过价格歧视收智商税。这应该是9天内第6次炮轰苹果了,任泽平的再次发言引来了许多果粉的不满。今日午间,任泽平发……他们竟然曾经是队友,有些出乎意料在足球世界里,有些球员总是被联系在一起,密不可分。比如当年米兰的古利特、范巴斯滕和里杰卡尔德三剑客,曼联的约克和科尔黑风双煞。他们在场上就是战斗小组,代表着某支球队的某一段历史……在家请客聚餐绝对不能少了这3道硬菜,荤素均匀色香味俱全,真香随着人们生活水平的提高,越来越多的人不再满足于在外吃饭,特别是家里来了亲朋好友,难得见面,更是应该在家里吃才有一家人的感觉,就算是除外吃几顿,不可能一天三顿都在外面解决吧?那这……
三星新折叠屏专利曝光是表?是手机?IT之家3月14日消息今年MWC世界移动通信大会前,三星率先发布了旗下折叠屏手机产品GalaxyFold。在这款手机上市之前,三星新的折叠屏专利遭到曝光。今日,外媒Sam……三星GalaxyS9S9冰蓝配色上架官网售价5499元起IT之家11月13日消息三星GalaxyS9、GalaxyS9冰蓝新配色现已在官网开启预订,其中GalaxyS9128GB版本售价5499元,GalaxyS9128GB版本售价……国外设计师放出三星GalaxyS10想象渲染图前摄三种位置IT之家11月13日消息IT之家本月初报道,根据韩媒TheBell的消息,三星下一代旗舰S10手机将于明年2月份的MWC2019大展上公布亮相,MWC2019将于2019年2月……三星GalaxyS轻奢版手机京东秒杀2299元IT之家11月11日消息三星GalaxyS轻奢版(SMG8750)4GB64GB版本今天在京东参与秒杀活动,2299元近期最低价,感兴趣的用户可以关注一下。配置方面,三星……美女云集不分国籍的盛大狂欢随着国门出境的开放,暹罗泰国这个多姿多彩的旅游王国即将迎来大批世界游客,浓厚的小乘佛教传承,开放包容的文化,独特的自然景观,全年适宜的温度,以上种种因素,支撑着这个旅游王国的繁……台媒非S10专属,明年三星中端机也将用超声波屏下指纹IT之家11月11日消息此前有消息称,三星的GalaxyS10将使用高通公司生产的超声波屏下指纹传感器。通常,当涉及到新的和高科技功能时,厂商通常会将它们首先用于旗舰手机,并且……MagicUI3。0来了!荣耀20荣耀20PRO即将升级IT之家10月18日消息今日,荣耀官方公布了全新的MagicUI3。0系统,带来了杂志化界面设计、深色模式,支持手机与PC多屏协同。此外,荣耀20与荣耀20PRO即将升级Mag……一图看懂2019年华为荣耀手机平板EMUI10升级计划IT之家9月29日消息华为Mate30系列在9月26日正式发布,会上华为正式发布了Mate30和Mate30Pro,新手机预装了最新的EMUI10系统,与此同时,EMUI10还……荣耀Play3官宣今晚发布,号称吓人新系列IT之家9月4日消息今天荣耀手机正式公布了荣耀Play3,号称档位王者,吓人的新系列,荣耀Play3今晚亮相武汉新品发布会,同荣耀20S一起发布。值得注意的是,似乎荣耀跳……荣耀9X8GB128GB升级版价格公布开售2099元IT之家10月1日消息今天荣耀手机官微宣布荣耀9X全新升级的8GB128GB版本正式开售,2099元。荣耀9X首发有三个版本:4GB64GB版售价1399元,6GB64GB版售……曝红米K50终极版配置大升级Note12上200W快充红米红米作为主打性价比的品牌,它的每款产品都备受关注。眼下部分骁龙8新机已经发布,红米下半年新机应该要提上了日程。前不久,卢伟冰就在微博预热了旗下的两款新机,其中一款号称是K5……荣耀9X新配色全息冰岛白10月1日正式开售IT之家9月25日消息荣耀手机刚刚宣布,荣耀9X新配色全息冰岛白将于10月1日正式开售。荣耀9X系列全系搭载7nm制程的麒麟810芯片,采用TypeC设计,配备4000m……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网