背景
一个新的技术的出现,都能有效或者间接的解决原有技术的痛点。起因我们公司的项目有着大量的重复代码。不同的端中存在大量重复的组件/代码。
我们公司的业务存在两个端,员工端/企业端,首先他们是两个独立分离的项目,导致改动一小点东西,改/提交/合并都是要进行两次重复的操作,包括我们公司的流水线也是独立分开的,导致每次测试的同学也要重新起项目两次。
1. 什么是微前端?
- 微前端类似于一种微服务的架构(比如:k8s),而k8s是由多个微服务组成的复杂应用程序的绝佳平台。微前端可以将前端应用分解成一些更小、更简单能够独立开发/测试/部署的应用,能够有效的解决代码跨包复用的问题(采用Monorepo),从而提高开发效率,节约时间成本。
- 实现微前端要解决以下基本问题
- js/元素/样式
- 数据通信
2. 微前端的实现方式
2.1 iframe
- HTML 内联框架元素 (
<iframe>
),它能够将另一个 HTML 页面嵌入到当前页面中,方便快捷成本较低。 - 为什么说iframe可以实现微前端?微前端的本质就是一个系统集成多个子应用,需要考虑到如何解决css/js相互污染等问题,微前端中很重要的一步就是实现css/js的隔离,而对于iframe来说,有先天的优势,因为这些浏览器提供了原生硬隔离方案,已经统统被浏览器解决了
2.2 Module Federation
- webpack5提供的 Module Federation(联邦模块,多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。
2.3 其他
- microApp
- qiankun
- 等其他微前端框架
总结:无论是iframe,还是联邦模块,还是其他微前端框架等,没有最好的框架,各有优劣。只有最适合业务,最适合当下团队的框架,接下来就重点讲解iframe/联邦模块,这种较“轻”就能实现微前端的方案,
3. 项目架构图
- 我们带着解决业务的角度去衡量采用哪种最合适的方案,前者它们是独立分开的包,后者采用Monorepo实现单个仓库中管理多个项目。
3. iframe
3.1 起手式
<!-- 员工端 -->
<style>
.staff {
height: 100px;
background: pink;
}
</style>
<body>
<div class="staff">staff</div>
</body>
<!-- 企业端 -->
<style>
.enterprise {
height: 100px;
background: skyblue;
}
</style>
</head>
<body>
<div class="wrap">
<iframe width="100%" src="http://127.0.0.1:5500/1.iframe/staff.html" frameborder="0"></iframe>
<div class="enterprise">enterprise</div>
</div>
</body>
- 自己本地开启一个服务,直接应用iframe标签src属性引入即可,此时我们把员工端的代码嵌入到企业端中
- 此时我们就能简单的实现,代码复用的问题
3.2 父子应用如何通信
此时我们把staff,当作“子应用”,而应用enterprise的我们称之为“父应用”
3.2.1 staff
<script>
const staffEl = document.querySelector(".staff");
// 监听消息
window.addEventListener("message", (e) => {
const data = e.data;
const divEl = document.createElement("div");
divEl.innerHTML = `姓名:${data.name},年龄:${data.age}`;
staffEl.appendChild(divEl);
});
</script>
3.2.2 enterprise
<script>
const btnEl = document.querySelector(".send");
const iframeEl = document.querySelector("iframe");
// 发送数据
btnEl.addEventListener("click", () => {
const info = { name: "ice", age: 23 };
const targetOrigin = "http://127.0.0.1:5500";
iframeEl.contentWindow.postMessage(info, targetOrigin);
});
</script>
- postMessage/message 父应用发送数据/子组件接受数据,具体API用法查看MDN文档
4. iframe的特点
4.1 优点
- iframe有天然解决css/js/元素隔离问题的优势,我们无需关心。
- 学习成本低,难度低
4.2 缺点
-
ui样式问题
- 样式存在差异,会有空白区域,如果要求居中显示,还要使用resize自动居中,比较麻烦
-
丢失了返后退/前进的按钮
-
慢
- 每次进入,浏览器都要重新加载资源,还会导致一些不必要的代码执行
4.3 总结
- 如果业务只是单独toB,针对企业的后台管理系统,其实采用iframe无伤大雅,不需要追求极致的ui/性能等问题,就是一个不断取舍的过程
5. Module Federation
它允许多个 webpack 构建一起工作。 从运行时的角度来看,多个构建的模块将表现得像一个巨大的连接模块图。 从开发者的角度来看,模块可以从指定的远程构建中导入,并以最小的限制来使用。
5.1 前置知识
Monorepo
在版本控制系统中,Monorepo(“mono”意为“单一”,“repo”是“存储库”的缩写),单个仓库中管理多个项目
将多个项目集成到一个仓库下,共享工程配置,同时又快捷地共享模块代码,成为趋势,这种代码管理方式称之为 MonoRepo。
Learn
- lerna是可以实现Monorepo的开发工具,用于管理包含多个软件包的js项目
- lerna可以 单包/多包 运行打包
- 将大型代码仓库分割成多个独立版本化的 软件包(package)对于代码共享来说非常有用。
- 优势:
- 代码共享,自动Link
- 安装依赖,如果存在相同版本包,优先安装到根目录
5.2 Learn介绍
命令介绍
// 初始化项目
npx lerna init
// 输出目录结构
├── packages //存放源代码
├── lerna.json
├── package.json
其中packages
中存放多个子包(多应用),即上方提到的staff
,enterprise
两个端的代码,为了更接近企业项目,这里采用creare-react-app + craco
创建这两个项目,实现组件的复用/通信
// 目录输出
.
├── lerna.json
├── package-lock.json
├── package.json
└── packages
├── enterprise
│ ├── README.md
│ ├── craco.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── src
│ │ ├── ...
│ └── tsconfig.json
└── staff
├── README.md
├── craco.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── ...
└── tsconfig.json
创建项目,可以直接采用进入到packages
文件夹中,直接采用cli
命令,也可以采用lerna create staff
创建文件夹,再组织你的目录结构
如何运行/打包 项目
// package.json
"scripts": {
"start:staff": "lerna run start --scope staff",
"start:enterprise": "lerna run start --scope enterprise",
"start": "lerna run --parallel start",
"build:staff": "lerna run build --scope staff",
"build:enterprise": "lerna run build --scope enterprise",
"build": "lerna run --parallel build"
}
- 定义脚本,在scripts中的脚本,会去
node_modules/.bin
中寻找可执行文件,运行/打包项目,相当于npx lerna run ...
parallel
并行的执行脚本
5.3 起手式
首先要了解两个概念
- host:可以使用别的模块,可以把它想象成消费者
- remote:可以被别的模块所使用,可以把它想象成商家
- 一个模块,可以成为host/remote/两者兼任
本质上,它就是一个plugin,ModuleFederationPlugin
,插件中有几个重要的参数
- name 应用的名称/唯一,(消费的时候会使用到)
- filename 文件到名称(chunk的名称)
- exposes 导出的模块,只有导出的模块才会被打包到chunk中
- remotes 加载远程模块
- shared 用来共享依赖,比如host中采用
react
,远程的模块也是react
,那么配置该属性,则优先采用本地的包,否则采用远程应用的包
staff 员工端 (商家)
- 我们以员工端为基座,把它当作
remote
(商家),可以让项目enterprise
所使用(即它为消费者)
// craco.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// 端口号
devServer: {
port: 3002,
},
webpack: {
configure: {
output: {
// 极为重要
publicPath: "auto",
},
},
plugins: {
add: [
new ModuleFederationPlugin({
name: "staff",
filename: "staffEntry.js",
exposes: {
"./Button": "./src/Button.tsx",
"./Report": "./src/Report.tsx",
},
shared: ["react", "react-dom"],
}),
],
},
},
};
- publicPath
- 对于外部加载的文件/资源,如果指定了一个错误的值,则在加载这些资源时会收到 404 错误。
- ModuleFederationPlugin的属性,其中导出了
Button
和Report
组件,其表现形式为,一个独立chunk
(即staffEntry
),n个expose chunk
,供其他消费者消费,本质上就是通过类似于CDN的方式,动态加载该模块,热插拔效果,其表现形式如图所示
enterprise 企业端 (消费者)
// craco.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
devServer: {
port: 3001,
},
webpack: {
plugins: {
add: [
new ModuleFederationPlugin({
name: "enterprise",
filename: "enterpriseEntry.js",
remotes: {
staff: "staff@http://localhost:3002/staffEntry.js",
},
}),
],
},
},
};
remotes
, 即引入该地址下的chunk
import React from "react";
import Button from "staff/Button";
import Report from "staff/Report";
function App() {
return (
<div style={{ height: "300px", backgroundColor: "pink" }}>
<h1>enterprise</h1>
<Button />
<Report />
</div>
);
}
export default App;
- 其中Button/Report组件完美的被服复用到enterprise中。
- 加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个 chunk 的加载操作中。如果没有 chunk 加载操作,就不能使用远程模块,所以需要动态加载根组件(具体实现查看项目源代码)
实现效果如下:
- 我们可以清楚的看见,我在
enterprise
中使用了staff
中的组件,而其本质就是动态加载了staff
中的资源,其资源路径为http://localhost:3002/static/js/...
,3002端口正是staff
的资源
6. Module Federation特点
6.1 优点
- 采用Monorepo可以方便/快捷/高效的共享代码块,可以让跨应用间真正做到模块共享
- 相同版本依赖提升到顶层只安装一次,节省磁盘内存
- 部署可以一次性并行,部署多个项目
- 只是组件的复用,ui样式/数据通信 可以动态的传入,可以通过props进行定制化
- webpack5的内置模块,更加轻量级
6.2 缺点
git clone
下来,速度会比单个的慢,版本控制也会比原先繁琐一些- 多个项目代码都在一个仓库中,remote出错,会影响到其他host
- 相比iframe来说,有一定学习成本,但是基于 Webpack 的生态,学习成本、改造成本、实施成本都比较低
7. 结语
停更了一段时间,也非常庆幸有人私信我催我更新,995的工作有一些疲惫,在闲暇的时刻也给自己充充电,希望看到这篇文章的你们也是,热爱生活,努力工作。
最后附上github
项目链接,希望可以帮助到大家~
原文链接:https://juejin.cn/post/7235237072439935031 作者:上山人