本文转载自微信公众号「前端星辰」,作者旋律 。转载本文请联系前端星辰公众号。
什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端借鉴了微服务的架构理念,将一个庞大的前端应用拆分为多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用联合为一个完整的应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
特性
- 技术栈无关 主框架不限制接入应用的技术栈,子应用可自主选择技术栈
- 独立开发/部署 各个团队之间仓库独立,单独部署,互不依赖
- 增量升级 当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性
- 独立运行时 微应用之间运行时互不依赖,有独立的状态管理
- 提升效率 应用越庞大,越难以维护,协作效率越低下。微应用可以很好拆分,提升效率
目前可用的微前端方案
微前端的方案目前有以下几种类型:
基于 iframe 完全隔离的方案
作为前端开发,我们对 iframe 已经非常熟悉了,在一个应用中可以独立运行另一个应用。它具有显著的优点:
- 非常简单,无需任何改造
- 完美隔离,JS、CSS 都是独立的运行环境
- 不限制使用,页面上可以放多个 iframe 来组合业务
当然,缺点也非常突出:
- 无法保持路由状态,刷新后路由状态就丢失
- 完全的隔离导致与子应用的交互变得极其困难
- iframe 中的弹窗无法突破其本身
整个应用全量资源加载,加载太慢
这些显著的缺点也催生了其他方案的产生。
基于 single-spa 路由劫持方案
single-spa 通过劫持路由的方式来做子应用之间的切换,但接入方式需要融合自身的路由,有一定的局限性。
qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台。它对 single-spa 做了一层封装。主要解决了 single-spa 的一些痛点和不足。通过 import-html-entry 包解析 HTML 获取资源路径,然后对资源进行解析、加载。
通过对执行环境的修改,它实现了 JS 沙箱、样式隔离 等特性。
京东 micro-app 方案
京东 micro-app 并没有沿袭 single-spa 的思路,而是借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 webComponents 组件,从而实现微前端的组件化渲染。
在 Vite 上使用微前端
我们从 我们从 UmiJS 迁移到了 Vite 之后,微前端也成为了势在必行,当时也调研了很多方案。
为什么没用 qiankun
qiankun 是目前是社区主流微前端方案。它虽然很完善、流行,但最大的问题就是不支持 Vite。它基于 import-html-entry 解析 HTML 来获取资源,由于 qiankun 是通过 eval 来执行这些 js 的内容,而 Vite 中的 script 标签类型是 type="module",里面包含 import/export 等模块代码, 所以会报错:不允许在非 type="module" 的 script 里面使用 import。
退一步实现,我们采用了 single-spa 的方式,并使用 systemjs 的方式进行了微前端加载方案,也踩了不少的坑。single-spa 没有一个友好的教程来接入,文档虽然多,但大多都在讲概念,当时让人觉得有一种深奥的感觉。
后来看了它的源码发现,这都是些什么……里面大部分代码都是围绕路由劫持而展开的,根本没有文档上那种高大上的感觉。而我们又用不到它路由劫持的功能,那我们为什么要用它?
从组件化的层面来说 single-spa 这种方式实现得一点都不优雅。
- 它劫持了路由,与 react-router 和组件化的思维格格不入
- 接入方式一大堆繁杂的配置
- 单实例的方案,即同一时刻,只有一个子应用被展示
后来琢磨着 single-spa 的缺点,我们可以自己实现一个组件化的微前端方案。
如何实现一个简单、透明、组件化的方案
通过组件化思维实现一个微应用非常简单:子应用导出一个方法,主应用加载子应用并调用该方法,并传入一个 Element 节点参数,子应用得到该 Element 节点,将本身的组件 appendChild 到 Element 节点上。
类型约定
在此之前我们需要约定一个主应用与子应用之间的一个交互方式。主要通过三个钩子来保证应用的正确执行、更新、和卸载。
类型定义:
- export interface AppConfig {
- // 挂载
- mount?: (props: unknown) => void;
- // 更新
- render?: (props: unknown) => ReactNode | void;
- // 卸载
- unmount?: () => void;
- }
子应用导出
通过类型的约定,我们可以将子应用导出:mount、render、unmount 为主要钩子。
React 子应用实现:
- export default (container: HTMLElement) => {
- let handleRender: (props: AppProps) => void;
- // 包裹一个新的组件,用作更新处理
- function Main(props: AppProps) {
- const [state, setState] = React.useState(props);
- // 将 setState 方法提取给 render 函数调用,保持父子应用触发更新
- handleRender = setState;
- return <App {...state} />;
- }
- return {
- mount(props: AppProps) {
- ReactDOM.render(<Main {...props} />, container);
- },
- render(props: AppProps) {
- handleRender?.(props);
- },
- unmount() {
- ReactDOM.unmountComponentAtNode(container);
- },
- };
- };
Vue 子应用实现: React 实现 其核心代码仅十余行,主要处理与子应用交互 (为了易读性,隐藏了错误处理代码): 完成,现在已经实现了主应用与子应用的装载、更新、卸载的操作。现在,它是一个组件,可以同时渲染出多个不同的子应用,这点就比 single-spa 优雅很多。 entry 子应用地址,当然真实情况会根据 dev 和 prod 模式给出不同的地址: Vue 实现 如何让子应用也能独立运行 single-spa 等众多方案,都是将一个变量挂载到 window 上,通过判断该变量是否处于微前端环境,这样很不优雅。在 ESM 中,我们可以通过 import.meta.url 传入参数来判断: 入口导入修改: 浏览器兼容性 IE 浏览器已经逐步退出我们的视野,基于 Vite,我们只需要支持 import 的特性浏览器就够了。当然,如果考虑 IE 浏览器的话也不是不可以,很简单:将上面代码的 import 替换为 System.import 即 systemjs,也是 single-spa 的所推崇的用法。 模块公用 我们的子组件必须要使用 mount 、unount 模式吗?答案是不一定,如果我们的技术栈都是 React 的话。我们的子应用只导出一个 render 就够了。这样用的就是同一个 React 来渲染,好处是子应用可以消费父应用的 Provider。但有个前提是两个应用之间的 React 必须为同一个实例,否则就会报错。 我们可以将 react、react-dom 、styled-componets 等常用模块提前打包成 ESM 模块,然后放到文件服务中使用。 更改 Vite 配置添加 alias: 这样就能愉快地使用同一份 React 代码了。还能抽离出主应用和子应用之间的公用模块,让应用总体积更小。当然如果没上 http2 的话,就需要考虑颗粒度的问题了。 在线 CDN 方案:https://esm.sh 还有个 importmap 方案,兼容性不太好,但未来是趋势: 父子通信 组件式微应用,可以传递参数而通信,完全就是 React 组件通信的模型。 资源路径 在 Vite 的 dev 模式中,子应用里面静态资源一般会这样引入: 图片的路径:/basename/src/logo.svg,在主应用显示就会 404。因为该路径只是存在于子应用。我们需要配合 URL 模块使用,这样路径前面会带上 origin 前缀: 当然这样使用比较繁琐,我们可以将其封装为一个 Vite 插件自动处理该场景。 路由同步 项目使用 react-router,那么它可能会存在路由不同步的问题,因为不是同一个 react-router 实例。即路由之间出现不联动的现象。 在 react-router 支持自定义 history 库,我们可以创建: 最终子应用使用同一份 history 模块。当然这不是唯一的实现,也不是优雅的方式,我们可以将路由实例 navigate 传递给子应用,这样也能实现路由的交互。 注意:子应用的 basename 必须与主应用的 path 名称保持一致。这里还需要修改 Vite 的配置 base 字段: JS 沙箱 因为沙箱在 ESM 下不支持,因为无法动态改变执行环境中模块 window 对象,也无法注入新的全局对象。 一般 React、Vue 项目也很少修改全局变量,做好代码规范检查才是最主要的。 CSS 样式隔离 自动 CSS 样式隔离是有代价的,一般我们建议子应用使用不同的 CSS 前缀,再配合 CSS Modules 基本上能实现需求。 打包部署 部署可以根据子应用的 base 放置在不同的目录,并将名称对应。配置好 nginx 转发规则就可以了。我们可以将子应用统一路由前缀,便于 nginx 将主应用区分开并配置通用规则。 比如将主应用放置在 system 目录,子应用放置在 app- 开头的目录: 优点 1. 简单 核心不足 100 行代码,无需多余的文档 2. 灵活 通过约定的方式接入,也可以渐进增强 3. 透明 无任何劫持方案,更多逻辑透明性 4. 组件化 组件化的渲染及参数通信 5. 基于 ESM 支持 Vite,面向未来 6. 向下兼容 可选 SystemJS 方案,兼容低版本浏览器 示例代码在 Github,感兴趣的朋友可以 clone 下来学习。由于我们的技术栈是 React,所以这里示例的主应用的实现用的是 React 。 微前端组件(React):https://github.com/MinJieLiu/micro-app 微前端示例:https://github.com/MinJieLiu/micro-app-demo 微前端的方案适合团队场景的最好,打造一个团队能掌控的方案尤为重要。 参考资料: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import主应用实现
浏览器
Chrome
Edge
Firefox
Internet Explorer
Safari
import
61
16
60
No
10.1
Dynamic import
63
79
67
No
11.1
import.meta
64
79
62
No
11.1
有示例吗
结语