DEV Community

Cover image for webpack4 module-federation
zhangHongEn
zhangHongEn

Posted on • Edited on

webpack4 module-federation

module-federation It is a new feature of webpack5 update, and it is easier to share front-end code, This article will introduce module-federation/webpack-4 ealization principle and its difference from webpack5

module-federation/webpack-4 ealization principle

Briefly explain the implementation principle, how do webpack4 and webpack5 achieve interoperability? There are three key points

  • usemf(Use the sdk output by webpack5 build to simulate a webpack5 environment to load module-federation in a non-webpack5 environment)
  • Follow the loading process of module-federation(1. init all remote container 2. merge shareScopes 3. Restore the sharing rules of webpack5-share); output 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
              }
          }
      }
    }
    
    
  • Finally, a capability that webpack4 lacks, enabling jsonp-chunk to support waiting for dependencies (remote modules) to load

Implement the above process through plug-ins
Enlarge picture
Image description

  1. Add a new entry to implement the loading process of module-federation and output the container
  2. Intercept the module loading of remotes, no longer load local modules directly, but use remote modules
  3. Intercept shared module loading, no longer load local modules directly, but use remote modules
  4. Shared requests are intercepted, but the shared bundle still needs to be output, and the function merge shareScopes will be loaded

It introduces the two red parts in the figure, how to change the webpack4 loading process to support loading remote modules

  • Intercept imports, reserve dependency tags
    1. Set alias, transfer remotes to a non-existent url (no existence can be intercepted in the second step)
    2. Forward remotes to specific loader in "compiler.resolverFactory.plugin("resolver normal") --> resolver.hooks.resolve.tapAsync" hook
    3. Leave a string in the loader to mark that the current module depends on the remote module, get and export the value of the remote module
    4. The "compilation.mainTemplate.hooks.jsonpScriptchunk" hook makes the jsonp chunk wait for the remote module to load before executing

Source code analysis

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

// module-federation/webpack-4/lib/plugin.js
apply(compiler) {
    // 1. Generate unique jsonpFunction global variables to prevent conflicts
    compiler.options.output.jsonpFunction = `mfrename_webpackJsonp__${this.options.name}`
    // 2. Generate 4 dummy modules for spare
    this.genVirtualModule(compiler)
    // 3. Initialize the remote module mapping relationship in entry chunks
    // 4. Load all container initialization dependency collections (shareScopes) in entry chunks
    this.watchEntryRecord(compiler)
    this.addLoader(compiler)
    // 5. Generate mf entry file (usually remoteEntry.js)
    this.addEntry(compiler)
    this.genRemoteEntry(compiler)
    // 6. Intercept webpack compilation of remotes and shared modules
    this.convertRemotes(compiler)
    this.interceptImport(compiler)
    // 7. Make webpack jsonp chunk wait for remote dependencies to load
    this.patchJsonpChunk(compiler)
}
Enter fullscreen mode Exit fullscreen mode

1. Generate unique jsonpFunction global variables to prevent conflicts

compiler.options.output.jsonpFunction = `mfrename_webpackJsonp__${this.options.name}`
Enter fullscreen mode Exit fullscreen mode

2. Generate 4 dummy modules for spare

Just register these 4 file code modules as webpack virtual modules, which can be introduced and used by subsequent processes

3. Initialize the remote module mapping relationship in entry chunks

4. Load all container initialization dependency collections

Initialize all containers (other mf modules), and export the loading process as a promise to mark the completion of the initialization phase (all jsonp chunks need to wait for the initialization phase to complete)

module-federation/webpack-4/lib/virtualModule/exposes.js

Image description

5. Generate mf entry file (usually remoteEntry.js)

// 1. Add mf entry using singleEntry
new SingleEntryPlugin(compiler.options.context, virtualExposesPath, "remoteEntry").apply(compiler)

// 2. Copy the last file generated by the remoteEntry entry and rename it
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]
      }
    })
})
Enter fullscreen mode Exit fullscreen mode
  1. output container api(module-federation/webpack-4/lib/virtualModule/exposes.js)
`
  /* eslint-disable */
  ...
  const {setInitShared} = require("${virtualSetSharedPath}")

  // All exposed modules are preset here with dynamic-import
  const exposes = {
    [moduleName]: async () {}
  }

  // 1. Register the container globally in a global-like manner
  module.exports = window["${options.name}"] = {
    async get(moduleName) {
      // 2. Use code splitting to expose exported modules
      const module = await exposes[moduleName]()
      return function() {
        return module
      }
    },
    async init(shared) {
      // 4. Merge shares and wait for the init phase to complete
      setInitShared(shared)
      await window["__mfplugin__${options.name}"].initSharedPromise
      return 1
    }
  }

  `
Enter fullscreen mode Exit fullscreen mode

6. Intercept webpack compilation of remotes and shared modules

  1. Set aliases for remotes and shared modules, identify special paths, and forward them to a non-existing file path(Only non-existing file paths can be intercepted by resolver hooks and forwarded)(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}`
    })
Enter fullscreen mode Exit fullscreen mode
  1. Intercept remotes, shared aliases, and forward to import-wpm-loader.js to generate code for requesting remote resources(module-federation/webpack-4/lib/plugin.js)
compiler.resolverFactory.plugin('resolver normal', resolver => {
  resolver.hooks.resolve.tapAsync(pluginName, (request, resolveContext, cb) => {
      if (is an alias from remotes, shared) {
          // Forward to import-wpm-loader with pkgName parameter
          cb(null, {
            path: emptyJs,
            request: "",
            query: `?${query.replace('?', "&")}&wpm&type=wpmPkg&mfName=${this.options.name}&pkgName=${encodeURIComponent(pkgName + query)}`,
          })
      } else {
          // request native module
          cb()
      }
  });
});
Enter fullscreen mode Exit fullscreen mode
  1. Generate code to request remote resources(module-federation/webpack-4/lib/import-wpm-loader.js)
module.exports = function() {
    `
    /* eslint-disable */
    if (window.__wpm__importWpmLoader__garbage) {
      // 1. Leave a code mark to identify the dependent remote module, which is used to make the chunk wait for the remote dependency to load
      window.__wpm__importWpmLoader__garbage = "__wpm__importWpmLoader__wpmPackagesTag${pkgName}__wpm__importWpmLoader__wpmPackagesTag";
    }
    // 2. When entering the code of this module, the remote module has been loaded, you can use get to get the synchronization value of the module, and return
    module.exports = window["__mfplugin__${mfName}"].get("${decodeURIComponent(pkgName)}")
    `
}
Enter fullscreen mode Exit fullscreen mode

7. Make webpack jsonp chunk wait for remote dependencies to load

  1. Use regular matching to the remote module that the jsonp chunk depends on, so that the chunk waits for the dependency to load
  2. Make webpack jsonp load function support jsonp waiting to load dependencies(module-federation/webpack-4/lib/plugin.js)

Differences with webpack5

module-federation/webpack-4The plugin has implemented the main capabilities of module-federation, and can be found in webpack4 and webpack5 refer to each other , The following describes which parameters are not supported by the plugin

unsupported parameter

options.library

The priority of this parameter is not very high, the implementation in webpack4 is more complicated, and there are still problems in using it in webpack5, see for details "https://github.com/webpack/webpack/issues/16236" , Therefore, the implementation in webpack4 is similar to setting library.type = "global"

options.remotes.xxx.shareScope

The same mf container can only be initialized with one shareScope. If inconsistent webpack is set by using shareScope multiple times, it will report an error, and the shareScope can be set too much, which is confusing. Even in pure webpack5, the performance is unpredictable. It is recommended to use options. shared.xxx.shareScope, options.shareScope alternative

module-federation ecological package

The webpack-4 plugin has not yet integrated the ability of webpack-5 related packages(ssr、typescript、hmr、dashboard...), However, 4 and 5 interoperability has been achieved, which can help you to use webpack5 to implement new projects without refactoring existing projects.

Supported parameters

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

Top comments (0)