微前端解决方案:qiankun
微前端解决方案:qiankun
qiankun 简介
微前端是一种将前端应用分解为更小、更易管理的独立部分的架构模式。每个部分可以由不同的团队独立开发、测试和部署,使用不同的技术栈,最终组合成一个完整的应用。 qiankun
是基于 single-spa 封装的微前端框架,提供了更加完善的微前端解决方案。它解决了 single-spa 在实际应用中的一些痛点,如样式隔离、JS 沙箱、预加载等。
优势
- 技术栈无关:不同子应用可以使用不同的技术栈
- 独立开发部署:各团队可以独立工作,提高开发效率
- 渐进式升级:可以逐步迁移老项目,降低重构风险
- 团队自治:每个团队对自己的模块拥有完全控制权
qiankun 的核心特性
- 基于 single-spa:享受 single-spa 生态的同时,提供更完善的 API
- 技术栈无关:任意技术栈的应用均可使用/接入
- HTML Entry 接入方式:像使用 iframe 一样简单
- 样式隔离:自动隔离子应用样式,避免样式污染
- JS 沙箱:确保子应用之间、子应用与主应用间的 JS 隔离
- 资源预加载:在浏览器空闲时间预加载未打开的微应用资源
- umi 插件:提供@umijs/plugin-qiankun 一键切换成微前端架构
核心原理解析
HTML Entry
qiankun 采用 HTML Entry 的方式接入子应用,相比 JS Entry 有以下优势:
1
2
3
4
5
6
7
8
9
// HTML Entry方式
registerMicroApps([
{
name: "react-app",
entry: "//localhost:7100",
container: "#subapp-viewport",
activeRule: "/react",
},
]);
这种方式像在页面中嵌入一个 iframe 一样。
JS 沙箱机制
qiankun 提供了三种沙箱模式:
- SnapshotSandbox:快照沙箱,兼容性最好
- LegacySandbox:单例沙箱,基于 Proxy 实现
- ProxySandbox:多例沙箱,支持多个子应用同时运行
1
2
3
4
5
6
7
8
9
10
11
12
13
// 沙箱示例
class ProxySandbox {
constructor() {
this.proxyWindow = new Proxy(window, {
get: (target, prop) => {
// 获取属性逻辑
},
set: (target, prop, value) => {
// 设置属性逻辑
},
});
}
}
样式隔离
qiankun 提供了两种样式隔离方案:
- Scoped CSS:自动添加 data 属性进行样式隔离
- Shadow DOM:使用 Shadow DOM 实现完全隔离
实战:搭建 qiankun 微前端项目
1. 主应用搭建
首先创建主应用:
1
2
3
npx create-react-app main-app
cd main-app
npm install qiankun
配置主应用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// src/App.js
import { registerMicroApps, start } from "qiankun";
const microApps = [
{
name: "vue-app",
entry: "//localhost:8081",
container: "#subapp-container",
activeRule: "/vue",
},
{
name: "react-app",
entry: "//localhost:8082",
container: "#subapp-container",
activeRule: "/react",
},
];
// 注册微应用
registerMicroApps(microApps, {
beforeLoad: (app) => {
console.log("before load", app);
},
beforeMount: (app) => {
console.log("before mount", app);
},
afterUnmount: (app) => {
console.log("after unmount", app);
},
});
// 启动qiankun
start({
prefetch: true, // 预加载
sandbox: true, // 开启沙箱
});
function App() {
return (
<div className="App">
<header>
<nav>
<a href="#/vue">Vue应用</a>
<a href="#/react">React应用</a>
</nav>
</header>
<main id="subapp-container"></main>
</div>
);
}
export default App;
2. Vue 子应用改造
创建 Vue 子应用并进行微前端改造:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vue.config.js
const { name } = require("./package.json");
module.exports = {
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
port: 8081,
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main framework", props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
3. React 子应用改造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// config-overrides.js
const { name } = require("./package.json");
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = "umd";
config.output.jsonpFunction = `webpackJsonp_${name}`;
return config;
},
devServer: (configFunction) => {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.headers = {
"Access-Control-Allow-Origin": "*",
};
return config;
};
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
function render(props) {
const { container } = props;
ReactDOM.render(
<App />,
container
? container.querySelector("#root")
: document.querySelector("#root")
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log("[react] react app bootstraped");
}
export async function mount(props) {
console.log("[react] props from main framework", props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container
? container.querySelector("#root")
: document.querySelector("#root")
);
}
进阶配置和优化
应用间通信
qiankun 提供了多种应用间通信方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 主应用
import { initGlobalState } from "qiankun";
// 初始化全局状态
const actions = initGlobalState({
user: "qiankun",
token: "xxx",
});
// 监听全局状态变化
actions.onGlobalStateChange((newState, prev) => {
console.log("主应用观察者:", newState, prev);
});
// 子应用中
export async function mount(props) {
const { onGlobalStateChange, setGlobalState } = props;
// 监听状态变化
onGlobalStateChange((newState, prev) => {
console.log("子应用观察者:", newState, prev);
});
// 修改全局状态
setGlobalState({ user: "new user" });
}
预加载优化
1
2
3
4
5
6
7
8
9
10
11
12
13
import { registerMicroApps, start, prefetchApps } from 'qiankun';
// 注册微应用
registerMicroApps([...]);
// 启动qiankun
start();
// 手动预加载指定的微应用静态资源
prefetchApps([
{ name: 'app1', entry: '//localhost:7001' },
{ name: 'app2', entry: '//localhost:7002' },
]);
样式隔离配置
1
2
3
4
5
6
start({
sandbox: {
strictStyleIsolation: true, // 启用严格样式隔离
experimentalStyleIsolation: true, // 实验性样式隔离
},
});
常见问题和解决方案
1. 路由冲突问题
主应用和子应用都使用 hash 路由时可能出现冲突:
1
2
3
4
5
6
7
8
9
10
11
12
// 解决方案:子应用使用memory路由
// Vue Router
const router = new VueRouter({
mode: window.__POWERED_BY_QIANKUN__ ? "abstract" : "hash",
routes,
});
// React Router
const basename = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
<BrowserRouter basename={basename}>
<App />
</BrowserRouter>;
2. 公共依赖优化
避免重复打包公共依赖:
1
2
3
4
5
6
7
8
// webpack externals配置
module.exports = {
externals: {
react: "React",
"react-dom": "ReactDOM",
"react-router-dom": "ReactRouterDOM",
},
};
3. 开发环境代理配置
1
2
3
4
5
6
7
8
9
10
11
// 主应用代理配置
module.exports = {
devServer: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
},
},
},
};
部署相关
独立部署
每个子应用可以独立部署到不同的服务器:
1
2
3
4
5
6
7
8
9
10
11
const microApps = [
{
name: "vue-app",
entry:
process.env.NODE_ENV === "development"
? "//localhost:8081"
: "//vue-app.example.com",
container: "#subapp-container",
activeRule: "/vue",
},
];
Nginx 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name main-app.example.com;
location / {
try_files $uri $uri/ /index.html;
}
}
server {
listen 80;
server_name vue-app.example.com;
location / {
add_header Access-Control-Allow-Origin *;
try_files $uri $uri/ /index.html;
}
}
性能监控和调试
性能监控
1
2
3
4
5
6
7
8
9
// 监控微应用加载性能
registerMicroApps(microApps, {
beforeLoad: (app) => {
console.time(`${app.name} load`);
},
afterMount: (app) => {
console.timeEnd(`${app.name} load`);
},
});
调试技巧
- 开启沙箱日志:
1
2
3
4
start({
sandbox: { loose: true },
singular: false,
});
- 使用 qiankun devtools:
1
2
3
4
// 开发环境下启用
if (process.env.NODE_ENV === "development") {
window.__QIANKUN_DEVELOPMENT__ = true;
}
This post is licensed under CC BY 4.0 by the author.