preface

Recently, the concept of microfront-end has been mentioned more and more. It adopts the concept of microservices. We can break an application into multiple modules that can be independently developed and deployed, and then combine them into a complete App at runtime.

In this way, we can use different technologies to develop different parts of the application. For example, this module is already developed with React and we can continue to use React. The new module team prefers Vue and we can implement it with Vue. We can have a dedicated team to maintain individual modules, which will be more convenient to maintain. It changed the way we worked as a team.

Since Webpack5, there has been built-in support for microfront end development, and a new feature called Module Federation (I don’t know what the proper term is) provides enough power for us to implement microfront end development.

Without further words, we still feel a concept and process of the whole through a simple example. We’re going to implement a simple App and transform it into a micro front end via WebPack.

Let’s get started!

This time all the configuration is done manually by us. First we create a new empty directory and execute it in the project:

npm init -y
Copy the code

And then in order to use Webpack,

npm add webpack webpack-nano -D
Copy the code

We can then configure the entire packaging process by creating a new webpack.config.js file in the root directory.

There is a difference between runtime and development-time configurations. You might write two files, webpack.production.js and webpack.development.js, to configure different environments. However, this may make our configuration object become large and bloated, which is not easy to maintain. We need to find the configuration we want to modify in a large number of configurations, and the configuration of each environment is not completely different, so we have to encapsulate ah, we have to abstract ah, we have to think of ways to reuse ah!

So what are we going to do?

Can we break up this large configuration object into configuration objects with specific functions to maintain separately?

For example, if our project uses the mini-html-webpack-plugin to generate the final index.html file, we could write a separate function to export the configuration of the page

exports.page = ({title}) => ({
    plugins: [new MiniHtmlWebpackPlugin({
        context: {title}
    })]
})
Copy the code

Such subsequent when we want to change the page relevant configuration we will know to modify the page function, we can even replace a new plug-in, and the configuration need only need to call this function to get the configuration, do not need to care about the details, they change for us is no perception, nature will not be affected. Our configuration can then be reused as a function in various environments.

The problem is, after all, WebPack only recognizes the configuration form it knows, so we need to merge the small configuration objects returned by these functions into one large, complete configuration object. Note that object. assign is not very friendly to arrays and will lose data. You can implement your own logic, or use webpack-merge.

To better manage the WebPack configuration without the complexity of the configuration, we can create a new webpack.parts.js file where we define small functions to return configuration objects that configure specific functions.

Then in webpack.config.js, we can import these functions, and we can use the mode passed by the runtime to determine which environment needs to be packaged and dynamically generate the final configuration:

const {mode} = require('webpack-nano/argv')
const parts = require('./webpack.parts')
const {merge} = require('webpack-merge')

const commonConfig = merge([
    {mode},
    {entry: ["./App"]},
    parts.page({title: 'React Micro-Frontend'}),
    parts.loadJavaScript()
])

const productionConfig = merge([parts.eliminateUnusedCss()])

const developmentConfig = merge([{entry: ['webpack-plugin-serve/client']}, parts.devServer()])

const getConfig = (mode) => {
    process.env.NODE_ENV = mode
    switch (mode) {
        case 'production':
            return merge([commonConfig, productionConfig])
        case 'development':
            return merge([commonConfig, developmentConfig])
        default:
            throw new Error(`Trying to use an unknown mode, ${mode}`);
    }
}

module.exports = getConfig(mode)
Copy the code

This minimizes the bloat in our configuration files.

One unit –

Then we also need to configure our development environment, we certainly do not want to manually refresh the page during development, here we use a plugin webpack-plugin-serve to do real-time updates:

exports.devServer = () => ({ watch: true, plugins: [ new WebpackPluginServe( { port: Process.env.PORT || 8000, host: Static: './dist', liveReload: true, waitForBuild: true})]})Copy the code

Then we use React as the front-end framework:

npm add react react-dom
Copy the code

To enable the compiler to properly understand our React component, we use Babel:

npm add babel-loader @babel/core @babel/preset-env @babel/preset-react -D
Copy the code

Configure babel-loader:

exports.loadJavaScript = () => ({
    module: {
        rules: [
            { test: /\.js$/, include: APP_SOURCE, use: "babel-loader" },
        ],
    },
});
Copy the code

Don’t forget to add a.babelrc file

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
  }
    ],
    [
      "@babel/preset-react"
  ]
  ]
}
Copy the code

Now that our React component is handled correctly, we can start writing our component.

Happy business code session ~

First up is our Header component:

import React from "react";

const Header = () => {
    return <header>
        <h1>Micro-Frontend With React</h1>
    </header>
}

export default Header;
Copy the code

Then there is our Main component:

import React from "react";
import Header from "./Header";

const Main = () => {
    return (
        <main>
            <Header/>
            <span>a Demo for Micro-Frontend using Webpack5</span>
        </main>
    );
}

export default Main
Copy the code

Finally, the entry file:

import ReactDOM from "react-dom";
import React from "react";
import Main from "./Main";

const container = document.createElement("div");
document.body.appendChild(container);
ReactDOM.render(<Main/>, container);
Copy the code

Open the package.json file and configure the following script:

"scripts": {
  "build": "wp --mode production",
  "start": "wp --mode development"
  }
Copy the code

Now we can preview our App by executing NPM run start on the terminal.

MF It’s coming!

Next we’ll transform it into a micro front end, making the Header a separate module and the rest a separate module, using the ModuleFederationPlugin.

First we need to configure the plugin:

const {ModuleFederationPlugin} = require("webpack").container;

exports.federateModule = ({
                              name,
                              filename,
                              exposes,
                              remotes,
                              shared,
                          }) => ({
    plugins: [
        new ModuleFederationPlugin({
            name,
            filename,
            exposes,
            remotes,
            shared,
        }),
    ],
});
Copy the code

Filename is a file provided for loading by other services, an exposes module is required, remotes specifies other services to be used, and shared is a configuration common module (such as Lodash).

  • providesexposesThe option indicates that the current application is aRemote.exposesThe inside module can be replaced by the otherHostThe reference mode isimport(${name}/${expose}).
  • providesremotesThe option indicates that the current application is aHost, can be quotedremoteexposeThe module.

We need to configure these two modules in webpack.config.js:

const componentConfig = {
    App: merge(
        {
            entry: [path.join(__dirname, "src", "bootstrap.js")],
        },
        parts.page({title: 'React Micro-Frontend'}),
        parts.federateModule({
            name: "app",
            remotes: {mf: "mf@/mf.js"},
            shared: sharedDependencies,
        })
    ),
    Header: merge(
        {
            entry: [path.join(__dirname, "src", "Header.js")],
        },
        parts.federateModule({
            name: "mf",
            filename: "mf.js",
            exposes: {"./Header": "./src/Header"},
            shared: sharedDependencies,
        })
    ),
};
Copy the code

Because we put all the code in one project to simplify the code, it is more common for each module to have its own code repository, which can be implemented using different techniques. [name]@[protocol]://[domain]:[port][filename]

In order to simulate independent compilation of multiple projects, we also use component names to set different configurations. For the Header, we don’t want to run it directly in the browser, but for the App, we want to see the complete page in the browser, so we move the page-related configuration to the App configuration. Webpack.config.js also needs to take a component name as a parameter when dynamically generating configuration objects.

const {mode, component} = require('webpack-nano/argv') ... const getConfig = (mode, component) => { switch (mode) { case 'production': return merge([commonConfig, productionConfig, componentConfig[component]]) case 'development': return merge([commonConfig, developmentConfig, componentConfig[component]]) default: throw new Error(`Trying to use an unknown mode, ${mode}`); }}Copy the code

And then we’re going to change the path in Main to import the Header

import Header from "mf/Header";
Copy the code

The final step is to load all of this through a boot file called bootstrap.js

import("./App");
Copy the code

This is because the js files exposed by remote need to be loaded first. If app.js is not asynchronous, it will rely on mf.js when importing headers. Running mf.js directly may cause problems when mf.js is not loaded completely.

As can be seen from the Network panel, mf.js is loaded before app.js, so our app.js must be asynchronous logic.

NPM run build — — Component Header NPM run build — — Component Header NPM run start — — Component Header NPM run start — — Component App

Write in the last

Overall, this provided a new way for teams to collaborate and share code, which was a bit invasive, and our project had to rely on WebPack. Personally, I don’t see any problem, since most projects now use WebPack, but for those who care, vite takes advantage of the browser’s native modularity capabilities to provide a code-sharing solution. Today we just implemented a small demo with Module Federation. It is not enough to explain the management of micro-front-end and Webpack in one article. There are many other things to talk about

Full Demo code