前言
我们团队正在做一个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...
结束语
这是我们第一次玩微前端,可能有很多地方不完美,还望各位大佬多多包涵
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。