微前端 ④ qiankun

什么是 qiankun

qiankun 是一个微前端的解决方案,对 single-spa 进行了封装。

执行流程

CSS 沙箱如何实现

strictStyleIsolation 严格模式, 通过 ShadowDOM 实现,将子应用最外层的元素升级为 ShadowDOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var containerElement = document.createElement("div");
containerElement.innerHTML = appContent;
var appElement = containerElement.firstChild;
if (strictStyleIsolation) {
if (supportShadowDOM) {
var innerHTML = appElement.innerHTML;
appElement.innerHTML = "";
var shadow;
if (appElement.attachShadow) {
shadow = appElement.attachShadow({
mode: "open",
});
} else {
shadow = appElement.createShadowRoot();
}
shadow.innerHTML = innerHTML;
}
}

另一种是开启了 experimentalStyleIsolation 实验性沙箱配置,原理是尝试通过遍历 sheet 样式表的每一条样式,为每条样式添加一个私有化的选择器

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
// 给子应用的最外层元素设置一个私有属性,用于添加选择器
// css.QiankunCSSRewriteAttr 默认私有化属性 data-qiankun
// appInstanceId 子应用的 name 属性
appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);

// 创建私有化的选择器 prefix =>div[data-qiankun="setting"]
var prefix = ""
.concat(tag, "[")
.concat(QiankunCSSRewriteAttr, '="')
.concat(appName, '"]');

// 获取并便利所有的 style 标签
var styleNodes = appElement.querySelectorAll("style") || [];
_forEach(styleNodes, function (stylesheetElement) {
// 创建新的style标签,并将当前style中的样式复制到新标签中
var styleNode = document.createElement("style");
styleNode.appendChild(
document.createTextNode(stylesheetElement.textContent || "")
);

// 获取 CSSStyleSheet 对象,并将 cssRules 转为数组, 遍历并重写规则
var sheet = styleNode.sheet;
var rules = arrayify(sheet.cssRules);
var css = "";
rules.forEach(function (rule) {
switch (rule.type) {
case RuleType.STYLE:
/**
1 对于 html body :root 几个根元素
会直接用 prefix 替换掉,防止对基座应用样式污染

2 html + body 格式的演示qiankun认为是非标准的样式不做处理

3 html > body html body 性质的选择器会将 html 替换为空字符串

4 检查 a,html or *:not(:root) 根元素前插入了其他字符的选择器
同样需要替换为 a,[prefix] *:not([prefix]) 的形式

5 其他选择器会在选择器前添加前缀
[prefix] .text {}

最后拼接字符串
*/

css += ruleStyle(rule, prefix);
break;
case RuleType.MEDIA:
case RuleType.SUPPORTS:
/**
媒体查询 @media screen and (max-width: 300px) {}
能力检测 @supports (display: grid) {}

会递归调用重写方法,将中间的选择器重写

*/
css += ruleStyle(rule, prefix);
break;
}
});
});

js 沙箱

快照沙箱

如果平台不支持 Proxy ,则使用快照沙箱。 通过记录 window 对象上的属性变化,会复或保存状态。

这种方式的沙箱只关注应用激活和卸载时的差异,并不能控制属性的来源,因为是在全局 window 对象上进行操作,所以全部应用的对全局属性的操作,都会反映在 window 对象上。

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
function iter(win, cb) {
for (const k in win) {
if (win.hasOwnProperty(k) || k === "clearInterval") {
cb(k);
}
}
}

class SnapshotSandBox {
deleteProps = new Set();
modifyPropsMap = {};
proxy = window;
snapshot = {};

active() {
this.snapshot = {};
// 激活时记录当前的window快照,不关心 window 上是否有其他应用的属性。
// 只关心本次激活和卸载时属性的变化, 就是子应用的属性变化。
iter(window, (k) => {
this.snapshot[k] = window[k];
});

// 之前卸载时检测出修改或添加的属性,本次激活时要添加回来
// 虽然添加和修改的属性都是在 window 上,但是应用卸载后可能被其他应用删除或重写
// 这里使用记录值恢复之前的状态

Object.keys(this.modifyPropsMap).forEach((k) => {
window[k] = this.modifyPropsMap[k];
});

// 删除卸载时记录的删除值
// 同样因为其他同框架的子应用可能会添加相同属性

this.deleteProps.forEach((k) => {
delete window[k];
});

this.sandboxRunning = true;
}

inactive() {
this.deleteProps.clear();
this.modifyPropsMap = {};

// 和刚激活应用时的快照对比,检查新增或修改的属性
iter(window, (k) => {
if (window[k] !== this.snapshot[k]) {
this.modifyPropsMap[k] = window[k];
}
});

// 快照中有,但是当前 window 上没有,那么属性被删除

iter(this.snapshot, (k) => {
if (!window.hasOwnProperty(k)) {
this.deleteProps.add(k);
// 恢复环境, 应用中删除的属性,退出应用是需要还原
window[k] = this.snapshot[k];
}
});
this.sandboxRunning = false;
}
}

由于快照沙箱,对属性的操作都是在 window 上,因此多子应用的时候无法隔离子应用的状态,会导致冲突。只能适用于单实例。

proxy 沙箱
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
const createFakeWindow = (globalContext, speed) => {
const fakeWindow = {};
const propertiesWithGetter = new Map();
// 获取 window 静态属性
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
// 属性描述符
// configurable 为假表示不能 修改,不能删除

const dec = Object.getOwnPropertyDescriptor(globalContext, p);
return dec?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(
descriptor,
"get"
);

// FAQ window 引用属性,为什么需要要修改属性描述符
if (
p === "self" ||
p === "window" ||
p === "parent" ||
p === "top" ||
(speed && p === "document")
) {
descriptor.configurable = true;

// 属性访问器和, writable 属性描述符不能同时指定
if (!hasGetter) {
descriptor.writable = true;
}
}

// 记录所有所有访问器得属性
if (hasGetter) propertiesWithGetter.set(p, true);

// FAQ 所有 configurable 为假得属性,就一定是环境级别属性属性么
Object.defineProperties(fakeWindow, p, Object.freeze(descriptor));
}
});
// Object.keys(window).forEach(k=>{
// if( has)
// const propDescriptor = Object.getOwnPropertyDescriptor(window,k);
// c
// })
};

let activeSandboxCount = 0;

class ProxySandBox {
sandboxRunning = true;
activeSandboxCount = 0;
document = document;
constructor(name, globalContext = window, opts) {
this.name = name;
this.globalContext = globalContext;
this.type = "proxy";
const { updatedValueSet } = this;
const { speedy } = opts || {};
const { fakeWindow, propertiesWithGetter } = createFakeWindow(
globalContext,
!!speedy
);

const proxy = new Proxy(fakeWindow, {
get(target, p) {
// this.registerRunningApp()

// 处理对一些特殊属性值得处理

//Symbol.unscopables 提供了一种机制,允许对象指定哪些属性在 with 语句中不应该被暴露为局部变量。如果一个属性在对象的 Symbol.unscopables 列表中,它将不会出现在 with 语句的作用域中。
// const obj = {
// a: 1,
// b: 2,
// [Symbol.unscopables]: {
// b: true // 使 'b' 在 with 语句中不可见
// }
// };

if (p === Symbol.unscopables) return unscopables;
if (p === "window" || p === "self" || p === "globalThis") {
return proxy;
}

// 如果获取得上级 window, 允许逃逸
if (p === "top" || p === "parent") {
if (globalContext === globalContext.parent) {
return proxy;
}
return globalContext[p];
}

if (p === "hasOwnProperty") {
// proxy.hasOwnProperty.call({a},'a')
return function hasOwnProperty(key) {
if (this !== proxy && this !== null && typeof this === "object") {
return Object.prototype.hasOwnProperty.call(this, key);
}

// proxy.hasOwnProperty("a")
return (
fakeWindow.hasOwnProperty(key) ||
globalContext.hasOwnProperty(key)
);
};
}

if (p === "document") {
return this.document;
}

if (p === "eval") {
return eval;
}
// customProp {configurable:false} => {configurable:true}
const actualTarget = propertiesWithGetter.has(p)
? globalContext
: p in target
? target
: globalContext;
const value = actualTarget[p];

// 校验了是不是frozen的属性, 如果是需要直接返回
// propertyDescriptor.configurable === false
// && (propertyDescriptor.writable === false || (propertyDescriptor.get && !propertyDescriptor.set)),
if (isPropertyFrozen(actualTarget, p)) {
return value;
}
// 非原生全局属性直接返回 addEventListener
// isNativeGlobalProp 枚举了 所有的全局属性
// useNativeWindowForBindingsProps 记录所有执行时需要绑定原生window方法 [fetch,true]
if (!isNativeGlobalProp(p) && !useNativeWindowForBindingsProps.has(p)) {
return value;
}

// 如果不处理 fetch.bind(this) 实际时绑定的proxyWindow 导致报错
const boundTarget = useNativeWindowForBindingsProps.get(p)
? nativeGlobal
: globalContext;

// 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
// 目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
// @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
const rebindTarget2Fn = (target, fn) => {
const isCallable = (fn) =>
typeof fn === "function" && fn instanceof Function;
// bind 函数的名称会以 bound开头 "bound fn"
const isBoundedFunction = (fn) =>
fn.name.indexOf("bound ") === 0 && !fn.hasOwnProperty("prototype");

const isConstructable = () => {
const hasPrototypeMethods =
fn.prototype &&
// class有constructor属性
fn.prototype.constructor === fn &&
// 构造器需要有原型链
Object.getOwnPropertyNames(fn.prototype).length > 1;
if (hasPrototypeMethods) return true;
// 假设以下视为构造函数
// 1. 有 prototype 并且 prototype 上有定义一系列非 constructor 属性
// 2. 函数名大写开头
// 3. class 函数

let constructable = hasPrototypeMethods;
if (!constructable) {
const fnString = fn.toString();
const constructableFunctionRegex = /^function\b\s[A-Z].*/;
const classRegex = /^class\b/;
constructable =
constructableFunctionRegex.test(fnString) ||
classRegex.test(fnString);
}

return constructable;
};

if (
isCallable(fn) &&
!isBoundedFunction(fn) &&
!isConstructable(fn)
) {
const boundValue = Function.prototype.bind.call(fn, target);

// 拷贝原有方法的静态属性
Object.getOwnPropertyNames(fn).forEach((key) => {
// boundValue might be a proxy, we need to check the key whether exist in it
if (!boundValue.hasOwnProperty(key)) {
Object.defineProperty(
boundValue,
key,
Object.getOwnPropertyDescriptor(fn, key)
);
}
});

// 如果原方法 prototype 属性设置了不可枚举,会导致原型链丢失
// 赋值的时候不能使用 = 或 Object.assign, 因为赋值操作会向上查询原型链
// 如果描述符被设 writable =false 或没有 set 属性访问器,会抛出错误
// Cannot assign to read only property 'prototype' of function
if (
fn.hasOwnProperty("prototype") &&
!boundValue.hasOwnProperty("prototype")
) {
Object.defineProperty(boundValue, "prototype", {
value: fn.prototype,
enumerable: false,
writable: true,
});
}

// 有一些库会使用 /native code/.test(fn.toString()) 检测是不是原生的方法
// 如果不特殊处理,所有toString 返回的都是 [object fakeWindow] 或 [object Window]]
if (typeof fn.toString === "function") {
const valueHasInstanceToString =
fn.hasOwnProperty("toString") &&
!boundValue.hasOwnProperty("toString");
const boundValueHasPrototypeToString =
boundValue.toString === Function.prototype.toString;

if (valueHasInstanceToString || boundValueHasPrototypeToString) {
// valueHasInstanceToString? 有自定义的 toString 使用原方法
// : 使用原生的 toString
const originToStringDescriptor =
Object.getOwnPropertyDescriptor(
valueHasInstanceToString ? fn : Function.prototype,
"toString"
);

Object.defineProperty(
boundValue,
"toString",
Object.assign(
{},
originToStringDescriptor,
originToStringDescriptor?.get
? null
: { value: () => fn.toString() }
)
);
}
}
}
};
return rebindTarget2Fn(boundTarget, value);
},
});
}

active() {
if (!this.activeSandboxCount) activeSandboxCount += 1;
this.sandboxRunning = true;
}
inActive() {
this.sandboxRunning = false;
}
patchDocument(doc) {
this.document = doc;
}
}
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信