webpack 5 模块联邦入门教程

原文地址:Getting Started With Federated Modules[1], by Jacob Ebey。微信公众号不支持外链添加,点击文末“阅读原文”可查看链接内容。

译注:本文将带大家快速地学会在 webpack 5 进行模块联邦功能的配置。需要说明的是,原文代码有点古旧,翻译的过程中我将代码统一转换成最新的了。

我们要构建的项目

我们将构建两个单独的单页应用程序(SPA),并使用模块联邦功能在运行期间共享组件。

应用程序A将包含一个 SayHelloFromA 组件,该组件将被 Application B 消费,而 Application B 将包含一个 SayHelloFromB 组件,该组件将被 Application A 消费。具体如下:

webpack 5 模块联邦入门教程

这种架构将允许每个单页应用程序独立开发和部署,并即时接收来自其他联合应用程序(federated applications)的更新,而无需进行任何部署。

简而言之

你可以在此处找到这个示例的完整源代码:https://github.com/baooab/federated-libraries-get-started[2]

配置环境

首先,让我们配置环境。为了简单起见,我们将使用 yarn mono-repo 结构,但 Module Federation 的理念是允许团队自主操作的,在现实世界中,你的 SPA 很可能是在独立的仓库中,而不是我们这里 mono-repo 结构。

译注:没有安装 yarn 包管理器的同学,可以通过 npm install -g yarn 完成安装。

创建一个新项目文件夹,并使用以下 package.json 文件来同时运行两个 SPA:

「package.json」

{
  "name""federation-example",
  "private"true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev""wsrun --parallel dev",
    "build""yarn workspaces run build"
  },
  "devDependencies": {
    "wsrun""^5.2.4"
  }
}

现在我们将创建两个文件夹,用于存放我们的单页应用程序,这些文件夹位于名为 application-aapplication-b 的目录下,分别包含以下 package.json 文件:

「packages/application-a/package.json」

{
  "name""application-a",
  "version""1.0.0",
  "private"true,
  "scripts": {
    "dev""webpack serve",
    "build""webpack --mode=production"
  },
  "license""ISC",
  "dependencies": {
    "react""^18.2.0",
    "react-dom""^18.2.0"
  },
  "devDependencies": {
    "@babel/core""^7.21.8",
    "@babel/preset-react""^7.18.6",
    "babel-loader""^9.1.2",
    "html-webpack-plugin""^5.5.1",
    "webpack""^5.83.1",
    "webpack-cli""^5.1.1",
    "webpack-dev-server""^4.15.0"
  }
}

「packages/application-b/package.json」

{
  "name""application-b",
  "version""1.0.0",
  "private"true,
  "scripts": {
    "dev""webpack serve",
    "build""webpack --mode=production"
  },
  "license""ISC",
  "dependencies": {
    "react""^18.2.0",
    "react-dom""^18.2.0"
  },
  "devDependencies": {
    "@babel/core""^7.21.8",
    "@babel/preset-react""^7.18.6",
    "babel-loader""^9.1.2",
    "html-webpack-plugin""^5.5.1",
    "webpack""^5.83.1",
    "webpack-cli""^5.1.1",
    "webpack-dev-server""^4.15.0"
  }
}

两个 package.json 文件只有 name 属性不同,其余全一样。

项目根目录下安装依赖:

# 也会安装 mono-repo 中的依赖
> yarn

编写单页应用程序

接下来是编写我们的 SPA React 应用程序。我们需要在每个项目中创建一个 src 目录,并包含以下文件:

「packages/application-{a,b}/src/index.js」

import('./bootstrap')

「packages/application-{a,b}/src/bootstrap.jsx」

import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './app'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

我们还需要在每个包中添加一个 public 目录,包含以下 HTML 模板,承载 SPA 项目,稍后我们还会做一些修改:

「packages/application-{a,b}/public/index.html」

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
</head>
<body>
  <div id="root"></div>
</body>
</html>

现在我们可以为每个应用程序实现两个 app.jsx 文件,以容纳我们的共享组件:

「packages/application-a/src/app.jsx」

import React from 'react'

export default function SayHelloFromA({
  return <h1>Hello From Application A!</h1>
}

「packages/application-b/src/app.jsx」

import React from 'react'

export default function SayHelloFromB({
  return <h1>Hello From Application B!</h1>
}

最后,我们为每个应用程序添加配置文件 webpack.config.js

「packages/application-{a,b}/webpack.config.js」

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container

module.exports = (env, argv) => {
  const mode = argv.mode || 'development'

  return {
    mode,
    entry'./src/index',
    devServer: {
      static: path.join(__dirname, 'dist'),
      port3001,
    },
    devtool'source-map',
    resolve: {
      extensions: ['.jsx''.js''.json']
    },
    module: {
      rules: [
        {
          test/.jsx?$/,
          loader'babel-loader',
          exclude/node_modules/,
          options: {
            presets: ['@babel/preset-react']
          }
        }
      ]
    },

    plugins: [
      new HtmlWebpackPlugin({
        template'./public/index.html',
      }),
    ],
  }
}

在根目录,现在通过运行以下命令,就可以在 http://localhost:3001 和 http://localhost:3002 访问您的两个单页应用:

> yarn dev # 如果报错,执行 npm run dev

配置模块联邦

现在我们有两个独立的单页应用程序正在运行,让我们继续将每个单页应用程序作为联合容器(Federated Container)和消费者(Consumer)。我们通过利用 webpack 5 核心中的新 ModuleFederationPlugin 来实现这一点。

首先,我们将向 Application A 添加 ModuleFederationPlugin,代码如下:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
+ const { ModuleFederationPlugin } = require('webpack').container

module.exports = (env, argv) => {
  const mode = argv.mode || 'development'

  return {
    mode,
    entry: './src/index',
+    output: {
+      publicPath: 'auto',
+    },
    devServer: {
      static: path.join(__dirname, 'dist'),
      port: 3001,
    },
    devtool: 'source-map',
    resolve: {
      extensions: ['.jsx', '.js', '.json']
    },
    module: {
      rules: [
        {
          test: /.jsx?$/,
          loader: 'babel-loader',
          exclude: /node_modules/,
          options: {
            presets: ['@babel/preset-react']
          }
        }
      ]
    },

    plugins: [
+      new ModuleFederationPlugin({
+        name: 'application_a',
+        filename: 'remoteEntry.js',
+        library: { type: 'var', name: 'application_a' },
+        exposes: {
+          './SayHelloFromA': './src/app'
+        },
+        remotes: {
+          'application_b': 'application_b'
+        },
+        shared: {
+          react: { singleton: true },
+          'react-dom': { singleton: true }
+        }
+      }),
      new HtmlWebpackPlugin({
        template: './public/index.html',
      }),
    ],
  }
}

项目运行期间,指定了 Application A 将其 app 组件作为名为 SayHelloFromA 的联合模块公开给外部;同时,从 application_b 导入时,模块代码都来源于 Application B。

我们对 Application B 执行相同的操作,指定其 app 组件作为名为 SayHelloFromB 的联合模块公开给外部;同时,从 application_a 导入时,模块代码都来源于 Application A。

「packages/application-b/webpack.config.js」

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
+ const { ModuleFederationPlugin } = require('webpack').container

module.exports = (env, argv) => {
  const mode = argv.mode || 'development'

  return {
    mode,
    entry: './src/index',
+    output: {
+      publicPath: 'auto',
+    },
    devServer: {
      static: path.join(__dirname, 'dist'),
      port: 3002,
    },
    devtool: 'source-map',
    resolve: {
      extensions: ['.jsx', '.js', '.json']
    },
    module: {
      rules: [
        {
          test: /.jsx?$/,
          loader: 'babel-loader',
          exclude: /node_modules/,
          options: {
            presets: ['@babel/preset-react']
          }
        }
      ]
    },

    plugins: [
+      new ModuleFederationPlugin({
+        name: 'application_b',
+        filename: 'remoteEntry.js',
+        library: { type: 'var', name: 'application_b' },
+        exposes: {
+          './SayHelloFromB': './src/app'
+        },
+        remotes: {
+          'application_a': 'application_a'
+        },
+        shared: {
+          react: { singleton: true },
+          'react-dom': { singleton: true }
+        }
+      }),
      new HtmlWebpackPlugin({
        template: './public/index.html',
      }),
    ],
  }
}

在我们开始使用暴露的组件之前,最后一步是在运行期间,指定期望消费的容器远程入口(Remote Entries for the Containers)。我们只需向希望消费的 HTML 模板中添加一个脚本标签即可。

「packages/application-a/public/index.html」

<head>
  <!-- 加载应用程序 B 的远程入口 -->
  <script src="http://localhost:3002/remoteEntry.js"></script>    
</head>

「packages/application-b/public/index.html」

<head>
  <!-- 加载应用程序 A 的远程入口 -->
  <script src="http://localhost:3001/remoteEntry.js"></script>    
</head>

远程入口文件中是给 webpack 解析单独导入的模块使用的,很小,能避免传输不必要的信息;还负责启用项目间使用的共享库,在这种情况下,当  Application A 请求 Application B 的 SayHelloFromB 组件时,不需要额外加载 Application B 中的 React、ReactDOM 资源了,因为 Application A 中已经有了一份副本。

使用联合组件

现在我们的两个 SPA 应用程序,既是宿主容器(Container Hosts)也是消费者(Consumers),就可以开始使用共享的组件了。在 webpack 配置中,我们已经指定了容器名称为 application_aapplication_b,所以我们会从这些容器中导入组件。

从 Application A 开始,在 bootstrap.jsx 文件中可以渲染 SayHelloFromB 组件了:

import React from 'react'
import ReactDOM from 'react-dom/client'

+ import SayHelloFromB from 'application_b/SayHelloFromB'

import App from './app'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
+  <>
    <App />
+    <SayHelloFromB />
+  </>
)

Application B 类似,从 application_a 导入组件:

import React from 'react'
import ReactDOM from 'react-dom/client'

+ import SayHelloFromA from 'application_a/SayHelloFromA'

import App from './app'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
+  <>
    <App />
+    <SayHelloFromA />
+  </>
)

一些注意事项

查看 Application A 的网络日志,你会发现我们从 Application A 加载了两个文件:remoteEntry.js 文件和包含 SayHelloFromB 组件的 977.js

译注:大家本地测试时,跟原文作者这里截图的可能稍有不同。因为本文写作时,作者当时使用的 beta 版本,这块后面有过变动,但不影响大家对此概念的理解,特此说明。

webpack 5 模块联邦入门教程

第一次 Application B 时,你会注意到我们已经缓存了 Applicatino B 和 Application A 的 remoteEntrie.js 文件 。

webpack 5 模块联邦入门教程

参考资料

[1]

Getting Started With Federated Modules: https://module-federation.github.io/blog/get-started

[2]

https://github.com/baooab/federated-libraries-get-started: https://github.com/baooab/federated-libraries-get-started


原文始发于微信公众号(写代码的宝哥):webpack 5 模块联邦入门教程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/244018.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!