教程 - 企业模块联合指南

2025-06-11

教程 - 企业模块联合指南

更新:2022年4月17日

请参阅我的企业模块联合系列的第 2 部分,了解比下面描述的方法更简单的实现多环境设置的方法。

更新:2021年9月11日

可以完全避免硬编码 URL 和环境变量。请参阅下方 Zack Jackson 的评论,其中阐明了如何使用 Promise new Promise 在运行时推断远程位置。

企业模块联合方法

本指南适用于哪些人?

如果您所在的组织有以下要求,那么本指南可能会引起您的兴趣:

  • 多种开发环境(、、、localdevstagingprod
  • 跨多个域(URL)共享的多个应用程序

介绍

优势

模块联合是 Webpack 5 中一个令人兴奋的新功能。正如其创建者 Zack Jackson 所描述的:

模块联合允许 JavaScript 应用程序从另一个应用程序动态加载代码,并在此过程中共享依赖项。

这种强大的编排微前端架构将使组织更容易解耦其应用程序并在团队之间共享。

限制

尽管模块联合具有诸多优势,但当将其应用于环境要求更复杂的组织时,我们仍会看到其局限性。

让我们看下面的例子:

webpack.dev.js

new ModuleFederationPlugin({
  remotes: {
    FormApp: "FormApp@http://localhost:9000/remoteEntry.js",
    Header: "Header@http://localhost:9001/remoteEntry.js",
    Footer: "Footer@http://localhost:9002/remoteEntry.js",
  },
  ...
}),
Enter fullscreen mode Exit fullscreen mode

webpack.prod.js

new ModuleFederationPlugin({
  remotes: {
    FormApp: "FormApp@http://www.formapp.com/remoteEntry.js",
    Header: "Header@http://www.header.com/remoteEntry.js",
    Footer: "Footer@http://www.footer.com/remoteEntry.js",
  },
  ...
}),
Enter fullscreen mode Exit fullscreen mode

您可能首先注意到的是,URL 在 Webpack 配置中是硬编码的。虽然这种设置可以正常工作,但如果有多个应用程序分布在多个环境中,则扩展性会很差。

另一个需要考虑的因素是代码部署。如果远程应用 URL 发生变化,团队必须记住同时更改远程应用和主机应用的配置。需要对不同项目中的多个文件进行更改,这会增加生产环境中出现错误和代码崩溃的可能性。

结论

我们需要一种方法来动态地为 local 和 remote 分配适当的环境上下文entrypoints。然而,抽象出分配环境上下文的逻辑将导致模块联合无法containers在 Webpackbuild过程中知道在何处以及如何加载 remote;因为 Webpack 配置中将不再存在绝对 URL 路径。我们需要能够在环境上下文建立后动态加载 remote 应用。

高层概述

该存储库采用了几种已记录技术的修改来支持完全动态的多环境设置。

MutateRuntimePlugin.js

Module Federation Author 开发的这个插件Zack Jackson允许利用 WebpackMutateRuntime编译钩子进行publicPath动态变异。

此代码片段通过在期间初始化的变量赋值来拦截和变异devonChurch实现MutateRuntimePlugin.jspublicPathruntime

多环境架构

该讨论线程和代码示例概述了通过上述方法通过突变注入本地和远程devonChurch的方法。entrypointsruntimepublicPath

该方法还采用了配置文件,其中包含所有本地和远程URL 以及当前环境.json的全局映射。entrypoint

动态远程容器

此代码片段通过 Webpack 文档描述了在运行时动态初始化远程的公开方法containers

Webpack 配置

在实施上述技术的过程中,我gotchyas在设置更高级的 Webpack 配置时遇到了一些问题。我记录了这些问题及其修复方法,以便您可以避免这些陷阱。

项目设置

在深入研究项目代码之前,让我们简单讨论一下项目的结构和底层配置。

| dynamic-container-path-webpack-plugin (dcp)
| -----------
Enter fullscreen mode Exit fullscreen mode
| Shared Configs
| -----------
| map.config.json
| bootstrap-entries.js
Enter fullscreen mode Exit fullscreen mode
| Host / Remote
| -----------
| chunks.config.json
| * environment.config.json
| webpack.common.js
| webpack.dev.js
| webpack.prod.js
| index.html
Enter fullscreen mode Exit fullscreen mode
| Host
| -----------
| bootstrap.js
| load-component.js
Enter fullscreen mode Exit fullscreen mode
| Remote
| -----------
| bootstrap.js
Enter fullscreen mode Exit fullscreen mode

动态容器路径 webpack 插件

我的修改版本MutateRuntimePlugin.js. 可以从 进行安装publicPath并可用作插件,并在您的 Webpack 配置中进行自定义。runtimenpm

共享配置

map.config.json包含本地和远程端点 URL 的全局对象。

bootstrap-entries.jschunks根据当前环境使用正确的 URL引导 Webpack 。

主机/远程

chunks.config.jsonentrypoints是应用程序初始化所需的 Webpack 和供使用的远程应用程序命名空间的数组。

environment.config.jsonbootstrap-entries.js是一个键/值对,指示当前环境。这可以通过构建管道设置。但为了简单起见,我们将在本教程中设置环境。

Webpack 配置文件的使用,webpack-merge使我们能够减少 Webpack 样板代码(加载器、常见的 Webpack 环境配置等)。为了简化跨应用程序的配置,我们推荐使用这种架构。

index.html将包含一个脚本引用,bootstrap-entries.js以便它可以引导 Webpack chunks,从而runtime可以加载我们的联合模块。

主持人

bootstrap.js充当本地和远程代码的异步屏障。这是 Module Federation 正常工作所必需的文件。您可以在此处阅读更多相关信息。我们还将在此处设置逻辑以延迟加载我们的远程应用程序。

load-component.js代码直接摘自本指南中引用的Dynamic Remote ContainersWebpack 文档。此文件将动态加载远程应用的共享库,并与主机协商。

偏僻的

与 类似Hostbootstrap.js它作为我们本地和远程代码的异步屏障。

通过全局变量赋值改变 publicPath

publicPath关于作业选项的讨论

我们的第一步是确定一种动态变异的方法publicPath。在回顾解决方案之前,让我们先通过Webpack 文档来简要讨论一下我们的选择。

我们可以使用DefinePlugin设置环境变量来修改publicPath,但是,我们将无法轻松地扩展到具有多个环境的多个远程。

一个可行的方案是利用 Webpack 的publicPath:功能auto,根据上下文自动确定值(例如: )。我们甚至可以在动态远程示例仓库document.currentScript中实际操作。Zack Jackson

虽然此选项确实满足了我们从 webpack 配置中移除硬编码 URL的需求,但遗憾的是,现在我们需要通过 在主机中定义远程路径App.js,从而违背了将硬编码 URL 排除在代码之外的初衷。另一个缺点是style-loader它依赖于静态方法publicPath在 HTML 中内联嵌入样式,导致我们无法使用它。请参阅此 GitHub 问题线程

剩下的最后一个选项就是publicPath动态修改。下一节我们将讨论如何利用 Webpack 的复杂钩子之一,并编写一个支持publicPath运行时自定义修改的 Webpack 插件。

外包逻辑可以runtime减少硬编码的 Webpack 构建配置,减少维护,并提高配置的可重用性。

高层概述

我们可以publicPath通过引用和修改 Module Federation Author 的自定义 Webpack 插件来进行变异Zack Jackson,该插件使用MutateRuntime编译钩子进行publicPath动态变异。

首先我们来看一下完成的插件的API:

const  DynamicContainerPathPlugin =
    require('dynamic-container-path-webpack-plugin');
const  setPublicPath =
    require('dynamic-container-path-webpack-plugin/set-path');

 new DynamicContainerPathPlugin({
   iife: setPublicPath,
   entry: 'host',
 }),
Enter fullscreen mode Exit fullscreen mode

DynamicContainerPathPlugin接受两个参数。iife是一个立即调用的函数表达式,它将作为entry它的参数。

iife在插件内部执行时,它将用作查找正确环境的entry参数。当返回时,将结果值赋给 Webpack 的内部变量。keyiifeDynamicContainerPathPluginpublicPath

利用PublicPathRuntimeModule

让我们深入了解一下dynamic-container-path-plugin 的工作原理。

注意:本指南假设您已了解 Webpack 插件的基本工作原理。如需了解更多信息,请参阅此处的Webpack 文档。

首先我们调用apply(compiler)来访问Webpack的编译生命周期:

apply(compiler) {

};
Enter fullscreen mode Exit fullscreen mode

接下来,我们需要一种方法来在完成编译之前拦截 Webpack。我们可以使用make钩子来实现:

compiler.hooks.make.tap('MutateRuntime', compilation => {});
Enter fullscreen mode Exit fullscreen mode

在这个make钩子中,我们可以访问 Webpack 的编译钩子,从而创建一个新的构建。我们可以使用该runtimeModule钩子直接进行publicPath赋值,并调用自定义方法changePublicPath进行动态publicPath重新赋值:

compilation.hooks.runtimeModule.tap('MutateRuntime', (module, chunk) => {
  module.constructor.name === 'PublicPathRuntimeModule'
      ? this.changePublicPath(module, chunk)
      : false;
  });
});
Enter fullscreen mode Exit fullscreen mode

changePublicPath方法

changePublicPath调用两个方法。第一个方法使用 Webpack 在构建时设置的内部全局变量来getInternalPublicPathVariable剥离值,并仅返回内部变量。publicPath's__webpack_require__.p

getInternalPublicPathVariable(module) {
  const [publicPath] = module.getGeneratedCode().split('=');
  return [publicPath];
}
Enter fullscreen mode Exit fullscreen mode

第二种方法接受派生的setNewPublicPathValueFromRuntime内部publicPath变量作为参数。该变量使用提供给 Webpack 插件的自定义逻辑重新赋值。__webpack_require__.pgetInternalPublicPathVariable

然后在构建时publicPath新值分配给module._cachedGeneratedCode等于__webpack_require__.p我们的内部 Webpack变量。publicPath

setNewPublicPathValueFromRuntime(module, publicPath) {
  module._cachedGeneratedCode =
    `${publicPath}=${this.options.iife}('${this.options.entry}');`;
  return  module;
}
Enter fullscreen mode Exit fullscreen mode

iifeentry

在上一节中,我们介绍了该方法如何setNewPublicPathValueFromRuntime分配新publicPath值。在本节中,我们将介绍 中包含的逻辑iffe

`${publicPath}=${this.options.iife}('${this.options.entry}');`;
Enter fullscreen mode Exit fullscreen mode

让我们再次缩小到使用我们的原始 API 设置DynamicContainerPathPlugin

const DynamicContainerPathPlugin =
    require('dynamic-container-path-webpack-plugin');
const setPublicPath =
    require('dynamic-container-path-webpack-plugin/set-path');

 new DynamicContainerPathPlugin({
   iife: setPublicPath,
   entry: 'host',
 }),
Enter fullscreen mode Exit fullscreen mode

DynamicContainerPathPlugin带有通过分配的逻辑publicPathsetPublicPath但您可以修改以满足您自己的需要。

dynamic-container-path-webpack-plugin/set-path包含以下代码:

module.exports = function (entry) {
  const { __MAP__, __ENVIRONMENT__ } = window;
  const { href } = __MAP__[entry][__ENVIRONMENT__];
  const publicPath = href + '/';
  return publicPath;
};
Enter fullscreen mode Exit fullscreen mode

__MAP____ENVIRONMENT__,稍后会介绍,是我们将在运行时设置的全局变量。这些全局变量的值将被赋值给我们从jsonURL 映射(下文会介绍)中获取的数据。

entryentrypoint用作在 中查找当前的键__MAP__。是从 中提取并分配给 的href结果值,然后将其分配给 Webpack 的内部变量,正如我们在上一节中所述。__MAP__publicPathpublicPath

创建端点的全局映射

如前所述,模块联合的一个缺点是依赖于硬编码的 URL,而这些 URL 在更复杂的组织需求下扩展性较差。我们将定义一个json对象,其中包含主机和远程 URL 的全局引用,entrypoint这些 URL 将被存储库引用。

{
  "Host": {
    "localhost": {
      "href": "http://localhost:8000"
    },
    "production": {
      "href": "https://dynamic-host-module-federation.netlify.app"
    }
  },
  "RemoteFormApp": {
    "localhost": {
      "href": "http://localhost:8001"
    },
    "production": {
      "href": "https://dynamic-remote-module-federation.netlify.app"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

HostRemoteFormApp引用entrypoint我们稍后将在存储库中定义的 Webpack 名称。

每个都entrypoints包含环境 URL;key引用环境名称并property href包含硬编码的 URL。

编写脚本来引导 Chunks

支持多环境设置的关键是根据运行时的当前环境动态分配适当的端点 URL。

我们将创建一个名为的文件bootstrap-entries.js,其任务如下:

  • dynamic-container-path-webpack-plugin获取配置文件并将其分配给全局变量以供变异使用publicPath
  • 配置文件和新定义将在页面上publicPath注入本地和远程。chunks

初始设置

首先,我们将定义一个iife,以便它将立即执行index.html

(async () => {
  // our script goes here
})();
Enter fullscreen mode Exit fullscreen mode

接下来我们将设置逻辑来确定当前环境:

注意:请参阅部分中的代码片段A Quick Note on environment.config.js以了解构建管道配置。

const environment = () =>
  location.host.indexOf('localhost') > -1 ? 'localhost' : 'production';
Enter fullscreen mode Exit fullscreen mode

由于我们将引用相对于各个存储库的配置文件,因此我们有一个小函数来获取适当的基本路径:

const getBasePath = environment() == 'localhost' ? './' : '/';
Enter fullscreen mode Exit fullscreen mode

接下来,我们将获取一个名为的文件assets-mainfest.json

对于production构建,资产通常通过使用 Webpack 的contentHash功能进行缓存破坏。此文件将由生成webpack-assets-manifest,并允许我们获取,而chunks无需知道contentHash每次production构建时分配的动态生成值:

const getManifest = await fetch('./assets-manifest.json').then(response =>
  response.json()
);
Enter fullscreen mode Exit fullscreen mode

接下来,我们将定义一个const配置文件数组:

const configs = [
  `https://cdn.jsdelivr.net/gh/waldronmatt/
        dynamic-module-federation-assets/dist/map.config.json`,
  `${getBasePath}chunks.config.json`,
];
Enter fullscreen mode Exit fullscreen mode

第一个配置引用了我们之前定义的端点的全局映射。

注意:我使用jsdeliver服务map.config.json,以便bootstrap-entries.js所有存储库都能从一处引用。对于关键任务应用程序,请研究更强大的云替代方案。

第二个配置是应用程序初始化所需的数组entrypoints,以及用于消费的远程应用程序命名空间。每个存储库的配置都是唯一的,稍后会介绍。

获取配置并分配给全局变量

现在我们的实用函数和配置文件引用已经定义,下一步是获取我们的配置并将它们分配给全局定义的变量。

首先,我们将并行获取配置文件。我们希望确保在变量赋值之前获取所有配置:

const [map, chunks] = await Promise.all(
  configs.map(config => fetch(config).then(response => response.json()))
);
Enter fullscreen mode Exit fullscreen mode

接下来,我们将environment和赋值map给全局变量。这一步至关重要,因为它将用于dynamic-container-path-webpack-plugin重新赋值 的值publicPath

window.__ENVIRONMENT__ = environment();
window.__MAP__ = map;
Enter fullscreen mode Exit fullscreen mode

entrypoints从页面获取 JavaScript并注入

最后,我们将循环遍历每个chunk定义chunks.config.js并返回代码:

注意:正如我们稍后将在本节中看到的,chunks.config.js包含两个数组,其中包含对本地和远程 Webpack 的名称引用chunks

首先,我们获取所有本地数据chunks并返回代码。由于webpack-assets-manifest没有生成remoteEntry.js(模块联邦用于引导远程文件)的条目,我们仅通过名称获取。

注意: 在存储库中remoteEntry.js被视为local chunkremote

...chunks.entrypoints.map(chunk => {
    return chunk !== 'remoteEntry'
        ? fetch(`./${getManifest[`${chunk}.js`]}`)
            .then(response => response.text())
        : fetch(`${chunk}.js`).then(response => response.text());
}),
Enter fullscreen mode Exit fullscreen mode

接下来,我们将获取所有远程信息chunks并返回代码。首先,我们chunk根据当前环境为每个远程信息获取相应的端点。

然后我们使用派生的端点值并将其分配给,remoteEntry.js以便我们可以正确地获取远程。

...chunks.remotes.map(chunk => {
    const { href } = map[chunk][environment()];
    return fetch(`${href}/remoteEntry.js`).then(response => response.text());
}),
Enter fullscreen mode Exit fullscreen mode

最后,chunk我们为每个标签创建一个script标签,将返回的代码分配给它,并将其附加到页面以供执行。

.then(scripts =>
    scripts.forEach(script => {
        const element = document.createElement('script');
        element.text = script;
        document.querySelector('body').appendChild(element);
    })
);
Enter fullscreen mode Exit fullscreen mode

总的来说,我们的代码应该如下所示:

(async () => {
  const environment = () =>
    location.host.indexOf('localhost') > -1 ? 'localhost' : 'production';

  const getBasePath = environment() == 'localhost' ? './' : '/';

  const getManifest = await fetch('./assets-manifest.json').then(response =>
    response.json()
  );

  const configs = [
    `https://cdn.jsdelivr.net/gh/waldronmatt/
        dynamic-module-federation-assets/dist/map.config.json`,
    `${getBasePath}chunks.config.json`,
  ];

  const [map, chunks] = await Promise.all(
    configs.map(config => fetch(config).then(response => response.json()))
  );

  window.__ENVIRONMENT__ = environment();
  window.__MAP__ = map;

  await Promise.all([
    ...chunks.entrypoints.map(chunk => {
      console.log(`Getting '${chunk}' entry point`);
      return chunk !== 'remoteEntry'
        ? fetch(`./${getManifest[`${chunk}.js`]}`).then(response =>
            response.text()
          )
        : fetch(`${chunk}.js`).then(response => response.text());
    }),
    ...chunks.remotes.map(chunk => {
      const { href } = map[chunk][environment()];
      return fetch(`${href}/remoteEntry.js`).then(response => response.text());
    }),
  ]).then(scripts =>
    scripts.forEach(script => {
      const element = document.createElement('script');
      element.text = script;
      document.querySelector('body').appendChild(element);
    })
  );
})();
Enter fullscreen mode Exit fullscreen mode

稍后,我们将介绍如何在我们的存储库中实现代码。

关于environment.config.js

为简单起见,我们将在本教程中定义用于确定环境的逻辑bootstrap-entries.js。但是,您可能更喜欢根据构建管道来定义它。如果您是这种情况,下面是一些代码片段,您可以用来代替我们将在后续章节中介绍的环境逻辑:

environment.config.js- (将按存储库创建)

{
  "environment": "localhost"
}
Enter fullscreen mode Exit fullscreen mode

bootstrap-entries.js

const configs = [
  `${getBasePath}environment.config.json`,
    ...
]

...

const [{ environment }, ... ] = await Promise.all(
  configs.map(config => fetch(config).then(response => response.json()))
);

...

window.__ENVIRONMENT__ = environment;
Enter fullscreen mode Exit fullscreen mode

项目设置

现在是时候把学到的知识付诸实践了。由于我们讲解了具体的文件和配置,您可以参考此处的仓库。我们只讲解重要的文件和配置。

config/目录

chunks.config.json我们将在项目根目录下的文件夹中创建一个名为 的文件config。该文件包含本地和远程入口点的列表。

{
  "entrypoints": ["Host"],
  "remotes": ["RemoteFormApp"]
}
Enter fullscreen mode Exit fullscreen mode

注意A Quick Note on environment.config.js:您可以选择在此目录中定义使用构建管道的环境配置文件集。有关更多信息,请参阅 部分。

environment.config.js- (将按存储库创建)

{
  "environment": "localhost"
}
Enter fullscreen mode Exit fullscreen mode

bootstrap.js

如果您在项目中的任何地方使用静态导入,则需要设置异步边界以确保模块联合正常工作。您可以通过设置一个名为 的文件并动态导入应用程序的bootstrap.js主文件来实现。.js

import('./app.js');
Enter fullscreen mode Exit fullscreen mode

注意:要进一步了解该主题,请参考以下链接:

动态延迟加载远程容器

load-component.js在 下创建一个名为 的文件。我们将复制/粘贴Webpack 文档中关于动态远程容器 的/src/代码。此代码允许动态加载远程容器。

const loadComponent = (scope, module) => {
  return async () => {
    await __webpack_init_sharing__('default');
    const container = window[scope];
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
};

export default () => loadComponent;
Enter fullscreen mode Exit fullscreen mode

接下来,我们将复制/粘贴Webpack 文档中有关延迟加载bootstrap.js的更多代码。我们将在文件中动态导入的下方修改并实现此代码app.js

const lazyLoadDynamicRemoteApp = () => {
  const getHeader = document.getElementById('click-me');
  getHeader.onclick = () => {
    import(/* webpackChunkName: "RemoteFormApp" */ './load-component')
      .then(module => {
        const loadComponent = module.default();
        const formApp = loadComponent('FormApp', './initContactForm');
        formApp();
      })
      .catch(() => `An error occurred while loading ${module}.`);
  };
};

lazyLoadDynamicRemoteApp();
Enter fullscreen mode Exit fullscreen mode

之所以不需要硬编码 URL 就可以工作,是因为我们publicPath在运行时动态分配,获取适当的入口点,并将代码注入到页面上。

由于这包括remoteEntry.js,反过来,它又加载到我们的远程,我们可以自动访问远程范围FormApp,现在我们能够仅使用./initContactForm位于远程存储库中的相对路径成功加载它。

注意:如果您不想延迟加载应用程序并正常动态导入它们,请将上面的代码替换为以下内容bootstrap.js

import('./load-component').then(module => {
  const loadComponent = module.default();
  const formApp = loadComponent('FormApp', './initContactForm');
  formApp();
});
Enter fullscreen mode Exit fullscreen mode

引用bootstrap-entries.js文件

之前,我们设置了自定义代码来在运行时引导 Webpack 块。现在是时候在我们的代码库中引用它了,index.html正如我们在本节中介绍的那样Reference for Use in Repositories(更多信息请参阅此处)。我们将对所有仓库重复此过程。

https://cdn.jsdelivr.net/gh/waldronmatt/dynamic-module-federation-assets@1.1.1/dist/bootstrap-entries.js

<script
  preload
  src="https://unpkg.com/regenerator-runtime@0.13.1/runtime.js"
></script>
<script preload <!-- reference the bootstrap-entries.js link above -->
  src=`...`>
</script>
Enter fullscreen mode Exit fullscreen mode

我们提供的文件bootstrap-entries.js是脚本的转换和缩小版本,以支持旧版浏览器并提高性能。

注意: regenerator-runtime需要提供支持async/await

注意:我们可以使用preload这些脚本来提高页面性能。

注意:我们之前设置的硬编码 URL 的全局映射也位于dynamic-module-federation-assets仓库中(即bootstrap-entries.js所在位置)。原因是这个文件在我们所有的仓库中都是通用的。如果我们需要添加、删除或更改 URL,只需在一个位置进行一次即可。

Webpack 配置

Webpack 合并

主机和远程仓库使用 Webpack Merge 来复用通用配置,并减少需要安装的依赖项数量。在本教程中,我使用我自己的可共享配置(可在此处找到)

开发配置

我们至少需要一个开发服务器和热重载设置以及来自 Webpack 合并配置的配置默认值。

我们正在向开发服务器标头添加一个配置以忽略CORS。您可以添加可选的 linters 和任何其他所需的配置。主机和远程存储库的最终代码webpack.dev.js如下:

const commonConfig = require('./webpack.common.js');
const extendWebpackBaseConfig = require('@waldronmatt/webpack-config');
const path = require('path');
const webpack = require('webpack');

const developmentConfig = {
  devServer: {
    contentBase: path.resolve(__dirname, './dist'),
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers':
        'X-Requested-With, content-type, Authorization',
    },
    index: 'index.html',
    port: 8000,
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
};

module.exports = extendWebpackBaseConfig(commonConfig, developmentConfig);
Enter fullscreen mode Exit fullscreen mode

生产配置

我们可以利用 Webpack 的splitchunks功能来分割代码以及动态加载的远程代码和本地代码。

由于我们的远程FormApp需要额外的依赖项,我们可以告诉 Webpack 将属于库的代码拆分到单独的文件中。

cacheGroups: {
  vendor: {
    name:  `Vendors-${mainEntry}`,
    chunks:  'async',
    test: /node_modules/,
  },
},
Enter fullscreen mode Exit fullscreen mode

注意:代码块的名称非常重要。它必须是唯一的,以避免与远程服务器发生命名空间冲突。使用主入口点的名称以及描述代码拆分性质的命名系统(vendors在我们的例子中)可能是保持名称唯一性的好方法。

注意:如果您还记得之前的内容,为了使模块联合能够正常工作,我们需要设置一个异步边界,以便支持静态导入。现在我们所有的代码都是异步的,这意味着我们还需要将其设置chunksasync我们的配置。

我们可以重复此过程来拆分入口点之间共享的代码。主机和远程存储库的最终代码如下所示:

const commonConfig = require('./webpack.common.js');
const extendWebpackBaseConfig = require('@waldronmatt/webpack-config');
const chunks = require('./config/chunks.config.json');
const mainEntry = chunks.entrypoints[0];

const productionConfig = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          name: `Vendors-${mainEntry}`,
          chunks: 'async',
          test: /node_modules/,
          priority: 20,
        },
        common: {
          name: `Common-${mainEntry}`,
          minChunks: 2,
          chunks: 'async',
          priority: 10,
          reuseExistingChunk: true,
          enforce: true,
        },
      },
    },
  },
};

module.exports = extendWebpackBaseConfig(commonConfig, productionConfig);
Enter fullscreen mode Exit fullscreen mode

通用配置

最后,我们将设置 Webpack 和 Module Federation 正常运行所需的核心配置。

主机模块联合配置

主机将包含远程依赖项版本共享契约。我们通过声明该shared属性来实现这一点。为了方便起见,我们使用了一个可选插件,automatic-vendor-federation以便更轻松地获取版本数据并从协商过程中排除库。

const ModuleFederationConfiguration = () => {
  const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
  const packageJson = require('./package.json');
  const exclude = ['express', 'serverless-http'];

  return new ModuleFederationPlugin({
    shared: AutomaticVendorFederation({
      exclude,
      packageJson,
      shareFrom: ['dependencies'],
      jquery: {
        eager: true,
      },
    }),
  });
};
Enter fullscreen mode Exit fullscreen mode

远程模块联合配置

远程配置将包含范围namemodule在存储库中公开的相对路径,以及最后用于引导远程的远程入口点的默认名称:

const ModuleFederationConfiguration = () => {
  return new ModuleFederationPlugin({
    name: 'FormApp',
    filename: 'remoteEntry.js',
    exposes: {
      './initContactForm': './src/form/init-contact-form',
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

DynamicContainerPathPlugin

接下来我们配置DynamicContainerPathPlugin设置publicPathruntime

const DynamicContainerPathPlugin =
  require('dynamic-container-path-webpack-plugin');
const setPublicPath =
  require('dynamic-container-path-webpack-plugin/set-path');

new  DynamicContainerPathPlugin({
    iife:  setPublicPath,
    entry:  mainEntry,
}),
Enter fullscreen mode Exit fullscreen mode

基本配置

下一步是配置入口点、输出配置和剩余的插件。首先,我们将设置主入口点。引用的文件应该是bootstrap.js,它是静态导入的异步边界。

target:  'web',
entry: {
  [mainEntry]: ['./src/bootstrap.js'],
},
Enter fullscreen mode Exit fullscreen mode

输出配置的publicPath默认值为/。可以忽略该值,因为DynamicContainerPathPlugin运行时会修改该值。

output: {
  publicPath:  '/',
  path:  path.resolve(__dirname, './dist'),
},
Enter fullscreen mode Exit fullscreen mode

runtimeChunk: single

这些存储库中使用的 Webpack 合并配置已runtimeChunksingle设置为优化默认值,以便在所有生成的块之间共享运行时文件。

在撰写本文时,模块联合存在一个问题,即此设置不会清空联合容器运行时;从而导致构建中断。我们通过设置runtimeChunk为 来覆盖此问题false

optimization: {
  runtimeChunk:  false,
},
Enter fullscreen mode Exit fullscreen mode

HtmlWebpackPlugin

此插件用于生成html。我们不希望我们的js资源被复制,HtmlWebpackPlugin因为我们已经在运行时动态注入了入口点,不再需要在编译时引导它们。我们将使用它excludeChunks来实现这一点:

new  HtmlWebpackPlugin({
  filename:  'index.html',
  title:  `${mainEntry}`,
  description:  `${mainEntry} of Module Federation`,
  template:  'src/index.html',
  excludeChunks: [...chunks.entrypoints],
}),
Enter fullscreen mode Exit fullscreen mode

其他插件

我们正在添加ProvidePlugin定义 jQuery(我们主要使用这个库来测试模块联合库协商过程)。

我们还将添加包含块映射的目录的CopyPlugin复制并生成缓存破坏资产的映射。config/WebpackAssetManifest

new webpack.ProvidePlugin({
  $:  'jquery',
  jQuery:  'jquery',
}),
new CopyPlugin({
  patterns: [{ from:  'config', to:  '' }],
}),
new WebpackAssetsManifest({}),
Enter fullscreen mode Exit fullscreen mode

整个代码应该如下所示:

webpack.common.js

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const { ModuleFederationPlugin } = require('webpack').container;
const DynamicContainerPathPlugin = require('dynamic-container-path-webpack-plugin');
const setPublicPath = require('dynamic-container-path-webpack-plugin/set-path');
const chunks = require('./config/chunks.config.json');
const mainEntry = chunks.entrypoints[0];

const commonConfig = isProduction => {
  // HOST M.F. Configuration
  const ModuleFederationConfiguration = () => {
    const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
    const packageJson = require('./package.json');
    const exclude = ['express', 'serverless-http'];

    return new ModuleFederationPlugin({
      shared: AutomaticVendorFederation({
        exclude,
        packageJson,
        shareFrom: ['dependencies'],
        jquery: {
          eager: true,
        },
      }),
    });

    // REMOTE M.F. Configuration
    const ModuleFederationConfiguration = () => {
      return new ModuleFederationPlugin({
        name: 'FormApp',
        filename: 'remoteEntry.js',
        exposes: {
          './initContactForm': './src/form/init-contact-form',
        },
      });
    };
  };

  return {
    target: 'web',
    entry: {
      [mainEntry]: ['./src/bootstrap.js'],
    },
    output: {
      publicPath: '/',
      path: path.resolve(__dirname, './dist'),
    },
    optimization: {
      runtimeChunk: false,
    },
    plugins: [
      new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
      }),
      new CopyPlugin({
        patterns: [{ from: 'config', to: '' }],
      }),
      new WebpackAssetsManifest({}),
      new HtmlWebpackPlugin({
        filename: 'index.html',
        title: `${mainEntry}`,
        description: `${mainEntry} of Module Federation`,
        template: 'src/index.html',
        excludeChunks: [...chunks.entrypoints],
      }),
      new DynamicContainerPathPlugin({
        iife: setPublicPath,
        entry: mainEntry,
      }),
    ].concat(ModuleFederationConfiguration),
  };
};

module.exports = commonConfig;
Enter fullscreen mode Exit fullscreen mode

结论

如果你已经读到这里,谢谢你,恭喜你!你可以在以下代码库中找到所有代码:

虽然有很多内容需要介绍,但最终结果是一个支持完全动态、多环境配置的解决方案。

总结一下,本指南涵盖了以下内容:

  • 模块联合的高层概述及其优点和缺点。
  • 问题和期望的技术成果的摘要。
  • 已确定的各种解决方案和项目结构的概述。
  • 如何publicPath动态地改变和引导块。
  • 核心项目文件和 Webpack 配置概述。

最后,我们将回顾使用此方法的优点和缺点,以便您做出明智的决定,确定这是否适合您:

优点:

  • 更轻松地支持多个测试环境,而无需增加捆绑配置的复杂性(硬编码 URL)
  • URL 仅需在一个位置更新一次(map.config.js)。
  • 环境上下文设置可以推迟到构建管道。
  • 尽管远程和主机容器在运行时初始化,您仍然可以利用模块联合的所有当前功能(库协商等)
  • 大多数配置代码(包括 Webpack 配置)都可以捆绑并重用为其他项目的脚手架。
  • 继续利用高级 Webpack 功能以及模块联合,包括代码拆分、延迟加载、缓存清除、webpack 合并支持等。

缺点

  • 存储库依赖于单个全局 URL 映射文件。需要仔细规划以确保将停机时间降至最低。
  • 重命名入口点需要在项目级别 ( chunks.config.js) 和全局级别 ( map.config.json) 进行更新。任何引用远程的主机应用程序也需要chunks.config.js更新其引用。
  • 所涵盖的配置增加了相当大的复杂性,并且需要团队对 Webpack 有更深层次的了解。

替代方法

旨在提供与上述功能类似的功能的替代方法可以在以下存储库中找到:

动态远程供应商共享示例

具有运行时环境变量的模块联合动态远程

具有供应商共享和同步导入的动态远程示例

补充阅读

我想分享一些有助于巩固我对模块联合的理解的参考资料:

模块联合概述和设置指南

近期 API 变更概述

详细回顾最近的 API 变化

模块联合中静态导入如何提升

依赖版本协商/合同指南

API 选项列表及其说明

模块联合播客概述

模块联盟播客幻灯片参考

企业微前端分析

执照

麻省理工学院

鏂囩珷鏉ユ簮锛�https://dev.to/waldronmatt/tutorial-a-guide-to-module-federation-for-enterprise-n5
PREV
我如何利用 Python、搜索推文 API 和 Twilio 解决纽约停车问题
NEXT
使用 Vrite 在 Dev.to 上更好地撰写博客 - 用于技术内容的无头 CMS