为什么需要微前端
设想如果你有一个维护了很久的项目A,有着相当的体量,现在业务需要创建一个新的项目B,并且把A集成到B中,应该怎么设计?在B中把A重构一遍吗?
再设想有一个规划的产品,体量很大,需要拆分成多个子业务,每个子业务之间耦合很少,每个子业务分配给不同的团队负责,这种场景应该怎么设计?
上面场景举例表现了巨石应用业务场景,巨石应用有几个特点:
- 体量巨大,一个大应用内含多个子应用。
- 子应用之间比较独立。(因为单独的一个应用很难增长为一个巨石的规模,不断有新的独立的子页面加入,就容易形成一个巨石)
- 不同子应用之间由不同团队负责。
巨石应用给开发带来一些问题:
- 逻辑耦合,牵一发动全身
- 整体构建和整体部署很慢,开发体验差
- 不同团队维护协作成本高
微前端就是巨石应用的解决方案。主要思想就是把巨石应用拆分。
微前端解决方案
对于巨石应用,微前端的解决方法是:
- 拆分成多个子应用。
- 主应用根据url路由加载子应用。
并且具有几个特点
- 技术栈无关。
- 子应用单独部署单独构建。
- 运行时隔离。
如何落地微前端
微前端应用开发思路
首先对微前端技术方案技术选型,接入微前端框架
通常使用微前端框架实现一个微前端应用有以下几个步骤:
- 创建主应用(也叫基座应用)和子应用
- 主应用注册子应用,其实就是告诉框架几个事情,这样主应用就可以在切换路由时候正确加载子应用:
- 有哪些子应用
- 子应用资源地址
- 子应用和路由之间的关系
- 子应用生命周期管理:不同框架方案不同,例如qiankun是在子应用导出几个固定的生命周期钩子,主应用在相应时机会调用子应用的生命周期钩子。
- 共享状态:不同框架方案不同,如qiankun是通过两种方式来处理不同微前端之间的共享状态:自定义hook函数和props传递。
qiankun
qiankun上手很容易,主要有3步
- 子应用导出生命周期钩子
/**
* 微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
- 子应用打包
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置
// 导出为umd,这样主应用才能访问到子应用导出的信息。
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_${packageName}`,
},
};
- 主应用注册子应用
import { registerMicroApps, start } from 'qiankun';
// 注册子应用
// 主要包括子应用访问地址、挂载容器、路由
// 这样一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
Module Federation和EMP
Module Federation实现微前端
模块联邦(Module Federation)出现的动机:多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。module federation —— webpack 官方
Webpack模块联邦(Webpack Module Federation)是 Webpack 5 中引入的一项新功能,它允许不同的 Webpack构建 之间共享代码并动态加载依赖项。具体来说,它允许将应用程序拆分成多个独立的 Webpack构建(或称为远程应用程序),这些构建可以在运行时共享代码和依赖项。
MF支持打包一个子应用并暴露模块,然后主应用打包时候注册子应用信息,这样主应用内部就可以访问子应用暴露的模块了。
除此之外MF还支持共享模块,类似externals,多个应用共享的模块可以单独生成,提升加载性能。
MF最核心的是ModuleFederationPlugin
插件,不管是主应用还是子应用,都用该插件打包。
使用MF构建微前端应用步骤:
- 打包子应用,暴露主应用需要的模块
- 打包主应用,注册子应用信息
- 主应用中导入子应用并调用其暴露的模块
示例
// 子应用打包
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
// 子应用名称,主应用注册子应用时候会用到
name: "microFrontEnd1",
// 打包出的文件名
filename: "remoteEntry.js",
// 暴露出的模块,<访问路径>:<文件路径>
exposes: {
"./MicroFrontEnd1Index": "./src/index",
},
}),
],
};
// 主应用打包
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
// 主应用名称
name: "container",
// 注册子应用
// <主应用中使用子应用的别名>: <子应用名称(子应用使用MF打包时候指定的)>@<子应用部署地址>
remotes: {
microFrontEnd1: "microFrontEnd1@http://localhost:8081/remoteEntry.js",
},
}),
],
};
// 主应用导入子应用,会异步加载子应用并执行
import "microFrontEnd1/MicroFrontEnd1Index";
刚刚提到MF还支持共享模块
plugins: [
new ModuleFederationPlugin({
// 声明共享模块
shared: {
react: { eager: true, singleton: true },
"react-dom": { eager: true, singleton: true },
"place-my-order-assets": {eager: true, singleton: true},
}
})
]
缺点
- 不提供路由控制等微前端方案,只是一个代码共享方案。
- 对环境要求略高,需要使用 webpack5,旧项目改造成本大。
- 远程模块 typing 的问题。
- 对代码封闭性高的项目,依旧需要做npm那一套管理和额外的拉取代码,还不如npm复用方便。
- 拆分粒度需要权衡,虽然能做到依赖共享,但是被共享的lib不能做tree-shaking,也就是说如果共享了一个lodash,那么整个lodash库都会被打包到shared-chunk中。虽然依赖共享能解决传统微前端的externals的版本一致性问题。
- webpack为了支持加载remote模块对runtime做了大量改造,在运行时要做的事情也因此陡然增加,可能会对我们页面的运行时性能造成负面影响。
- 运行时共享也是一把双刃剑,如何去做版本控制以及控制共享模块的影响是需要去考虑的问题。
EMP
EMP是基于Module Federation的微前端框架。
参考文章
微前端到底是什么?
2020年你必须要会的微前端 -(实战篇)_微前端有必要学吗?-CSDN博客
微前端的6种方式
从阿里QianKun看前端沙箱隔离_qiankun legacysandbox-CSDN博客
面试官:你真的懂微前端吗? 手写一个 mini qiankun!
【微前端】手把手教你从0到1实现基于Webpack5 模块联邦(Module Federation)的微前端~ - 掘金