webpack4 module federation

module-federation是webpack5更新的一项新特性, 可以更容易的共享前端代码, 本文将介绍@module-federation/webpack-4实现原理及其与webpack5的差异

背景

在公司内我们搭建了微前端包管理平台, 由于有大量webpack4的项目, 我们使用umd规范来共享资源, 也产出了和mf同等作用的插件import-http-webpack-plugin, 但是精力有限我们不打算在umd规范下建立微前端生态, 转而投入到有同样作用的mf, 借助其已有的各领域能力来继续搭建微前端生态。

现阶段mf的优势:

  1. webpack5内置插件
  2. webpack4(webpack4插件)、rollup/vite(vite插件)环境也有生态支持
  3. 非编译环境(usemf)
  4. 各领域相关能力均有提供(ssr、typescript、hmr、dashboard等)

module-federation/webpack-4实现原理

简单的解释下实现原理, webpack4和webpack5是怎么实现互通的呢? 有三个关键点

  • usemf(使用webpack5 build输出的sdk, 用于在非webpack5环境模拟一个webpack5环境来加载module-federation)
  • 遵循module-federation的加载流程(1. init all remote container 2. merge shareScopes 3. 还原webpack5-share的共享规则); 输出module-federation-container
    // container
    { 
        async init(shareScope){}, 
        get(name){ return async factory() } 
    }
    
    // shareScopes example
    {
      [default]: {
          [react]: {
              [18.0.2]: {
                  get() {
                    return async function factory() {
                        return module
                    }
                  },
                  ...other
              },
              [17.0.2]: {
                  get() {
                    return async function factory() {
                        return module
                    }
                  },
                  ...other
              }
          }
      }
    }
    
    
  • 最后是webpack4所欠缺的一项能力, 使jsonp-chunk支持等待依赖(远程模块)加载

通过插件实现上述流程(图示)

webpack4 module federation_第1张图片

  1. 增加一个新入口, 用来实现module-federation的加载流程, 并输出container
  2. 拦截remotes的模块加载, 不再直接加载本地模块, 而是使用远程模块
  3. 拦截shared的模块加载, 不再直接加载本地模块, 而是使用远程模块
  4. shared的请求都被拦截, 但仍需要输出shared bundle, 并将加载函数merge shareScopes

其中介绍图中两处红色部分, 如何改变webpack4加载流程使其支持加载远程模块

  • 拦截import, 预留依赖标记
    1. 设置alias, 将remotes转至一个不存在的url(不存在才可在第二步拦截)
    2. 在compiler.resolverFactory.plugin(“resolver normal”) --> resolver.hooks.resolve.tapAsync钩子将remotes转发至特定loader
    3. 在loader留下字符串标记当前module依赖远程模块, 获取并导出远程模块的值
  • jsonp chunk等待远程依赖加载
    1. 在compilation.mainTemplate.hooks.jsonpScriptchunk钩子使jsonp chunk等待远程模块加载完成后再执行

源码解析

https://github.com/module-federation/webpack-4

// module-federation/webpack-4/lib/plugin.js
apply(compiler) {
    // 1. 生成唯一的jsonpFunction全局变量防止冲突
    compiler.options.output.jsonpFunction = `mfrename_webpackJsonp__${this.options.name}`
    // 2. 生成4个虚拟模块备用
    this.genVirtualModule(compiler)
    // 3. 在entry chunks初始化远程模块映射关系
    // 4. 在entry chunks加载所有的container初始化依赖集合(shareScopes)
    this.watchEntryRecord(compiler)
    this.addLoader(compiler)
    // 5. 生成mf的入口文件(一般是remoteEntry.js)
    this.addEntry(compiler)
    this.genRemoteEntry(compiler)
    // 6. 拦截remotes、shared模块的webpack编译
    this.convertRemotes(compiler)
    this.interceptImport(compiler)
    // 7. 使webpack jsonp chunk等待远程依赖加载
    this.patchJsonpChunk(compiler)
    this.systemParse(compiler)
}

1. 生成唯一的jsonpFunction全局变量防止冲突

compiler.options.output.jsonpFunction = `mfrename_webpackJsonp__${this.options.name}`

2.生成4个虚拟模块备用

只是将这4个文件代码模块作为webpack虚拟模块来注册, 可被后续流程引入使用

3.在entry chunks初始化远程模块映射关系

4. 在entry chunks加载所有的container初始化依赖集合

初始化所有container(其他mf模块), 并将加载过程以promise形式导出, 以标识初始化阶段的完成(所有的jsonp chunk需要等待初始化阶段完成)

module-federation/webpack-4/lib/virtualModule/exposes.js
webpack4 module federation_第2张图片

5. 输出mf的入口文件(一般是remoteEntry.js)

  1. 生成入口文件(module-federation/webpack-4/lib/plugin.js)
// 1. 使用singleEntry添加mf入口
new SingleEntryPlugin(compiler.options.context, virtualExposesPath, "remoteEntry").apply(compiler)

// 2. 复制remoteEntry入口最后生成的文件, 并重命名
entryChunks.forEach(chunk => {
    this.eachJsFiles(chunk, (file) => {
      if (file.indexOf("$_mfplugin_remoteEntry.js") > -1) {
        compilation.assets[file.replace("$_mfplugin_remoteEntry.js", this.options.filename)] = compilation.assets[file]
        // delete compilation.assets[file]
      }
    })
})
  1. 暴露container api(module-federation/webpack-4/lib/virtualModule/exposes.js)
`
  /* eslint-disable */
  ...
  const {setInitShared} = require("${virtualSetSharedPath}")
  
  // 此处使用dynamic-import预设了所有exposes module
  const exposes = {
    [moduleName]: async () {}
  }
  
  // 1. 在全局以类似global的方式注册container
  module.exports = window["${options.name}"] = {
    async get(moduleName) {
      // 2. 使用代码分割来暴露导出的模块
      const module = await exposes[moduleName]()
      return function() {
        return module
      }
    },
    // 此处是某个scope之内的shared
    async init(shared) {
      // 4. 合并share、等待init阶段完成
      setInitShared(shared)
      await window["__mfplugin__${options.name}"].initSharedPromise
      return 1
    }
  }
  
  `

6. 拦截remotes、shared模块的webpack编译

  1. 将remotes、shared的模块设置别名, 标识特殊路径, 转发到一个不存在的文件路径(只有不存在的文件路径可以被resolver钩子拦截并继续转发)(module-federation/webpack-4/lib/virtualModule/plugin.js)
const { remotes, shared } = this.options
    Object.keys(remotes).forEach(key => {
      compiler.options.resolve.alias[key] = `wpmjs/$/${key}`
      compiler.options.resolve.alias[`${key}$`] = `wpmjs/$/${key}`
    })
    Object.keys(shared).forEach(key => {
      // 不存在的文件才能拦截
      compiler.options.resolve.alias[key] = `wpmjs/$/mfshare:${key}`
      compiler.options.resolve.alias[`${key}$`] = `wpmjs/$/mfshare:${key}`
    })
  1. 拦截remotes、shared的别名, 转发到import-wpm-loader.js生成请求远程资源的代码(module-federation/webpack-4/lib/plugin.js)
compiler.resolverFactory.plugin('resolver normal', resolver => {
  resolver.hooks.resolve.tapAsync(pluginName, (request, resolveContext, cb) => {
      if (是来自remotes、shared的别名) {
          // 携带pkgName参数转发至import-wpm-loader
          cb(null, {
            path: emptyJs,
            request: "",
            query: `?${query.replace('?', "&")}&wpm&type=wpmPkg&mfName=${this.options.name}&pkgName=${encodeURIComponent(pkgName + query)}`,
          })
      } else {
          // 请求本地模块
          cb()
      }
  });
});
  1. 生成请求远程资源的代码(module-federation/webpack-4/lib/import-wpm-loader.js), 2处关键代码
module.exports = function() {
    `
    /* eslint-disable */
    if (window.__wpm__importWpmLoader__garbage) {
      // 1. 留下代码标识, 标识依赖的远程模块, 用于让chunk等待远程依赖加载
      window.__wpm__importWpmLoader__garbage = "__wpm__importWpmLoader__wpmPackagesTag${pkgName}__wpm__importWpmLoader__wpmPackagesTag";
    }
    // 2. 进入此模块代码时, 远程模块已经加载完毕, 可以使用get获取模块的同步值, 并返回
    module.exports = window["__mfplugin__${mfName}"].get("${decodeURIComponent(pkgName)}")
    `
}

7. 使webpack jsonp chunk等待远程依赖加载

  1. 使用正则匹配到jsonp chunk依赖的远程模块, 使chunk等待依赖加载
  2. 使webpack jsonp加载函数支持jsonp等待加载依赖(module-federation/webpack-4/lib/plugin.js)

与webpack5的差异

@module-federation/webpack-4插件已经实现了module-federation的主要能力, 并且可以在webpack4和webpack5互相引用 , 下面说明下哪些参数是插件是未支持的

不支持的参数

options.library

此参数优先级不是很高, 在webpack4种实现较为复杂, 在webpack5中使用也仍有问题, 详见https://github.com/webpack/webpack/issues/16236 , 故在webpack4中的实现类似于设置了library.type = “global”

options.remotes.xxx.shareScope

同一个mf container只可以用一个shareScope初始化, 如果被多次使用shareScope设置的不一致webpack会报错, 并且shareScope可设置处过多比较混乱, 即使在纯webpack5中使用表现也不可预估, 建议使用options.shared.xxx.shareScope、options.shareScope替代

module-federation生态包

webpack-4插件暂未集成webpack-5相关包的能力(ssr、typescript、hmr、dashboard等), 但已实现4、5互通, 可以助您可以放心的使用webpack5实现新项目, 而无需重构已有项目

已支持的参数

  • options.remotes
  • options.name
  • options.shareScope
  • options.shared
  • options.exposes

你可能感兴趣的:(webpack,前端,1024程序员节)