了解微前端
微前端是最近一个很火的话题,前端就跟谈恋爱一样,真的是“合久必分,分久必合”。微前端不是炫技,而是一个解决方案。
微前端的发展历程
举个例子,公司内部有很多的系统,比如说人力系统、财务系统、工时管理、项目管理、论坛等等,当公司有新人入职时,短时间内他要记住全部的地址比较难。
如果地址不太多,存浏览器书签可以记住,但是地址有变动时(比如说新旧版本同时存在),本地书签就不好使。
为了解决这个问题,不同时期的做法:
早期: 比较常见的做法是做一个网址导航,这样能解决地址太多记不住的问题,以及地址变动的问题,但是系统之间的切换不太友好,得返回到导航页才能切换。例如 hao123。
中期: 把网址导航放到每个系统的左上角,点击弹出网址导航菜单。然后把网址导航单独做成一个 js
,维护一份,所有系统共用,缺点就是系统切换需要刷新页面,并且有太多的东西重复加载。例如 阿里云控制台。
后期: 分为主子系统,网址导航和页面顶部/底部放主系统,不重复加载,内容区用 iframe
呈现子系统,点击导航,只切换内容,不刷新页面。这就是微前端的雏形。
什么时候应该用微前端呢
- 项目引入其他的项目时(合)
一些遗产项目(或维护项目),也还能用,不想大改动,需要整合到一起时。
再比如,一个系统的某些功能/页面,另一个系统已经存在,需要复用时,可以考虑微前端。
- 当你的项目非常庞大时(分)
当然了,“庞大”这个定义很模糊,怎样算庞大,又应该如何拆分?这得根据具体情况来判断
举个例子,你们在做“人力资源管理系统”,包含了很多个模块:人员档案、入职离职升职、考勤、绩效、薪酬社保、培训、招聘等等。现在你们有个客户,只需要其中几个模块,并且需要源码。
买全部功能吧,客户不愿意付那么多钱。给全部代码吧,公司又觉得亏了。当然了,也可以挑选出客户需要的代码,但是如果多来几个这样的客户,那就需要做很多的无用功。
如果能把项目拆分一下,分为一个基础功能和 N 个附加功能,各功能独立成一个系统,独立开发部署,这样就能自由组合售卖,公司和客户皆大欢喜。
微前端不适用的情况
项目非常大,但是没有自由组合售卖的需求,需要重构时,这个时候优先考虑技术选型(vue
、react
、angular
),不建议用微前端。
即使一个项目几百个页面,只要管理得当、文档齐全、做好自动化和工程化,还是能很好地迭代的。
常见微前端方案
网上的各种微前端方案看着非常炫酷,各种名词也很高大上,实际上换汤不换药。真正能落地到生产的,可复用的方案就只有 iframe
和 sing-spa
( qiankun
基于 single-spa
)。思路是一样的,细节不同而已。
微前端技术的核心是加载和执行。除 iframe
方案外,加载的方案有三种:JS entry
、 HTML entry
和 config entry
;执行文件比较麻烦,并不是一个简单的 script/style
标签就能实现(虽然也可以用,但是不能称之为一个成熟的方案,因为要解决 js/css
污染)。
iframe
方案
iframe
在前端的发展过程中始终扮演着重要角色:
跨 HTML 页面加载:从多页应用到单页应用时(点击菜单不刷新页面),iframe
是其中一个解决方案。后来三大框架崛起,iframe
渐渐淡出视野。
跨系统加载:一个系统中加载另一个系统时,iframe
是浏览器提供的原生方案,好用又简单,所以也是一个微前端方案。
iframe
方案除了用户体验差一点,其他的几乎没啥大问题,拥有完美的 js/css
隔离,不需要解决资源的加载问题。只要你的领导没啥意见,客户能接受,iframe
也是微前端一个很好的选择。
iframe
的门槛很低,但是,你想完善这个方案,付出和回报是不成比例的。要优化的问题太多了(具体有哪些问题,看文末链接)。
PS:chrome80+
版本 cookie
的改版,又劝退了一波 iframe
方案:使用 http
协议时,如果 iframe
和主系统跨站,iframe
居然不能存取自己的 cookie
,这样系统局限性太大了(这个问题有机会会详细讲)
single-spa
方案
single-spa
其实做的事情比较少:定义了子项目的生命周期(初始化、加载、卸载、更新)、子系统状态,以及关联路由与子项目的加载/卸载时机,错误捕获等。
但是子项目的每个生命周期做什么事情,以及子项目的资源如何加载和执行,子项目如何卸载,都需要自己来完成。
好在有一些插件来帮助我们完成这些事情:
single-spa-vue
/(single-spa-react
): 子项目的加载和卸载函数systemjs-webpack-interop
: 修正资源的加载路径vue-cli-plugin-single-spa
: 修改一些webpack
打包配置sysyem.js
:加载js
资源和公共插件
这四个插件 + single-spa
就能实现微前端,一个完整的 single-spa
微前端加载执行流程:
由于 single-spa
需要根据页面的 location
来判断子项目,用不同的值( hash
或 path
)判断,对不同的子项目会产生不同的影响。如何选择判断的依据,以及如何消除这些影响,可以查看这个:主项目路由的 hash 与 history 之争
虽然这样就能实现微前端,但是给我的感觉就是拼凑出来的微前端,每一步都很巧妙。比如说子项目的 css
打包到 js
,去掉 splitChunks
,这样子项目就只剩下一个 app.js
需要加载和执行。
稍微复杂一点的项目,使用 single-spa
都非常吃力,很多事情都需要靠自己动手解决,比如说:
js/css
污染问题;- 子项目
html
中其他的js/css
文件,需要自己手动加载; - 只有父项目给子项目传值,项目间的通信需要自己写;
- 打包部署比较麻烦;
single-spa
的官方在线实例:single-spa-examples
qiankun
方案
qiankun
的初衷就是像 iframe
一样简单好用,但是用户体验远远高于 iframe
。
在 single-spa
的基础上,qiankun
做了这些事情:
- 解决了子项目加载问题,只需要给一个入口即可
- 增加了
js/css
沙箱(说实话,没有完美的沙箱,可以兼容所有情况)。 - 增加了预请求
- 增加了项目间的双向通信
- 支持手动加载子项目
这样一来,子项目只需要打包部署一份,既可以独立访问,又可以被主项目集成,对子项目的改动也不多。
qiankun
上手很容易,但是解决问题很麻烦,尤其是 js
沙箱带来的插件冲突问题。建议先了解下 singles-spa
及沙箱的原理。
qiankun
方案的高级用法:
- 主项目和子项目共享依赖
qiankun
的嵌套- 子项目实现
keep-alive
这些在以前的文章都有介绍(链接在文末)
微前端痛点:难以解决的 css
隔离问题
这里主要指的是qiankun
和 single-spa
方案。
如果同时只存在一个子应用运行,子项目间的css
污染很好解决:
single-spa
可以通过修改body
的class
,子项目将样式写到这个class
里面来解决。qiankun
则通过子项目卸载时移除子项目的style
标签解决。
但是主应用和子应用之间的样式污染,以及多个子应用同时运行时的 css
污染,难以解决。
qiankun
在 css
污染的问题上做了很多的新的尝试:
shadow dom
来隔离(sandbox : { strictStyleIsolation: true }
开启)
如果不了解 shadow dom
,可以看看 MDN的介绍 。说实话,shadow dom
隔离效果挺好,就类似于 iframe
。但是会带来新的问题:
-
dom
不通,导致子项目拿不到根节点解决方案:子项目查找根节点从容器开始,而不是
document
function render({ container } = {}) { // 省略无关内容 instance = new Vue({ router, store, render: h => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); }
-
由于
react
的点击事件都是document
代理,shadow dom
会导致document
无法访问到子项目的DOM
,导致点击事件无效:github.com/facebook/re…
解决方案:react-shadow-dom-retarget-events
-
append
到body
的弹窗样式不生效:大部分情况下弹窗不需 要append to body
,但是在弹窗中打开另一个弹窗(即嵌套弹窗)则需要append to body
,Popover
弹出框是强制append to body
的。 -
兼容性堪忧:Firefox(从版本 63 开始),Chrome,Opera 和 Safari 默认支持 Shadow DOM。基于 Chromium 的新 Edge 也支持 Shadow DOM;而旧 Edge 未能撑到支持此特性
-
子项目的字体文件无法生效,显示异常
- 将子项目的样式都包裹在一个特殊的选择器中。(
sandbox : { experimentalStyleIsolation: true }
开启)
也就是会把子项目的样式局限到容器范围内生效,所有的样式都会包裹在容器里面,这是一个实验性的功能,生产不建议用,效果如下:
// div[data-qiankun="app-vue-hash"] 是 qiankun 动态加的
div[data-qiankun="app-vue-hash"] .app-main {
font-size: 14px;
}
这个方案同样存在 append to body
元素样式无法生效的问题。
这两个方案并不完美,CSS
污染还得靠规范解决:如果是 vue
项目,每个组件都写上 scoped
属性,全局样式都用特殊的 class
名称/前缀,但是这样一来,有没有 css
沙箱都不重要了,从根源上避免了CSS
污染。
微前端的衍生需求:跨项目组件共享
一般项目间的组件共享,用私有 npm
就可以了,但是,如果项目的技术栈不同,比如说:想在 react
项目中使用 vue
组件,npm
包就比较麻烦。
借鉴微前端的做法:
- 项目
A
在入口文件export
这个组件实例化的函数 - 项目
A
打包成umd
格式,并将css
打包到js
,去掉chunk-vendors.js
。 - 打包项目
A
,将其app.js
引入到项目B
中,调用实例化函数即可
具体代码如下:
项目 A
的入口文件:
import Vue from 'vue';
import HelloWorld from '@/components/HelloWorld.vue'
import store from './store';
import i18n from './i18n';
// 该组件如果没有用到 store 和 i18n 等全局的插件,
// 并且调用的地方也是 vue 技术栈,则可以直接 export 这个组件:
// export HelloWorld;
// 使用时直接注册:
// components: {
// HelloWorld: window['app-vue-hash'].HelloWorld
// },
export const createHelloWorld = container => {
return new Vue({
store,
i18n,
render: h => h(HelloWorld),
}).$mount(container);
}
项目 A
的打包配置:
module.exports = {
publicPath: 'http://localhost:8080/',
css: {
extract: false
},
// 自定义webpack配置
configureWebpack: {
output: {
library: `app-vue-hash`,
libraryTarget: 'umd',// 把子应用打包成 umd 库格式
}
},
chainWebpack: config => {
config.optimization.delete('splitChunks')
}
};
项目 B
使用这个组件:
<div id=appVueHash></div>
<script src=http://localhost:8080/js/app.js></script>
<script>
window['app-vue-hash'].createHelloWorld('#appVueHash')
</script>
注意的事项:
- 该组件一定要与路由无关,因为调用时的路由实例和组件实例化时路由实例不是同一个。
- 如果有参数需要传给组件,可以详细写一下
render
函数。 - 该组件需要手动销毁,函数调用返回的
vue
实例,直接调用$destroy
即可。 - 该组件及其子孙组件引用资源的前缀需要通过
publicPath
设置,也可以动态修改,如果没有引用资源,则忽略。
其实还可以再优化一下:直接将这个组件挂载到 window
上,并且不需要打包 umd
。
window.createHelloWorld = container => {
return new Vue({
store,
i18n,
render: h => h(HelloWorld),
}).$mount(container);
}
打包配置去掉 configureWebpack
,使用也很简单: window.createHelloWorld('#appVueHash')
。
同理,qiankun
的子项目也可以这样写,让父项目拿到生命周期函数。
微前端方案如何选型
不要以为跑通了 qiankun
的官方 demo
,就可以直接用了。实际项目的复杂程度可能比这个复杂 N 倍,每一点差别都会带来新的问题。
所以经常有人出现这种情况:沙箱开启吧,项目跑步起来(一堆报错);不开沙箱吧,js/css
污染很严重。甚至是本地都跑得好好的,但是部署又不行了(这个问题相对好解决)。
选择 qiankun
之前务必想清楚两件事:
-
项目的资源是否能正确加载
-
如果项目有用到
webpack
确认下:修改
webpack
的publicPath
项,能否解决所有的资源加载问题,可以看看vue-cli3
的介绍:HTML 和静态资源。简单判断:资源文件(
js/css
除外)是否放在了static
目录(vue-cli2
) 或者public
目录(vue-cli3
),如果没有,或者有但是不多,修改起来也不麻烦的,就可以用qiankun
。精确判断:写死
webpack
的publicPath
项,值为服务器 A 的地址,然后打包,把index.html
放到服务器 B 上,其他所有资源放到服务器 A 上(记得允许跨域),然后访问项目(服务器 B 的地址),看看是否正常(模拟qiankun
加载)。 -
如果项目没有
webpack
,比如说jQuery
项目。-
如果资源都放在
cdn
,或者说资源的链接都是完整的URL
,那就可以放心大胆的接入。 -
如果资源都是相对链接,jQuery 老项目的资源加载问题 这个方案能否解决,如果还不行,那就得慎重。
-
jQuery
的多页应用,优先考虑iframe
-
-
-
js/css
污染是否难以解决-
模块化之后,
js
污染的问题几乎很少了,并且js
沙箱比较完美,即使有问题,也是插件之间的插件冲突,比较好解决 -
css
是否会污染 ,这个没有一个很好的标准判断,以vue
为例,如果所有的组件样式都带上了scoped
,基本上避免了 99% 的污染,剩下的一些全局样式,如果都使用特殊的class/id
,那就完美,有没有沙箱都不会污染。理想很丰满,现实很残忍,如果项目全都是全局样式,那就要三思了。
-
如果上述两个问题都 OK
,就可以尝试使用 qiankun
。
最后,没有十全十美的方案,有问题别慌,对症下药,逐个击破。只要肯花时间,iframe
也可以是一个完美的方案。
附录
我记录了自己从 iframe
到 single-spa
,再到 qiankun
的技术选型历程,以及其中的一些坑,感兴趣可以了解下:
- 从0实现一个前端微服务(上)
- 从0实现一个single-spa的前端微服务(中)
- 从0实现一个single-spa的前端微服务(下)
- qiankun 微前端方案实践及总结
- qiankun 微前端实践总结(二)
如果有什么问题或者错误,欢迎指出!