Welcome to wechat public account: Front Reading Room

Module Federation

motivation

Multiple independent builds can make up an application, and there should be no dependencies between these independent builds, so they can be developed and deployed separately.

This is often called a micro front end, but it’s not limited to that.

The underlying concept

We distinguish between local and remote modules. A local module is a normal module that is part of the current build. The remote module is not part of the current build and is loaded at run time from a so-called container.

Loading a remote module is considered an asynchronous operation. When using remote modules, these asynchronous operations are placed in the next chunk load operation between the remote module and the entry. If there is no chunk load operation, remote modules cannot be used.

Chunk loading operations are typically implemented by calling import(), but support is also available for things like require.ensure or require([…] ).

Containers are created by container portals that expose asynchronous access to specific modules. Exposed access is divided into two steps:

  1. Loading modules (asynchronous)
  2. Execution module (synchronous)

Step 1 is completed during chunk loading. Step 2 is completed during interleaved execution with other (local and remote) modules. In this way, the order of execution is not affected by module conversion from local to remote or from remote to local.

Containers can be nested, and containers can use modules from other containers. Containers can also loop dependencies between them.

Advanced concepts

Each build acts as a container, and other builds can also act as containers. In this way, each build can access modules exposed by other containers by loading modules from the corresponding container.

A shared module is a module that can be overridden or used as a module to provide overrides to nested containers. They typically point to the same modules in each build, such as the same libraries.

The packageName option allows you to find the desired version by setting the packageName. By default, it automatically inferences module requests, and when you want to disable automatic inferences, set requiredVersion to false.

Building blocks

ContainerPlugin (low level)

The plug-in uses the specified public module to create an additional container entry.

ContainerReferencePlugin (low level)

This plug-in adds specific references to containers that are external resources (externals) and allows remote modules to be imported from these containers. It also calls the Override API for those containers to provide an overload for them. Local overloading (through the __webpack_override__ or Override API when the build is also a container) and the specified overloading are provided to all referenced containers.

ModuleFederationPlugin (high level)

The ModuleFederationPlugin combines the ContainerPlugin and ContainerReferencePlugin.

Concept of the target

  • It can both expose and use any module type supported by WebPack
  • Block loading should load everything needed in parallel (Web: single round trip to the server)
  • Control from consumer to container
    • Rewriting a module is a one-way operation
    • Sibling containers cannot override each other’s modules.
  • Concepts apply independently of the environment
    • It can be used on web, Node.js, etc
  • Relative and absolute requests in sharing
    • Will always be available, even when not in use
    • The relative path is resolved to config.context
    • RequiredVersion is not used by default
  • Module request in share
    • Available only when in use
    • All equal module requests used in the build are matched
    • All matching modules will be provided
    • RequiredVersion will be extracted from package.json at this location in the figure
    • When you have nested node_modules, you can provide and use multiple different versions
  • Module requests with a slash at the end of the share will match all module requests with this prefix

Use cases

Each page is built separately

Each page of a single-page application is exposed from the container in a separate build. The main application shell is also built independently and references all pages as remote modules. In this way, each page can be deployed individually. Deploy the principal application when a route is updated or a new route is added. The principal application defines common libraries as shared modules to avoid duplication in page builds.

Use component libraries as containers

Many applications share a common component library that can be built into a container that exposes all components. Each application uses components from the component library container. Changes to component libraries can be deployed individually without the need to redeploy all applications. Applications automatically use the latest version of the component library.

Dynamic remote container

The container interface supports get and init methods. Init is an async-compliant method that takes only one argument: a shared scope object. This object is used as a shared scope in the remote container and is populated by a module provided by host. You can use it to dynamically connect a remote container to a host container at run time.

init.js

(async() = > {// Initialize the shared scope to fill it with supplied modules that know about this build and all remotes
  await __webpack_init_sharing__('default');
  const container = window.someContainer; // Or get containers from elsewhere
  // Initialize the container, which may provide shared modules
  await container.init(__webpack_share_scopes__.default);
  const module = await container.get('./module'); }) ();Copy the code

The container attempts to provide a shared module, but if the shared module is already in use, a warning is issued and the provided shared module is ignored. The container can still use it as a degrade module.

You can implement A/B testing by dynamically loading different versions of A shared module.

Tip

Make sure the container is loaded before attempting to connect the remote container dynamically.

Example:

init.js

function loadComponent(scope, module) {
  return async() = > {// Initialize the shared scope to fill it with supplied modules that know about this build and all remotes
    await __webpack_init_sharing__('default');
    const container = window[scope]; // Or get containers from elsewhere
    // Initialize the container, which 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');
Copy the code

Dynamic Remote based on Promise

In general, remote is configured using a URL as shown in the following example:

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host'.remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js',},}),],};Copy the code

But you can also pass a promise to remote that will be invoked at run time. You should call this promise with any module that matches the GET /init interface described above. For example, if you want to pass which version of the federated module you should use, you can do the following with a query parameter:

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host'.remotes: {
        app1: `promise new Promise(resolve => { const urlParams = new URLSearchParams(window.location.search) const version = urlParams.get('app1VersionParam') // This part depends on how you plan on hosting and versioning your federated modules const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js' const script = document.createElement('script') script.src = remoteUrlWithVersion script.onload = () => { // the injected script has loaded and is available on window // we can now resolve this Promise const proxy = { get: (request) => window.app1.get(request), init: (arg) => { try { return window.app1.init(arg) } catch(e) { console.log('remote container already initialized') } } } resolve(proxy) } // inject this script with the src set to the versioned remoteEntry.js document.head.appendChild(script); }) `,},// ...})]};Copy the code

Note that when using this API, you must resolve an object that contains the GET /init API.

A dynamic Public Path

Provide a host API to set the publicPath

You can allow host to set the publicPath of a remote module at run time by exposing the remote module.

This is especially useful when you mount independently deployed child applications on subpaths in the host domain.

Scene:

You have a host app at my-host.com/app/* and a subapp at foo-app.com. Child applications are also mounted on the host domain, so, foo-app.com is expected to be accessible via my-host.com/app/foo-app and my-host.com/app/foo/* requests are redirected to foo-app.com/* via a proxy.

Example:

webpack.config.js (remote)

module.exports = {
  entry: {
    remote: './public-path',},plugins: [
    new ModuleFederationPlugin({
      name: 'remote'.// The name must match the entry name
      exposes: ['./public-path'].// ...})]};Copy the code

public-path.js (remote)

export function set(value) {
  __webpack_public_path__ = value;
}
Copy the code

src/index.js (host)

const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');

//boostrap app e.g. import('./boostrap.js')
Copy the code

Infer publicPath from script

One could infer the publicPath from the script tag from document.currentScript.src and set it with the __webpack_public_path__ module variable at runtime.

Example:

webpack.config.js (remote)

module.exports = {
  entry: {
    remote: './setup-public-path',},plugins: [
    new ModuleFederationPlugin({
      name: 'remote'.// The name must match the entry name
      // ...})]};Copy the code

setup-public-path.js (remote)

// Use your own logical derivation of publicPath and set it using the __webpack_public_path__ API
__webpack_public_path__ = document.currentScript.src + '/.. / ';
Copy the code

Tip

The output.publicPath configuration item can also be set to’ auto’, which will automatically determine a publicPath for you.

troubleshooting

Uncaught Error: Shared module is not available for eager consumption

The application is eagerly executing an application running as a global host. The following options are available:

You can set dependencies to immediate dependencies in the advanced API for module federation, which does not place modules in asynchronous chunks, but instead provides them synchronously. This allows us to use these shared modules directly in the initial block. Note, however, that since all provided and degraded modules are downloaded asynchronously, it is recommended that you only provide it somewhere in your application, such as the shell.

We strongly recommend using asynchronous boundaries. It will break up the initialization code into larger chunks to avoid any additional overhead to improve overall performance.

For example, your entrance might look like this:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />.document.getElementById('root'));
Copy the code

Let’s create the bootstrap.js file and put the contents of the entry file into it. Then we will import bootstrap into the entry file:

index.js

+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />.document.getElementById('root'));
Copy the code

bootstrap.js

+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />.document.getElementById('root'));
Copy the code

This method is effective, but has limitations or disadvantages.

Set the dependent eager property to true via the ModuleFederationPlugin

webpack.config.js

// ...
new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      eager: true,}}});Copy the code

Uncaught Error: Module “./Button” does not exist in container.

The error might not show “./Button”, but the information looks similar. This problem usually occurs when upgrading webpack Beta.16 to Webpack beta.17.

In the ModuleFederationPlugin, change the object reception:

new ModuleFederationPlugin({
  exposes: {-'Button': './src/Button'
+   './Button':'./src/Button'}});Copy the code

Uncaught TypeError: fn is not a function

The error here may be that the remote container is missing, make sure to add it before using it. If you have loaded a container for a container that is trying to use a remote server and you still see this error, add the remote container file for the host container to the HTML as well.

Conflicts between modules from multiple remote

If you want to load multiple modules from different remote, it is recommended to set output.uniquename for your remote build to avoid conflicts between multiple Webpack runtimes.

Welcome to wechat public account: Front Reading Room