共计 3396 个字符,预计需要花费 9 分钟才能阅读完成。
前言
我们团队正在做一个 XX 系统,技术栈是React
,目前该系统日渐庞大,开发及维护成本加大,且每次必须把整个项目一起打包,费时费力。经考虑后决定将其拆分成多个项目,由它们组合成一个完整系统,微前端架构是非常好的选择。
微前端差不多有以下几个好处:
- 单项目维护:比如将
商品模块
单拉出来形成一个项目,它可以由一个小组单独维护,实现良好解耦 - 复杂度降低:不需要在整个集成式的庞大系统内开发,避免巨大的代码量,开发时编译速度快,提高开发效率
- 容错性:单独项目发生错误不会影响整个系统
- 技术栈灵活:vue、react、angular 等包括其他前端技术栈都可以使用,会 vue 的不需要再学 react
对我们来说最大的好处是 单项目维护
。
展示
UI 示例图
我们将整个微前端分为两个部分:
- 主项目(Main):红色框部分,作为整个项目的父级,负责展示菜单模块、头部模块
- 子项目(Sub-apps):蓝色框部分,子项目的作用是具体的业务展示
动图展示
注意看地址栏变化 ,其中包含 /app1/xxx
和/app2/xxx
,乍一看这是一个项目中两个页面的切换,实际上是 来自两个独立的项目,app1 和 app2 来自不同的 git 仓库。
微前端架构图
整个流程大概为:用户访问 index.html, 此时运行模块加载器 Js,加载器会根据整个系统的配置文件(project.config) 去注册各个项目,系统会先加载主项目(Main), 然后会根据路由前缀动态加载对应的子项目
我们这个架构也参考了网上很多好的文章,其中核心文章可参考 https://alili.tech/archive/11…
关于 project.config
大概如下
[
{
isBase: false,
name: 'app1',
version: '1.0.0',
// 通过该路由前缀匹配加载当前入口文件
hashPrefix: '/app1',
// 入口文件
entry: 'http://www.xxxx.com/app1/dist/singleSpaEntry.js',
// 顶级 Store
store: 'http://www.xxxx.com/main/dist/store.js'
}
......
]
技术细节
single-spa
我们找了些实现微前端的仓库,对比后决定使用 single-spa。
我们技术栈是 react,在子项目入口中需要使用 single-spa-react 来构建,关键代码如下:
import singleSpaReact from 'single-spa-react';
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
domElementGetter
});
export function bootstrap(props) {return reactLifecycles.bootstrap(props);
}
export function mount(props) {return reactLifecycles.mount(props);
}
export function unmount(props) {return reactLifecycles.unmount(props);
}
如果你使用 vue,可以使用 single-spa-vue
然后在系统入口文件中,把所有的项目注册进来:
import * as singleSpa from 'single-spa';
singleSpa.registerApplication(
'app1',
() => SystemJS.import('app1-entry.js'),
() => location.hash.startsWith(`#/app1`),
props
);
具体可参考 single-spa 官网 https://single-spa.js.org 这里有很多例子
Webpack 与 SystemJs
我们使用的 lerna 统一管理所有项目的依赖包,所有依赖包的版本统一,这样非常方便维护。
使用 webpack 的 dll 功能,将所有项目的公用依赖包抽离,比如 react、react-dom、react-router、mobx 等
为了方便项目动态加载,我们也参考网上大佬的想法,使用了 systemjs,只不过我们使用的是 0.20.19 版本,配合 systemjs,在 Webpack 中需要改一下 libraryTarget:
output: {
publicPath: 'http://www.xxxxx.com/',
filename: '[name].js',
chunkFilename: '[name].[chunkhash:8].js',
path: path.resolve(__dirname, 'release'),
libraryTarget: 'amd', // 注意 这里使用 amd 的规范
library: 'app1'
},
我们没有使用 umd 规范,也没有使用 systemjs 里的 Import Maps
功能,而是直接通过 project.config 来动态加载模块入口。
app 之间通信
关于这个也看了一些大佬的方案,大概就是所有的项目里有个 store,在注册入口时将所有 store 放进队列,需要更新 store 里的状态时,调用 dispatch 将所有 store 同步。
我的做法和传统单页应用一样,一个系统应该只有一个顶级 Store,由于顶级 Store 里存的一般是整个系统的公用状态 比如菜单、用户信息等,我把它放在 Main 项目里,但打包时这个 Store 是单独抽离的:
entry: {
singleSpaEntry: './src/singleSpaEntry.js',
store: './src/store' // 单独一个入口
},
在注册时,将这个 Store 传入每个项目中:
// 顶级 Store
const mainStore = await SystemJS.import(storeURL);
singleSpa.registerApplication(
'app1',
() => SystemJS.import('http://www.x.com/app1/entry.js'),
hashPrefix('/app1'),
{mainStore}
);
singleSpa.registerApplication(
'app2',
() => SystemJS.import('http://www.x.com/app2/entry.js'),
hashPrefix('/app1'),
{mainStore}
);
这样就可以达到只管理这一个 Store 就可以,非常方便。
注意:我使用的是 Mobx 作为状态管理
前端部署
我们部署的方式非常简单,我自己写了一个 webpack 插件用于把打包后的 dist 传到 OSS 然后将项目信息传给服务端,服务端根据我传入的项目信息组织成 project.config,然后用户在访问 index.html 时会获取 project.config,此时 single-spa 根据配置注册所有项目,然后根据路由来拉取对应的项目入口文件 js 文件。
把子项目的挂载 DOM 放在 Main 项目里
我们的需求是 Main 作为整个项目的 Layout,其中子项目的挂载 Dom 也在 Main 项目里,这就必须等到 Main 项目完全渲染完成后,才能挂载子项目。我参考了网上有些微前端的实现,把 domElementGetter 方法借鉴了过来:
function domElementGetter() {let el = document.getElementById('sub-module-wrap');
if (!el) {el = document.createElement('div');
el.id = 'sub-module-wrap';
}
let timer = null;
timer = setInterval(() => {if (document.querySelector('#content-wrap')) {document.querySelector('#content-wrap').appendChild(el);
clearInterval(timer);
}
}, 100);
return el;
}
demo
demo 地址:https://github.com/Vibing/mic…
结束语
这是我们第一次玩微前端,可能有很多地方不完美,还望各位大佬多多包涵