Webpack: 构建微前端应用

Module Federation 通常译作“模块联邦”,是 Webpack 5 新引入的一种远程模块动态加载、运行技术。MF 允许我们将原本单个巨大应用按我们理想的方式拆分成多个体积更小、职责更内聚的小应用形式,理想情况下各个应用能够实现独立部署、独立开发(不同应用甚至允许使用不同技术栈)、团队自治,从而降低系统与团队协作的复杂度 —— 没错,这正是所谓的微前端架构。

An architectural style where independently deliverable frontend applications are composed into a greater whole —— 摘自《Micro Frontends》。

英文社区对 Webpack Module Federation 的响应非常热烈,甚至被誉为“A game-changer in JavaScript architecture”,相对而言国内对此热度并不高,这一方面是因为 MF 强依赖于 Webpack5,升级成本有点高;另一方面是国内已经有一些成熟微前端框架,例如 qiankun。不过我个人觉得 MF 有不少实用性强,非常值得学习、使用的特性,包括:

  • 应用可按需导出若干模块,这些模块最终会被单独打成模块包,功能上有点像 NPM 模块;
  • 应用可在运行时基于 HTTP(S) 协议动态加载其它应用暴露的模块,且用法与动态加载普通 NPM 模块一样简单;
  • 与其它微前端方案不同,MF 的应用之间关系平等,没有主应用/子应用之分,每个应用都能导出/导入任意模块,等等

请添加图片描述

  • 参考:Webpack 5 之 模块联合(Module Federation)

简单示例

Module Federation 的基本逻辑是一端导出模块,另一端导入、使用模块,实现上两端都依赖于 Webpack 5 内置的 ModuleFederationPlugin 插件:

  1. 对于模块生成方,需要使用 ModuleFederationPlugin 插件的 expose 参数声明需要导出的模块列表;
  2. 对于模块使用方,需要使用 ModuleFederationPlugin 插件的 remotes 参数声明需要从哪些地方导入远程模块。

接下来,我们按这个流程一步步搭建一个简单的 Webpack Module Federation 示例,首先介绍一下示例文件结构:

MF-basic
├─ app-1
│  ├─ dist
│  │  ├─ ...
│  ├─ package.json
│  ├─ src
│  │  ├─ main.js
│  │  ├─ foo.js
│  │  └─ utils.js
│  └─ webpack.config.js
├─ app-2
│  ├─ dist
│  │  ├─ ...
│  ├─ package.json
│  ├─ src
│  │  ├─ bootstrap.js
│  │  └─ main.js
│  ├─ webpack.config.js
├─ lerna.json
└─ package.json
  • 提示:为简化依赖管理,示例引入 lerna 实现 Monorepo 策略,不过这与文章主题无关,这里不做过多介绍

其中,app-1app-2 是两个独立应用,分别有一套独立的 Webpack 构建配置,类似于微前端场景下的“微应用”概念。在本示例中,app-1 负责导出模块 —— 类似于子应用;app-2 负责使用这些模块 —— 类似于主应用。

我们先看看模块导出方 —— 也就是 app-1 的构建配置:

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

module.exports = {
  mode: "development",
  devtool: false,
  entry: path.resolve(__dirname, "./src/main.js"),
  output: {
    path: path.resolve(__dirname, "./dist"),
    // 必须指定产物的完整路径,否则使用方无法正确加载产物资源
    publicPath: `http://localhost:8081/dist/`,
  },
  plugins: [
    new ModuleFederationPlugin({
      // MF 应用名称
      name: "app1",
      // MF 模块入口,可以理解为该应用的资源清单
      filename: `remoteEntry.js`,
      // 定义应用导出哪些模块
      exposes: {
        "./utils": "./src/utils",
        "./foo": "./src/foo",
      },
    }),
  ],
  // MF 应用资源提供方必须以 http(s) 形式提供服务
  // 所以这里需要使用 devServer 提供 http(s) server 能力
  devServer: {
    port: 8081,
    hot: true,
  },
};
  • 提示:Module Federation 依赖于 Webpack5 内置的 ModuleFederationPlugin 实现模块导入导出功能。

作用模块导出方,app-1 的配置逻辑可以总结为:

  1. 需要使用 ModuleFederationPluginexposes 项声明哪些模块需要被导出;使用 filename 项定义入口文件名称;
  2. 需要使用 devServer 启动开发服务器能力。

使用 ModuleFederationPlugin 插件后,Webpack 会将 exposes 声明的模块分别编译为独立产物,并将产物清单、MF 运行时等代码打包进 filename 定义的应用入口文件(Remote Entry File)中。例如 app-1 经过 Webpack 编译后,将生成如下产物:

MF-basic
├─ app-1
│  ├─ dist
│  │  ├─ main.js
│  │  ├─ remoteEntry.js
│  │  ├─ src_foo_js.js
│  │  └─ src_utils_js.js
│  ├─ src
│  │  ├─ ...
  • main.js 为整个应用的编译结果,此处可忽略;
  • src_utils_js.jssrc_foo_js.js 分别为 exposes 声明的模块的编译产物;
  • remoteEntry.jsModuleFederationPlugin 插件生成的应用入口文件,包含模块清单、MF 运行时代码。

接下来继续看看模块导入方 —— 也就是 app-2 的配置方法:

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

module.exports = {
  mode: "development",
  devtool: false,
  entry: path.resolve(__dirname, "./src/main.js"),
  output: {
    path: path.resolve(__dirname, "./dist"),
  },
  plugins: [
    // 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境
    new ModuleFederationPlugin({
      // 使用 remotes 属性声明远程模块列表
      remotes: {
        // 地址需要指向导出方生成的应用入口文件
        RemoteApp: "app1@http://localhost:8081/dist/remoteEntry.js",
      },
    }),
    new HtmlWebpackPlugin(),
  ],
  devServer: {
    port: 8082,
    hot: true,
    open: true,
  },
};

作用远程模块使用方,app-2 需要使用 ModuleFederationPlugin 声明远程模块的 HTTP(S) 地址与模块名称(示例中的 RemoteApp),之后在 app-2 中就可以使用模块名称异步导入 app-1 暴露出来的模块,例如:

// app-2/src/main.js
(async () => {
  const { sayHello } = await import("RemoteApp/utils");
  sayHello();
})();

到这里,简单示例就算是搭建完毕了,之后运行页面,打开开发者工具的 Network 面板,可以看到:
请添加图片描述

其中:

  • remoteEntry.jsapp-1 构建的应用入口文件;
  • src_utils_js.js 则是 import("RemoteApp/utils") 语句导入的远程模块。

总结一下,MF 中的模块导出/导入方都依赖于 ModuleFederationPlugin 插件,其中导出方需要使用插件的 exposes 项声明导出哪些模块,使用 filename 指定生成的入口文件;导入方需要使用 remotes 声明远程模块地址,之后在代码中使用异步导入语法 import("module") 引入模块。

这种模块远程加载、运行的能力,搭配适当的 DevOps 手段,已经足以满足微前端的独立部署、独立维护、开发隔离的要求,在此基础上 MF 还提供了一套简单的依赖共享功能,用于解决多应用间基础库管理问题。

依赖共享

上例应用相互独立,各自管理、打包基础依赖包,但实际项目中应用之间通常存在一部分公共依赖 —— 例如 Vue、React、Lodash 等,如果简单沿用上例这种分开打包的方式势必会出现依赖被重复打包,造成产物冗余的问题,为此 ModuleFederationPlugin 提供了 shared 配置用于声明该应用可被共享的依赖模块。

例如,改造上例模块导出方 app-1 ,添加 shared 配置:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      filename: `remoteEntry.js`,
      exposes: {
        "./utils": "./src/utils",
        "./foo": "./src/foo",
      }, 
      // 可被共享的依赖模块
+     shared: ['lodash']
    }),
  ],
  // ...
};

接下来,还需要修改模块导入方 app-2,添加相同的 shared 配置:

module.exports = {
  // ...
  plugins: [
    // 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境
    new ModuleFederationPlugin({
      // 使用 remotes 属性声明远程模块列表
      remotes: {
        // 地址需要指向导出方生成的应用入口文件
        RemoteApp: "app1@http://localhost:8081/dist/remoteEntry.js",
      },
+     shared: ['lodash']
    }),
    new HtmlWebpackPlugin(),
  ],
  // ...
};

之后,运行页面可以看到最终只加载了一次 lodash 产物(下表左图),而改动前则需要分别从导入/导出方各加载一次 lodash(下表右图):

添加 shared改动前
imgimg

注意,这里要求两个应用使用 版本号完全相同 的依赖才能被复用,假设上例应用 app-1 用了 lodash@4.17.0 ,而 app-2 用的是 lodash@4.17.1,Webpack 还是会同时加载两份 lodash 代码,我们可以通过 shared.[lib].requiredVersion 配置项显式声明应用需要的依赖库版本来解决这个问题:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      // 共享依赖及版本要求声明
+     shared: {
+       lodash: {
+         requiredVersion: "^4.17.0",
+       },
+     },
    }),
  ],
  // ...
};

上例 requiredVersion: "^4.17.0" 表示该应用支持共享版本大于等于 4.17.0 小于等于 4.18.0 的 lodash,其它应用所使用的 lodash 版本号只要在这一范围内即可复用。requiredVersion 支持 Semantic Versioning 2.0 标准,这意味着我们可以复用 package.json 中声明版本依赖的方法。

requiredVersion 的作用在于限制依赖版本的上下限,实用性极高。除此之外,我们还可以通过 shared.[lib].shareScope 属性更精细地控制依赖的共享范围,例如:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      // 共享依赖及版本要求声明
+     shared: {
+       lodash: {
+         // 任意字符串
+         shareScope: 'foo'
+       },
+     },
    }),
  ],
  // ...
};

在这种配置下,其它应用所共享的 lodash 库必须同样声明为 foo 空间才能复用。shareScope 在多团队协作时能够切分出多个资源共享空间,降低依赖冲突的概率。

requiredVersion/shareScope 外,shared 还提供了一些不太常用的 配置,简单介绍:

  • singletong:强制约束多个版本之间共用同一个依赖包,如果依赖包不满足版本 requiredVersion 版本要求则报警告:

image.png

  • version:声明依赖包版本,缺省默认会从包体的 package.jsonversion 字段解析;
  • packageName:用于从描述文件中确定所需版本的包名称,仅当无法从请求中自动确定包名称时才需要这样做;
  • eager:允许 webpack 直接打包该依赖库 —— 而不是通过异步请求获取库;
  • import:声明如何导入该模块,默认为 shared 属性名,实用性不高,可忽略。

示例:微前端

Module Federation 是一种非常新的技术,社区资料还比较少,接下来我们来编写一个完整的微前端应用,帮助你更好理解 MF 的功能与用法。微前端架构通常包含一个作为容器的主应用及若干负责渲染具体页面的子应用,分别对标到下面示例的 packages/hostpackages/order 应用:

MF-micro-fe
├─ packages
│  ├─ host
│  │  ├─ public
│  │  │  └─ index.html
│  │  ├─ src
│  │  │  ├─ App.js
│  │  │  ├─ HomePage.js
│  │  │  ├─ Navigation.js
│  │  │  ├─ bootstrap.js
│  │  │  ├─ index.js
│  │  │  └─ routes.js
│  │  ├─ package.json
│  │  └─ webpack.config.js
│  └─ order
│     ├─ src
│     │  ├─ OrderDetail.js
│     │  ├─ OrderList.js
│     │  ├─ main.js
│     │  └─ routes.js
│     ├─ package.json
│     └─ webpack.config.js
├─ lerna.json
└─ package.json

先看看 order 对应的 MF 配置:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      name: "order",
      filename: "remoteEntry.js",
      // 导入路由配置
      exposes: {
        "./routes": "./src/routes",
      },
    }),
  ],
};

注意,order 应用实际导出的是路由配置文件 routes.js。而 host 则通过 MF 插件导入并消费 order 应用的组件,对应配置:

module.exports = {
  // ...
  plugins: [
    // 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境
    new ModuleFederationPlugin({
      // 使用 remotes 属性声明远程模块列表
      remotes: {
        // 地址需要指向导出方生成的应用入口文件
        RemoteOrder: "order@http://localhost:8081/dist/remoteEntry.js",
      },
    })
  ],
  // ...
};

之后,在 host 应用中引入 order 的路由配置并应用到页面中:

import localRoutes from "./routes";
// 引入远程 order 模块
import orderRoutes from "RemoteOrder/routes";

const routes = [...localRoutes, ...orderRoutes];

const App = () => (
  <React.StrictMode>
    <HashRouter>
      <h1>Micro Frontend Example</h1>
      <Navigation />
      <Routes>
        {routes.map((route) => (
          <Route
            key={route.path}
            path={route.path}
            element={
              <React.Suspense fallback={<>...</>}>
                <route.component />
              </React.Suspense>
            }
            exact={route.exact}
          />
        ))}
      </Routes>
    </HashRouter>
  </React.StrictMode>
);

export default App;

通过这种方式,一是可以将业务代码分解为更细粒度的应用形态;二是应用可以各自管理路由逻辑,降低应用间耦合性。最终能降低系统组件间耦合度,更有利于多团队协作。除此之外,MF 技术还有非常大想象空间,国外有大神专门整理了一系列实用 MF 示例:Module Federation Examples,感兴趣的读者务必仔细阅读这些示例代码。

总结

Module Federation 是 Webpack 5 新引入的一种远程模块动态加载、运行技术,虽然国内讨论热度较低,但使用简单,功能强大,非常适用于微前端或代码重构迁移场景。

使用上,只需引入 ModuleFederationPlugin 插件,按要求组织、分割好各个微应用的代码,并正确配置 expose/remotes 配置项即可实现基于 HTTP(S) 的模块共享功能。此外,我们还可以通过插件的 shared 配置项实现在应用间共享基础依赖库,还可以通过 shared.requireVersion 等一系列配置,精细控制依赖的共享版本与范围。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/756865.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity保存玩家的数据到文件中(Unity的二进制序列化)

文章目录 文章运行环境什么是二进制序列化读写文件构造函数 自定义二进制序列化 文章运行环境 Unity2022 什么是二进制序列化 Unity中的二进制序列化是一种将游戏对象或数据结构转换为二进制格式的过程&#xff0c;以便于存储或网络传输。这使数据能够以高效的方式保存&…

鸿蒙开发设备管理:【@ohos.geolocation (位置服务)】

位置服务 说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import geolocation from ohos.geolocation;geolocation.on(‘locationChange’) on(type: ‘locationChange’, request: L…

容器进程

一、容器进程和宿主机进程的关系 容器在进程空间上和宿主机是隔离的&#xff0c;每创建一个容器&#xff0c;该容器都有一个独属的进程空间简称PID NameSpace。但是容器本质也是一个进程&#xff0c;自然是由其父进程创建的&#xff0c;这个可以使用ps aux命令验证。 | 容器视…

Leetcode - 133双周赛

目录 一&#xff0c;3190. 使所有元素都可以被 3 整除的最少操作数 二&#xff0c;3191. 使二进制数组全部等于 1 的最少操作次数 I 三&#xff0c;3192. 使二进制数组全部等于 1 的最少操作次数 II 四&#xff0c;3193. 统计逆序对的数目 一&#xff0c;3190. 使所有元素都…

冯雷老师:618大退货事件分析

近日冯雷老师受邀为某头部电商36名高管进行培训&#xff0c;其中聊到了今年618退货潮的问题。以下内容整理自冯雷老师的部分授课内容。 一、引言 随着电子商务的蓬勃发展&#xff0c;每年的618大促已成为消费者和商家共同关注的焦点。然而&#xff0c;在销售额不断攀升的同时…

MySQL之如何分析慢查询

1、一个SQL语句执行很慢&#xff0c;如何分析&#xff1f; 可使用“explain”或者“desc”命令获取MySQL如何执行select语句的信息。 语法&#xff1a;直接在select语句前加关键字 explain或desc explain select job_desc from xxl_job_info where id 1; 2、执行计划中五个重…

Python的一个非常cool的库Gradio

Python的一个非常cool的库Gradio Gradio简介 Gradio是一个开源的Python库&#xff0c;它允许用户为机器学习模型、API或任何Python函数快速构建演示或Web应用程序。Gradio的目标是简化AI模型的可视化和交互过程&#xff0c;使得即使没有前端开发背景的用户也能够轻松地创建和…

装载问题(回溯法)

#include<iostream> using namespace std; int n;//货物的数量 int c;//轮船的总的载重量 int cw;//轮船当前的载重量 int r;//货物的总重量 int w[1000];//n个货物各自的重量 int x[1000];//当前最优解 int bestx[1000];//最优解 int bestw;//货物的最优载重量 void Bac…

os实训课程模拟考试(大题复习)

目录 一、Linux操作系统 &#xff08;1&#xff09;第1关&#xff1a;Linux初体验 &#xff08;2&#xff09;第2关&#xff1a;Linux常用命令 &#xff08;3&#xff09;第3关&#xff1a;Linux 查询命令帮助语句 二、Linux之进程管理—&#xff08;重点&#xff09; &…

论文翻译 | PRCA:通过可插拔奖励驱动的上下文适配器拟合用于检索问答的黑盒大语言模型

摘要 检索问答(ReQA)任务采用检索增强框架&#xff0c;该框架由检索器和生成器组成。 生成器根据检索器检索到的文档制定答案。将大型语言模型(llm)作为生成器是有益的&#xff0c;因为它们具有先进的QA功能&#xff0c;但它们通常太大而无法根据预算限制进行微调&#xff0c;而…

RpcRrovider分发rpc服务(OnMessage和Closure回调)

目录 1.完善rpcprovider.cc的OnConnection 2.完善rpcprovider.cc的OnMessage 3.完整rpcprovider.h 4.完整rpcprovider.cc 这篇文章主要完成&#xff0c;protobuf实现的数据序列化和反序列化。 1.完善rpcprovider.cc的OnConnection rpc的请求是短连接的&#xff0c;请求一次…

Java--回顾方法的调用

1.静态方法与非静态方法 1.当二者皆为静态方式时&#xff0c;可直接类名.方法名调用其方法 2.当调用的方法是静态&#xff0c;被调用的方法为非静态时&#xff0c;调用将会报错 3.出现2情况可通过进行实例化这个类的方式进行调用&#xff0c;如图所示 4.当处于一个类下&#xf…

如何把项目文文件/文件夹)上传到Gitee(全网最细)

目录 1、首先必须要有一个Gitee官网的账号 2、点击右上角的号&#xff0c;点击新建仓库 3、按照下图步骤&#xff0c;自己起仓库名字&#xff0c;开发语言 4、点击初始化readme文件 5、在自己的电脑上选择姚上传的文件夹&#xff0c;或者文件&#xff0c;这里都是一样的&a…

新奥集团校招面试经验分享、测评笔试题型分析

一、走进新奥集团 新奥集团成立于1989年&#xff0c;总部位于河北廊坊&#xff0c;是中国领先的清洁能源企业集团。业务涵盖城市燃气、能源化工、环保科技等多个领域&#xff0c;致力于构建现代能源体系&#xff0c;提升生活品质。 二、新奥集团校招面试经验分享 新奥集团的…

哥斯拉短视频:成都柏煜文化传媒有限公司

哥斯拉短视频&#xff1a;巨兽传奇的视听盛宴 在短视频的海洋中&#xff0c;成都柏煜文化传媒有限公司 有一种特殊的存在总能吸引人们的目光&#xff0c;那就是以哥斯拉为主题的短视频。这些视频以震撼的视觉效果、扣人 ​心弦的剧情和独特的怪兽文化&#xff0c;为我们呈现了…

UE5基本操作(二)

文章目录 前言相机的移动速度修改默认地图使用初学者内容包文件夹结构 总结 前言 在我们的上一篇文章中&#xff0c;我们已经介绍了一些Unreal Engine 5&#xff08;UE5&#xff09;的基本操作。UE5是一款强大的游戏开发引擎&#xff0c;它提供了许多工具和功能&#xff0c;使…

C++进阶

C进阶 一、细节1.cout与输出缓冲区2.constexpr3.NULL和nullptr是不同的类型4.关于inline5.函数杂合用法6.const char*、char const*、char * const7.进程地址空间&#xff0c;所谓静态区常量区不准8.位运算9.多态9.1 内存切片9.2 转型9.3 构造函数和析构函数里是静态绑定9.4 dy…

如何将Hive表的分区字段插入PG表对应的时间戳字段?

文章目录 1、背景描述2、场景分析 1、背景描述 数据仓库的建设通常是为业务和决策服务的。在数仓开发的应用层阶段&#xff0c;BI可以直接从主题层/业务层取数&#xff0c;而前端需要根据具体的作图需求通过后端查询数据库 作图的指标需要根据主题层/业务层做查询计算&#xf…

C#——SortedList 排序列表详情

SortedList 排序列表 SortedList 类用来表示键/值对的集合&#xff0c;这些键/值对按照键值进行排序&#xff0c;并且可以通过键或索引访问集合中的各个项。 我们可以将排序列表看作是数组和哈希表的组合&#xff0c;其中包含了可以使用键或索引访问各项的列表。如果您使用索…

文章分享 | seq-DAP-seq和ChIP-seq联合检测揭示花发育器官转录因子调控机制

技术简介 MADS转录因子的同源蛋白SEPALLATA3 (SEP3)和AGAMOUS (AG)是调控拟南芥花发育分化的重要DNA结合蛋白。在雌蕊发育阶段&#xff0c;SEP3和AG形成异源四聚体&#xff0c;通过识别CArG-box序列来调控基因的表达。Seq-DAP-seq技术与ChIP-seq联合使用&#xff0c;可用于分析…