前端"巨型"工程解耦

1,215 阅读4分钟

前记

适合vue技术栈、正在做前端工程化相关的开发者

背景

当20+前端开发,花费1年+时间,共同迭代开发一个前端vue2.0工程时,毫无疑问,结果肯定是这个前端工程会变成一个“巨无霸”,像这样

汉堡.jpg

各个子程序之间互相关联,引用。

这会带来以下一系列问题:

  1. 项目启动慢
  2. 热更新慢
  3. 开发时,内存占用较大,出现爆栈几率增加
  4. 构建慢,部署环境(开发,联调,测试)时间拉长
  5. 只能构建提测全量前端包,测试已验证通过的是否还需重新验证
  6. 不满足单品(一个单独功能的子系统)部署
  7. ...

说明一下 这里的 单品\color{red}{单品} 指的是:独立的子系统 + 对应子系统独立后台(在客户那边部署的时候可能不会被踢出的灵魂拷问:“我只想要 a 系统的功能,你们为什么把 b,c的也给部署上?”)。

预研

导致上面系列问题的主要因素就是项目体积较大,耦合严重。那么解决问题的思路就有了---- 减低项目体积\color{red}{减低项目体积},也就是把这个 “巨无霸” 拆分成多个不同风味的 “小肉饼”。

iframe和single-spa优缺点

总结其他开发者的经验,加个人实践体验得出,两个方法对比结果如下:

技术方案优点缺点
iframe方案1、css、js完全隔离,各微前端间绝对不会互相影响1、加载耗时较长;2、为了加载最新的子应用,可能每次都得重新获取资源;3、被集成存在滚动条问题;4、弹框问题
single-spa方案1、有现成封装好库,能直接使用;2、可以实现页面无缝切换;3、按需加载1、全局污染问题

经对比,最终敲定使用基于single-spa的乾坤框架\color{red}{乾坤框架}

乾坤框架:是阿里旗下,基于single-spa的微前端实现库,协议类型MIT。

stateDiagram-v2
用户 -->  master: 访问/master
master --> childApp1:加载
master --> childApp2:加载
master --> ......
master --> childAppn:加载

实施

我们的 “巨无霸” 环境依赖:

  1. vue全家桶
  2. element、layer(历史遗留)
  3. echarts、jsplumb、g6

我们的 “巨无霸” 布局长这样:

企业微信截图_20210926163620.png

所以就很好划分,导航和菜单作为master应用,内容区作为childApp应用。

提炼master应用

  1. 将内容区剔除干净,并且能正常启动。
  2. 安装乾坤框架 npm i qiankun -S
  3. 在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);
  1. 启动乾坤
// 在对应chilApp容器container的地方启动乾坤
<div id="yourContainer"></div>

import { start } from "qiankun";

mounted(){
  start();
}

// 更优方案--使用window变量来避免重复启动qiankun
mounted(){
    if(!window.qiankunStarted){
        window.qiankunStarted = true;
        start();
    }
}

调整childApp应用

  1. 在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);
}
  1. 修改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代码调整

  1. 原来的start方法删除,使用loadMicroApp方式加载
this.microAppInstance = loadMicroApp({
    name:"childApp1",
    entry,
    container,
});

需要提前销毁childApp旧的实例,this.microAppInstance.unmount()。

childApp中的调整

  1. 增加内容区标识入口
// componentIdList
export default {
    "10000001":()=>import("10000001.vue"); //页面级
    "10000002":()=>import("10000002.vue");
    ...
}
  1. 动态组件加载内容区
// template
<component :is="componentID"></component>

// computed
componentID(){
    return componentIdList[ID];
}
  1. componentIdList 内容区标识入口
  2. 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

未完待续..........