使用 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 的应用之间关系平等,没有主应用/子应用之分,每个应用都能导出/导入任意模块;
  • 等等。

image.png

图片摘自:《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 面板,可以看到:

image.png

其中:

  • 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

提示:示例代码已上传到:MF-micro-fe,务必 Clone 下来辅助阅读。

先看看 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) => (<Routekey={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 等一系列配置,精细控制依赖的共享版本与范围。

总结

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Boost之log日志使用

不讲理论&#xff0c;直接上在程序中可用代码&#xff1a; 一、引入Boost模块 开发环境&#xff1a;Visual Studio 2017 Boost库版本&#xff1a;1.68.0 安装方式&#xff1a;Nuget 安装命令&#xff1a; #只安装下面几个即可 Install-package boost -version 1.68.0 Install…

【MySQL】十四,MySQL 8.0的隐藏索引

在MySQL 8.0之前的版本中&#xff0c;索引只能直接删除。如果删除后发现引起了系统故障&#xff0c;又必须进行创建。当表的数据量比较大的时候&#xff0c;这样做的代价就会非常高。 在MySQL 8.0中&#xff0c;提供了隐藏索引。如果想删除某个索引&#xff0c;那么在实际删除…

【ES6复习笔记】解构赋值(2)

介绍 解构赋值是一种非常方便的语法&#xff0c;可以让我们更简洁地从数组和对象中提取值&#xff0c;并且可以应用于很多实际开发场景中。 1. 数组的解构赋值 数组的解构赋值是按照一定模式从数组中提取值&#xff0c;然后对变量进行赋值。下面是一个例子&#xff1a; con…

爬虫数据存储:Redis、MySQL 与 MongoDB 的对比与实践

爬虫的核心任务是从网络中提取数据&#xff0c;而存储这些数据是流程中不可或缺的一环。根据业务需求的不同&#xff0c;存储的选择可能直接影响数据处理的效率和开发体验。本文将介绍三种常用的存储工具——Redis、MySQL 和 MongoDB&#xff0c;分析它们的特点&#xff0c;并提…

【Python】使用匿名函数Lambda解析html源码的任意元素(Seleinium ,BeautifulSoup皆适用)

一直都发现lambda函数非常好用&#xff0c;它可以用简洁的方式编写小函数&#xff0c;无需写冗长的过程就可以获取结果。干脆利落&#xff01; 它允许我们定义一个匿名函数&#xff0c;在调用一次性的函数时非常有用。 最近整理了一些&#xff0c;lambda函数结合BeautifulSou…

Bash语言的语法

Bash语言简介与应用 Bash&#xff08;Bourne Again SHell&#xff09;是一种Unix Shell和命令语言&#xff0c;在Linux、macOS及其他类Unix系统中被广泛使用。作为GNU项目的一部分&#xff0c;Bash不仅是对早期Bourne Shell的增强&#xff0c;还引入了许多特性和功能&#xff…

Ingress-Nginx Annotations 指南:配置要点全方面解读(下)

文章目录 1.HTTP2 Push Preload2.Server Alias3.Server snippet4.Client Body Buffer Size5.External Authentication6.Global External Authentication7.Rate Limiting8.Global Rate Limiting9.Permanent Redirect10.Permanent Redirect Code11.Temporal Redirect12.SSL Passt…

互联网路由架构

大家觉得有意义和帮助记得及时关注和点赞!!! 本书致力于解决实际问题&#xff0c;书中包含大量的架构图、拓扑图和真实场景示例&#xff0c;内容全面 且易于上手&#xff0c;是不可多得的良心之作。本书目的是使读者成为将自有网络集成到全球互联网 领域的专家。 以下是笔记内…

【Flutter_Web】Flutter编译Web第三篇(网络请求篇):dio如何改造方法,变成web之后数据如何处理

前言 Flutter端在处理网络请求的时候&#xff0c;最常用的库当然是Dio了&#xff0c;那么在改造成web端的时候&#xff0c;最先处理的必然是网络请求&#xff0c;否则没有数据去处理驱动实图渲染。 官方链接 pub https://pub.dev/packages/diogithub https://github.com/c…

Spring Boot @Conditional注解

在Spring Boot中&#xff0c;Conditional 注解用于条件性地注册bean。这意味着它可以根据某些条件来决定是否应该创建一个特定的bean。这个注解可以放在配置类或方法上&#xff0c;并且它会根据提供的一组条件来判断是否应该实例化对应的组件。 要使用 Conditional注解时&#…

项目上传到gitcode

首先需要在个人设置里面找到令牌 记住自己的账号和访问令牌&#xff08;一长串&#xff09;&#xff0c;后面git要输入这个&#xff0c; 账号是下面这个 来到自己的仓库 #查看远程仓库&#xff0c;是不是自己的云仓库 git remote -v # 创建新分支 git checkout -b llf # 三步…

【Rust自学】6.4. 简单的控制流-if let

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 6.4.1. 什么是if let if let语法允许将if和let组合成一种不太冗长的方式来处理与一种模式匹配的值&#xff0c;同时忽略其余模式。 可以…

【Git学习】windows系统下git init后没有看到生成的.git文件夹

[问题] git init 命令后看不到.git文件夹 [原因] 文件夹设置隐藏 [解决办法] Win11 win10

vscode添加全局宏定义

利用vscode编辑代码时&#xff0c;设置了禁用非活动区域着色后&#xff0c;在一些编译脚本中配置的宏又识别不了 遇到#ifdef包住的代码就会变暗色&#xff0c;想查看代码不是很方便。如下图&#xff1a; 一 解决&#xff1a; 在vscode中添加全局宏定义。 二 步骤&#xff1a…

【服务器主板】定制化:基于Intel至强平台的全新解决方案

随着数据处理需求不断增长&#xff0c;服务器硬件的发展也在持续推进。在这一背景下&#xff0c;为用户定制了一款全新的基于Intel至强平台的服务器主板&#xff0c;旨在提供强大的计算能力、优异的内存支持以及高速存储扩展能力。适用于需要高性能计算、大规模数据处理和高可用…

php怎么去除数点后面的0

在PHP中&#xff0c;我们可以使用几种方法来去除数字小数点后的0。 方法一&#xff1a;使用intval函数 intval函数可以将一个数字转化为整数&#xff0c;另外&#xff0c;它也可以去除小数点后面的0。 “php $number 123.4500; $number intval($number); echo $number; // 输…

数字后端培训项目Floorplan常见问题系列专题续集1

今天继续给大家分享下数字IC后端设计实现floorplan阶段常见问题系列专题。这些问题都是来自于咱们社区IC后端训练营学员提问的问题库。目前这部分问题库已经积累了4年了&#xff0c;后面会陆续分享这方面的问题。 希望对大家的数字后端学习和工作有所帮助。 数字后端项目Floor…

【递归,搜索与回溯算法 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)

优美的排列 题目解析 算法原理 解法 &#xff1a;暴搜 决策树 红色剪枝&#xff1a;用于剪去该节点的值在对应分支中&#xff0c;已经被使用的情况&#xff0c;可以定义一个 check[ ] 紫色剪枝&#xff1a;perm[i] 不能够被 i 整除&#xff0c;i 不能够被 per…

Java中各种数组复制方式的效率对比

在 Java 中&#xff0c;数组复制是一个常见的操作&#xff0c;尤其是在处理动态数组&#xff08;如 ArrayList&#xff09;时。Java 提供了多种数组复制的方式&#xff0c;每种方式在性能和使用场景上都有所不同。以下是对几种主要数组复制方式的比较&#xff0c;包括 System.a…

视频会议是如何实现屏幕标注功能的?

现在主流的视频会议软件都有屏幕标注功能&#xff0c;屏幕标注功能给屏幕分享者讲解分享内容时提供了极大的方便。那我们以傲瑞视频会议&#xff08;OrayMeeting&#xff09;为例&#xff0c;来讲解屏幕标注是如何实现的。 傲瑞会议的PC端&#xff08;Windows、信创Linux、银河…