一 为什么要使用微前端框架?
我们不能为了用而用某种技术,先要了解一下这种技术有什么优势,解决了什么问题。为什么要使用微前端架构,与单体架构相比,它有以下优势:
- 降低项目复杂度:随着应用规模的增大,前端代码变得庞大和复杂,单体架构逐渐发展成巨石应用,复杂度与日俱增。微前端将大型应用拆分为多个独立的子应用,每个子应用可以单独开发、测试和部署,降低了子应用之间的耦合度,使得子应用代码更容易管理和维护。降低了项目整体复杂度。
- 更自由的技术栈选择:单体应用通常需要统一技术栈,这限制了技术的自由灵活度。微前端框架允许不同子应用使用不同的技术栈,各团队可以根据需要选择最适合的技术工具,提升了技术栈选择的自由度。
- 更快的部署和发布:单体应用发展成巨石应用之后,发布和部署耗时较长(笔者参与的一个单体应用项目,打包出来的文件有30多M,打包部署比较耗时,一般至少需要十来分钟,不可谓慢),因为任何小的改动都需要重新构建和发布整个应用。微前端架构下,每个子应用可以独立部署和更新,显著的缩短了打包和部署时间,包括本地开发环境修改代码之后的热更新时间。
- 更好的用户体验:如果涉及到技术栈升级改造,微前端框架可以实现渐进式迁移,将老旧系统逐步替换为新的系统,用户体验不会受到大的干扰,且可以持续优化。
如果你的项目存在以上应用场景,那么使用微前端框架让你更好地应对规模增长带来的复杂性挑战,降低项目的复杂度,提升技术栈选择的自由度、部署灵活性和维护便利性, 带来更好的用户体验 。
二 qiankun使用方法
知道了微前端框架的好处,现在我们来看看qiankun微前端框架的用法。qiankun 是一个基于 single-spa 的微前端框架,它允许在一个页面中运行多个独立开发、独立部署的前端应用。qiankun 框架的几个组成要素是:
2.1. 子应用的注册和加载
qiankun 允许你注册多个子应用,并根据不同的路由规则加载这些子应用。每个子应用配置参数中的name
是子应用的名称,必须唯一(使用场景之一是:当配置样式隔离模式为experimentalStyleIsolation
,给所有样式添加一个限定范围的样式类名时会用到) ;entry
是子应用项目本地运行地址,container
是子应用的容器(子应用嵌入到主项目id为xx的地方),activeRule
是应用激活时的路由规则(子应用路由),这四个参数是必填参数,还有其它参数,这里就不一一介绍了。
javascript
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:9000',
container: '#subapp-container',
activeRule: '/app-vue',
},
// 可以注册更多的子应用
]);
start();
2.2. 路由劫持
qiankun 使用路由劫持技术来决定何时加载和卸载子应用。它通过监听浏览器的 URL 变化,判断当前的 URL 是否匹配某个子应用的 activeRule
,从而触发对应的加载或卸载操作。猜测实现原理应该和单页应用的路由原理差不多。
2.3. JS沙箱机制和样式隔离
JS沙箱机制
为了隔离各个子应用,防止它们互相干扰,qiankun 提供了沙箱机制。沙箱机制确保每个子应用运行在一个独立的上下文中,避免全局变量和样式污染。qiankun 提供了两种沙箱(默认的sandbox配置是快照沙箱):
- 快照沙箱:基于快照的机制,每次子应用卸载时,保存当前全局状态,再次加载时恢复。
- Proxy 沙箱:基于 ES6 的 Proxy 特性,对全局对象进行代理,确保每个子应用拥有独立的全局环境。
两种沙箱的应用场景:
- 快照沙箱:适用于多实例、频繁切换和需要状态恢复的场景,尽管有性能开销。
- 单实例沙箱:适用于单实例、生命周期较长和性能敏感的场景,虽然状态恢复能力较弱。
样式隔离
-
严格模式(strictStyleIsolation) 默认情况下的沙箱设置无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。 开启严格样式隔离模式。qiankun 会为每个微应用的容器包裹上一个shadow dom节点,确保微应用的样式不会对全局造成影响。
-
实验模式(experimentalStyleIsolation) qiankun 还提供了另一个实验性的样式隔离特性, 当设置为 true 时,qiankun 会改写子应用所添加的样式,给所有样式规则增加一个样式前缀限定其作用范围。
配置方法如下:
js
import { start } from 'qiankun';
// 启动子应用
start(
{
// 详细配置沙箱
sandbox: {
// 会为每个微应用的容器包裹上一个shadow dom 节点
strictStyleIsolation: true, // 启用严格样式隔离
// 如果为true,会为所有样式规则增加一个前缀选择器规则来限定其影响范围
experimentalStyleIsolation: false,
},
// 其它配置...
});
2.4. 生命周期管理
在 Qiankun 框架中,每个子应用需要按照规范暴露一定的生命周期函数,以便主应用能够在不同阶段正确加载和卸载子应用。这些生命周期函数主要包括 bootstrap
、mount
和 unmount
,有时还包括 update
(可选)。这些函数是子应用与主应用通信的关键点,确保子应用能够在正确的时机执行相应的逻辑。
子应用必须暴露的生命周期函数
- bootstrap:子应用的初始化函数。在子应用加载时,只执行一次。这个函数通常用于初始化全局变量、设置全局状态等操作。
- mount:子应用的挂载函数。在每次进入子应用时执行。这个函数通常用于渲染子应用的界面,将子应用的内容挂载到 DOM 中。
- unmount:子应用的卸载函数。在每次离开子应用时执行。这个函数通常用于销毁子应用的实例,清理全局状态和事件监听器等操作。
- update(可选):子应用的更新函数。这个函数在子应用需要更新时被调用,通常用于处理子应用的动态属性或状态变化。
js
export async function bootstrap() {
console.log('vue app bootstraped');
}
export async function mount(props) {
console.log('vue app mount', props);
}
export async function unmount() {
console.log('vue app unmount');
}
// update 函数(可选)
export async function update(props) {
console.log('更新子应用', props);
// 在这里处理动态属性或状态变化
}
2.5. 主应用与子应用通信
qiankun 提供了一套机制,使主应用和子应用之间能够进行通信。这包括通过 props 传递数据,以及使用全局状态管理工具(如 Redux 或 Pina/Vuex)进行状态共享。
2.6 qiankun的工作流程
- 注册子应用:在主应用中注册所有子应用,并指定每个子应用的入口、挂载容器和激活规则。
- 路由劫持:qiankun 劫持浏览器的路由变化,判断当前 URL 是否匹配某个子应用的激活规则。
- 加载子应用:如果 URL 匹配某个子应用,qiankun 加载该子应用的资源(HTML、CSS、JS),并在指定的容器中挂载子应用。
- 运行子应用:调用子应用的生命周期钩子函数,执行子应用的初始化和渲染逻辑。
- 卸载子应用:当 URL 变化时,如果不再匹配当前子应用,qiankun 卸载该子应用,调用其卸载钩子函数,清理子应用的资源。
三 动手实践
光说不练假把式。让我们动手用qiankun创建一个vue3和react子应用,并能让两个子应用进行通信,证明我们确实会用qiankun微前端框架了。
3.1 创建主应用
用create-vite
脚手架创建一个vue3
主应用,在主应用中安装qiankun
sql
npx create-vite mian-app --template vue
cd main-app
pnpm add qiankun
在主应用的入口文件src/main.js
中,注册和启动两个子应用, 实现子应用之间通信的方式是:在主应用中创建全局共享状态,因为qiankun会给每个子应用自动添加setGlobalState
方法,每个子应用可以通过此方法修改全局状态。现在问题是,怎么让子应用获取到全局状态最新值。有人说可以在子应用中使用onGlobalStateChange
方法监听全局状态的变化。可以是可以,但效率太低。我们只是想在子应用每次加载的时候,同步一下最新的全局状态数据。经过思考,发现可以定义一个getGlobalState
方法,获取最新的全局状态值,在注册子应用时,通过props属性传递给每个子应用,每个子应用在mount
事件里调用getGlobalState
方法就能获取到全局状态最新值。
js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { registerMicroApps, start, initGlobalState } from "qiankun";
const app = createApp(App);
app.use(router);
app.mount("#main-app");
// 初始化 state
let initState = { count: 0 };
const actions = initGlobalState(initState);
// 在主应用中监听贡献数据的变化
actions.onGlobalStateChange((state) => {
initState = state;
}, true);
const getGlobalState = () => initState;
// 注册子应用
registerMicroApps([
{
// 子应用的名称,必须唯一。
name: "react-app",
// 子应用项目本地运行地址
entry: "//localhost:8100",
// 子应用的容器(子应用嵌入到主项目id为container的地方)
container: "#container",
activeRule: "/react",
//子应用激活时的路由规则(子应用路由)
props: { getGlobalState },
},
{
name: "vue-app",
entry: "//localhost:8200",
container: "#container",
activeRule: "/vue",
props: { getGlobalState },
},
]);
start({ sandbox: { experimentalStyleIsolation: true } });
在App.vue中添加子应用导航和挂载节点
html
<template>
<li><router-link to="/react">react微应用</router-link></li>
<li><router-link to="/vue">vue微应用</router-link></li>
<!-- 挂载子应用,id要与注册子应用时的容器id相对应 -->
<div id="container"></div>
</template>
<script>
export default {
name: "App",
};
</script>
<style></style>
创建主应用的路由文件main-app\src\router\index.js
, 内容如下: 因为前面已经在App.vue
文件中添加了子应用dom挂载节点, 所以这里/react
和/vue
这两个路由的页面都设置成空壳,无需真正的创建文件,只是为了遵循vue-router
定义路由的规则而已,不报错而已。
js
import { createRouter, createWebHistory } from "vue-router";
import App from "../App.vue";
const routes = [
{
path: "/",
component: App,
},
{
path: "/react",
// 仅用来占位,未实际用到
component: { template: "<template></template>" },
},
{
path: "/vue",
component: { template: "<template></template>" },
},
];
const router = createRouter({
history: createWebHistory("/"),
routes,
});
export default router;
修改package.json中的脚本命令,在每条指令后面加个:main
, 用于区分应用,防止应用多了,不好辨识,容易搞混。
json
{
// ...
"scripts": {
"start:main": "vite",
"build:main": "vite build",
"preview:main": "vite preview"
},
}
3.2 创建React子应用
用create-vite
脚手架创建一个React
子应用,并安装必要的依赖(要添加一个依赖包vite-plugin-qiankun
)。为什么这里选择用create-vite
而非create-react-app
脚手架创建React应用? 因为create-react-app
默认使用的编译工具webpack
,没有create-vite
脚手架默认的编译工具vite
快。天下武功,唯快不破。
bash
npx create-vite react-app --template react
cd react-app
pnpm add vite-plugin-qiankun
在 src/main.jsx
入口函数中导出 qiankun 生命周期函数,子应用有两种打开方式,一个通过qiankun
主应用加载,而是独立运行,两种方式的初始化内容不一样,要通过qiankunWindow.__POWERED_BY_QIANKUN__
判断一下是以哪种方式打开的,执行对应的初始化流程。
js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/dist/helper";
const initQianKun = () => {
renderWithQiankun({
// 把要用的写在前面
mount(props) {
render(props);
},
bootstrap() {},
unmount() {},
});
};
const render = (props = {}) => {
// 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
const { container, setGlobalState, getGlobalState } = props;
const appDom = container ? container : document.getElementById("root");
ReactDOM.createRoot(appDom).render(
<React.StrictMode>
<App setGlobalState={setGlobalState} getGlobalState={getGlobalState} />
</React.StrictMode>
);
};
// 判断当前应用是否在主应用中
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render();
修改vite.config.js
, 修改子应用的端口号(须与注册时的端口保持一致), 另外要加一项server.origin
的配置,解决本地开发,主应用加载子应用时,图片显示不出来的问题。
js
import path from "path";
import { defineConfig } from "vite";
import qiankun from "vite-plugin-qiankun";
export default defineConfig({
// react-app' 是子应用名,与主应用注册时保持一致
// useDevMode 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响
plugins: [vue(), qiankun("react-app", { useDevMode: true })],
server: {
port: 8100,
open: true,
// 解决本地开发环境,主应用访问子应用时,子应用图片加载不出来的问题
origin: "http://localhost:8100",
headers: {
"Access-Control-Allow-Origin": "*",
},
},
// ...
});
在package.json中,修改脚本命令命令,区分应用:
json
{
// ...
"scripts": {
"start:react": "vite",
"build:react": "vite build",
"preview:react": "vite preview"
},
}
3.3 创建Vue3子应用
用create-vite
脚手架创建一个Vue3
子应用
bash
npx create-vite vue3-app --template vue
cd vue3-app
pnpm add vite-plugin-qiankun
修改 Vue3
子应用代码, 在 src/main.js
中导出 qiankun
生命周期函数,逻辑与创建React
子应用十分相似, 这里有一点要说明一下,就是要修改一下项目根目录下的index.html中的根容器的id, 因为用create-vite
创建的vue
项目,页面根容器的id
都是app
,为了使各个项目的id
不产生冲突,要修改一下根容器的id
,与app.mount(container ? container.querySelector("#vue3-app") : "#vue3-app");
这句的id保持一致。
js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { store } from "./store";
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/es/helper";
let app = null;
const render = (props = {}) => {
const { container, setGlobalState, getGlobalState } = props;
if (getGlobalState) {
store.count = getGlobalState().count;
}
app = createApp(App, { setGlobalState });
app.use(router);
app.mount(container ? container.querySelector("#vue3-app") : "#vue3-app");
};
const initQianKun = () => {
renderWithQiankun({
mount(props) {
render(props);
},
bootstrap() {
console.log("vue3 app bootstraped");
},
unmount() {
app.unmount();
app._container.innerHTML = "";
app = null;
},
});
};
// 如果不是通过qiankun启动的,则单独渲染
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render();
修改vite.config.js
,这里只列出与React
子应用配置有差别的部分
js
export default defineConfig({
// vue-app' 是子应用名,与主应用注册时保持一致
// useDevMode 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响
plugins: [vue(), qiankun("vue-app", { useDevMode: true })],
server: {
port: 8200,
origin: "http://localhost:8200", // 子应用引入到主应用之后,子应用中的图片在主应用下加载不出来、找不到,需要将origin设置成子应用本地运行地址
},
});
在package.json中添加如下命令:
json
{
// ...
"scripts": {
"start:vue3": "vite",
"build:vue3": "vite build",
"preview:vue3": "vite preview"
},
}
3.4 启动应用
在根目录下,打开三个终端,依次执行下面的命令,启动主应用与react和vue两个子应用
bash
cd main-app && yarn start:main
cd react-app && yarn start:react
cd vue3-app && yarn start:vue3
运行效果如下:可以看到,两个子应用切换正常,子应用之间可以进行通信,React
子应用的图片也加载正常,至此,我们已经掌握了qiankun
框架的用法,是不是感觉很有收获。
3.5 线上部署
本地开发跑通之后,还要考虑微前端的部署问题。这样才算打通了全流程。如果你使用 Nginx
网页服务器,可以参考以下 Nginx 配置示例:
bash
server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/your/main-app/dist;
# 防止使用history路由时,刷新页面报404
try_files $uri /index.html;
}
location /react/ {
alias /path/to/your/react-app/dist/;
try_files $uri /index.html;
}
location /vue/ {
alias /path/to/your/vue-app/dist/;
try_files $uri /index.html;
}
}
这一部分我没有亲测,可能会有点小坑,需要自己填一下。
最后
笔者还用过Nx微前端框架,qiankun
与Nx
微前端框架相比,有一点明显不如Nx
框架设计理念好,Nx
微前端框架,每个子应用的npm
依赖包,ts
, prettier
, eslint
, stylelint
, commitlint
, postcss
, jest
, vite
等的基本配置都是共享的,每个子应用可以基础配置的基础上定制本应用的一些特有属性,而qiankun
微前端框架,所用应用的依赖都是独立的,灵活是灵活,但我觉得,开发者更倾向把每个项目公共的配置提取出到子应用之外进行复用,这样能减少一些重复文件和配置,使项目更好维护。当然你也可能有不同的意见,只要是基于理性和事实,我也赞同你的意见。技术就是用来讨论和对比的,谁说的言之有理,我就会按照正确的道理去做。另外,本文的qiankun模版项目已上传到码云,如果你也对qiankun微前端框架感兴趣,欢迎下载学习交流。