前记
适合vue技术栈、正在做前端工程化相关的开发者
背景
当20+前端开发,花费1年+时间,共同迭代开发一个前端vue2.0工程时,毫无疑问,结果肯定是这个前端工程会变成一个“巨无霸”,像这样
各个子程序之间互相关联,引用。
这会带来以下一系列问题:
- 项目启动慢
- 热更新慢
- 开发时,内存占用较大,出现爆栈几率增加
- 构建慢,部署环境(开发,联调,测试)时间拉长
- 只能构建提测全量前端包,测试已验证通过的是否还需重新验证
- 不满足单品(一个单独功能的子系统)部署
- ...
说明一下 这里的 指的是:独立的子系统 + 对应子系统独立后台(在客户那边部署的时候可能不会被踢出的灵魂拷问:“我只想要 a 系统的功能,你们为什么把 b,c的也给部署上?”)。
预研
导致上面系列问题的主要因素就是项目体积较大,耦合严重。那么解决问题的思路就有了---- ,也就是把这个 “巨无霸” 拆分成多个不同风味的 “小肉饼”。
iframe和single-spa优缺点
总结其他开发者的经验,加个人实践体验得出,两个方法对比结果如下:
技术方案 | 优点 | 缺点 |
---|---|---|
iframe方案 | 1、css、js完全隔离,各微前端间绝对不会互相影响 | 1、加载耗时较长;2、为了加载最新的子应用,可能每次都得重新获取资源;3、被集成存在滚动条问题;4、弹框问题 |
single-spa方案 | 1、有现成封装好库,能直接使用;2、可以实现页面无缝切换;3、按需加载 | 1、全局污染问题 |
乾坤框架:是阿里旗下,基于single-spa的微前端实现库,协议类型MIT。
stateDiagram-v2
用户 --> master: 访问/master
master --> childApp1:加载
master --> childApp2:加载
master --> ......
master --> childAppn:加载
实施
我们的 “巨无霸” 环境依赖:
- vue全家桶
- element、layer(历史遗留)
- echarts、jsplumb、g6
我们的 “巨无霸” 布局长这样:
所以就很好划分,导航和菜单作为master应用,内容区作为childApp应用。
提炼master应用
- 将内容区剔除干净,并且能正常启动。
- 安装乾坤框架
npm i qiankun -S
- 在main.js中注册childApp
// main.js
import { registerMicroApps } from "qiankun";
const CHILDAPPCONFIG = [
{
name: 'child-app-name', //子应用名称,唯一
entry: 'localhost:7100/', //入口
container: '#yourContainer', //childApp容器
activeRule: '/yourActiveRule2', //激活规则
}
];
registerMicroApps(CHILDAPPCONFIG);
- 启动乾坤
// 在对应chilApp容器container的地方启动乾坤
<div id="yourContainer"></div>
import { start } from "qiankun";
mounted(){
start();
}
// 更优方案--使用window变量来避免重复启动qiankun
mounted(){
if(!window.qiankunStarted){
window.qiankunStarted = true;
start();
}
}
调整childApp应用
- 在main.js补充master需要用到的钩子函数
// main.js
let instance = null;
let routes = null;
function render(props = {}) {
const { container } = props;
routes = router;
instance = new Vue({
router: routes,
store,
i18n,
data: {
Bus: new Vue()
},
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
Vue.prototype.modal.initContext(instance);
Vue.prototype.customData.initContext(instance);
window._vm = instance;
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// master需要用到的钩子
export async function bootstrap() {
console.log('子应用开始加载...');
}
export async function mount(props) {
render(props);
return new Error("sorry,响应出问题"); // 这个return用来处理超时白屏问题
}
export async function unmount() {
if (instance) {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
routes = null;
}
}
export async function update(props) {
renderPatch(props);
}
- 修改vue.config.js中的配置
// vue.config.js
const name = "child-app-name"; //与主应用 registerMicroApps 时name对齐
module.exports = {
publicPath:"/childApp1/", //和 master 部署同一个nginx
devServer:{
headers:{
'Access-Control-Allow-Origin': '*', //配置本地开发环境被master使用
}
},
configureWebpack:{
// 生成环境build输出配置
output:{
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
}
}
}
结果
PS: 上面实施中,用到的是常规的基础配置,子应用与路由地址强相关。在实际开发使用过程中,大概率master应用直接使用loadMicroApp,并下发内容区标识到childApp,最后由childApp渲染内容区。
master调整之后的主体逻辑
stateDiagram-v2
用户操作 --> master:点击 等交互操作
master --> 渲染: 匹配不上childApp
master --> childApp1:loadMicroApp主动加载childApp子应用,并下发内容区标识ID
childApp1 --> master: 发现内容区标识不存在,通知master下发新标识
childApp1 --> 渲染: childApp使用ID1标识动态渲染内容区
master --> childApp2
childApp2 --> 渲染
master --> ......
...... --> 渲染
主动加载:master应用通过处理用户访问传入的唯一标识(通常是路由地址),匹配childApp子应用激活规则,配合nginx,主动拉取childApp的静态资源,并渲染到指定dom内的方式。
master代码调整
- 原来的start方法删除,使用loadMicroApp方式加载
this.microAppInstance = loadMicroApp({
name:"childApp1",
entry,
container,
});
需要提前销毁childApp旧的实例,this.microAppInstance.unmount()。
childApp中的调整
- 增加内容区标识入口
// componentIdList
export default {
"10000001":()=>import("10000001.vue"); //页面级
"10000002":()=>import("10000002.vue");
...
}
- 动态组件加载内容区
// template
<component :is="componentID"></component>
// computed
componentID(){
return componentIdList[ID];
}
- componentIdList 内容区标识入口
- ID 就是 master 传入子应用的内容区标识,可以是路由地址,可以通过initGlobalState传入的
总结
- 调用start方法 (启动乾坤) 的时候,必须保证 childApp容器dom已经存在。
- childApp在build的时候,publicPath写成相对路径好处:
- cookie和master共用
- 后面如果需要修改publicPath指向,可以直接在nginx修改,不用重新构建master。nginx配置如下:
/childApp1 { root /childApp1; #可以使用proxy_pass指向别的nginx index index.html; }
问题回溯
【问题1】内容区空白,报错:# Lifecycle function's promise did not resolve or reject
- 原因:mount没有返回值
- 处理: 在子应用main.js导出的mount中返回 return new Error("sorry,响应出问题");
【问题2】layer层级100000起步
- 原因:layer重复声明
- 处理:开启sandbox;或者载入子应用前设置window.layer=null
【问题3】内容区空白,报错:Uncaught (in promise) Error: [qiankun] You need to export lifecycle functions in childApp1 entry
- 原因:childApp1重复启动
- 处理:在loadMicroApp前调用 await this.microAppInstance.unmount(),销毁旧的。
【问题4】jsplumb连线报错:Uncaught TypeError: Failed to execute 'initMouseEvent' on 'MouseEvent': parameter 4 is not of type 'Window'
- 原因:子应用proxy window后,window的type发生变化
- 处理:设置sandbox:false
未完待续..........