概述
qinkun想必了解过微前端的盆友们都不陌生,它基于single-spa额外做了资源的加载与资源的隔离。我们知道一个页面是由html、css资源、js资源组成的,当我们需要在一个页面中进行两个不同前端应用的切换势必会造成资源的冲突,例如样式冲突、全局变量污染等。因此我们需要将子应用的资源进行隔离,使它们互不影响。 本文将主要介绍qiankun中对于js资源的隔离机制。
qiankun中的几种隔离机制
js隔离机制也称为js沙箱,在qiankun中总共有三种沙箱分别为snapshot、legacyProxy、proxy(如下图)。snapshot快照沙箱,它需要通过遍历window上所有对象来形成一个当前的window快照,性能较差。后续由于Es6的普及,使用Proxy来实现window快照性能更好因此产生了legacyProxy。但snapshot与lagacyProxy都仅支持单应用代理沙箱,后续的proxy采用了更好的机制完全囊括了legacyProxy的功能且支持多应用代理沙箱。
snapshotProxy 快照沙箱
快照沙箱的简单实现:
// 为了能在node环境下运行
const window = globalThis;
class SnapshotSandbox {
constructor(name) {
// 当前沙箱名称
this.name = name;
this.windowSnapshot = {};
this.modifyPropsMap = {};
this.isRunning = false;
}
active() {
// 1、记录快照
this.windowSnapshot = {};
for (const prop in window) {
if (Object.hasOwnProperty.call(window, prop)) {
const ele = window[prop];
this.windowSnapshot[prop] = ele;
}
}
// 2、恢复之前的变更
for (const key of Object.keys(this.modifyPropsMap)) {
window[key] = this.modifyPropsMap[key];
}
this.isRunning = true;
}
inactive() {
this.modifyPropsMap = {};
// 记录本次运行的变更,利用快照还原window
for (const prop in window) {
if (Object.hasOwnProperty.call(window, prop) && window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
this.isRunning = false;
}
}
const sandbox1 = new SnapshotSandbox("s1");
window.city = "fu zhou";
sandbox1.active();
window.city = 'hang zhou';
console.log('city1', window.city);
sandbox1.inactive();
console.log('city2', window.city);
sandbox1.active();
console.log('city3', window.city);
// 输出
// city1 hang zhou
// city2 fu zhou
// city3 hang zhou
在沙箱激活时
- 遍历整个window对象记录此时的window状态(也就是形成一个当前window快照)
- 根据上一次沙箱失活后window状态的变化,将当前window与上一次失活前的window状态保持同步
在沙箱失活时
- 记录此次运行中window状态的变化
- 根据window快照,对window的状态进行还原。简单来说就是遍历window属性,如果某个属性值与快照中的属性值不同,将它记录到变更中并将当前window中的该属性值还原。
总结:
很明显快照沙箱在实现过程中激活时与失活时都需要对window所有属性进行遍历,window的属性数量极大因此性能开销较大。并且快照沙箱会改变window上的属性,当同时存在多个微应用时势必会造成全局环境污染,因为快照沙箱仅支持单应用沙箱。
legacyProxySandbox 支持单应用的代理沙箱
legacyProxy实现思路与快照沙箱是基本一致的,唯一的区别在于legacyProxy使用了ES6 Proxy语法,无需再对window进行遍历提升了性能,但和快照沙箱一样,这种方法会对window上的属性进行修改,造成全局window属性的污染,同样无法支持多个微应用的代理。
proxySandbox 支持多应用的代理沙箱
多应用代理沙箱简单实现
// 当前活跃的沙箱数量
let activeSandbox = 0;
// 最近操作过属性的沙箱
let curSandbox = null;
// 全局上下文(在浏览器环境下是window,目前在node环境测试使用globalThis)
const globalContext = globalThis;
// 获取当前沙箱
function getCurSandbox() {
return curSandbox;
}
// 设置当前沙箱
function setCurSandbox(sandboxIns) {
curSandbox = sandboxIns;
}
// 沙箱构造函数
class ProxySandbox {
registerRunningApp(name, proxy) {
if (this.isRuning) {
const curApp = getCurSandbox();
if (!curApp || curApp.name !== name) {
setCurSandbox({name:name, window:proxy})
}
}
}
constructor(name) {
this.name = name;
this.isRuning = false;
const fakeWindow = Object.create({});
const proxy = new Proxy(fakeWindow, {
set: (target, prop, value) => {
// 赋值时,仅当沙箱激活时才可赋值
if (this.isRuning) {
target[prop] = value;
}
this.registerRunningApp(this.name, this.proxy);
},
get: (target, prop) => {
// 取值时,先找fakeWindow再找window
this.registerRunningApp(this.name, this.proxy);
return prop in target ? target[prop] : globalContext[prop];
}
});
this.proxy = proxy;
}
active() {
// 激活沙箱当前活跃沙箱加一
if (!this.isRuning) activeSandbox++;
this.isRuning = true;
}
inactive() {
// 让沙箱失活,当前活跃沙箱数量减一
--activeSandbox;
this.isRuning = false;
}
}
const pSandbox1 = new ProxySandbox("p1");
const pSandbox2 = new ProxySandbox("p2");
globalContext.city = "changsha";
globalContext.country = "China";
pSandbox1.active()
pSandbox2.active();
console.log("当前活跃沙箱数量", activeSandbox);
pSandbox1.proxy.city = "hangzhou";
console.log("当前活跃沙箱:", curSandbox.name);
console.log("沙箱1", pSandbox1.proxy.city, pSandbox1.proxy.country);
pSandbox2.proxy.city = "fuzhou";
console.log("当前活跃沙箱:", curSandbox.name);
console.log("沙箱2",pSandbox2.proxy.city, pSandbox2.proxy.country);
console.log("全局上下文city", globalContext.city);
pSandbox1.inactive();
pSandbox2.inactive();
console.log("pSandbox1", pSandbox1.proxy.city);
console.log("pSandbox2", pSandbox2.proxy.city);
console.log("当前活跃沙箱数量", activeSandbox);
// 当前活跃沙箱数量 2
// 当前活跃沙箱: p1
// 沙箱1 hangzhou China
// 当前活跃沙箱: p2
// 沙箱2 fuzhou China
// 全局上下文city changsha
// pSandbox1 hangzhou
// pSandbox2 fuzhou
// 当前活跃沙箱数量 0
相较于前两种沙箱,proxySandbox根本不存在window状态恢复的过程,每一个应用实例内部中都会维护一个fakeWindow,从而与window无关也不会对window上的属性造成影响。每次微应用中赋值时本质是对fakeWindow进行赋值,取值时则会先在fakeWindow中查询如不存在再在window上查询。
并且qiankun中还会记录当前获取沙箱的数量以及当前正在运行的沙箱,每次沙箱的属性的操作(包括但不限于取值赋值)都会触发正在运行沙箱的更新。