webpack module federation 在老旧项目的使用

745 阅读3分钟

背景

公司的项目架构是传统的jsp,html代码便嵌套在里面。短期内没有重构的计划,但中长期还是有可能的。 新功能的新增,是继续采用jsp的形式开发还是寻求其他方式,令人纠结。

恰好此时,微前端、webpack5 module federation也逐渐被推广出来。了解了module federation之后,我开始思考:能否利用webpack5的这个新特点,来解决我的困境呢?

module federation

官网 mf的一些特点,让我开始有点兴奋

特点

  1. 支持导出组件
  2. 支持引入其他项目导出的组件
  3. 可以与使用者公用基础库,如Vue、React等
  4. 可以指定基础库的版本
  5. 按需加载
  6. 引入组件已编译,故可以提高项目的编译速度
  7. ……

我关注的特点

  1. 支持项目导出组件供其他项目使用
  2. 导出的组件,是挂在window下的,可以通过window[container].get([module])获取相应的组件

想法

只在jsp项目,引入组件,且执行组件的渲染,具体的业务逻辑代码放在一个单独的,类似“基站”的项目。

这样既可以在当下的项目架构下继续开发,后期重构,也无需对这部分功能进行大改造。

基站跟项目的关系大概如下图:

image.png

界面构成如下图:

image.png

动手

材料

  1. (提供组件)一个vue项目
  2. (使用组件)一个jsp项目/一个简单html项目

提供组件的vue项目

采用@efox/emp脚手架搭建,导出了两个组件: table button 需要注意的是,因为使用组件者是没有webpack的项目,vue项目没法通过shared与之共享组件。于是通过cdn的方式引入vue和element-plus,webpack打包的组件,会在window下引入相应的库。

vue项目的emp-config.js

const withVue3 = require('@efox/emp-vue3')
const path = require('path')
const ProjectRootPath = path.resolve('./')
const { getConfig } = require(path.join(ProjectRootPath, './src/config'))
module.exports = withVue3(({ config, env, empEnv }) => {
  const confEnv = env === 'production' ? 'prod' : 'dev'
  const conf = getConfig(empEnv || confEnv)
  const port = conf.port
  const publicPath = conf.publicPath
  // 设置项目URL
  config.output.publicPath(publicPath)
  config.externals({
    'vue': 'Vue',
    'element-plus':'ElementPlus'
  })
  // 设置项目端口
  config.devServer.port(port)
  
  config.plugin('mf').tap(args => {
    console.info(args)
    args[0] = {
      ...args[0],
        name: "wComponent",
        // 被远程引入的文件名
        filename: 'table.js',
        exposes:{
          './table':'./src/components/Layout.vue', 
          './button':'./src/components/Button.vue'
        }
    }
    return args
  })
  // 配置 index.html
  config.plugin('html').tap(args => {
    args[0] = {
      ...args[0],
      ...{
        // head 的 title
        title: 'jsp-provider',
        // 远程调用项目的文件链接
        files: {
          js: ['https://unpkg.com/vue@3.0.7/dist/vue.global.js','https://unpkg.com/element-plus@1.0.2-beta.35/lib/index.full.js'],
          css: ['https://unpkg.com/element-plus/lib/theme-chalk/index.css']
        },
      },
    }
    return args
  })
})

使用组件者index.html

在这里,只需引入依赖的vue、element-plus和上面项目提供的组件入口http://localhost:8066/table.js,便可使用组件了。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" />
  <!-- EMP inject title -->
  <title>jsp-provider</title>
  <!-- EMP inject css -->
  
  <link rel="stylesheet" href="https://unpkg.com/element-plus/lib/theme-chalk/index.css" />
  <!-- EMP inject js -->
  
  <script src="https://unpkg.com/vue@3.0.7/dist/vue.global.js"></script>
  <script src="https://unpkg.com/element-plus@1.0.2-beta.35/lib/index.full.js"></script>
  <link rel="icon" href="http://localhost:8066/favicon.ico">
  <script src="http://localhost:8066/table.js"></script>
</head>

<body>
  <h3>这里是表格</h3>
  <div id="emp-root"></div>

  <h3>这里是按钮</h3>
  <div id="button"></div>
</body>
<script defer>
  window.wComponent.get('./table').then((factory)=>{
    var module = factory();
    var app = Vue.createApp(module.default);
    app.use(ElementPlus);
    app.mount('#emp-root');
  })

  window.wComponent.get('./button').then((factory)=>{
    var module = factory();
    var app = Vue.createApp(module.default);
    app.use(ElementPlus);
    app.mount('#button');
  })
</script>
</html>

分析

加载外部组件的形式

官方有一个Example给了我启发:

function loadComponent(scope, module) {
  return async () => {
    // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}

loadComponent('abtests', 'test123');

源码中下面这个代码,是直接在window取被共享的组件容器的。那么,非webpack的项目,也可以通过同样的方式拿到。

const container = window[scope]; // or get the container somewhere else

导出的组件源码

查看导出的文件,容器wComponent同过var定义的。

image.png

容器会暴露出两个方法

image.png

其中get方法可以通过传入模块的名字,获取到一个promise对象

image.png

在promise对象中,返回了一个factory,通过执行,得到相应的组件。 下面这个就是我们在Vue项目中导出的table组件

image.png

总结&实现

如愿拿到导出的组件,那接下来,就按照Vue组件如何渲染到DOM上实现就可以了。

window.wComponent.get('./table').then((factory)=>{
    var module = factory();
    var app = Vue.createApp(module.default);
    app.use(ElementPlus);
    app.mount('#emp-root');
  }

推荐文章

mp.weixin.qq.com/s/b5Gl_1yX1…

indepth.dev/posts/1173/…

zhuanlan.zhihu.com/p/120462530