每日优鲜前端团队 https://juejin.im/post/5d7f702ce51d4561f777e258#heading-10
我所在团队是做toB业务的,技术栈是Vue,团队目前有十多个典型的toB业务(菜单+内容布局),这些业务都是服务于一个大平台的,因为历史原因,每个业务都是独立的,都有一个html入口,所以当用户在这个大平台上使用这十多个业务的时候,每当切换系统时,页面都会刷新,体验很差;在开发层面,这十多个业务又有太多共同之处,每次修改成本都很高。
最近有一个很重要的需求,内容是这样的:从十多个项目中,每个项目抽取若干功能组成一个新项目,基于现有架构的话,每当点击来自不同系统的功能页面就要刷新一次,这是不可接受的。为了新需求X重复开发一遍这些业务功能又不现实,所以从技术角度来看,架构改造不可避免。
经过一番调研比对,我们决定使用当下比较火的 SingleSpa 来完成改造(iframe方案尝试过,不太适合我们的场景),目前改造已完成,我们实现了以下效果:
做微前端改造之前,蓝色系区域都是用公共包的方式由每个子项目引入,所以子项目运行的时候展示的蓝色系部分都是相同的,给人一种在使用同一个系统的错觉,实际上切换系统的时候整个页面都要重新载入。
微前端改造后,只有橘红色区域是变化的,页面也不再刷新。
图2展示了图1中的tab页签区以及子项目展示区。信息做了马赛克处理。
乍一看没什么特别的,但如果我说这些tab分别来自于不同git仓库的独立vue项目呢?
这就是这套微前端架构的强大之处,让不同单页vue项目可以随意组合成一个项目,而这些项目自己又是独立的vue项目。
仔细看图2中路由的变化,hash路由的第一级决定了要加载哪个子项目(work、sms、tms是三个不同的git工程),不同子项目间的切换也完全没有刷新?
为了让tab切换不刷新,这里使用了keep-alive去缓存页面,考虑到内存性能,在关闭tab页签时通过一些方法(主要是keep-alive的exclude属性)去除了keep-alive缓存,同时为了让子项目间的tab切换也不刷新,对图3下面提到的包装器也进行了不小的改造。让tab切换不刷新只是为了提升用户体验,这一步不是必要的,有一定的成本。
实现一套微前端架构,可以把其分成四部分(参考:alili.tech/archive/110… )
所以是这么个概念:电源(加载器)→电源适配器(包装器)→️电器底座(主项目)→️电器(子项目)️
主项目和子项目都需要用包装器包装,只不过主项目的配置写法有不同
加载器和包装器需要根据自己的需求做一些二次开发
总的来说是这样一个流程:用户访问index.html后,浏览器运行加载器的js文件,加载器去读取图4中的配置文件,然后注册配置文件中配置的各个项目后,首先加载主项目(菜单等),再通过路由判定,动态远程加载子项目。
这里有个vue微前端版demo,包含最基础的效果与源码,务必研究一下这个demo再结合以上理论来帮助理解 *远程加载的子项目资源要在chrome的network中的xhr那一栏才能看到
用户访问index.html后,js加载器会加载apps.config.js。
无论路由是什么,每次必会首先加载主项目,再根据路由来匹配要加载哪个子项目。 apps.config.js的生成如图3的绿色部分所示:
在资源服务器上起一个监听服务(我使用的是nodejs脚本+pm2守护),原有子项目的部署方式完全不变(前后端完全分离,资源带hash),当监听服务检测到文件改动时,去子项目部署文件夹里找它的index.html,把入口js用如下正则匹配出来,写入apps.config.js。
// content[i]为子项目文件夹名称。这段代码是nodejs脚本片段。
const reg = new RegExp(`src="\(\\/${content[i]}\\/index\\.\\w{8}.js\)`)
// 对应图中的 /brain/index.3c4b55cf.js
图4中的brain即是主项目,它的base属性为true,其余子项目的base属性为false
这里说的的项目打包都是基于webpack。
它是实现远程加载子项目的核心。
我们使用的是0.21版本的:github.com/systemjs/sy…
因为要动态通过http引入外部js,又不影响在开发的时候使用import、require方法,所以找到了systemjs来做这件事。
根据systemjs文档说明,我们只需要把子项目打成umd格式(umd糅合了AMD和CommonJS)的包即可动态外部加载。
// 每个子项目的webpack.config.js
output: {
path: xxx,
publicPath: xxx,
filename: '[name].[chunkhash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].chunk.js',
libraryTarget: 'umd', // 这里一定要写成umd,不然打出来的包system.js无法读取
library: xxx, //模块的名称
},
文档:www.webpackjs.com/configurati…
这么多同类型的vue项目,一定有大量的重复代码、重复引用,所以这是一块巨大的性能优化点,通过配置externals可以极大减小子项目打包出来的体积。
我并没有完全按照文档说明的方式来从CDN引入,原因是这样的:入口index.html只有一个,如果按文档来做,一次引入所有CDN资源,可能子项目A用得到这些,但子项目B用不到这些,而我只访问了子项目B而已,这样不就多加载了无用的资源吗?
经过一番调研,同样利用systemjs解决了这个问题
// 每个子项目自己的webpack.config.js,根据使用情况设置externals
externals: {
'axios': 'axios',
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'iview': 'iview',
'moment': 'moment',
'echarts': 'echarts',
'@mfb/pc-utils-micro':'@mfb/pc-utils-micro', // 私有公共方法包
'@mfb/pc-components-micro':'@mfb/pc-components-micro', // 私有公共组件包
// '@mfb/pc-components-micro':'@mfb/pc-components-micro-0.2.1', // 如果需要指定版本,则用这一行替换上一行
...
},
// index.html 整个微前端的唯一入口
<script src="system.js"></script>
<script>
SystemJS.config({
map: {
"Vue": "//xxx.cdn.cn/static/vue/2.5.17/vue.min.js",
"vue": "//xxx.cdn.cn/static/vue/2.5.17/vue.min.js", // 因为iview前置需要vue,是小写的,就又声明了一次
"Vuex": "//xxx.cdn.cn/static/vuex/3.0.1/vuex.min.js",
"VueRouter": "//xxx.cdn.cn/static/vueRouter/3.0.1/vue-router.min.js",
"iview": "//xxx.cdn.cn/static/iview/3.3.2/iview.min.js",
"moment": "//xxx.cdn.cn/static/moment/2.22.2/moment.min.js",
"axios": "//xxx.cdn.cn/static/axios/0.15.3/axios.min.js",
"echarts": "//xxx.cdn.cn/static/echarts/4.2.1/echarts.min.js",
"@mfb/pc-utils-micro": "//xxx.cdn.cn/static/mfb-pc-utils-micro/mfb-pc-utils-micro-0.0.6.js",
"@mfb/pc-components-micro": "//xxx.cdn.cn/static/mfb-pc-components-micro/mfb-pc-components-micro-0.0.42.js",
"@mfb/pc-components-micro-0.2.1": "//xxx.cdn.cn/static/mfb-pc-components-micro/mfb-pc-components-micro-0.2.1.js" // 如果需要指定版本
}
})
</script>
如此一来,systemjs只是在加载index.html时注册了这些CDN地址,不会直接去加载,当子项目里用到的时候,systemjs会接管模块引入,systemjs会去上面注册的map中查找匹配的模块,就再动态去加载资源。这样就避免了不同子项目在这套架构下产生的多余加载。
按我们的配置,webpack打包后,externals配置的模块不会打包进bundle,会被摘出来按umd规范通过requre/define方式去加载。
看systemjs源码会发现它重新定义了require和define方法,所以它能接管externals的外部引入过程。
我最直白的感受是实现了项目级别的模块化,把不同项目变成了一个个模块来拼装组合,也就是说模块化从项目内提升到了项目本身。
总结一下使用这套架构收到的好处,分为以下几点:
也是有很多麻烦之处,需要消耗一定成本:
不过跟收益比起来,这些成本就不算什么了~
最后要说一下,并不是所有场景都适合微前端,尤其是项目规模小、数量少的场景不建议使用。
什么样的场景适合这套架构呢?一般有以下特征:
可能你会问:为什么不一开始就把所有需要整合的功能用一个git来维护?
答:理想是美好的,谁也没有先知能力,随着公司业务发展亦或是组织架构的改变、人员更迭,以上场景是几乎不可避免的;我很难想象十多个项目的好几百个功能都在一个git里管理起来有多困难。
可能你还会问,那我把需要整合的业务整合成到一个git仓库呢?
答:这当然是一个解决办法,前提是整合的成本你能接受;并且将来还有这类需求呢?每次都要手动整合业务代码到同一个git仓库吗?假设所有人都只维护这个整合完的git仓库,并行的需求线多了,上线时间会不会拥挤?一个功能产生了致命错误,会不会所有功能跟着出问题?
最后我想说:
我们做这套框架的初衷是解决眼前的问题,然而发现它附带的潜力价值却比想象的多得多。