webpack5 模块联合 基本应用

2,070 阅读4分钟

多个单独的构建应该形成一个应用程序。这些独立的构建不应该相互依赖,因此可以单独开发和部署它们。

这通常被称为微前端,但不限于此。

更多基础概念参考

配置

ModuleFederationPlugin插件配置如下:

{
    name: "app1", // 当前应用名称
    remotes: { // 引用的远程应用
     在当前应用中使用的名称:远程应用名称@远程应用入口 
      app2: "app2@http://localhost:3002/remoteEntry.js",
    },
    library: { // 参考 https://www.webpackjs.com/configuration/output/#output-library
    	type: "var", 
        name: "app1"
     },
    exposes: { // 导出项
      "./Button": "./src/Button",
    },
    shared: { // 需要共享的资源
    	react: {
          import: "react", // “react”包将使用一个提供的备用模块
          shareKey: "newReact", // 在此名称下,共享模块将被置于共享范围中
          shareScope: "default", // 将使用具有此名称的共享作用域。注意:经测试该值不能修改。
          singleton: true // 只允许共享模块的单一版本
          requiredVersion: deps.react, // 需要的版本
        }, 
        "react-dom": { singleton: true } },
}

基本使用

主应用

webpackconfig.js

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

plugins: [
  new ModuleFederationPlugin({
    name: "app1", // 当前应用名称
    remotes: { // 
      app2: "app2@http://localhost:3002/remoteEntry.js",
    },
    shared: { 
    	react: { singleton: true }, 
        "react-dom": { singleton: true } },
  })
]

app.js

const RemoteButton = React.lazy(() => import("app2/Button"));

远程应用

webpackconfig.js

plugins: [
  new ModuleFederationPlugin({
    name: "app2",
    library: { type: "var", name: "app2" },
    filename: "remoteEntry.js",
    exposes: {
      "./Button": "./src/Button",
    },
    shared: { react: { singleton: true }, "react-dom": { singleton: true } },
  })
],

官方代码实例

动态主机

动态加载远程。

  • 首先加载远程代码
  • 其次加载远程导出

注意:webpack模块联合配置不需要配置remotes选项。

代码实例:

  const useDynamicScript = (args) => { // 加载脚本
      return new Promise((resolve, reject) => {
        if (!args.url) {
          reject()
        }
        const element = document.createElement("script");

        element.src = args.url;
        element.type = "text/javascript";
        element.async = true;

        element.onload = () => {
          console.log(`Dynamic Script Loaded: ${args.url}`);
          resolve()
        };

        element.onerror = () => {
          console.error(`Dynamic Script Error: ${args.url}`);
          reject()
        };

        document.head.appendChild(element);
      })
  };
  
  function loadComponent(scope, module) { // 加载模块
    return async () => {
      // Initializes the share scope. This fills it with known provided modules from this build and all remotes
      await __webpack_init_sharing__("default");

      const container = window[scope]; // or get the container somewhere else
      // Initialize the container, it may provide shared modules
      await container.init(__webpack_share_scopes__.default);
      const factory = await window[scope].get(module);
      const Module = factory();
      return Module;
    };
  }
  
  // 调用
  await useDynamicScript({url: 'http://localhost:8080/remoteEntry.js'})
  let moudle = await loadComponent('remote', './remoteModule')()

官方代码实例

嵌套远程

案例描述:A应用依赖B应用,B应用依赖C应用。A应用引用B应用。

使用注意:当A应用引用B应用时,需要在A的webpack模块联合配置remotes选项时同时配置上B和C。

官方代码实例

共享srote

redux存储并动态地注入redux存储。

官方代码实例

vuex存储。

方案一: 保证多个应用之间共享同一个vue、vuex实例。最终的结果为,在vue组件中,谁启动vuex实例归属于谁。

远程组件代码:

<template>
  <div>
    {{$store.state.test}}
  </div>
</template>

主机引用代码:

<template>
  <div>
    <Home></Home>
  </div>
</template>
<script>
async function getRemote() { // 加载远程组件
  let service = await import("Base/BaseMainApp"); // 加载远程导出
  let { // 取出home组件
    default: { Home },
  } = service;
  return Home
  }

export default {
  components: { // 注册为本地组件
    Home: () => getRemote()
  }
}
</script>

此时$store.state.test展示的数据为主机中的store数据

方案二: vuex srote进行应用级别的隔离。远程应用需将store或者vue实例导出,此时直接操作store或者使用vue实例进行操作即可。

动态注册vuex文档模块动态注册部分。

自我修复

remote应用依赖并期望在host 应用中提供共享依赖项,但是host 应用并没有提供相关的共享依赖。此时,当host应用引用remote应用时,程序会自动查找依赖。官方代码实例

共享路由

仅导出路由配置,在单独的应用中路由实例仅有一个。

官方代码实例1

官方代码实例2

共享不同版本依赖

方案一

两个使用两个不同版本lodash的联合应用程序。

-app1使用lodash@4.10.0。

-app2使用lodash@4.11.0以及lodash.nth--功能在lodash@4.10.0中不可用。

在如下shared的配置中声明不同的依赖后,应用程序会自动选择相应版本。

主应用 webpack.config.js

new ModuleFederationPlugin({
  name: "app1",
  remotes: {
    app2: "app2@http://localhost:3002/remoteEntry.js",
  },
  shared: {
    react: "react",
    "react-dom": "react-dom",
    [`lodash-${require("lodash").VERSION}`]: "lodash",
  },
}),

远程应用 webpack.config.js

new ModuleFederationPlugin({
  name: "app2",
  filename: "remoteEntry.js",
  exposes: {
    "./Example": "./src/Example",
  },
  shared: {
    react: "react",
    "react-dom": "react-dom",
    [`lodash-${require("lodash").VERSION}`]: "lodash",
  },
}),

官方代码实例

** 注意:经实验发现程序始终会寻找当前应用下的相应版本。**

方案二

将不同版本的依赖作为依赖进行导出,供其他应用使用。

webpack.config.js

new ModuleFederationPlugin({
    name: "app2",
    library: { type: "var", name: "app2" },
    filename: "remoteEntry.js",
    exposes: {
      "./Button": "./src/Button",
      "./ModernComponent": "./src/ModernReactComponent",
      "./newReact": require.resolve("react"),
      "./newReactDOM": require.resolve("react-dom"),
    },
    shared: [
    "react-dom",
   "react"
  ],
}),

自动共享依赖

将所有依赖项添加为共享模块,版本推断自package.json的依赖关系中 ,使用版本来自package.json,依赖项将自动使用最高可用包。在联合应用程序中,基于package.json,联合应用程序中可能存在多个不同的版本

  • 注意,这不会影响嵌套路径,如“lodash/pull”

  • 请注意,这将禁用对这些包的某些优化

  • 可能会导致bundle大小问题

** 注意:经实验发现程序始终会寻找当前应用下的相应版本。**

webpack.config.js

const deps = require("./package.json").dependencies;


new ModuleFederationPlugin({
    name: "app1",
    filename: "remoteEntry.js",
    remotes: {
      app2: "app2@http://localhost:3002/remoteEntry.js",
    },
    exposes: {
      "./Button": "./src/Button",
    },
    shared: {
      ...deps,
      react: {
        singleton: true,
      },
      "react-dom": {
        singleton: true,
      },
    },
  }),