Original text: webpack.docschina.org/concepts/mo…

motivation

Multiple individual builds should form one application. These independent builds should not depend on each other, so they can be developed and deployed separately.

This is often referred to as a microfront end, but is not limited to that.

Low-level concept

We distinguish between local and remote modules. Local modules are normal modules that are part of the current build. A remote module is a module that 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 a remote module, these asynchronous operations are placed in the next block load operation between the remote module and the entry point. Without a block load operation, it is not possible to use a remote module.

Block loading operations are typically import () calls, but older constructs like require.ensure or require ([…] ) is also supported.

A container is created through a container entry that exposes asynchronous access to a particular module. Accessing the exposed channel is divided into two steps:

  • Loading modules (asynchronous)
  • Execute (synchronize).

Step 1 is done during block loading. Step 2 is completed during interactions with other (local and remote) modules. This way, the order of evaluation is not affected by the conversion from a local module to a remote module or otherwise.

You can nest a container. Containers can use modules from other containers. Cyclic dependencies between containers are also possible.

rewrite

The container can mark selected local modules as overwritable. Consumers of containers can provide “overrides,” which are modules that replace one of the container’s rewritable modules. When a user provides a local module, all modules in the container use replacement modules instead of local modules. When the consumer does not provide replacement modules, all modules of the container use local modules.

The container manages rewritable modules in such a way that they do not need to be downloaded when they are overwritten by consumers. This is usually done by breaking them up into different chunks.

The provider of the replacement module, on the other hand, only provides asynchronous loading capabilities. It allows the container to load replacement modules only when needed. When the container does not request a replacement module, the provider manages the replacement module in such a way that no download is required at all. This is usually done by breaking them up into different chunks.

“Name” is used to identify the module in the container that can be overridden.

Overrides are provided in a manner similar to how containers expose modules, in two steps:

  • Loading modules (asynchronous)
  • Execute (synchronize).

When nesting is used, providing an override for a container automatically overwrites modules with the same “name” in the nested container.

Container rewriting must be provided before the module is loaded. Overrides used in the initial block can only be overridden by synchronous modules that do not use Promises. Once executed, the rewritable item is no longer rewritable.

Advanced concepts

Each generation acts as a container and the others as containers. In this way, each build can access it by loading any other exposed modules from its container.

Shared modules are modules that can be overridden and provided as overrides of nested containers. They typically point to the same module in each build, such as the same library.

The packageName option allows you to set the packageName to find the desired version. Automatic inference is requested for modules by default, and requiredVersion is set to false if automatic inference should be disabled.

Building blocks

OverridablesPlugin(low level)

This plug-in makes specific modules “rewritable”. The local API (webpack_override) allows you to provide overrides.

webpack.config.js

const OverridablesPlugin = require('webpack/lib/container/OverridablesPlugin');
module.exports = {
  plugins: [
    new OverridablesPlugin([
      {
        // we define an overridable module with OverridablesPlugin
        test1: './src/test1.js',
      },
    ]),
  ],
};
Copy the code

src/index.js

__webpack_override__({
  // here we override test1 module
  test1: () => 'I will override test1 module under src',
});
Copy the code

ContainerPlugin(low level)

This plug-in creates an additional container entry using the specified public module. It also uses the OverridablesPlugin internally and exposes the Override API to consumers of the container.

ContainerReferencePlugin(low level)

This plug-in adds specific references to containers as external and allows remote modules to be imported from those containers. It also calls the rewrite API for these containers to provide overrides for them.  Local overrides (via __webpack_override__ or override API when also a container was made) and specified overrides were provided to all referenced containers.

ModuleFederationPlugin(high level)

This plug-in combines the ContainerPlugin and ContainerReferencePlugin. Overrides and overrides are combined into a single list of specified shared modules.

Concept of the target

  • It should be possible to expose and use any module type supported by WebPack.

  • Block loading should load everything needed in parallel (Web: one-way round trip to the server).

  • Control from consumer to container

    • Overwriting modules is a one-way operation.
    • Sibling containers cannot overwrite each other’s modules.
  • Concepts should be context-neutral.

    • Can be used for Web, Node, etc.
  • Relative and absolute requests in sharing:

    • Will always be available, even when not in use.

    • Will be relative to the configuration context.

    • RequiredVersion is not used by default.

  • Module requests in the share:

    • Provided only when in use.

    • All equal module requests used in the build will be matched.

    • All matching modules will be provided.

    • RequiredVersion will be extracted from package.json at this location in the diagram.

    • When you have nested node_modules, you can provide and use multiple different versions.

Requests in shared modules with trailing/will match all module requests with this prefix.

Use cases

Each page is generated separately

Each page of a single page application is exposed from a container generation in a separate build. The application shell is also a separate build that references all pages as remote modules. This allows you to deploy each page individually. When adding a new application route, or updating the application route. The application shell defines common libraries as shared modules to avoid repeating them in page builds.

Component libraries act as containers

Many applications share a common component library that can be built as a container exposed for each component. Each application uses components in the component library container. Changes to component libraries can be deployed individually without redeploying all applications. The latest version of the application library automatically uses components.

Dynamic remote container

The container interface supports get and init methods. Init is an asynchronous compatible method that is called with one argument: a shared scope object. This object is used as a share scope in the remote container and is populated by modules provided by the host. You can use it to dynamically connect remote containers to host containers at run time.

init.js

(async () => {// initialize the share scope. Populate it with this version and all remotely supplied known modules await __webpack_init_sharing__('default'); const container = window.someContainer; Init (__webpack_share_scopes__. Default); const module = await container.get('./module'); }) ();Copy the code

The container attempts to provide a shared module, but if it is already in use, warnings and supplied shared modules are ignored. The container may still use it as a backup.

In this way, you can dynamically load A/B tests that provide different versions of shared modules. T> Make sure the container is loaded before attempting to dynamically connect to the remote container.

init.js

Function loadComponent(scope, module) {return async () => {// Initializes the shared scope. Populate it with this version and all remotely supplied known modules await __webpack_init_sharing__('default'); const container = window[scope]; Init (__webpack_share_scopes__. Default); const factory = await window[scope].get(module); const Module = factory(); return Module; }; } loadComponent('abtests', 'test123');Copy the code

See full implementation

troubleshooting

Uncaught Error: Shared module is not available for eager consumption

The application is eagerly executing an application that runs as an omni host. Options are available:

You can set the dependency to eager in the ModuleFederation’s high-level API, which does not put modules into asynchronous blocks but instead provides them synchronously. This allows us to use these shared modules in the initial block. Be careful though, as all supplied and backup modules will be downloaded. It is recommended that you only provide it at one point in your application, such as a shell.

We strongly recommend using asynchronous boundaries. It will break up a larger chunk of initialization code to avoid any additional round-tripping and improve overall performance.

For example, your entry would 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 a bootstrap (bootstrap.js) to archive and move the contents of the entry into it, and then bootstrap the import entry:

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

The methods mentioned below work, but there are some limitations or drawbacks.

The dependency on the ModuleFederationPlugin is set to eager: true

webpack.config.js

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

Use the Bundle Loader as an alternative to setting the dependency to “eager”. This approach performs poorly because it introduces additional round-trips.

const config = { entry: 'bundle-loader! ./bootstrap.js' };Copy the code

It can also be set through module rules

webpack.config.js

const config = {
  module: {
    rules: [
      {
        test: /bootstrap\.js$/,
        loader: 'bundle-loader',
        options: {
          lazy: true,
        },
      },
    ]
  }
};
Copy the code

But you must change the entry point to something like this:

index.js

- import('./bootstrap');
+ import bootstrap from './bootstrap';
+ bootstrap();
Copy the code

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

It might not say “./Button “, but the error message looks similar. This problem usually occurs when upgrading from Webpack Beta.16 to Webpack Beta.17.

In ModuleFederationPlugin. Change the exposure to:

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

Uncaught TypeError: fn is not a function

You may be missing a remote container, make sure it has been added. If you have already loaded the container for the remote you are trying to use and still see this error, add the remote container file for the host container to the HTML as well.