使用remote-plugin方式,分离并加载项目

289 阅读3分钟

需求背景

当设计到前端接入多团队业务的情况,目前大家大多使用微前端的方式,通过主应用对接多个子应用的方式进行接入。

但是这种微前端接入需要子应用单独部署,一般还要单独去启动一个项目,整体来说太重。

如果我们的需求颗粒度很小,只是一些通用的业务模块,或者在多个项目中都有的模块,是否可以考虑使用插件的形式插入项目,这样复用性和复杂度相对于微前端的方式,都会更加简洁,高效。

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的加载会有问题,详情参见我踩过的坑