前言
本文大部分内容来自官方文档、开源项目维护者的博客、社区博客。另外阅读本文可能需要你已经对微前端有个大致的了解。
微前端的核心价值
微前端(Micro Frontends)是一种前端架构模式,它将前端应用程序拆分成一个基座应用和多个小型子应用,子应用可以独立开发、测试、部署和升级,并且不限制子应用的技术栈。
它是一种微服务架构,对于子应用的拆分理念类似微服务的服务拆分,被拆分的子应用之间应该是独立的。
我们这里不过多介绍微前端到底是什么,相信看这篇文章的同学都已经对微前端有个初步的了解了。
对于微前端的核心价值,Qiankun
官方文档中提供的链接 微前端的核心价值 讲的很好,这里对文章内容简单做一个总结:
- 微前端的核心价值在于 "技术栈无关",这才是它诞生的理由。
- 确保遗留的屎山代码能平滑的迁移,以及确保在若干年后还能用上时下热门的技术栈。
- 微前端首先解决的,是如何解构巨石应用。
- 微前端的使命,文章的作者认为是:
推荐大家去看一下原文,文章下面的评论争论的比较激烈,很多都是针对第一点技术栈无关
的
我个人觉得,对于中小型公司来讲,技术栈统一是非常重要的事,特别是小公司,受限于规模不太可能有很多开发同学在同一个项目上进行开发。使用过多不同的技术栈反而会导致开发周期变长,且代码变得难以维护。
试想一下,一个中小型公司,只有一个前端团队,但是同时使用了React, Angular, Vue 三种框架进行开发,在面对人员离职,休假,排期冲突等等问题时,会导致开发同学需要修改他不熟悉的技术栈的业务代码,不同的组件库,不同的语法,不同的api,必然要花费更多的时间去实现。
而统一技术栈,不仅有利于提升开发速度和代码质量,也有利于组内的开发同学进行技术沉淀。
我一直觉得如果一个人说他自己同时精通三种不同框架写法用法,那他一定是在装B。你可能理解三种框架的实现,因为框架的设计理念总有些共通之处,但是各种写法,和各种不同的api你在写的时候,必然要花时间去翻官方文档,或是问谷歌问chatgpt。
不过由于作者是阿里的,大公司在应对多个不同的团队在同一个巨石应用上进行开发时,技术栈无关就显得特别重要了。
综上,我还是觉得 拆解巨石应用 才是大部分公司对于微前端的核心诉求。
微前端的几种实现方案
推荐阅读无界官方文档 微前端是什么
想要实现一个微前端框架,就必须要考虑到下面几点:
- 怎么去加载子应用。
- 怎么实现子应用之间的隔离,子应用间的隔离包括:
- 隔离声明的全局变量或者挂载在window上的属性和方法
- 样式隔离
- 元素隔离
Iframe 方案
Single SPA 、 Qiankun 的方案
主要实现思路:
- 预先注册子应用(激活路由、子应用资源、生命周期函数)
- 在监听路由的变化后,根据子应用提供的注册信息,匹配到对应的子应用,然后加载子应用资源,顺序调用生命周期函数并最终渲染到容器。
Qiankun 则进一步对
single-spa
方案进行完善:
- 子应用资源由
js
列表修改进为一个url
,通过html entry
的方案大大减轻注册子应用的复杂度。- 实现应用间隔离,支持
js
隔离(自己实现的沙箱) 和css
隔离 (类似于vue
的scoped
,严格模式下基于webcomponent shadow dom
)。- 增加资源预加载能力,预先加载子应用
html
、js
、css
资源,然后缓存下来,加快子应用的打开速度。
Wujie
微前端实现浅析
如何加载子应用
Qiankun
推荐阅读 可能是你见过最完善的微前端解决方案
主应用劫持到 url change
事件, 根据要访问的 url
和子应用注册时的配置, 匹配要加载的子应用入口, 然后通过 import-html-entry plugin
插件,通过 fetch
获取到 html
入口文件(这就是为什么需要子应用资源允许跨域),拿到 html
字符串后,会调用方法通过一大堆正则去匹配获取 html
中包括内联和外联的js、css
、注释等等,然后把
- 其中的
js
放到沙箱中执行 (with(proxyWindow){code}
) css
全部以内联形式嵌入html
模板中- 渲染出来的
html
插入到注册子应用时配置的container
中。
如下图所示 :
无界
推荐阅读:
无界主要基于 iframe
和 webcomponent
实现,
- 创建
iframe
,插入到document
中。 - 根据配置的子应用
url
,使用基于乾坤的import-html-entry plugin
二次封装的插件,加载入口html
内容,后面的操作跟Qiankun
基本一样,分别加载并解析出html css js
代码,跟Qiankun
唯一的区别是,Wujie
支持ESM
的代码。 - 创建
webcomponent
,并把处理后的html css
挂载上去。 - 通过代理建立
iframe
和webcomponent
的连接,然后将子应用的js
注入主应用同域的iframe
中运行, 渲染页面。
沙箱
Qiankun
参考
Qiankun
有三种沙箱, SnapshotSandbox
, LegacySandbox
, ProxySandbox
SnapshotSandbox
就是对 window
做个快照(浅拷贝),子应用在 UnMount
后拿的 window
跟快照进行对比,记录被修改的 Props,在子应用下次加载时,重新应用修改。
LegacySandbox
优化了上面的过程,不再每次都进行比较(太费时间),而是通过代理 window
对象,监听对 window
的修改,直接记录新增和修改的属性,在子应用下次加载时,重新应用修改。
前两种沙箱都是直接对 window
直接进行操作,所以全局只能有一个子应用被加载,而 ProxySandbox
为了避免直接操作真实的 window
, 它基于 Window
对象做了一个假的 fakeWindow
,然后给每个子应用都分配一个 fakeWindow
。通过 Proxy
进行代理,当子应用想要修改 window
对象时,就修改自己的 fakeWindow
, 在访问的时候, 优先从 fakeWindow上拿,子应用卸载时就把自己的 fakeWindow
存起来,在重新加载时再还回去。子应用之间互不影响。
前两种沙箱属于最后一种的降级方案。
无界
由于 iframe
天然拥有独立的上下文环境,所以无界把子应用的 js
代码放在 iframe
内执行,借助iframe
完美实现了沙箱。
这里简单解释下 Wujie iframe
是怎么跟 webcomponent
连接起来的:
渲染页面以 Vue
为例,需要给 Vue
指定一个 DOM
作为挂载点,Vue
会将组件挂载到该 DOM
上,而把 JS
代码放在 iframe
中执行后,使用 document.querySelector
查找到的 DOM
是 iframe
内的 DOM
,但实际我们是想让它挂载到 webcomponent
内,此时可以我们通过代理,直接劫持 iframe
内的 document.querySelector
方法,让它返回 webscomponent
内的 DOM
, 这样 iframe
和 webcomponent
就连接起来了。
除了 document.querySelector
之外,Wujie
还劫持了其它很多类似的方法,保证在 iframe
中操作 DOM
时,实际上操作的是 webcomponent
。
在IE
环境下webcomponent
和proxy
都无法支持,Wujie
会自动降级,采用另一个 iframe
替换webcomponent
,用Object.defineProperty
替换proxy
来做代理。
样式隔离
webcomponent
一个重要的特性就是shadow dom
,shadow dom
是一项web
标准,用于封装组件并实现样式隔离。 它允许将组件的HTML
结构、样式和行为封装在一个独立的DOM
树中,从而与主文档的DOM
树相互隔离。也就是说外界的样式影响不了它,它内部的样式也无法影响到外部。
Qiankun
Qiankun 有两种样式隔离的方案可供选择
一种就是上面说的 shadow dom
,这种方案会导致像弹框这种挂载到 body
上的元素,样式丢失。其实也不是样式丢失,是因为弹框的样式在子应用 shadow dom
内, dom
元素挂载在外面,而 shadow dom
内的样式无法对外部的元素生效。
另一种是借鉴了 scoped css
的思路。
对所有样式加了一层 data-qiankun=“应用名”
的选择器来隔离。
无界
无界也是借助的 shadow dom
,对于弹框问题 由于无界劫持了 document
的属性和方法,document.body
的 appendChild
或者 insertBefore
被劫持了, 所以弹框在挂载时会直接插入到 webcomponent
,所以不存在这种问题。
元素隔离
元素隔离的意思是子应用只能对自身的元素进行增,删,改,查的操作。
京东 Micro App
对于元素隔离的解释:
Micro App
Micro App
通过代理 Document, Element
原型链上的方法来实现把选择器的范围控制到当前子应用内。简单来说就是子应用内调用的 document.getElementByxxx
方法已经被重写了,会根据对应子应用的标识做查询范围限制。
Qiankun
后来也采用此方案做了优化。
无界
在上文我们提到过无界子应用是代码实例运行在 iframe
中,DOM
渲染在 webcomponent
中,并且通过代理document
,建立了 iframe 与 webcomponent
的关系。
此时 iframe
中子应用实例执行 document.getElement
时,获取到的 DOM
元素其实就是子应用渲染在对应 webcomponent
中的元素。
微前端框架选型
可以参考无界的官方文档,对当前的几种微前端方案做了对比:
目前 Qiankun2.0
不能很好的支持 Vite
,虽然可以通过社区插件解决一部分问题,但还是有各种缺陷,Qiankun3.0
快要发布了,希望发布后能够解决。
挑一部分解释下:
在开发环境下,如果我们使用 vite 来构建 vue3 子应用,
基于 vite 的构建机制,vite 以 原生 ESM 方式提供源码,然后在子应的 html
的入口文件的 script
标签上携带 type=module
。
qiankun父应用引入子应用,本质上是将 index.html
作为入口文件,并通过 import-html-entry
这个库去加载子应用所需要的资源列表 js、css,然后通过 eval("xxxxxcode")
直接执行。
这时候 qiankun 去加载子应用获取 main.js
的代码时就出问题了, eval()
它不支持诸如 export 和 import 的模块语法,所以就直接报错了:
<script type="module" src="/src/main.js"></script>
function () {
// main.js 中的代码
import xx from 'xx'
import xx1 from 'xx1'
// 省略...
}
Uncaught SyntaxError: Cannot use import statement outside a module
这里顺便提一下,在最开始的时候,import-html-entry甚至会过滤掉 type=module
的 script,后面才支持的 type=module
,详见这个PR # feat: support of module scripts 。
而在生产模式下
vite2 不支持
runtime publicPath
,这项能力在 webpack 中由内置变量__webpack_public_path__
提供,runtime publicPath
是 qiankun 加载子应用的核心 (由 import-html-entry 模块提供) ,用于预加载及引入异步脚本和加载静态资源。esm 会使 qiankun 的 js 沙箱失效,qiankun 内部的 js 沙箱将 window 对象进行了代理,以防止全局作用域被污染,但 esm 模块始终具有自己独立的 顶级作用域,也就是说它访问到的 window 是全局作用域下的,而不是 qiankun 沙箱中提供的代理 window,虽然可以通过在生产环境打包为 umd 格式的方式来避免使用 esm,但有些本末倒置了。
所以当前如果需要为项目接入微前端,而且想要用 Vite
的话,Wujie
是目前的最优选,而且 Wujie
的接入成本也是最低的。但是 Wujie
在社区里没有 Qiankun
那么有知名度,用的人也没那么多,可能有坑还没被发现,希望 Wujie
能够一直维护下去,不会成为一个 KPI项目吧。