一种解决多系统web应用的策略,Module Federation(模块联邦)

发布时间 2023-04-18 16:57:06作者: 风行者夜色

前言

针对很多大型的web应用,往往会衍生出很多子应用,而这些子应用之间有时候又往往需要进行交互或者复用一些功能或者组件,这个时候有没有一个比较好的策略来实现这样的交互呢。答案是有的,试试webpack5提供的Module Federation

先来个示例

万事先实操,然后再谈别的,不付诸实践的想法都是空想。

准备

  • 准备是使用lerna作为管理多项目的工具,所以先全局安装npm install --global lerna

项目生成

  • 通过在文件目录下运行lerna init,生成以下目录

    mf-test

    packages // 项目文件夹

    .gitignore // git忽略文件

    lerna.json // lerna的配置文件

    package.json // 整个工程的配置文件

  • 新建三个空的文件夹,mainapp1app2,使用lerna create命令来生成,删掉生成的test文件夹以及lib文件夹,开始改造

    packages

    main
    app1
    app2

  • 分别在三个文件夹下增加一个最简单的src和webpack.config.js以及在已有的package.json中改造

    src/index.js(app1, app2, main)

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

    src/App.js(app1, app2)

    import React, { useState } from 'react';
    // app1就是app1,app2就显示app2
    const App = () => {
      return (
        <div>
          <h2>App 1</h2>
        </div>
      )
    };
    
    export default App;
    

    src/App.js(main)

     import React, { useState } from 'react';
     const App1 = React.lazy(() => import('app1/App'));
     const App2 = React.lazy(() => import('app2/App'));
     // 引用远程的app1和App2去进行切换展示
     const App = () => {
        const [show, setShow] = useState(true);
        return (
          <div>
            <button onClick={() => setShow(!show)}>切换子页面</button>
            <React.Suspense fallback="Loading Button">
              { show ? <App1 /> : <App2 /> }
            </React.Suspense>
          </div>
        )
     };
    
     export default App;
    

    webpack.config.js,基本复用一份配置

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { ModuleFederationPlugin } = require('webpack').container;
    const path = require('path');
    
    module.exports = {
      entry: './src/index',
      mode: 'development',
      devServer: {
        static: {
          directory: path.join(__dirname, 'dist'),
        },
        port: 3001, // main设置为3000,App1设置为3001,APP2设置为3002
      },
      output: {
        publicPath: 'auto',
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            options: {
              presets: ['@babel/preset-react'],
            },
          },
        ],
      },
      //http://localhost:3001/remoteEntry.js
      plugins: [
        new ModuleFederationPlugin({
          name: 'app1', // 模块的名称
          remotes: {
            // 定义远程模块的名称,app1就定义main和app2,app2就定义app1和main,main就定义app1和app2,这样他们就能互相引用对方模块中的组件
            main: `main@${getRemoteEntryUrl(3000)}`,
            app2: `app2@${getRemoteEntryUrl(3002)}`,
          },
          filename: 'remoteEntry.js', // 作为远程模块导出时的js文件名
          exposes: {
            './App': './src/App' // 需要导出以供其它模块使用的组件或方法
          },
          shared: { react: { singleton: true }, 'react-dom': { singleton: true } }, // 公共的第三方库,这样就不会出现子项目中一些第三方库版本不一致导致的一些兼容问题
        }),
        new HtmlWebpackPlugin({
          template: './public/index.html',
        }),
      ],
    };
    
    function getRemoteEntryUrl(port) {
      const { CODESANDBOX_SSE, HOSTNAME = '' } = process.env;
    
      // Check if the example is running on codesandbox
      // https://codesandbox.io/docs/environment
      if (!CODESANDBOX_SSE) {
        return `//localhost:${port}/remoteEntry.js`;
      }
    
      const parts = HOSTNAME.split('-');
      const codesandboxId = parts[parts.length - 1];
    
      return `//${codesandboxId}-${port}.sse.codesandbox.io/remoteEntry.js`;
    }
    

    package.json,就是加一些项目依赖

    {
      "name": "@mf-test/app1", // 这个名称随便定义,目前我是定义的@mf-test/app1,@mf-test/app2,@mf-test/main
      "version": "0.0.0",
      "private": true,
      "devDependencies": {
        "@babel/core": "7.18.6",
        "@babel/preset-react": "7.18.6",
        "babel-loader": "8.2.5",
        "html-webpack-plugin": "5.5.0",
        "serve": "14.0.0",
        "webpack": "5.72.1",
        "webpack-cli": "4.10.0",
        "webpack-dev-server": "4.8.1"
      },
      "scripts": {
        "start": "webpack-cli serve",
        "build": "webpack --mode production",
        "serve": "serve dist -p 3001",
        "clean": "rm -rf dist"
      },
      "dependencies": {
        "react": "^16.13.0",
        "react-dom": "^16.13.0"
      },
      "useWorkspaces": true,
      "description": "> TODO: description",
      "author": "",
      "homepage": "",
      "license": "ISC",
      "publishConfig": {
        "registry": "https://bnpm.byted.org/"
      }
    }
    

运行

  • 在根目录下执行lerna bootstrap去安装对应需要的包,然后npm run start就是可以跑起来了,效果如图:

  • 点击按钮就可以切换对应的子应用。

小结

至此,已经完成了一个最基本的多子系统应用,可以完成基本的子系统切换,对于一些大型的项目改造来说还是比较有益的,在此基础上可以增加一个专门存放公共组件方法的子系统,提供给所有的其它子系统使用。它的原理说白了,就是异步加载模块之后同步执行模块去处理互相之间的引用,至于再底层一点的实现,后面有时间可以看看它的源码再说。