webpack5 模块联邦(Module Federation)实践

394 阅读3分钟

模块联邦是什么

模块联邦时webpack5中推出的一项特性,他典型的一个表现是一个主应用拖着一群子应用,子应用可以根据实际情况,通过网络的形式复用主应用的代码,这些代码可以是普通的函数,也可以是react,vue写的组件。

应用场景

一般来说一个微前端应用本身就不可能轻量,所以可以使用qiankun。模块联邦个人感觉可以有以下应用场景

  1. 在减少应用集群打包时间,打包体积上有用武之地。
  2. 动态更新页面或组件:比如子应用a 导入了主应用的一个组件群,或者一个命名空间的库函数,只需要在主应用更新,所有的子应用都能得到更新。比如一个由electron 生成的客户端应用,是不是就可以实现某种程度上的”热跟新“?

实践

image.png



// app2 webpack.config.js
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const deps = require('./package.json').dependencies;


const ModuleFederationPlugin  = webpack.container.ModuleFederationPlugin;

module.exports = {
    entry: "./index.js",
    mode: "development",
    devServer: {
      hot: true,
      static: {
        directory: path.join(__dirname, 'dist'),
      },
      port: 3001,
      // headers: { "Access-Control-Allow-Origin": "*" },
  },
    output: {
      hashFunction: 'xxhash64',
      path: path.resolve("./dist"),
      filename: 'js/[name].js',
      publicPath: '/',
      chunkFilename: 'js/[name].js',
      publicPath: 'auto'
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          loader: 'babel-loader',
          exclude: /node_modules/,
          options: {
            presets: ['@babel/preset-react'],
          },
        },
      ],
    },
    // 其他配置...
    plugins: [
      new ModuleFederationPlugin({
        name: "app2",
        filename: "remoteEntry.js",
        exposes: {
          // 暴露的模块
          "./Module1": "./src/Module1",
        },
        // shared: ["lodash"], // 共享的模块列表,需要和主应用程序一致
        shared: {
          react: {
            requiredVersion: deps.react,
            import: 'react', // the "react" package will be used a provided and fallback module
            shareKey: 'react', // under this name the shared module will be placed in the share scope
            shareScope: 'default', // share scope with this name will be used
            singleton: true, // only a single version of the shared module is allowed
          },
          'react-dom': {
            requiredVersion: deps['react-dom'],
            singleton: true, // only a single version of the shared module is allowed
          },
        }
      }),
      new HtmlWebpackPlugin({
        template: "./index.html",
        title:"wcx-html"
      })
    ],
  };


// app3 webpack.config.js
const webpack = require("webpack")
const ModuleFederationPlugin  = webpack.container.ModuleFederationPlugin;
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: "./index.js",
    mode: "development",
    devServer: {
        hot: true,
        static: {
            directory: path.join(__dirname, 'dist'),
        },
        port: 3002,
        headers: { "Access-Control-Allow-Origin": "*" },
    },
    output: {
        hashFunction: 'xxhash64',
        path: path.resolve("./dist"),
        filename: 'js/[name].js',
        publicPath: '/',
        chunkFilename: 'js/[name].js'
    },
    module: {
        rules: [
          {
            test: /\.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            options: {
              presets: ['@babel/preset-react'],
            },
          },
        ],
    },    
    plugins: [
        new ModuleFederationPlugin({
            name: 'app3',
            // remotes: {
            //   app2: 'app2@http://localhost:3001/remoteEntry.js',
            // },
            shared: {
                react: {
                  import: 'react', // the "react" package will be used a provided and fallback module
                  shareKey: 'react', // under this name the shared module will be placed in the share scope
                  shareScope: 'default', // share scope with this name will be used
                  singleton: true, // only a single version of the shared module is allowed
                },
                'react-dom': {
                  singleton: true, // only a single version of the shared module is allowed
                },
            }           
        }),
        new HtmlWebpackPlugin({
            template: "./index.html",
            title:"wcx-html"
        })
    ]
}

app3 目录结构

image.png


// index.js


import ("./src/bootstrap")


// bootstrap js

import App from "../src/AppIndex"

import React from "react"
import {createRoot} from "react-dom/client"

const rootNode = document.querySelector("#root")

const root = createRoot(rootNode)

root.render(<App/>)

// AppIndex.js

import { init,loadRemote } from '@module-federation/runtime'

import React,{useEffect, useState} from "react"

init({
    name: 'app3',
    remotes: [
      {
        name:'app2',
        entry: 'http://localhost:3001/remoteEntry.js'
      },
    ]
})
// async function test() {

//   const rlt = await loadRemote(`app2/Module1`);
//   const target = rlt.default
//   target()
// }
// test()

export default function App() {
    const [Component, setComponent] = useState(null)
    const [isReady, setIsReady] = useState(false)
    const loadComponent = async() => {
        const { default: component} = await loadRemote("app2/Module1")
        console.log('component', component)
        setComponent(() => component)
        setIsReady(true)
    }
    useEffect(() => {
        loadComponent()
    })
    return (
        <div>
            this is App component from app3
                {isReady ? <Component/> : null}
                {/* <Component /> */}
        </div>
    )
}

app2 目录结构

image.png


// Module1.js
import React from "react"
export default function() {
    return (
        <div>this is a component from app2</div>
    )
}

通过webpack dev-sever起服务后查看效果

image.png

在UMI4.1.10中使用模块联邦

消费模块的配置


//.umirc.ts

import { defineConfig } from '@umijs/max';

const remoteMFName = 'templateLib';

export default defineConfig({
  antd: {},
  access: {},
  model: {},
  initialState: {},
  request: {},
  layout: {
    title: '@umijs/max',
  },
  routes: [
    {
      path: '/',
      redirect: '/home',
    },
    {
      name: '首页',
      path: '/home',
      component: './Home',
    },
    {
      name: '权限演示',
      path: '/access',
      component: './Access',
    },
    {
      name: ' CRUD 示例',
      path: '/table',
      component: './Table',
    },
  ],
  mf: {
    name: remoteMFName,
    library: {
      type: "window",
      name: remoteMFName
    },
    remoteHash: false
  },
  publicPath: 'http://127.0.0.1:8002/',
  npmClient: 'pnpm',
});

image.png

生产模块配置


// .umirc.ts
import { defineConfig } from '@umijs/max';

export default defineConfig({
  antd: {},
  access: {},
  model: {},
  initialState: {},
  request: {},
  layout: {
    title: '@umijs/max',
  },
  routes: [
    {
      path: '/',
      redirect: '/home',
    },
    {
      name: '首页',
      path: '/home',
      component: './Home',
    },
    {
      name: '权限演示',
      path: '/access',
      component: './Access',
    },
    {
      name: ' CRUD 示例',
      path: '/table',
      component: './Table',
    },
  ],
    // 已经内置 Module Federation 插件, 直接开启配置即可
    mf: {
      remotes: [
        {
          // 可选,未配置则使用当前 remotes[].name 字段
          aliasName: 'templateLib',
          name: 'templateLib',
          entry: 'http://localhost:8002/remote.js',
        },
      ],
  
      // 配置 MF 共享的模块
      shared:{},
    },
  npmClient: 'pnpm',
});



实际页面使用的方式


const HomePage: React.FC = () => {

  const RemoteComponent = React.lazy(() => {
    return rawMfImport({
      entry: "http://localhost:8002/remote.js",
      // entry: "http://localhost:3001/remote.js",
      moduleName: "Module1",
      remoteName: "templateLib"
      // remoteName: "app2"
    })
  })
  return (
    <Suspense fallback="loading">
            <RemoteComponent />
    </Suspense>
  );
};

export default HomePage;


补充

webpack5 内部帮你解决了跨域的问题,所以你不需要关心跨域相关的问题.