解决模块共享问题之加载远程组件
需求场景描述
在B端的多个业务系统中,模块A 存在于存在业务系统甲中,强依赖该业务系统;模块A的体积随着需求的不断更新迭代也在一天天变大,加载的速度也在一天一天的变慢。
加载慢的原因有多个方面,本身模块A的功能很大很复杂,大量组件,以及网络请求,以及强制的接口依赖顺序等等各方面原因;
项目的脚手架依旧使用的是 vue2.x + element ui ,这熟悉的配方,熟悉的味道; 但是因为是内网项目且对应的客户群体尤为特殊,我们自己封装了统一的业务系统脚手架以及魔改的它妈都认不出的Element UI,作为我们的业务统一的基座;
就在这样的项目情况下,项目乙 也需要集成 模块A, 而且整个的业务流程模块A在乙项目中作为前置流程,有了这个前置才能流转到项目甲;
是的没错,就这么扯,模块A既满足了项目乙的业务,还满足了系统甲的业务,然后构成了整个业务的闭环;
因为只是流程的不同和部分内容的不同(很多都是共用的),我们决定将业务进行耦合;决定不拆分。
目前的解决方案
经过了第一版的 iframe 方案;
又经过了第二版的 独立组件按需集成 方案;
在运行了很长一段时间后(半年至一年)一切看起来都很完美;因为毕竟有了对比就能凸显出来优势对吧;
如果不出意外,这个方案将继续沿用,既然这样说那么意外就马上要来了.....
没错 问题已经凸显出来了,这次的问题不是客户反馈的,是测试同学反馈的;
因为 项目A,项目B 都依赖了公共组件包,所以在每次提测需求的时候,项目A,项目B,都要单独构建,但是因为项目A,和项目B是不同的测试同学负责,导致测试A项目的同学经常性的遗忘构建项目B,就直接进行开始测试了。导致报了很多不复存在的bug, 最关键是有一两次都要进行发版了,最后发现项目B根本没有正确的构建,所以整个测试过程测了个寂寞;
测试同学的工作方式这些我们不予置评,他们就提出了能不能不每次都要去构建两个项目;
这其实咋心里也明白,这问题远远不止构建麻烦,真正的问题在于 公共组件包打了补丁或者迭代了需求后,就必须 A,B两个项目同时走发布流程,部署到生产环境,因为又是不同的部门负责A, B项目,导致 A在加班发版的时候,经常给B负责的同学打电话让别人提测提发布....
那其实正常的迭代 这样是没有问题的,但是遇到紧急的或者高频率的提测或发布,就显得不那么友好。
本着生产环境跑的好好的,能不动就不动的原则,我们理应回绝这合理的要求;
但是这毕竟不是咋的做事风格啊 ,问题都凸显了,那就去解决啊
那就整起来啊;必须得整起来,加班也得整起来;
经过一番思考 我们畅想,如果B项目能够实时加载A项目的模块,那问题不就得到了完美解决了吗, 那首选方案就是 iframe 啊
但是 iframe 存在的问题,我们已经很明了啊,难道一切又要回到最初? 那中间的过程不是白瞎了吗?
这时候就想到了 公共js 的问题;也就是下面我们要讲到的 远程组件;
加载远程组件
既然想到了,那就去实践啊,毕竟实践了就能知道行不行,为什么不行,哪儿不行。
首先我们坚信这个方案是可行的;
公共组件 我们首先需要打包成UMD 或者。IIFE 格式
参考打包构建脚本:
//rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'
import builtins from 'rollup-plugin-node-builtins'
import globals from 'rollup-plugin-node-globals'
import { ugligy } form 'rollup-plugin-node-ugligy'
export default {
input: './src/index.js',
output: {
format: 'iife',
file: './dist/index.js',
name: 'ComononUI'
},
plugins: [
globals(),
builtins(),
commonjs(),
vue(),
ugligy()
]
}
通过rollup.js 我们 js打包成了 iife 的js文件;
那么我们接下来就需要想办法把这个文件运行起来;
我们这里分三步
第一步 把文件拷贝到A项目的public目录下,然后通过浏览器直接输入地址确定能够访问到该文件
这个操作比较简单,不做过多的叙述
在使用jenkins 构建的时候,我们配合 nodejs 脚本,
在 npm ci 之后
把node_modules/commonUi/dist/index.js 文件 拷贝到 public 下
第二部 我们在项目B去加载这个文件;
// loadConponent.js
export function loadComponent(url){
const clearScript = (script)=>{
if (script.parentNode) {
script.parentNode.removeChild(script)
}
script.onload = null
script.onerror = null
script = null
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
document.body.append(script)
script.src=url+'?t='+new Date().getTime()
script.onload=()=>{
resolve()
clearScript(script)
}
script.onerror=()=>{
reject(new Error('fetch script failed'))
clearScript(script)
}
})
}
第三部 使用组件
我们通过动态组件
<conponent :is="xxx" :props="props" />
<template>
<component v-if="com" :is="com"></component>
</template>
<script>
import { loadComponent } from './loadConponent.js'
import * as Vue from 'vue'
export default {
name: 'Test',
data(){
return {
com:null
}
},
mounted() {
window.Vue=Vue
loadComponent('http://127.0.0.1/projectA/commonUI/dist/index.js').then(()=>{
console.log("window['commonUI']>>> ",window['commonUI'])
window['commonUI'] && window['commonUI'].install(window.Vue)
this.com = window['commonUI']
window['commonUI'] = null
})
},
}
</script>
这样我们就完成了远程组件的加载应用
关于组件中的 peerDependencies 部分的处理
把所以peerDependencies 中依赖的包 都通过全局的方式挂载在winodw 上
在组件 install 完成后,即可卸载
例如:
import * as Vue from 'vue'
improt * as utils form 'utils'
window.Vue = Vue
window.utuls=utils
需要注意的点是:需要在远程脚本加载之前挂载,不然脚本加载就会报错,peerDependencies 的包的变量找不到, 当然我们也可以一开始在 rollup.config.js 中 申明好 globals
如果不声明的话,在运行 rollup 的时候 也会报警告:
以 axioa 为例:
(!) Missing global variable name Use output.globals to specify browser global variable names corresponding to external modules
axios (guessing 'axios')
你不配置的话,只能根据 guessing 的提示信息进行包名的挂载了
目前该方案已经调试通过,但是不会应用集成在项目中,如你所想它确实存在或多或少的问题。
接下来,我们将使用 模块联邦的形式在进行一次实践,
模块联邦无疑是现阶段比较优雅的方案;此方案在实践的过程中也是遇到了许多问题,截止目前已经逐步彻底解决,并且集成至项目中;已经在测试环境中正式开跑。毕竟它更适合模块共享这类业务场景,比如微前端 对吧。
当然在这过程中也浅浅的尝试了 vue3-sfc-loader 这个方案,它也是支持vue2 的效果也是不错的。
最后的最后
下面请上我们今天的主角:有请小趴菜
转载自:https://juejin.cn/post/7353226130822823990