preface

Development Prerequisite: Completed a project in the previous period of time. However, there was a similar project requirement recently, with parts of the UI roughly the same as the previous project, and some components and functions different. So in order to reuse components rather than simply copy, choose to use a micro front end as the technical framework for this Monorepo.

Several micro front-end frameworks were investigated: After icestark, Qiankun and WebPack 5 Module Federation, I personally think that since both projects use React + typescript stack, using Module Federation provided by WebPack 5, It is enough to meet the current demand, and there is no need to apply such “heavy” concepts as ICestark and Qiankun.

In addition, WebPack is more widely used, with more application examples for reference and easier to use.

Begin to learn

Two concepts

Before you start, you need to understand two concepts: remote and Host

  • remote: a module that can be used or relied on by other modulesmerchants
  • host: modules that use other modules and depend on other modulesconsumers

A module that can be remote, host, or both

Create an instance

mkdir module-federation-demo
cd ./module-federation-demo
Copy the code

Here you use the scaffolding tool you wrote to create the foundation of the project

yarn global add frontend-scaffold-cli

fsc-cli init component-app
fsc-cli init main-app
Copy the code

Modify component_app

After installing the respective dependencies, first change the webpack.base.config.js directory in the component-app build directory

/ /... const { ModuleFederationPlugin } = require('webpack').container; const rootDir = process.cwd() module.exports = { output: { //... publicPath: http://localhost:8080/ } //... plugins: [ //... new ModuleFederationPlugin({ name: 'component_app', library: { type: "var", name: "component_app" }, filename: 'remoteEntry.js', exposes: { "./Button": path.resolve(rootDir, 'src/components/Button') }, shared: [{ react: { singleton: true, }, 'react-dom': { singleton: true, } }], }), ] }Copy the code

Here we create a button.tsx file in component-app/ SRC/Components as a test

import React from 'react'

const Button: React.FC = () => {
  const handleClick = () => {
    console.log('click click')
  }

  return (
    <button onClick={handleClick}> hello module federation </button>
  )
}

export default Button
Copy the code

Create bootstrap. TSX in component-app/ SRC and write the project entry file to:

import React from 'react' import ReactDOM from 'react-dom' import Apps from './Apps' import './css/output.css' const rootId = 'root' const rootElement = document.getElementById(rootId) if (! rootElement) { throw new Error(`Unable to find element with id '${rootId}'`) } ReactDOM.render(<Apps />, rootElement)Copy the code

Modify the entry file index.tsx for Component-app

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

Modify main_app

Modify the build/webpack base. Config. Js

/ /... const { ModuleFederationPlugin } = require('webpack').container; module.exports = { output: { //... publicPath: 'http://localhost:8081/', } //... plugins: [ //... new ModuleFederationPlugin({ name: 'main_app', remotes: { 'component-app': "component_app@http://localhost:8080/remoteEntry.js" }, shared: [{ react: { singleton: true, }, 'react-dom': { singleton: true, } }], }), ] }Copy the code

As with Component-app, modify the entry file

//bootstrap.tsx import React from 'react' import ReactDOM from 'react-dom' import Apps from './Apps' import './css/output.css' const rootId = 'root' const rootElement = document.getElementById(rootId) if (! rootElement) { throw new Error(`Unable to find element with id '${rootId}'`) } ReactDOM.render(<Apps />, rootElement)Copy the code
//index.tsx
import ('./bootstrap')
Copy the code

The test results

We introduced our Button component at Component_app in the app. TSX file

import React from 'react'
import Button from 'component-app/Button'

const Apps = (): React.ReactElement => {
  return (
    <div>
      <Button/>
    </div>
  )
}

export default Apps
Copy the code

Then, first run our component_app, then run main_app

As you can see, our Button component is displayed on the page and the component’s click event is successfully bound

Let’s look at the JS resource request for main_app

As you can see,component_appThe generatedremoteEntry.jsThe files have also been successfully imported

Project optimization

Lerna to introduce

It would be a waste of time if we had to run CD./component-app and yarn start every time we developed, then go to another directory and repeat the same steps. We introduced Lerna to help us manage multiple packages.

First install and import globally

yarn global add lerna

lerna init
Copy the code

Then modify lerna.json

{"command": {"bootstrap": {/* Hoist the top node_modules when multiple packages have the same dependencies */ "hoist": {/* hoist the top node_modules when multiple packages have the same dependencies */ "hoist": { True, /* During bootstrap, pass the argument to NPM install */ "npmClientArgs": [" -- no - package - lock ", "- no - ci"]}}, / * use yarn as a package management tool * / "npmClient" : "yarn" and "packages" : ["packages/*"], /* Use yarn workspace, specify */ "useWorkspaces": true, "version": "0.0.0"}Copy the code

Modify the package. The json

{
    //...
    "workspaces": [
      "packages/*"
    ],
}
Copy the code

The program

Here we need to put our component-app and main-app into the packages directory generated by Lerna. We can just copy and paste, but don’t copy node_modules

The project structure becomes

Then install our dependency YARN Install in the home directory

In package.json in your home directory, modify scripts

{"scripts": {// separate start component-app "start:component": Lerna exec --scope component-app -- yarn dev", // start main-app "start:main": "Lerna exec --scope main-app -- yarn dev", // open component-app "start":" NPX lerna run dev --parallel"},}Copy the code

Record on pit

Module “./Button” does not exist in container

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

Library name base (component-app) must be a valid identifier when using a var declaring library type….

new ModuleFederationPlugin({
-    library: { type: "var", name: "component-app" },
+    library: { type: "var", name: "component_app" },
});
Copy the code

React throws the “Invalid hook call” error.

new ModuleFederationPlugin({
-    shared: ['react', 'react-dom'],
+    shared: [{
+        react: {
+        singleton: true,
+     },
+     'react-dom': {
+        singleton: true,
+     }
+    }],
});
Copy the code

The source code

Github.com/wbh13285517…