阿里妹导读
我们对微前端框架的内容做了一个详细的介绍,并从零开始用Typescript实现了微前端的基本功能。
然后,我们对该微前端框架实现扩展,实现
运行环境隔离(沙箱)
css样式隔离
应用间通讯(含父子通信、子应用间通信)
全局状态管理(全局store的简单使用)
利用应用缓存和预加载子应用提高加载性能
一、前置准备
再开发我们自己的微前端框架之前,我们需要做一定的架构设计准备。我们考虑微前端架构设计时的整体思路,并画出项目架构图。
1.1 微前端框架实现思路
采用路由分发式(本文使用的是hash模式)
主应用控制路由匹配和子应用加载,共享依赖加载
子应用做功能,并接入主应用实现主子控制和联动
1.2 分析框架& 项目架构图
首先分析需求:
1.主应用功能:
e.通信(父子通信、子父通信)
2.子应用功能:
b.监听通信(主应用传递过来的数据)
3.微前端框架功能
l.通信机制
4.服务端的功能:提供数据服务
整体项目架构图如下:
二、开发微前端框架
本节我们开始开发主子应用并且实现微前端框架的基础功能
* 首先,我们实现主子应用的开发:按照我们的实际项目需求进行子应用的搭建和改造工作,每种子应用改造的方式大同小异;并且开发主应用,主应用起着整体的调度工作,按照对应的路由匹配规则渲染对应的子应用
* 然后我们实现微前端框架的基础功能,包括:应用注册、路由拦截、主应用生命周期添加、微前端生命周期添加、加载和解析html及js、渲染、执行脚本文件等内容。
2.1 准备子应用
本文采用Vue3技术栈开发主应用,并准备了3个不同技术栈的子应用:
子应用:
vue2子应用(实现home主页)
React15子应用(博客页)
项目目录结构
注:在开发微前端框架前,我们首先需要准备3个子应用,子应用的具体实现并非重点,完整代码可见:blog-website-mircroFE-demo:https://code.alibaba-inc.com/sunxiaochun.sxc/blog-website-mircroFE-demo,项目目录结构如下:
.├── main 主应用├── micro 微前端框架├── vue2 子应用├── react15 子应用├── react16 子应用└── README.md
当我们有了几个子应用后,需要对其进行一些改造,从而使其能接入微前端。
对于Vue2/3子应用,为其添加vue.config.js,配置关键点:设置devServer里的contentBase和headers允许跨域和output
// vue2/vue.config.jsconst packageName = 'vue2';const port = 9004;// 设置端口号module.exports = {outputDir: 'dist', // 打包的目录assetsDir: 'static', // 打包的静态资源filenameHashing: true, // 打包出来的文件,会带有hash信息publicPath: 'http://localhost:9004',devServer: {contentBase: path.join(__dirname, 'dist'),hot: false,disableHostCheck: true,port,headers: {'Access-Control-Allow-Origin': '*', // 本地服务的跨域内容},},// 自定义webpack配置configureWebpack: {output: {library: `${packageName}`,//设置包名,从而可以通过window.vue2获得子应用内容libraryTarget: 'umd',},},};
对于react15/16子应用,改造webpack.config.js,注意修改webpack.config.js里的output和devServer
// react16/webpack.config.jsconst path = require('path')module.exports = {entry: {path: ['./index.js']},// +++output: {path: path.resolve(__dirname, 'dist'),filename: 'react15.js',library: 'react15',libraryTarget: 'umd',umdNamedDefine: true,publicPath: 'http://localhost:9002/'},devServer: {// 子应用配置本地允许跨域headers: { 'Access-Control-Allow-Origin': '*' },contentBase: path.join(__dirname, 'dist'),compress: true,port: 9002,historyApiFallback: true,hot: true,}}
当新建好3个子应用后,我们的目录结构是这样的
.├── build 全局启动脚本├── main 主应用├── vue2 子应用├── react15 子应用├── react16 子应用├── package.json 定义start启动脚本└── README.md
// package.json"scripts": {// 在根目录的package.json配置start命令"start": "node ./build/run.js"}
// build/run.jsconst childProcess = require('child_process')const path = require('path');const filePath = {vue2:path.join(__dirname,'../vue2'),react15:path.join(__dirname,'../react15'),react16:path.join(__dirname,'../react16'),}// cd 子应用目录;npm start启动项目function runChild(){Object.values(filePath).forEach(item =>{childProcess.spawn(`cd ${item} && yarn start`,{stdio:"inherit",shell:true});})}runChild();
2.2 主应用开发
主应用负责所有子应用的卸载、更新和加载整个流程,主应用是链接子应用和微前端框架的工具。
主应用需要负责整体页面布局,使用vue3开发主应用main,其主体框架如下:
<!--main/src/App.vue --><template><MainNav /><div class="sub-container"><Loading v-show="loading" /><div v-show="!loading" id="micro-container">子应用内容</div></div><Footer /></template>
有了主应用之后,首先需求在主应用中注册子应用的信息。需要存储的子应用信息:
name:子应用唯一id
activeRule:激活状态(即在哪些路由下渲染该子应用)
container: 渲染容器(子应用要放在哪个容器里显示)
entry 子应用的资源入口:(从哪里获取子应用的文件)
// main/store/sub.tsexport interface IAppDTO {name:string;activeRule:string;container: string;entry:string;}export const subNavList: IAppDTO[] = [{name:'react15',activeRule:'/react15',container:'#micro-container',entry:'//localhost:9002/',},{name:'react16',activeRule:'/react16',container:'#micro-container',entry:'//localhost:9003/',},{name:'vue2',activeRule:'/vue2',container:'#micro-container',entry:'//localhost:9004/',},]
// main/micro/start.ts/** 微前端框架提供的方法 */export const registerMicroApps = (appList:any[]) => {(window as any).appList = appList;}
最后在主应用中注册子应用
// main/src/main.jsimport { subNavList} from'./store/sub'import { registerApp } from './util'/** 注册4个子应用,即只需要将传入的subNavList进行保存即可 */registerApp(subNavList)
2.3 实现路由拦截
有了子应用列表后,我们需要启动微前端,以便来渲染对应的子应用,本节实现的是:主应用控制路由匹配和子应用加载
下面我们编写微前端中路由拦截方法,实现思路:
监听路由切换 & 重写路由切换事件pushState和replaceState
并且监听浏览器的前进/后退按钮window.onpopstate
// main/micro/router/rewriteRouter// 给当前的路由跳转打补丁 globalEvent 原生事件 eventName 自定义事件名称export const patchRouter = (globalEvent,eventName) => {return function (){// 1.创建新事件const e = new Event(eventName);// 2.原生事件代码函数执行globalEvent.apply(this,arguments);//this指向globalEvent// 3.触发刚创建好的事件window.dispatchEvent(e);}}export const turnApp = ()=>{console.log("路由切换了");}// 重写window的路由跳转export const rewriteRouter = ()=>{window.history.pushState = patchRouter(window.history.pushState,'micro_push');window.history.replaceState = patchRouter(window.history.replaceState,'micro_replace');window.addEventListener('micro_push',turnApp),window.addEventListener('micro_replace',turnApp)// 监听返回事件window.onpopstate = function(){turnApp();//路由切换防范}}
// main/micro/startimport {rewriteRouter} from './router/rewriteRouter';rewriteRouter();// 实现路由拦截
子应用注册时保存了子应用列表的信息,对于当前路由,需要找到其对应的子应用。如下。我们编写一个根据当前路由获取子应用的通用方法
// main/micro/utils/index.js/*** 根据当前路由获取子应用(利用activeRule判断)*/export const currentApp = ()=>{const currentUrl = window.location.pathname;return filterApp('activeRule',currentUrl)}/*** 查找子应用的函数*/const filterApp = (key,value)=>{const currentApp = getList().filter(item=> item[key] === value);return currentApp && currentApp.length ? currentApp[0] : {};}
// main/micro/start.tsimport { getList, setList } from "./const/subApps";import {rewriteRouter} from './router/rewriteRouter';const { currentApp} = require('./utils/index.js')export const start = ()=>{// 1.验证当前子应用列表是否为空const apps = getList();// 子应用列表为空if(!apps.length){throw Error('子应用列表为空,请正确注册')}// 有子应用的内容,查找到符合当前路由的子应用const app = currentApp();if(app){const {pathname,hash} = window.location;const url = pathname+hashwindow.history.pushState('','',url);}(window as any).__CURRENT_SUB_APP__ = app.activeRule;}
2.4 生命周期
实现路由拦截后,对于如何挂载和卸载该路由下的子应用,就需要去实现一套生命周期。
子应用通常有下面三个生命周期:
1.bootstrap:开始加载应用,用于配置子应用的全局信息等
2.mount:应用进行挂载,用来渲染子应用
3.unmount:应用进行卸载,用于销毁子应用
这是一个协议接入,只要子应用实现了boostrap、mount和unmount这三个生命周期狗子,有这三个函数导出,我们的框架就可以知道如何加载这个子应用。
// /vu2/main.jslet instance = null;const render = () => {//Vue2子应用本身创建实例new Vue()// 在微前端框架中,这个实例的执行与销毁,都应该交给主应用去执行instance = new Vue({router,render: h => h(App)}).$mount('#app-vue')}// 如果不在微前端环境下,直接执行if(!window.__MICRO_WEB__){render();}// 以vue2子应用为例,定义其生命周期的内容,这里我们只是简单的打印一些内容export const bootstrap = () => {console.log("vue2子应用开始加载");}export const mount = () =>{render();console.log("vue2子应用渲染成功");}export const unmount = () =>{console.log('vue2子应用卸载',instance);}
主应用的生命周期主要有三个:
1.beforeLoad:挂载子应用前,开始加载
2.mounted 挂载子应用后,渲染完成,
3.destoryed 卸载子应用卸载完成,
我们改写微前端框架提供的方法,加入生命周期的内容
// main/micro/start.tsexport interface ILifeCycle {beforeLoad?: any;mounted?: any;destroyed?: any;}export const registerMicroApps = (appList:any[],lifeCycle:ILifeCycle) => {// 1.设置子应用列表setList(appList)// 2. 生命周期lifeCycle.beforeLoad[0]();setTimeout(()=>{lifeCycle.mounted[0]();},2000)setMainLifeCycle(lifeCycle);}
// main/src/utils/index.tsimport { registerMicroApps, start } from "../../micro"import { loading } from '../store'/*** 注册主应用* @param list 子应用列表*/export const registerApp = (list: any[]) =>{// 注册到微前端框架里registerMicroApps(list,{beforeLoad:[()=> {loading.changeLoading(true);console.log("开始加载");}],mounted:[()=>{loading.changeLoading(false);console.log('渲染完成');}],destroy:[()=>{console.log('卸载完成');}]});// 启动微前端框架start();}
微前端的生命周期:如果路由变化时,对子应用进行对应的销毁和加载操作
首先,编写微前端框架监听子应用是否做了切换的方法isTurnChild、以及根据路由获取子应用的方法findAppByRoute
// micro/utils/index.js/*** 子应用是否做了切换* */export const isTurnChild = () =>{const {pathname} = window.location;let prefix = pathname.match(/(\/\w+)/)if (prefix) {prefix = prefix[0]}// 上一个子应用window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;if(window.__CURRENT_SUB_APP__ === prefix){return false;}const currentApp = window.location.pathname.match(/(\/\w+)/);if(!currentApp){return;}// 当前子应用window.__CURRENT_SUB_APP__ = currentApp[0] ;return true}/*** 根据路由获取子应用*/export const findAppByRoute = (router)=>{return filterApp('activeRule',router)}
实现思路是:微前端挂载到主应用,执行注册函数时,传入的就是主应用的生命周期,遍历执行这个主应用的所有生命周期,微前端框架就可以实现将子应用注册到主应用。
// main/micro/lifeCycle/index.tsimport { findAppByRoute } from "../utils";import { getMainLifeCycle } from '../const/mainLifeCycle'import {IAppDTO} from '../../src/interface/IAppDTO'import { loadHtml } from "../loader";export const lifecycle = async ()=>{// 1.获取到上一个子应用const prevApp = findAppByRoute((window as any).__ORIGIN_APP__);// 2.获取到要跳转的子应用const nextApp = findAppByRoute((window as any).__CURRENT_SUB_APP__)// 没有下一个子应用,后续方法无需执行if(!nextApp){return;}// 如果有下一个子应用,卸载上一个子应用if(prevApp && prevApp.unmount){if(prevApp.proxy){prevApp.proxy.inactive();//将沙箱销毁}await destroyed(prevApp);}// 加载下一个子应用const app = await beforeLoad(nextApp);// 渲染下一个子应用await mounted(app);}// 微前端-加载子应用export const beforeLoad = async (app:IAppDTO) =>{await runMainLifeCycle('beforeLoad')app && app.bootstrap && app.bootstrap();const subApp = await loadHtml(app) // 获取的子应用的内容subApp && subApp.bootstrap && subApp.bootstrap();return subApp;}// 微前端-渲染子应用export const mounted = async (app:IAppDTO)=>{app && app.mount && app.mount({appInfo:app.appInfo,entry:app.entry});await runMainLifeCycle('mounted');}// 微前端-卸载子应用export const destroyed = async(app:IAppDTO)=>{app && app.unmount && app.unmount();await runMainLifeCycle('destroyed')}// 对应的执行主应用的生命周期export const runMainLifeCycle = async(type:string) => {const mainlife = getMainLifeCycle();// 等待所有生命周期执行完成await Promise.all(mainlife[type].map(async item => await item()));}
// micro/router/routerHandleconst {isTurnChild} = require('../utils/index.js')const { lifecycle } =require('../lifeCycle')export const turnApp = ()=>{if(isTurnChild()){// 路由切换的时候,微前端的生命周期执行lifecycle();}}
开始加载vue2子应用开始加载vue2渲染成功渲染完成vue2卸载卸载完成开始加载react15开始加载react15渲染成功渲染完成
2.5 获取需要展示的页面
为了展示子应用页面,首先需要做的:主应用获取子应用的生命周期,结构,方法,文件等。这样主应用才能控制子应用的渲染和加载。
主应用的生命周期有三个:beforeLoad开始加载、mounted 渲染完成、destoryed 卸载完成。实际上,我们在主应用生命周期beforeLoad中去获取页面内容(加载资源):
但是直接赋值给容器,容器是没法解析html中的标签,对于link和script(src,js代码)元素,需要专门提取出来这些元素进行处理
因为所有的网站都是以HTML作为入口文件的,这份HTML中实际已经包含了子应用的所有信息(网页结构、js与css资源等),在微前端框架中,可以通过找到HTML中的静态资源并加载,从而渲染出对应的子应用。
// micro/loader/index.jsimport { IAppDTO } from "../../src/interface/IAppDTO";// 发送请求,调用原生fetch获取页面html的内容export const fetchResource = (url:string) =>{// 会触发跨越限制,所以要对子应用的配置文件进行改造(见2.1.2节)return fetch(url).then(async res =>await res.text())}// 加载html的方法(核心思路:根据子应用入口,找到其对应的html,并渲染至对应容器)export const loadHtml = async (app:IAppDTO) =>{// 1.子应用需要显示在哪里const container = app.container; //#id内容// 2.子应用的入口const entry = app.entry;// 3. 获取htmlconst html = await parseHtml(entry)//4. 渲染至对应容器const ct = document.querySelector(container);if(!ct){throw new Error('容器不存在,请查看');}ct.innerHTML = html;return app;}// 解析HTML的内容export const parseHtml = async (entry:string) =>{const html = await fetchResource(entry);const div = document.createElement('div');div.innerHTML = html;return html}
// micro/lifeCycle/index.js// 微前端-加载子应用export const beforeLoad = async (app:IAppDTO) =>{await runMainLifeCycle('beforeLoad')app && app.bootstrap && app.bootstrap();const subApp = await loadHtml(app) // 获取的子应用的内容并渲染至容器中subApp && subApp.bootstrap && subApp.bootstrap();return subApp;}
上面我们只加载了HTML资源,但实际上子应用除了dom资源外,还有script资源也需加载。
实现思路:在getResource方法中我们递归寻找元素,将 link、script元素找出来并做对应的解析处理即可。对于dom资源,直接进行渲染,对于script资源有两种情况:
2.外部scriptUrl链接资源
我们分别进行处理
// micro/loader/index.js/*** 解析子应用所有的资源* @param entry 子应用入口* @returns 返回子应用所有的dom和script资源*/export const parseHtml = async (entry:string) =>{const html = await fetchResource(entry);let allScript = [];const div = document.createElement('div');div.innerHTML = html;//html中包含标签、link、scriptconst [dom,scriptUrl,script] = await getResource(div,entry);const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)));allScript = script.concat(fetchedScripts);return [dom,allScript]}/*** 获取对应的子应用的资源* @param root 容器* @param entry 入口* @returns dom:子应用html scriptUrl:外链js script:js脚本*/export const getResource = async(root:any,entry:string)=>{const scriptUrl:any[] = [];const script:any[] = [];const dom = root.outerHTML;// 深度解析function deepParse(element:any){const children = element.children;const parent = element.parent;// 1. 处理script中的内容if(element.nodeName.toLowerCase() === 'script'){const src = element.getAttribute('src');if(!src){script.push(element.outerHTML);// script没有src属性,即没有外链其他的js资源,直接在script中书写的内容}else{if(src.startsWith('http')){scriptUrl.push(src);}else{scriptUrl.push(`http:${entry}/${src}`)}}if(parent){parent.replaceChild(document.createComment('此js文件已经被微前端替换'),element)}}// link中也会有js的内容if(element.nodeName.toLowerCase() === 'link'){const href = element.getAttribute('href');if(href.endsWith('.js')){if(href.startsWith('http')){scriptUrl.push(href);}else{scriptUrl.push(`http:${entry}/${href}`)}}}for(let i=0;i<children.length;i++){deepParse(children[i]);}}deepParse(root)return [dom,scriptUrl,script]}
congratulations!🎉
到现在我们就实现了在一个vue3主应用中加载3个不同技术栈(vue2、react15、react16)的子应用,并且成功在页面上展示了出来!
三、微前端框架-辅助功能
上文我们已经解决了微前端中应用的加载和切换,本节我们给微前端添加其他辅助功能,例如:预加载、应用通讯、全局store等功能
3.1 运行环境隔离 - 沙箱
为了避免应用间发生冲突,不同子应用之间的运行环境应该进行隔离,防止全局变量的互相污染。沙箱有两种实现:快照沙箱 & 代理沙箱
快照沙箱就是在应用沙箱挂载和卸载的时候记录快照,在应用切换时依据快照来恢复环境。
具体实现思路是:记录当前执行的子应用的变量,当子应用切换的时候,将变量置为初始值。
// main/micro/sandbox/snapShotSandbox// 快照沙箱,应用场景:比较老版本的浏览器export class SnapShotSandbox {constructor(){// 1. 代理对象this.proxy = window;this.active();}// 沙箱激活active(){// 创建一个沙箱快照,用for循环window,new map一个新对象作为快照对象this.snapshot = new Map();// 遍历全局环境for(const key in window){this.snapshot[key] = window[key];}}// 沙箱销毁inactive(){for(const key in window){if(window[key]!== this.snapshot[key]){// 还原操作window[key] = this.snapshot[key];}}}}
代理沙箱是利用ES6的proxy去代理window,监听window的改变,是更现代化的沙箱方法。
具体实现思路:设置一个空对象defaultValue去储存子应用的变量
沙箱销毁的时候inactive也只需要将defaultValue置为{}
// main/micro/sandbox/proxySandbox.js// 代理沙箱let defaultValue = {} // 子应用的沙箱容器export class ProxySandbox{constructor(){this.proxy = null;this.active();}// 沙箱激活active(){//子应用需要设置属性this.proxy = new Proxy(window,{get(target, key) {if (typeof target[key] === 'function') {return target[key].bind(target)}return defaultValue[key] || target[key]},set(target,key,value){defaultValue[key]=value;return true;}})}// 沙箱销毁inactive(){defaultValue = {};}}
3.2 CSS样式隔离
利用沙箱解决了JS之间的副作用冲突,接下来我们需要解决CSS之间的冲突,为CSS做样式隔离。
常用样式隔离方法有:
1.css modules:利用webpack配置打包机制进行css模块化处理,通过编译生成不冲突的选择器名
2.shadow dom :创建个新的元素进行包裹,但语法较新,兼容性较差,设置shadow dom的步骤:
a.设置mode 利用attachShadow得到shadow
3.minicss (本框架选用):一个webpack插件,该插件将css打包成单独的文件,然后页面通过link进行引用
module: {rules: [{test: /\.(cs|scs)s$/,use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']},]},
4.csss in js:将应用的css样式写在javaScript文件里,最终生成不冲突的选择器。
3.3 应用间通信
对于应用通讯,有两种实现方式:props和customevent
这里有一个父子通信的实例场景:子应用动态控制主应用nav的显示与隐藏。如下,在react16子应用中编写了login登陆页面,在该页面中不应显示导航条nav
实现如下:首先,子应用需要一个控制主应用中nav显示和隐藏的方法。所以我们在主应用的store中维护一个navStatus属性,表示导航条nav是否显示和隐藏至,暴露一个修改该navStatus属性的方法changeNav
// main/src/store/nav.tsimport { ref } from 'vue';// 控制导航nav显示的状态navStatusexport const navStatus = ref(true);// 更改navStatus显示的方法export const changeNav = (type:boolean) => navStatus.value = type;
将该状态绑定至主应用导航条中,<MainNav v-show="navStatus"/>
然后,将主应用的所有store内容(包含navStatus)用Props的方式传递给所有子应用。
// 将主应用的store用props的方式传递给子应用import * as appInfo from '../store/index';export const subNavList: IAppDTO[] = [{name:'vue2',activeRule:'/vue2',container:'#micro-container',entry:'//localhost:9004/',appInfo},{name:'react15',activeRule:'/react15',container:'#micro-container',entry:'//localhost:9002/',appInfo,},{name:'react16',activeRule:'/react16',container:'#micro-container',entry:'//localhost:9003/',appInfo},]
// main/micro/lifeCycle/index.ts// 微前端-渲染子应用export const mounted = async (app:IAppDTO)=>{app && app.mount && app.mount({// ++ 给子应用传递appInfoappInfo:app.appInfo,// ++entry:app.entry});await runMainLifeCycle('mounted');}
// react16/index.jsexport const mount = (app) =>{// ++ 利用主应用传递过来的方法changeNav来隐藏navapp.appInfo.header.changeNav(false);// +++render();console.log("react16渲染成功");}
另一种应用间通讯的模型是挂一个事件总线,应用之间不直接相互交互,都去一个事件总线上注册和监听事件。实现思路:通过new CustomEvent对象去监听事件(on)和触发事件(emit),如下:
首先,利用customEvent创建Custom类
// main/micro/customevent/index.jsexport class Custom{// 事件监听on(name,cb){window.addEventListener(name,(e)=>{cb(e.detail)})}// 事件触发emit(name,data){const event = new CustomEvent(name,{detail:data});window.dispatchEvent(event);}}
// main/micro/start.tsconst { Custom } = require('./customevent/index.js');const custom = new Custom();custom.on('test',(data:any)=>{console.log("监听到的数据:",data);});window.custom = custom;
3.4 全局状态管理 - 全局store
建立微前端中的全局状态管理基于发布订阅模式,通过主应用监听某个方法、子应用添加订阅者来监听到一些全局状态的改变。
具体实现思路是:利用store保存数据,observer管理订阅者(subscribeStore方法用于添加订阅者),并提供获取和更新store的getStore和updateStore的方法。
// main/micro/store/index.tsexport const creatStore = (initData:{} = {}) => (() => {// 利用闭包去保存传参的初始数据let store = initData;// 管理所有的订阅者,依赖const observers = [];// 获取storeconst getStore = () => {return store;}// 更新storeconst updateStore = (newValue) => new Promise((res) => {if (newValue !== store) {// 执行保存store的操作let oldValue = store;// 将store更新store = newValue;res(store);// 通知所有的订阅者,监听store的变化observers.forEach(fn => fn(newValue, oldValue));}})// 添加订阅者,fn为方法const subscribeStore = (fn) => {observers.push(fn);}// 整个store本质是一个闭包函数,把方法return出去return { getStore, updateStore, subscribeStore }})()
3.5 提高加载性能
当前,当切换不同的子应用时,都会重新加载页面,若页面资源非常多,加载会比较缓慢,所以需要在微前端框架中对应用进行缓存,提升加载性能。
应用缓存的实现思路是:
定义一个cache对象,根据子应用的appName来做缓存
如果当前子应用的html已经解析并且加载过,就返回已经加载过的内容。如果没有,则走正常加载和解析的流程
//main/micro/loader/index.tsconst cache = {};// 根据子应用的name做缓存/*** 得到子应用所有的资源* @param entry 子应用入口 name:appName*/export const parseHtml = async (entry:string,name:string) =>{// 如果命中应用缓存,直接返回缓存内容,没有则继续进行资源解析加载流程if(cache[name]){return cache[name];}const html = await fetchResource(entry);let allScript = [];const div = document.createElement('div');div.innerHTML = html;const [dom,scriptUrl,script] = await getResource(div,entry);const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)));allScript = script.concat(fetchedScripts);// 将该子应用的资源保存至缓存对象cache中cache[name] = [dom,script];return [dom,allScript]}
提升加载性能的另一个思路是预加载子应用,即在启动微前端框架时,先获取当前要加载的子应用,再预加载剩下的所有的子应用。
// main/micro/loader/prefetch.tsimport { getList } from "micro/const/subApps";import { parseHtml } from './index'export const prefetch = async() => {// 1. 获取到所有子应用列表 - 不包含当前正在显示的const list = getList().filter(item => window.location.pathname.startsWith(item.activeRule));// 2. 预加载剩下的子应用Promise.all(list.map(async item => await parseHtml(item.entry,item.name)));}
四、写在最后
在之前的内容中,我们对微前端框架的内容做了一个详细的介绍,并从零开始用Typescript实现了微前端的基本功能,对这样的一个简易微前端框架,仍有可以扩展和继续学习的地方。例如
现有热门的微前端框架(qiankun、single-spa、icestark)是如何实现微前端框架的应用注册&加载、沙箱隔离、全局状态管理、预加载这些功能?
参考资料:
从零到一实现企业级微前端框架,保姆级教学:https://juejin.cn/post/7004661323124441102
微前端连载 6/7:微前端框架 - qiankun 大法好:https://juejin.cn/post/6846687602439897101
从零打造微前端框架:实战“汽车资讯平台”项目:https://coding.imooc.com/class/chapter/520.html#Anchor
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
文章引用微信公众号"阿里开发者",如有侵权,请联系管理员删除!