微前端

简介

Wujie

  • 腾讯
  • 特点
    • 基于 WebComponent 容器 + iframe 沙箱,子应用运行在 iframe 中,利用 Shadow DOM 实现 CSS 隔离,JS 天然隔离于 iframe 上下文
    • 适合追求低接入成本,适用于快速迭代和混合技术栈的项目

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
## 整体流程
# 主应用端点 /sqbiz
# 主应用点击菜单(从后台获取或从本地读取的菜单路径),如:/sqbiz/module/jxc/xxx(需要加上主应用端点)
# 主应用配置好如果以 /sqbiz/module/ 开头的路径,则跳转到子应用,此时可执行 history.pushState 推送路由
# 主应用执行 router.beforeEach, to.path=/module/jxc/xxx (打印的vue-router的值会自动刨去主应用端点 /sqbiz)
# 此时浏览器路径为 http://localhost/sqbiz/module/jxc/xxx 则触发activeRule路径,从而通过fetch(ajax)请求地址 http://localhost/sqbiz/jxc/xxx (entry路径)
# 主应用执行 qiankun.beforeLoad
# 此时由 nginx 访问到子应用 http://localhost/sqbiz/jxc/xxx
# 子应用执行 main.bootstrap(qiankun)
# 主应用执行 qiankun.beforeMount
# 子应用执行 main.mount(qiankun)
# 子应用执行 router.beforeEach, to.path=/xxx (打印的vue-router的值会自动刨去子应用端点 /sqbiz/module/jxc/,注意此处是基于浏览器地址来的)

## qiankun路由配置
registerMicroApps(
[{
name: 'sqbiz-module-jxc',
entry: '/sqbiz/jxc/',
container: '#subapp-viewport',
activeRule: '/sqbiz/module/jxc'
}, {
name: 'sqbiz-plugin-minions-app',
entry: '//localhost/sqbiz/minions/',
container: '#subapp-viewport',
activeRule: '/sqbiz/plugin/minions'
}],
callback
);

## 主应用配置
# VUE_APP_PUBLIC_PATH=sqbiz 主应用端点
# vue.config配置
let publicPath = process.env.VUE_APP_PUBLIC_PATH ? ('/' + process.env.VUE_APP_PUBLIC_PATH + '/') : '/'
module.exports = {
publicPath: publicPath,
outputDir: process.env.VUE_APP_PUBLIC_PATH || 'dist', // 会在项目目录创建 sqbiz 产出文件夹
}
# 路由配置
const prefix = process.env.VUE_APP_PUBLIC_PATH ? ('/' + process.env.VUE_APP_PUBLIC_PATH + '/') : '/'
new VueRouter({
base: prefix,
mode: 'history',
routes: []
})

## 子应用配置
# VUE_APP_PUBLIC_PATH=sqbiz/jxc 子应用端点
# VUE_APP_QIANKUN_MAIN_BASE=/sqbiz 为主应用的端点,如果在根目录下,留空即可
# vue.config配置
let publicPath = process.env.VUE_APP_PUBLIC_PATH ? ('/' + process.env.VUE_APP_PUBLIC_PATH + '/') : '/'
module.exports = {
publicPath: publicPath,
outputDir: process.env.VUE_APP_PUBLIC_PATH || 'dist', // 会在项目目录创建 sqbiz/jxc 产出文件夹
}
# 路由配置,注意此处前面需要加主应用的端点
const prefix = process.env.VUE_APP_PUBLIC_PATH ? ('/' + process.env.VUE_APP_PUBLIC_PATH + '/') : '/'
new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? process.env.VUE_APP_QIANKUN_MAIN_BASE + '/module/jxc/' : prefix,
mode: 'history',
routes: []
})

# nginx配置
server {
listen 80;
server_name localhost;

gzip on;
gzip_types text/plain application/x-javascript application/javascript text/javascript text/css application/xml text/xml;

location /sqbiz/api/ {
proxy_pass http://127.0.0.1:8800/api/;
}

location = /sqbiz/index.html {
add_header Cache-Control "no-cache, no-store";
root D:/gitwork/oschina/sqbiz/sqbiz-web/sqbiz-main;
index index.html index.htm;
}

location = /sqbiz/jxc/index.html {
add_header Cache-Control "no-cache, no-store";
root D:/gitwork/oschina/sqbiz/sqbiz-web/sqbiz-module/sqbiz-jxc;
index index.html index.htm;
}

location ^~ /sqbiz/jxc/ {
root D:/gitwork/oschina/sqbiz/sqbiz-web/sqbiz-module/sqbiz-jxc; # 子模块 sqbiz-jxc 根目录
try_files $uri $uri/ /sqbiz/jxc/index.html;
if ($request_filename ~* .*\.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
}

location ^~ /sqbiz/ {
root D:/gitwork/oschina/sqbiz/sqbiz-web/sqbiz-main; # 主模块根目录
try_files $uri $uri/ /sqbiz/index.html;
if ($request_filename ~* .*\.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
}

location = / {
# rewrite / http://192.168.1.100/sqbiz/ break;
rewrite / http://$server_name/sqbiz/ break;
}
}

沙箱隔离

  • 由于主应用和子应用在同一个窗口,因此不进行沙箱隔离,则主子应用访问到同一个window,可能导致数据混乱
  • qiankun 做沙箱隔离主要分为三种:legacySandBox、proxySandBox、snapshotSandBox
  • 其中 legacySandBox、proxySandBox 是基于 Proxy API 来实现的,在不支持 Proxy API 的低版本浏览器中,会降级为 snapshotSandBox。在现版本中,legacySandBox 仅用于 singular 单实例模式,而多实例模式会使用 proxySandBox
  • proxySandbox
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function createFakeWindow(global: Window) {
// 对于non-configurable且有getter类型的参数单独放置在Map中,读取时效率更高(其实fakeWindow中也存在),如document(父子应用共享)
const propertiesWithGetter = new Map<PropertyKey, boolean>();
// 虚拟一个window对象,如进入子应用则是使用的此对象
const fakeWindow = {} as FakeWindow;

// copy the non-configurable property of global to fakeWindow
// 如 top/self/window/document 等是全局共享的
// make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
// 将 top/self/window 设置成 configurable 和 writable,否则通过 Proxy 进行 getOwnPropertyDescriptor 代理时会报错
Object.getOwnPropertyNames(global).filter(...).forEach(...)

return {
fakeWindow,
propertiesWithGetter,
};
}

// 每次进入微应用会实例化 ProxySandbox, 重新创建虚拟window
export default class ProxySandbox implements SandBox {
// window 值变更记录
private updatedValueSet = new Set<PropertyKey>();

// 激活沙箱
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}

// 注销沙箱
inactive() {
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
...this.updatedValueSet.keys(),
]);
}
// ...
}

constructor(name: string) {
// 原始window,即主应用window
const rawWindow = window;
// 创建虚拟window
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);

const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
// We must kept its description while the property existed in rawWindow before
if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
// 原window中有的,且虚拟window中不存在的。此时如果修改,则是直接修改的原window,需要判断是否为 writable
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// 虚拟window中存在的,或原window不存在的,全部保存到虚拟window中
target[p] = value;
}

// 对于白名单中的全局变量,可直接修改原window的值
if (variableWhiteList.indexOf(p) !== -1) {
rawWindow[p] = value;
}

// 将修改过的全局变量保存,注销时会打印出来
updatedValueSet.add(p);

this.latestSetProp = p;

return true;
}
},

// 取值:propertiesWithGetter > 模拟window > 原始window
get(target: FakeWindow, p: PropertyKey): any {}
})
}
}

常见问题

icestark飞冰

  • 飞冰icestark,为飞冰(ice生态的一个微前端解决方案
  • 主应用和微应用皆支持 React/Vue/Angular… 等不同框架
  • 支持VSCode拖拽组件

参考文章

ChatGPT开源小程序