模块联邦-Module Federation的实践

772 阅读3分钟

前言

工作中,常常会有这样的需求,项目A与项目B是两个完全独立的项目工程,项目A需要在项目B中运行,例如,网页中的在线客服,或者其中一个项目的部分代码是可复用的,或者是可以继承的,那webpack 5中的模块联邦Module Federation就可以帮助我们达到这样的目的~

什么是 Module Federation?

官网上首先引出了“容器”的概念,那什么是容器呢,容器就是独立部署的应用,每个应用都可以是看作是提供远程加载模块的容器入口。模块加载分为本地模块与远程模块加载,本地模块是当前构建的一部分,远程模块不属于当前构建,在运行时从容器中加载。

容器之间是可以互相嵌套的,容器可以使用来自其他容器的模块。容器之间也可以循环依赖

从这句话可以理解,容器不单单是两个项目之间的依赖,其实是可以看作有一个“容器群”,容器之间可以共享资源、共享依赖。

u=1134423912,2578633351&fm=253&fmt=auto&app=120&f=JPEG.webp

项目实践

在我们的项目实践中,使用lerna管理多包应用,packages目录下的每个应用互相独立,子应用的根目录下都有webpack.config.js配置。

那让我们来整体看一下,如何配置Module Federation?

// webpack.config.js文件
const {ModuleFederationPlugin} = require("webpack").container;

const name = 'GCPWebFrameworkBill'
// mode是自定义的环境变量 
const { version } = require('./package.json')
const port = 9091
const cdnBaseUrl = 'https://cdn.developer.xxxx.com'
const host = mode === 'development' ? `http://localhost:${port}` : cdnBaseUrl
const libVersion = mode === 'development' ? 'latest' : version
const publicPath = `${host}/gcp/web-framework-bill/${libVersion}/`

module.exports = {
  entry: "./src/index",
  mode: "development",
  devServer: {
    static: path.join(__dirname, "dist"),
    port,
  },
  output: {
    publicPath,
  },
  plugins: [
    new ModuleFederationPlugin({
      name, // 容器名称-这里是'GCPWebFrameworkBill'
      exposes: {
        './startPage': './lib/app/startPage.js',
        './BaseWebPage': './lib/app/baseWebPage.js',
        './cores/CommonPage': './lib/cores/page.js',
        './cores/CommonModule': './lib/cores/module.js'
      },
      remotes: { // 远程容器
        @gcbp/web-framework: `GCBPWebFramework@${cdnBaseUrl}/gcp/web-framework/2.1.0/gcp.js`,
      },
      shared: { // 共享组件库
        vue: `Vue@${cdnBaseUrl}/vendors/vue/2.6.14/vue.min.js`,
        vuex: `Vuex@${cdnBaseUrl}/vendors/vuex/3.6.2/vuex.min.js`,
        '@geip/basic-components': [
          `GCPDesignPro@${cdnBaseUrl}/gcp/gcp-design-pro/latest/index.js`,
          `${cdnBaseUrl}/gcp/gcp-design-pro/latest/theme-default/index.css`
        ],
        '@gcbp/gcp-forms': [
          `GCPForms@${cdnBaseUrl}/gcp/gcp-forms/latest/umd/index.js`,
          `${cdnBaseUrl}/gcp/gcp-forms/latest/umd/index.css`
        ]
      },
    })
  ],
};

上面的配置说明:

  1. 插件ModuleFederationPlugin接收一个对象,里面需要关注的是nameexposessharedremotes属性配置,name是独一无二的容器名称,exposes是对外暴露的资源文件,shared是共享组件库、框架库,remotes是引用的远程仓库。
  2. exposes接收一组资源映射,注意写法,官方强调是'./startPage': './lib/app/startPage.js',如果写成'/startPage',会报这样的错 Uncaught Error: Module "./startPage" does not exist in container。这里提供官方的故障排除指南
  3. remotes配置需要依赖的远程仓库,项目部署到cdn上,注意publicPath,相应的,资源更新后会重新在Jenkins上部署最新版本。
  4. shared配置共享组件库,不同于npm包,这些资源库不用放在package.json文件里,也就不会下载到本地目录的node_modules,它们会在运行时,会被优先下载依赖。

那在项目中使用import()异步加载暴露出来的模块

// await放在页面顶层,会等待异步资源加载完成,代码才会继续执行
const { default: BasePage } = await import('@gcbp/web-framework/BasePage')

总结

本文偏向对Module Federation的落地实践,对运行时本地模块资源与远程模块加载资源的顺序等、以及工作原理没有太多篇幅。可以参考其他博主的深入分析如 Shenfq的Webpack5 跨应用代码共享 - Module Federation

参考

- Webpack.js Module Federation Example

-官网 Module Federation