Micro front-end

Before we look at module federation in WebPack, let’s take a look at the micro front end. If you don’t know about the micro front end, you can refer to the two articles “Micro front End (1) – Concept” and “Micro Front End (2) – Implementation” to familiarize yourself with its basic concepts.

Generally, our micro front end model is as follows:

In the micro front end, there is a container application whose job is to load each micro application.

Microapplications need to do a few things:

  • Provide two methods: one isMount method, which the container will call to render the microapplication. The other one isUnloading method, used to uninstall microapplications, and they will all be used withInterface methodsProvides the form to the container call;
  • provideRemote entry fileThe address of the container application is selected at the appropriate timeDynamic loadingThe document is obtained atMount methodAfter that, perform microapplication rendering;
  • provideMicro application IDIs used to identify itself and is required for operations on microapplications.
  • One of the firstThe routing addressAfter the micro-application is mounted, determine the view display of the micro-application.

The container application does the following:

  • Load the remote microapplication (download the remote JS entry file) and perform the rendering;
  • Uninstall microapplications at a reasonable opportunity.

These are the basic functions of the micro front end. Next, let’s look at Module federation for Webpack.

Module joint

Typically, modules produced in webPack builds are stored locally and used directly by the current application. The concept of remote modules was introduced in WebPack 5 to allow the runtime to asynchronously load remote modules by using the currently built application as a container. The abbreviation MF will be used to refer to module federation below.

Webpack provides a way to dynamically load modules. You can use import or the older methods require.ensure or require([…]). ).

Remember what the container application did in the previous section when we talked about the micro front end? By dynamically loading WebPack, you’ve already done what container applications are supposed to do. So it’s safe to assume that microapplications themselves can function as container applications.

When we apply a microapplication as a container, its architectural model shifts, resulting in the following model:

It can be seen that when our micro applications become container applications, each application exists equally in the architecture, and container applications can rely on each other and load and use each other.

In the microfront, there is no architectural constraint on the complexity of microapplications, which means it could just be a button that sets the planet on fire, which is a joke. But we should classify it commercially as a valuable reusable module that is not tied to the framework.

How to divide, share and load modules through building tools is the functional significance of WebPack 5 Module Federation.

MF VS micro front end

Let’s continue thinking about the difference between module federation and microfront ends.

In the microfront-end:

  • Loading microapplications must bepredefinedInterface methods (Mounted, unmount, etc.) to achieve dynamic micro applicationsmountanduninstallAnd so on, which means every microapplication mustmanualTo achieve theseInterface methods;
  • in”Micro Front End (2) – Implementation”In, we learned about microapplications inIndie development modelThe interface method is usually called manually to dynamically load the view.
  • If we want to share the modules of one microapplication to other microapplications, it is not easy. This means that you need to isolate the module and use it in a way that makes sense for other micro-applicationsThe remote loading.
  • The application of microswitchAre usually made ofRoute Status changeTo trigger it.

In module federation:

  • Above we saw that modules combine eachMicro applicationIt could be aThe container applicationSo they can be mutualRely onandloading;
  • Each application allowsexposedAn object reception (iso) multiple interfaces, other applications available inDynamic remote loadingAfter the application, use its interface directly. This addresses the microfront mentioned aboveModule SharedProblem;
  • Very flexible in module use when youreferenceaRemote moduleYou can use it just like a normal NPM package, and of course you canLazy loadingModule;
  • There is no connection between the remote module and the route, and the loading opportunity is completely determined by the host application itself.

It is worth noting that the federated module, as the technical extension of the micro-front-end, still has the characteristics of the micro-front-end, that is, each container application should be developed and deployed independently, and the team should be autonomous.

The architectural model for module federation is more like the one shown below, but not just this one, because it is very flexible. It depends on how you share modules and combine them.

In the architecture diagram above:

  • APP A,APP B,APP CAre loaded and used remotelyThe UI component libraryThe exposedButtonandTextComponents,TableComponents are not stabilized and we are not prepared to expose them to external use;
  • APP BandAPP CIn theListModules are shared toAPP A(e.g., the order list for business B and the order list for business C can be integrated directly into business A);
  • The authenticationApplication as a public module, byAPP A,APP BandAPP CInstead of adding an additional authentication module to the new application, it will serve as the base service.

ModuleFederationPlugin

Webpack 5 implements module interface exposure and remote module declarations through the ModuleFederationPlugin.

The ModuleFederationPlugin combines the ContainerPlugin and ContainerReferencePlugin.

The ContainerPlugin plugin uses the specified public module to create an additional container entry, which means additional container entry files are generated in addition to the configured outputs.

module.exports = {
  output: {
    filename: 'main.js',}};Copy the code

The ContainerReferencePlugin allows us to use the import standard syntax when using a remote module, so we need to declare the remote module in advance.

The ModuleFederationPlugin allows you to build a run-time stand-alone module as a provider or consumer concept, which each application can become.

const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({/* options */})]};Copy the code

You can see all the options here.

Container entry file

First, we need to provide a container entry file that other applications can load remotely:

new ModuleFederationPlugin({
    name: "ui_lib".// Container name
    filename: 'ui.js'.// Container entry file
})
Copy the code

You need to provide a unique container name (name) and filename (filename). If filename is not provided, the build will generate a filename with the same name as the container name.

After the build, additional container entry files for UI.js are generated in the dist directory.

Expose multiple modules

You can expose any module you want to share. It can be network libraries, common business modules, UI components, routes, hooks, whatever you think you can share. It sounds exciting, and it is.

We expose the module via an expose option:

new ModuleFederationPlugin({
  name: "ui_lib".// Container name
  filename: 'ui.js'.// Container entry file
  exposes: {
    "./components": "./src/components/",}})Copy the code

If the entry to our UI library was in the project SRC /components/index.js file, it would look like this:

export { default as Button } from './button/index.jsx'
export { default as Text } from './text/index.jsx'
Copy the code

Shared module

Containers typically have underlying, repetitive dependency libraries (e.g. React, Vue, and so on). Like the shared libraries that introduced the micro front end article, we need to exclude them from our container and load them as asynchronous modules.

Not only that, MF does versioning management for the shared module, you can get relevant information in this PR exchange.

Also, we used the shared option in the ModuleFederationPlugin plugin to specify asynchronous module loading for common modules, which functions like webpack externals and allows external dependency libraries to be loaded at runtime.

new ModuleFederationPlugin({
  name: "ui_lib".filename: 'ui.js'.exposes: {
    "./components": "./src/components/",},shared: {
    react: { singleton: true},
    "react-dom": { singleton: true}}})Copy the code

You can see all the shared options here.

Pay attention to the point

1. If you want to use shared modules for local startup, specify the eager: true option, otherwise the following error will occur.

Uncaught Error: Shared module is not available for eager consumption
Copy the code

This option allows the shared module to be used directly during initialization, meaning that it is not loaded as an asynchronous module.

Note that the eager option is enabled, which pushes the module directly into the container file to be loaded and used as a synchronous module.

You can also fix this problem by manually loading the shared module asynchronously.

First, let’s make some changes to the original SRC /index.js file.

Your previous entry file looked like this:

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

After the modification, we only retain the asynchronous loading function:

import('./bootstrap');
Copy the code

Then I create the bootstrap.js startup file in the same directory and copy in the contents of the original index.js:

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

This enables manual asynchronous loading.


2. We use the version of the specified shared module with the requiredVersion option.

It has two values: requiredVersion, when a string value, represents the semantic version number that follows the Semver specification.

You can use the version of the package name in the Dependencies field in package.json to keep the version of the shared module consistent with that in package.json. If inconsistent, a warning is printed.

const deps = require("./package.json").dependencies;
// other code...
new ModuleFederationPlugin({
  name: "ui_lib".filename: 'ui.js'.exposes: {
    "./components": "./src/components/",},shared: {
    react: {
      requiredVersion: deps.react,
      singleton: true,},"react-dom": {
      requiredVersion: deps['react-dom'].singleton: true,}}})Copy the code

If requiredVersion is a Boolean value, it indicates whether automatic version inference is enabled. When it is true (the default), the requesting module automatically inferences from the version corresponding to the package name in package.json.

Using remote Modules

First, we also use the ModuleFederationPlugin to pre-declare which modules are remote, which is set using the Remotes option:

new ModuleFederationPlugin({
  name: "app_b".remotes: {
    "@lumin-ui": 'ui_lib@http://localhost:3003/ui.js',}})Copy the code

When we use it at runtime, it is no different from the import syntax we normally use:

import { Button, Text } from '@lumin-ui/components';
Copy the code

It looks really cool! For an architectural upgrade, we didn’t need to change any code to replace the local build module with a remote one.

The demo source code

You can find the demo source code for the above use case at the following address. The use case is not complete, but shows the basic functionality of MF.

Github.com/dun-cat/web…

Start the project by following these steps:

# install dependencies
npm run bootstrap

# Start a project
npm run start
Copy the code

Other interesting use cases

One of the UI library use cases in MF was demonstrated above, and you can find more examples here.

shared-routing

I suggest you take a look at the shared-routing use case, which shows how a complete application can be partitioned and, more importantly, how each module gets the entire application.

You must understand that module partitioning also means project partitioning and task partitioning. When developing independently, it is often necessary to determine how to ensure the correctness of the entire application. So we expect our modules to run across the entire application, and this example provides a good solution.

Read more:

> webpack.docschina.org/concepts/mo…

> www.bilibili.com/video/BV1z5…

> www.youtube.com/watch?v=-ei…

> github.com/module-fede…

> www.nicolasdelfino.com/blog/micro-…