在一个公司里,特别是企业级软件里,经常会要求把多个业务线集成在一个站点上。这会造成什么问题。
- 随着业务增长,前端代码也就越来越多,不过开发环境还是线上环境打包时间都会越来越长。
- 随着业务增多,相对的每个业务的更新也会增多,发版频率会变得越来越多。每次其中一个业务更新一个问题,基本上其它业务都需要进行升级、测试和上线。
一、微前端要解决的什么问题呢
上线慢
现在的前端,都是工程化项目,每次上线都需要进行编译,随着业务扩展,代码量的增多,编译速度会越来越慢。每次发版编译甚至需要一二十分钟。如果碰到需要npm安装依赖,这将又是一场灾难。
耦合度高
由于所有的业务都在一个项目里,即使我们文件目录做的多漂亮,依然避免不了会存在相互引用的问题,一旦影响范围考虑的不全面,代码的更新很可能会影响我们想不到的问题地方。
技术单一
在同一个项目里一旦我们技术选型定了,即使新的功能来了,我们依然只能按照之前框架来做。
二、微前端实现方案
由于上诉问题,所以有了微前端的出现,它是一种类似于后端微前端的前端架构,根据我们要解决的问题,我们知道一个微前端的方案需要满足以下几个条件:
- 主应用中可以集成子应用
- 单独部署
- 团队自治
- 可以相互通信
- 样式和js隔离
在早期我们其实已经有了微前端的实现方案,只是用iframe来实现的。随着前端技术的发展,出现single-spa、qiankuan、icestark等。
接下来我们简单说下几种实现方案
iframe
iframe其实可以满足微前端的条件,完美隔离,js、css都是独立的运行环境。不限制使用,页面可以多个iframe来组合业务。当然它的缺点也很明显:
1,无法保持路由状态,刷新后路由状态丢失
2,子应用的相互之间交互比较困难
路由转发模式
是指通过路由,转发到不同独立前端的应用上,常用的方案是nginx进行代理分发。
优点:实现简单、子应用间技术栈无关、不需要对先用应用改造
缺点:
- 每次切换应用时,浏览器会重新加载页面;
- 多个子应用无法并存
- 子应用间通信比较困难
- 子应用的信息不通用,比如登录信息
single-spa
可以参考其官网single-spa.js.org/
优点:
- 切换应用时,浏览器不需要重新加载
- 完全与技术无关
- 多个子应用可以并存
缺点:
- 需要对原有应用进行改造
- 有额外的学习成本
- 关于子应用加载、隔离、通信等需要自己实现
- 子应用间相同资源重复加载
qiankun
qiankun主要是基于single-spa进行的二次封装,它具有single-spa所有的优点,解决了需要开发人员自己编写加载子应用、通信、隔离等逻辑,实现了样式隔离、js沙箱等功能。同样存在子应用间相同资源重复加载的问题。下图是qiankun官网给到的能力图:
具体可参考官网地址:qiankun.umijs.org/zh
接下来我们主要说下qiankun在项目中的使用以及遇到的问题。
三、qiankun在企业里的使用
先来看下下图qiankun的工作流程
我们先来看下主应用和子应用是如何配置,以下是官网中的给出的例子
qiankun有两种加载机制,一种是基于路由的加载,一种是手动加载,我们今天只说基于路由的加载
1.1、主应用配置
1,按照qiankun组件
$ yarn add qiankun # 或者 npm i qiankun -S
2,在主应用中配置子应用信息
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
1.2、子应用配置
子应用不需要按照额外的qiankun的组件
1,导出相应的生命周期钩子(如果不需要特殊处理,这一步也可以不做处理),以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
2,配置微应用的打包工具(这一步是需要的)
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
2、路由劫持
以上边主项目里的配置为例
1,url变化时会触发window上的hashchange或者popstate,我们知道vue-router也是通过这两个事件拦截的路由变化,所有的路由变化都经过qiankun内部函数urlReroute统一处理。
2,urlReroute会根据路由的地址,和注册子应用的activeRule进行匹配。
3,匹配到对应子应用后,会调用对应的entry的配置内容,进行对子应用的加载,使用fetch进行加载对应的js和css
4,渲染对应的子应用到container: '#yourContainer'元素上
3、沙箱
其实是对window对象上属性进行备份和重置的过程。
qiankun用到了三种沙箱机制:
快照沙箱:通过对window对象的浅拷贝来处理。我们在子应用加载时会有两步操作:a、会先把window对象备份,之后的对window操作,b、会之前是否有diff内容,如果有恢复之前子应用的window属性。当移除子应用时,一是会恢复之前window上内容,二是将diff记录下来。
单例沙箱:其实和快照沙箱机制差不多,只是区别是记录变化通过proxy代理实现
proxySandbox(代理沙箱):每个子应用都会基于window主要属性生成一个单独的proxySandbox,之后子应用的更改都在proxySandbox上,所以不会影响window对象,如果一页面有多个子应用,也不会相互影响。前边种如果一个页面有多个子应用的会收到影响。而且省去了对比时间。
4、父子通信
1,在微前端中我们会经常存在子应用需要主应用拿数据的情况,这种情况我们可以使用官网提供的的initGlobalState方法,将主应用要传递给的子应用的数据通过参数传过去,子应用通过props来获取值。
当主应用
下边是官方的一个示例
主应用
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
我们使用initGlobalState进行初始化数据,如果之后主应用数据改变了,可以通过setGlobalState,将改变的数据通知到对应的微应用里
微应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
2,还有一种如果可以通过注册全局自定义事件window.dispatchEvent和addEventListener来实现子应用调用主应用的事件
5、样式隔离
qiankun实现了single-spa 推荐的两种样式隔离方案:ShadowDOM 和 Scoped CSS。
我们先来说下ShadowDOM,qiankun 的源码实现也很简单,只是添加一个 Shadow DOM 节点,伪代码如下:
if (strictStyleIsolation) {
if (!supportShadowDOM) {
// 报错
// ...
} else {
// 清除原有的内容
const { innerHTML } = appElement;
appElement.innerHTML = '';
let shadow: ShadowRoot;
if (appElement.attachShadow) {
// 添加 shadow DOM 节点
shadow = appElement.attachShadow({ mode: 'open' });
} else {
// deprecated 的操作
// ...
}
// 在 shadow DOM 节点添加内容
shadow.innerHTML = innerHTML;
}
}
通过 Shadow DOM 的天然的隔离特性来实现子应用间的样式隔离。
另一个方案就是 Scoped CSS 了,说白了就是通过修改选择器来实现子应用间的样式隔离。
qiankun 会扫描子应用加载到的CSS 文本,通过正则匹配在选择器前加上子应用的名字,如果遇到元素选择器,就加一个爸爸类名给它。
比如:
.subApp.container {
//
}
6、部署
关于生产环境部署,官网也有说明,我这边说下在企业里我们是怎么部署的。
主应用正常部署,每个子应用都有自己的构建流水线,构建完成后通过nginx代理指向不用的子应用打包后地址,主应用配置对应的url规则即可。
这种方案操作比较方便,但是每次新开一个子应用都会需要nginx配置和流水线配置。
7、遇到的问题
1,资源重复加载的问题,
2,keep-alive失效的问题,解决办法是把数据存在了localstorage里。
3,子应用之间相互通信的问题,都需要通过主应用。
8、拆分微前端的颗粒度
拆分微应用的颗粒度应该是按照业务来划分的,不应该太细。以什么为准呢,子应用之间应该没有业务交互,如果需要业务交互,那么可以合并为同一个子应用。
示例代码地址: github.com/bubucuo/mir…