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

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

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

相关文章

apipost的安装和测试添加接口能否正常使用

1.进入官网&#xff0c;点击免费使用&#xff08;我是windows 64位&#xff0c;选合适自己的配置&#xff09; 2.开始安装 选仅为我安装——下一步 选择自己的安装目录——点安装 等待 运行——完成 3.apipost一些基本操作——实现添加内容 &#xff08;1&#xff09;新建接口…

《人人都是产品经理》:项目一图流

《人人都是产品经理》&#xff1a;项目一图流 项目一图流 项目一图流

FreeSWITCH 1.10.10 简单图形化界面22-JsSIP的demo测试并记录坑

FreeSWITCH 1.10.10 简单图形化界面22-JsSIP的demo测试 00 FreeSWITCH GUI界面预览01、安装FreeSWITCH GUI先看使用手册02. 使用手册在这里0、设置FreeSWITCH账号1、jssip的demo网站2、设置jssip账号并登录3、整理坑3.1 掉线问题3.11 解决3.2 呼叫问题13.21 解决13.3 呼叫问题2…

PAE:从潮流报告中提炼有效产品属性

本文将介绍PAE&#xff0c;一种用于包含 PDF格式的文本和图像的产品属性提取算法。目前大部分的方法侧重于从标题或产品描述中提取属性&#xff0c;或利用现有产品图像中的视觉信息。与之前的工作相比&#xff0c;PAE从潮流趋势报告的PDF文件中提取属性&#xff0c;提取的属性包…

spl实现循环计算

需求 需要对一批数据进行价格计算 这里面的一部分单价来自于历史记录&#xff0c;但是另外一部分的单价&#xff0c;需要边计算边存储 数据库结构 CREATE TABLE tbl_mix_trace_price (lot_id_out varchar(255) DEFAULT NULL COMMENT 产出,lot_id_in varchar(255) DEFAULT NULL…

谈一下MySQL的两阶段提交机制

文章目录 为什么需要两阶段提交&#xff1f;两阶段提交流程&#xff1f;两阶段提交缺点&#xff1f; 为什么需要两阶段提交&#xff1f; 为了保证事务的持久性和一致性&#xff0c;MySQL需要确保redo log和binlog的同步持久化。MySQL通过“两阶段提交”的机制来实现在事务提交…

小迪安全v2023 javaWeb项目

小迪安全v2023 javaWeb项目 文章目录 小迪安全v2023 javaWeb项目1. webgoat靶场1. 环境配置与docker操作 2. jwt令牌1. jwt 第四关 签名没验证空加密2. jwt 第五关 爆破签名密钥3. jwt 第八关 kid参数可控 1. webgoat靶场 1. 环境配置与docker操作 自行下载配置vmware的kali-…

《mysql篇》--查询(进阶)

目录 将查询结果作为插入数据 聚合查询 聚合函数 count sum group by子句 having 联合查询 笛卡尔积 多表查询 join..on实现多表查询 内连接 外连接 自连接 子查询 合并查询 将查询结果作为插入数据 Insert into 表2 select * from 表1//将表1的查询数据插入…

Linux开发讲课20--- QSPI

SPI 是英语 Serial Peripheral interface 的缩写&#xff0c;顾名思义就是串行外围设备接口&#xff0c;一种高速的&#xff0c;全双工&#xff0c;同步的通信总线&#xff0c;并且在芯片的管脚上只占用四根线&#xff0c;节约了芯片的管脚&#xff0c;为 PCB 的布局上节省空间…

Springcloud-消息总线-Bus

1.消息总线在微服务中的应用 BUS- 消息总线-将消息变更发送给所有的服务节点。 在微服务架构的系统中&#xff0c;通常我们会使用消息代理来构建一个Topic&#xff0c;让所有 服务节点监听这个主题&#xff0c;当生产者向topic中发送变更时&#xff0c;这个主题产生的消息会被…

多线程引发的安全问题

前言&#x1f440;~ 上一章我们介绍了线程的一些基础知识点&#xff0c;例如创建线程、查看线程、中断线程、等待线程等知识点&#xff0c;今天我们讲解多线程下引发的安全问题 线程安全&#xff08;最复杂也最重要&#xff09; 产生线程安全问题的原因 锁&#xff08;重要…

Hive笔记-6

6.2.8 聚合函数 1) 语法 count(*)&#xff0c;表示统计所有行数&#xff0c;包含null值&#xff1b; count(某列)&#xff0c;表示该列一共有多少行&#xff0c;不包含null值&#xff1b; max()&#xff0c;求最大值&#xff0c;不包含null&#xff0c;除非所有值都是null&a…

不同node版本的切换及其指定版本vue-cli脚手架下载

目录 一.清空本地已安装node.js版本 二.装nvm管理工具 三.安装指定node版本 四.使用nvm命令切换或删除指定node版本 五.在指定node版本下下载指定vue-cli脚手架 一.清空本地已安装node.js版本 1.按健winR弹出窗口&#xff0c;键盘输入cmd&#xff0c;然后敲回车。 2.输入…

win11 + ubuntu linux双系统:开机直接进入windows修复

https://zhuanlan.zhihu.com/p/666702893 这种 双系统直接进入win 的问题&#xff0c;应该属于引导坏了&#xff0c;即grub坏了。 原因&#xff1a;笔记本送修了&#xff0c;没拆掉硬盘&#xff0c;可能引导被售后搞坏了。 在win-磁盘管理中查看分区&#xff0c;linux的分区…

《SpringBoot+Vue》Chapter04 SpringBoot整合Web开发

返回JSON数据 默认实现 依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>在springboot web依赖中加入了jackson-databind作为JSON处理器 创建一个实体类对象…

学编程容易遇到的误区,请提前规避

随着互联网行业的蓬勃发展和编程技术的普及&#xff0c;越来越多的人开始对编程感兴趣。然而&#xff0c;编程学习并非一蹴而就&#xff0c;新手入门时常常会陷入误区&#xff0c;影响学习状态效率。 今天&#xff0c;我们来一起揭开编程学习常见的五大误区&#xff0c;希望能…

【融合ChatGPT等AI模型】Python-GEE遥感云大数据分析、管理与可视化及多领域案例应用

随着航空、航天、近地空间遥感平台的持续发展&#xff0c;遥感技术近年来取得显著进步。遥感数据的空间、时间、光谱分辨率及数据量均大幅提升&#xff0c;呈现出大数据特征。这为相关研究带来了新机遇&#xff0c;但同时也带来巨大挑战。传统的工作站和服务器已无法满足大区域…

如何激活typora

话不多说&#xff0c;直接上流程。 1、先下载最新的typora安装包&#xff0c;并安装成功。 Typora的安装 &#xff08;1&#xff09;官网下载&#xff1a;https://typora.xnxxxk.cn/index.html?qhclickid777dd93d58082a46 2、下载百度云我上传的两个exe&#xff0c;不大&…

嵌入式实验---实验五 串口数据接收实验

一、实验目的 1、掌握STM32F103串口数据接收程序设计流程&#xff1b; 2、熟悉STM32固件库的基本使用。 二、实验原理 1、STM32F103R6能通过查询中断方式接收数据&#xff0c;每接收到一个字节&#xff0c;立即向对方发送一个相同内容的字节&#xff0c;并把该字节的十六进…

Dahlia Hart: Stylized Casual Character(休闲角色模型)

此包包含两个发型和两个服装&#xff0c;每个都有多种颜色选择。每个发型都适合与物理资源一起使用&#xff0c;并包含各种表情和音素混合形状。 下载&#xff1a;​​Unity资源商店链接资源下载链接 效果图&#xff1a;