webpack之模块联合(翻译)

5,636 阅读8分钟

原文:webpack.docschina.org/concepts/mo…

动机

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

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

低级概念

我们区分本地和远程模块。本地模块是正常模块,是当前构建的一部分。远程模块是不属于当前构建的模块,并在运行时从所谓的容器加载。

加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口点之间的下一个块加载操作中。如果没有块加载操作,就不可能使用远程模块。

块加载操作通常是import()调用,但较旧的构造类似require.ensure或require([…])也受支持。

容器是通过容器入口创建的,该条目公开对特定模块的异步访问。访问外露通道分为两个步骤:

  • 加载模块(异步)
  • 执行(同步)。

第1步将在块加载期间完成。第2步将在与其他(本地和远程)模块交互期间完成。这样,求值顺序不受从本地模块到远程模块的转换或其他方式的影响。

可以嵌套一个容器。容器可以使用其他容器中的模块。容器之间的循环依赖也是可能的。

重写

容器可以将选定的本地模块标记为“可覆盖”。容器的使用者能够提供“覆盖”,即替换容器的一个可重写模块的模块。当用户提供本地模块时,容器的所有模块将使用替换模块而不是本地模块。当消费者不提供替换模块时,容器的所有模块将使用本地模块。

容器将以一种方式管理可重写的模块,当这些模块被使用者重写时,这些模块不需要被下载。这通常是通过将它们分成不同的块来实现的。

另一方面,替换模块的提供者只提供异步加载功能。它允许容器仅在需要时加载替换模块。当容器没有请求替换模块时,提供程序将以一种根本不需要下载的方式管理替换模块。这通常是通过将它们分成不同的块来实现的。

“name”用于标识容器中可重写的模块。

重写的提供方式与容器公开模块的方式类似,分为两个步骤:

  • 加载模块(异步)
  • 执行(同步)。

当使用嵌套时,为一个容器提供覆盖将自动覆盖嵌套容器中具有相同“名称”的模块。

必须在加载模块之前提供容器重写。在初始块中使用的重写只能由不使用Promises的同步模块重写覆盖。一旦执行,可重写项就不再是可重写的。

高级概念

每个生成充当容器,也将其他生成用作容器。通过这种方式,每个构建都可以通过从其容器加载任何其他公开的模块来访问它。

共享模块是可以重写并作为嵌套容器的重写提供的模块。它们通常指向每个构建中的同一个模块,例如同一个库。

packageName选项允许设置包名称以查找所需的版本。默认情况下,将为模块请求自动推断,如果应禁用自动推断,请将requiredVersion设置为false。

构建块

OverridablesPlugin(low level)

此插件使特定模块“可重写”。本地API(webpack_override)允许提供覆盖。

webpack.config.js

const OverridablesPlugin = require('webpack/lib/container/OverridablesPlugin');
module.exports = {
  plugins: [
    new OverridablesPlugin([
      {
        // we define an overridable module with OverridablesPlugin
        test1: './src/test1.js',
      },
    ]),
  ],
};

src/index.js

__webpack_override__({
  // here we override test1 module
  test1: () => 'I will override test1 module under src',
});

ContainerPlugin(low level)

此插件使用指定的公开模块创建一个附加的容器入口。它还在内部使用OverridablesPlugin,并向容器的使用者公开override api。

ContainerReferencePlugin(low level)

此插件将特定引用添加到容器作为外部,并允许从这些容器导入远程模块。它还调用这些容器的重写API来为它们提供重写。本地重写(当生成也是一个容器时,通过__webpack_override__或override API)和指定的重写提供给所有引用的容器。

ModuleFederationPlugin(high level)

此插件结合了ContainerPlugin和ContainerReferencePlugin。重写和可重写合并到指定共享模块的单个列表中。

概念目标

  • 应该可以公开和使用webpack支持的任何模块类型。

  • 块加载应该并行加载所需的所有内容(web:到服务器的单程往返)。

  • 从使用者到容器的控制

    • 覆盖模块是单向操作。
    • 同级容器不能覆盖彼此的模块。
  • 概念应与环境无关。

    • 可用于web,node等等。
  • 共享中的相对和绝对请求:

    • 将始终提供,即使不使用。

    • 将相对于配置上下文.

    • 默认情况下不使用requiredVersion。

  • 共享中的模块请求:

    • 仅在使用时提供。

    • 将匹配生成中所有使用的相等模块请求。

    • 将提供所有匹配的模块。

    • 将从 package.json中提取requiredVersion在图中的这个位置。

    • 当您有嵌套的node_modules时,可以提供和使用多个不同的版本。

在带有尾随/的共享模块中请求将匹配具有此前缀的所有模块请求。

用例

每页单独生成

单个页面应用程序的每个页面都从单独的生成中的容器生成中公开。应用程序shell也是一个单独的构建,将所有页面作为远程模块引用。这样就可以单独部署每个页面。当添加新的应用程序路由时,或更新应用程序路由。应用程序shell将常用库定义为共享模块,以避免在页面构建中重复这些库。

组件库作为容器

许多应用程序共享一个公共组件库,该库可以构建为每个组件都公开的容器。每个应用程序使用组件库容器中的组件。对组件库的更改可以单独部署,而无需重新部署所有应用程序。应用程序库的最新版本自动使用组件。

动态远程容器

容器接口支持getinit方法。init是一个异步兼容方法,使用一个参数调用:共享范围对象。此对象用作远程容器中的共享作用域,并由主机提供的模块填充。可以利用它在运行时动态地将远程容器连接到主机容器。

init.js

(async () => {
  // 初始化共享作用域。使用此版本和所有远程提供的已知模块填充它
  await __webpack_init_sharing__('default');
  const container = window.someContainer; // 或者到别的地方去拿容器
  // 初始化容器,它可以提供共享模块
  await container.init(__webpack_share_scopes__.default);
  const module = await container.get('./module');
})();

容器尝试提供共享模块,但如果已使用共享模块,则将忽略警告和提供的共享模块。容器可能仍将其用作备用。

通过这种方式,您可以动态加载提供不同版本共享模块的A/B测试。T> 在尝试动态连接远程容器之前,请确保已加载容器。

init.js

function 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;
  };
}

loadComponent('abtests', 'test123');

见全面实施

故障排除

Uncaught Error: Shared module is not available for eager consumption

应用程序正在急切地执行一个作为全向主机运行的应用程序。有选项可供选择:

您可以在modulefederation的高级API中将依赖项设置为eager,它不会将模块放入异步块中,而是同步提供它们。这允许我们在初始块中使用这些共享模块。但是要小心,因为所有提供的和后备模块都会被下载。建议只在应用程序的某一点上提供它,例如shell。

我们强烈建议使用异步边界。它将拆分较大块的初始化代码,以避免任何额外的往返,并提高总体性能。

例如,您的条目如下所示:

index.js

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

让我们创造引导程序(bootstrap.js)将条目的内容归档并移动到其中,然后将引导导入入口:

index.js

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

bootstrap.js

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

下面提到的方法可以工作,但也有一些限制或缺点。

对ModuleFederationPlugin的依赖设置为eager: true

webpack.config.js

// ...
new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      eager: true,
    }
  }
});

使用bundle loader作为将依赖项设置为“eager”的替代方法。这种方法性能较差,因为它会引入额外的往返行程。

const config = {
  entry: 'bundle-loader!./bootstrap.js'
};

也可以通过模块规则进行设置

webpack.config.js

const config = {
  module: {
    rules: [
      {
        test: /bootstrap\.js$/,
        loader: 'bundle-loader',
        options: {
          lazy: true,
        },
      },
    ]
  }
};

但必须将入口点改成这样:

index.js

- import('./bootstrap');
+ import bootstrap from './bootstrap';
+ bootstrap();

Uncaught Error: Module "./Button" does not exist in container.

它可能没有说“./Button”,但错误消息看起来类似。此问题通常在从webpack beta.16升级到webpack beta.17时出现。

在ModuleFederationPlugin中。将曝光更改为:

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

Uncaught TypeError: fn is not a function

您可能缺少远程容器,请确保已添加该容器。如果您已经为您尝试使用的远程加载了容器,但仍然看到此错误,请将主机容器的远程容器文件也添加到HTML中。