现代 WEB 开发中,越来越多的 IT 技术厂使用微前端
。所谓微前端,简单点就是多个单独构建子应用可以形成一个统一的应用程序。 这些单独的构建之间不存在依赖关系,因此可以单独开发和部署它们。
微前端简介
微前端现有的落地方案可以分为三类:
- 自组织模式
- 基座模式
- 模块加载模式
与基座模式相比,模块加载模式没有中心容器,这就意味着,我们可以将任意一个微应用当作项目入口,整个项目的微应用与微应用之间相互串联,打破项目的固定加载模式,彻底释放项目的灵活机动性,这样的模式,也被称为去中心化模式
。
其实这个方案在微前端的架构理念中早已提及,但直到 2020 年 10 月 Webpack 5 正式发布之后才被真正落地应用。
因为 Webpack 5 带来了一个全新特性:Module Federation
(模块联邦),这是我们使用模块加载模式实现微前端架构的核心特性。
今天,我们来看看 Module Federation
的基本使用,然后再通过解读源码的方式,带你深入了解 Webpack 5 实现微前端的工作原理,以及实战中常见的应用场景,详细介绍如何使用模块联邦落地微前端架构。
Module Federation 是什么
在官方文档中,关于 Module Federation
的动机中,有这样一段介绍:
注:如果你感兴趣,可以点击这里查看英文原文,以获取更多信息。
Module Federation
中文直译为“模块联邦”,为了方便我们这里简称为 MF
。如果你去 Webpack 官方文档中查看,最多可以从前面的“动机”中看到模糊的解释,而对于“模块联邦”准确的定义,其实并没有给出。
但是,根据 “动机”的描述,不难看出,MF
实际想要做的事,便是把多个无相互依赖、单独部署的应用合并为一个。
通俗点讲,MF
提供了能在当前应用中加载其他应用的能力。
所以,在 MF
中,如果一个模块想要载入其他模块:
- 就需要一个引入的动作;
- 同样如果想让其他模块使用,就需要一个导出的动作。
对此,可以引出下面两个概念
-
expose
:导出应用,被其他应用导入 -
remote
:引入其他应用
一个模块既可以导出给其他模块使用,又可以导入一个其他模块,这与“基座模式”完全不同。
要知道,无论是 single-spa
还是 qiankun
,加载不同模块,都需要有一个容器中心来承载;而在 MF
中,没有且也不需要容器中心。
总之
鉴于 MF
的能力,我们完全可以实现一个去中心化的应用部署群:
多个微应用单独部署在各自的服务器中,而每个微应用都可以引用其他应用,也能被其他应用导入使用,即每个应用都可以导出又导入,也就没有了容器中心的概念。
Module Federation 如何使用
下面我们再来说说具体的操作方法。
在本讲开篇我们说过,Module Federation
是 Webpack 5 中新增的特性,所以,我们需要安装对应版本的 Webpack 及所需的工具。
因为是多个应用之间互相导入导出,因此,我们这里需要创建至少两个应用,来展示相关配置操作,然后分别在两个应用下安装相关工具,这里我以 remoteApp
(用来引入子模块的父应用) 和 exposeApp
(用来导出模块的子应用) 两个应用为例进行展示。
exposeApp
(用来导出模块的子应用)项目基础配置
基础配置内容如下
使用 MF
,需要在配置文件中引入 ModuleFederationPlugin
,从名字就可以看出这是一个插件。因为是内置插件,所以也不需要单独安装,直接通过 require
关键字引入即可,这和一个普通插件的引入方式并没有区别。
const path = require("path");
const Mfp = require("webpack").container.ModuleFederationPlugin;
const deps = require("../package.json").dependencies;
module.exports = {
// mode 工作模式
mode: "development",
// 服务器
devServer: {
port: 4000,
},
// 插件
plugins: [
new Mfp({
// 当前微应⽤名称
name: "studentProject",
// 对外提供的打包后的⽂件名(引⼊时使⽤)
filename: "student.js",
// 暴露的应用内具体模块
exposes: {
// 格式 名称:代码路径
"./studentModule": path.join(__dirname, "../src/App"),
},
// 共享依赖
shared: {
...deps,
antd: { singleton: true },
react: {
singleton: true,
},
"react-dom": {
singleton: true,
},
},
}),
],
};
因为导出应用中的模块是具体可选的,因此需要将导出的模块进行单独文件的打包。
参数
filename
就是指定具体导出的模块文件名;name
参数则代表当前导出的应用名称;exposes
参数是一个对象,在这里具体指定哪个模块需要导出,对象中的属性表示具体导出模块的名字,值则是指定具体导出模块的路径及文件名。
总体来说,我们导出的是一个微应用,而组成微应用的,便是当前应用下的某些模块,可以是一个,也可以指定多个。
其他详细配置可以参考module-federation-examples 源码
需要注意的是,为了防止开发服务器的端口冲突,我这里将两个应用端口分别设置了 4000
及 4001
。
remoteApp
(用来引入子模块的父应用)项目基础配置
WebPack 配置
const Mfp = require("webpack").container.ModuleFederationPlugin;
const deps = require("../package.json").dependencies;
module.exports = {
plugins: [
new Mfp({
name: "userProject",
// 导⼊远程子应用模块
remotes: {
// 导⼊后给模块起个别名:“微应⽤名称@地址/导出的⽂件名”
remoteStudentProject: "student@http://localhost:4000/student.js",
},
// 共享依赖
shared: {
...deps,
antd: { singleton: true },
react: {
singleton: true,
},
"react-dom": {
singleton: true,
},
},
}),
],
};
如果只作为导入其他应用的微前端配置,其实非常简单,只需要在 remotes
中具体配置要导入的应用即可,具体的导入规则是:
导入应用别名:' 微应⽤名称@应用远程地址/导出的⽂件名 '
业务代码配置
导入的相关配置完成后,接下来如何在应用中使用导入的内容呢?我们在 src/index.js
中引入使用,具体代码如下:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
// 该组件是动态加载的
const Home = React.lazy(() => import("@/pages/Home"));
const About = React.lazy(() => import("@/pages/About"));
// MF导入
// import("子应用别名/子应用路径"))
const ChildApp = React.lazy(() => import("remoteStudentProject/studentModule"));
import store from "@/redux/store";
import "./index.less";
const App = () => (
<Router>
<div>
<ul>
<li>
<Link to='/'>Home</Link>
</li>
<li>
<Link to='/about'>About</Link>
</li>
<li>
<Link to='/ChildApp'>ChildApp</Link>
</li>
</ul>
<hr />
<React.Suspense fallback={<div>loading...</div>}>
<Switch>
<Route exact path='/'>
<Home />
</Route>
<Route path='/about'>
<About />
</Route>
{/*远程子应用 */}
<Route path='/ChildApp'>
<ChildApp />
</Route>
</Switch>
</React.Suspense>
</div>
</Router>
);
ReactDOM.render(<App />, document.getElementById("root"));
通过上面的代码你能够发现,引入微应用返回的是一个 Promise
,最终会返回一个 "模块对象" 的结果,
我们前面说过,MF
是去中心化的,一个微应用,既可以导出也可以导入,如果你想将 remoteApp
作为一个微应用导出,那么,你可以在配置中继续添加导出的配置选项,比如:
const Mfp = require("webpack").container.ModuleFederationPlugin;
const deps = require("../package.json").dependencies;
module.exports = {
plugins: [
new Mfp({
// 名称
name: "userProject",
// 导⼊远程子应用模块
remotes: {
// 导⼊后给模块起个别名:“微应⽤名称@地址/导出的⽂件名”
remoteStudentProject: "student@http://localhost:4000/student.js",
},
// 作为子应用还可以再次导出,同样可以被其他项目引用
exposes: {
"./userAbout": path.join(__dirname, "../src/About"),
},
// 共享依赖
shared: {
...deps,
antd: { singleton: true },
react: {
singleton: true,
},
"react-dom": {
singleton: true,
},
},
}),
],
};
最后,不管是导入还是导出,绝大多数插件模块都需要实例化对象,我们这里的 ModuleFederationPlugin
也不例外,使用它,就是将这个实例对象,放入 plugins
数组中,实例化时,在传入的对象中,设置不同的属性参数。
为了方便你记忆,我把前面用到的不同参数的含义整理在下面这张表格中:
其中,
remotes
代表导入远程模块,exposes
表示导出了当前模块,这样就完成了模块的导入和导出
这就是前面介绍的去中心化的体现——一个模块既可以导出又可以导入,不需要通过中心基座在各个微应用之间连接,任何一个微应用都可以当作一个中心,也都可以被其他模块导入。
通过以上简单的应用,我们对 MF
有了一个初步的认识,而上面的配置在 Webpack 打包时会执行怎样的操作呢?
打包后的结果代码,是如何加载远程模块的?自己的模块又是如何导出提供给其他应用导入的呢?这就需要我们阅读打包之后的代码去一探究竟了。
Module Federation 的构建解析
我们分别在 exposeApp
(用来导出模块的子应用) 和 remoteApp
(用来引入子模块的父应用) 中, 找到打包后的 dist
目录。
应用导出分析
其中 exposeApp
打包后的 student.js
文件,具体如下所示:
moduleMap
:通过exposes
生成的模块集合。
应用导入分析
而最关键的则是导入部分,在 remoteApp
中,我们找到打包后的 bundle.js
文件,其中具体代码如下:
我们能到 Promise
方式的代码加载
其实 Promise
的作用就是加载了指定 URL
路径的 em.js
,这就是我们导出的模块文件名。
- 其中
__webpack_require__.l
函数作用就是读取URL
路径,然后动态生成script
标签并插入到代码中
现在已经可以读取到远程模块的内容了,那如何加载指定的模块代码呢?
继续往下看,我们能够看到 webpack_require.f.remotes
的处理:
-
首先,
MF
会让 Webpack 以filename
作为文件名生成文件,并将具体代码打包到xxx,bundle.js
文件中。 -
其次,文件中以
var
的形式暴露了一个名为name
的全局变量,其中包含exposes
中配置的内容。 -
最后,先通过
remote
的init
方法将自身写入remote
中,再通过get
获取remote
中expose
的组件; -
而作为
remote
时,先判断是否有可用的共享依赖。若有,则加载这部分依赖;如果没有,则加载自身依赖。
Module Federation 的应用场景
英雄也怕无用武之地,让我们看看 MF
的应用场景有哪些。
最明显的就是微前端的应用,通过 remote
和 expose
可以将一个应用作为微应用导入导出,微应用之间相互独立,也可以互相导入导出,没有中心基座的限制。而由 YY 业务中台 Web 前端组团队
自主研发的 EMP 微前端方案
就是基于 MF
的能力而实现的。
再就是资源复用,以减少编译体积,可以将多个应用的通用组件进行单独部署,通过 MF
的功能在运行时引入到其他项目中,这样组件代码就不会编译到项目中,同时也能满足多个项目同时使用的需求,一举两得。
总结
我们通过对 Module Federation
特性的解读,简单了解了 Webpack 通过插件的方式导入导出一个模块,实现一个微前端架构应用。
从一定程度上来说,Module Federation
是目前去中心化模式唯一落地的技术,通过简单的配置就能够很轻松地完成一个微前端架构的基本模型。而去中心化的方案,让我们脱离了固定的中心基座,极大地增加了项目的灵活性。
但是,如果换一个角度想,没有了统一管理的基座中心,每一个微应用的管理维护就显得极其重要了,这也对我们开发者团队提出了挑战。此外,随着项目数量和规模越来越大,一个项目下的微应用必然增加,如果管理不到位,极有可能带来致命的混乱。
最后,也请你思考一下,你现在的项目是否适合做去中心化的微前端架构的升级呢?欢迎在评论区留言交流哦。
思考
- webpack 设置了
publicPath:"auto"
会有什么效果 - 当子项目单独抽离 CSS 时,在主项目中引用,可能会找不到 CSS 文件吗?如何解决
Module Federation
的导入导出与 ES6 的导入导出有什么区别- 如何搭建
Module Federation
并用于远程部署环境
最后
文章浅陋,欢迎各位看官评论区留下的你的见解!
觉得有收获的同学欢迎点赞,关注一波!
往期好文
- 15 张前端高清知识地图,强烈建议收藏
- 二维码扫码登录是什么原理
- 让我告诉你一些强无敌的 NPM 软件包
- 最全 ECMAScript 攻略
- 前端开发者应该知道的 Centos/Docker/Nginx/Node/Jenkins 操作(? 长文)
参考文档
- 精读《Webpack5 新特性 - 模块联邦》
- webpack 官方文档-动态远程容器
- webpack 5 模块联邦实现微前端疑难问题解决
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!