需求背景
当设计到前端接入多团队业务的情况,目前大家大多使用微前端的方式,通过主应用对接多个子应用的方式进行接入。
但是这种微前端接入需要子应用单独部署,一般还要单独去启动一个项目,整体来说太重。
如果我们的需求颗粒度很小,只是一些通用的业务模块,或者在多个项目中都有的模块,是否可以考虑使用插件的形式插入项目,这样复用性和复杂度相对于微前端的方式,都会更加简洁,高效。
remote-plugin的定义
主要是通过script标签的方式加载打包后的umd文件,并通过component的方式挂载到项目中。
<template>
<component
:is="mode"
v-if="mode"
:storeData="storeData"
:extraData="extraData"
v-bind="attrs"
></component>
</template>
<script>
import { markRaw, defineComponent } from "vue";
function asyncScript(url, name) {
// 动态script
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const target = document.getElementById("globalVue") || document.head;
script.type = "text/javascript";
script.src = url;
script.id = name;
script.onload = function () {
resolve(window);
};
script.onerror = () => {
console.error("插件加载失败");
reject();
};
target.parentNode?.appendChild(script);
});
}
export default defineComponent({
name: "RemotePlugin",
emits: ["loaded"],
props: {
pluginData: {
type: Object,
default: () => {},
},
extraData: {
type: Object,
default: () => {},
},
},
data() {
return {
mode: "",
attrs: {},
};
},
mounted() {
const { url, libraryName } = this.pluginData;
this.loadScript({
url,
libraryName,
});
},
methods: {
/**
* 加载js
* @param url js文件地址
* @param libraryName 包名
* @return {Promise<void>}
*/
async loadScript(config) {
const { url, libraryName } = config;
// @ts-ignore
let cp = window[libraryName];
if (cp) {
// 异步组件支持
if (cp.AsyncPluginComp) cp = cp.AsyncPluginComp;
this.mode = markRaw(cp);
this.attrs = {
[cp.__scopeId]: "",
};
this.$emit("loaded");
return;
}
await asyncScript(url, libraryName);
// @ts-ignore
cp = window[libraryName];
// 异步组件支持
if (cp.AsyncPluginComp) cp = cp.AsyncPluginComp;
cp.inheritAttrs = true;
this.mode = markRaw(cp);
this.attrs = {
[cp.__scopeId]: "",
};
console.log(cp);
this.$emit("loaded");
},
},
});
</script>
插件的开发
插件严格来说就是一个bundle文件,所以在任何地方只要可以打包为能运行的bundle文件,都可以进行插件的开发。可以单独启动项目,也可以在一个项目中使用monorepo的方式进行多个包的维护。
我是参考backstage的方式通过yarn workspace来进行多包的维护。
插件的打包
插件的第三方依赖基本上都是基于主应用,插件是寄生于主应用中,所以插件在打包过程中,需要使用external的方式减小打包的体积,并且需要使用app.use注册的第三方组件,也是需要和主应用公用一个Vue对象的。
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import { name } from './package.json';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), cssInjectedByJsPlugin()],
define: {
'process.env': {}
},
build: {
lib: {
name: name,
entry: 'src/app.js',
formats: ['umd'],
fileName: 'index'
},
outDir: 'lib',
rollupOptions: {
external: ['vue', 'vue-router'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
}
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
esbuild: {
charset: 'ascii'
}
});
主应用的适配
由于插件需要使用external的方式过滤掉了Vue,VueRouter等代码,当插件import的时候默认是找不到对应的包的,这时候就需要使用sdk加载的方式让插件能够加载到对应的依赖文件
// 主应用打包
plugins:[
//...
viteExternalsPlugin({
vue: 'Vue',
'vue-router': 'VueRouter',
'vue-demi': 'VueDemi'
})
]
<!--index.html模板-->
<script src="https://unpkg.zhimg.com/vue@3.3.8/dist/vue.global.js"></script>
<script src="https://unpkg.zhimg.com/vue-router@4.3.0"></script>
参数的传递
主应用需要传递某些特殊参数的时候,可以使用props进行传递。甚至可以传递provider或者store来共享存储对象。 也可以通过事件的方式进行通信。
不足
插件一般不适合开发太过于复杂的页面,插件寄生于主应用,所以他不能拥有自己的路由,如果非要使用的话,也只能通过主应用的Router.addRoutes来注册,但是不太建议这么使用(这样会破坏主应用的路由结构)。
如果主应用是一个微前端下的子应用,sdk的加载会有问题,详情参见我踩过的坑