前言
大家好这里是阳九,一个中途转行的野路子码农,热衷于研究和手写前端工具.
我的宗旨就是 万物皆可手写
新手创作不易,有问题欢迎指出和轻喷,谢谢
本文章适合有一定前端开发经验,使用过qiankun框架,对webpack有一定了解的前端攻城狮,如果没有请绕道恶补基础知识)
本文章内的代码只显示了关键思路,无法真实运行
沙箱隔离环境
首先 多个前端项目运行在同一个基座中可能会遇到的最大问题有两个
- JS代码的污染
- CSS的污染
这时我们就需要实现沙箱环境 , 也就是一个windows隔离环境
在乾坤中,每个微应用是运行在单个JS沙箱内的,在构建微应用时会创建一个沙箱,并将代码运行在该沙箱内。
实现JS沙箱
关键点: 对浏览器端window环境进行隔离 乾坤中根据场景提供了三种不同的沙箱:
快照沙箱(snapshotSandbox)
优缺点: snapshotSandbox会污染全局window,但是可以支持不兼容Proxy的浏览器。
基本思路: 在利用快照,赋值和还原window属性,达到进出沙箱的效果
- 激活沙箱,记录window的快照
windowSnapshot
将上一个沙箱修改过的属性赋值给window(还原修改) - 使用该沙箱,操作全局window
- 退出沙箱 找出修改了的属性存入
modifyPropsMap
还原window为快照状态
思路代码:
// 遍历window属性并执行回调
const iter = (window, callback) => {
for (const prop in window) {
if(window.hasOwnProperty(prop)) {
callback(prop);
}
}
}
class SnapshotSandbox {
constructor() {
this.proxy = window;
this.modifyPropsMap = {};
this.windowSnapshot = {};
}
// ------激活沙箱-------
active() {
// 记录active时window的快照
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});
// 将上一次沙箱修改过的属性赋值给window(还原修改,重新进入沙箱)
Object.keys(this.modifyPropsMap).forEach(p => {
window[p] = this.modifyPropsMap[p];
})
}
// ----- 使用该沙箱,在全局window上操作,退出时还原window为快照状态
// --------退出沙箱-------
inactive(){
iter(window, (prop) => {
// 修改后的window和快照时的window属性比对,找出修改了的属性,放入modifyPropsMap
if(this.windowSnapshot[prop] !== window[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop]; // 还原window为快照时的状态
}
})
}
}
快照沙箱使用
const sandbox = new SnapshotSandbox();
((window) => {
// 激活沙箱 (进入到沙箱)
sandbox.active();
window.name= '张三';
console.log(window.name); // 张三
// 退出沙箱 (切换到原本的window)
sandbox.inactive();
console.log(window.name); // undefined
// 重新激活沙箱(重新进入到沙箱)
sandbox.active();
console.log(window.name); // 张三
})(sandbox.proxy);
单例代理沙箱(LegacyProxySandbox)
基本思路: 使用proxy代理拦截window属性的set get操作记录变更
与快照沙箱一样,只是利用Proxy,赋值和还原window属性,达到进出沙箱的效果
同样会对window造成污染,但是性能比快照沙箱好,不用遍历window对象。
class LegacyProxySandbox {
constructor() {
this.addedPropsMapInSandbox = {}// 沙箱期间新增的全局变量
this.modifiedPropsOriginalValueMapInSandbox = {} // 沙箱期间更新的全局变量
this.currentUpdatedPropsValueMap = {}; // 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot
this.sandboxRunning = true;
const realWindow = window;// 真实window
const fakeWindow = {}; // 虚拟window
//! 使用proxy代理拦截window属性的set get操作记录变更
const proxy = new Proxy(fakeWindow, {
set: (target, prop, value) => {
// 非激活状态不做任何操作
if (!this.sandboxRunning) return true
// 新增window属性情况 记录新增值
if (!realWindow.hasOwnProperty(prop)) {
this.addedPropsMapInSandbox[prop] = value;
}
// 记录更新值的初始值
else if (!this.modifiedPropsOriginalValueMap[prop]) {
const originValue = realWindow[prop]
this.modifiedPropsOriginalValueMapInSandbox[prop] = originValue;
}
// 纪录此次修改的属性
this.currentUpdatedPropsValueMap[prop] = value;
// 将设置的属性和值赋给了当前window,还是污染了全局window变量
realWindow[prop] = value;
return true;
},
get: (target, prop) => realWindow[prop]
})
this.proxy = proxy;
}
// 激活时将之前修改的值重新赋值给window(进入沙箱)
active() {
if (!this.sandboxRunning) {
for (const key in this.currentUpdatedPropsValueMap) {
window[key] = this.currentUpdatedPropsValueMap[key];
}
}
this.sandboxRunning = true;
}
// 退出时还原window 还原为origin的值 删除新增的值
inactive() {
for (const key in this.modifiedPropsOriginalValueMapInSandbox) {
window[key] = this.modifiedPropsOriginalValueMapInSandbox[key];
}
for (const key in this.addedPropsMapInSandbox) {
delete window[key];
}
this.sandboxRunning = false;
}
}
代理沙箱
激活沙箱,每次对window
取值的时候,先从自己沙箱环境的fakeWindow
里面找,
如果不存在,就从rawWindow
(外部的window
)里去找;
当对沙箱内部的window
对象赋值的时候,会直接操作fakeWindow
,而不会影响到rawWindow
。
不会污染全局window,支持多个子应用同时加载。
// demo代码
class ProxySandbox {
active() {
this.sandboxRunning = true;
}
inactive() {
this.sandboxRunning = false;
}
constructor() {
const rawWindow = window;
const fakeWindow = {};
const proxy = new Proxy(fakeWindow, {
set: (target, prop, value) => {
if (this.sandboxRunning) {
target[prop] = value;
return true;
}
},
// 如果fakeWindow里面有,就从fakeWindow里面取,否则,就从外部的window里面取
get: (target, prop) => {
let value = prop in target ? target[prop] : rawWindow[prop];
return value
}
})
this.proxy = proxy;
}
}
使用
window.sex = '男';
let proxy1 = new ProxySandbox();
let proxy2 = new ProxySandbox();
((window) => {
proxy1.active();
console.log('修改前proxy1的sex', window.sex);// 男
window.sex = '女';
console.log('修改后proxy1的sex', window.sex);// 女
})(proxy1.proxy);
console.log('外部window.sex', window.sex); // 男(不影响外部window)
((window) => {
proxy2.active();
console.log('修改前proxy2的sex', window.sex);// 男
window.sex = '人妖';
console.log('修改后proxy2的sex', window.sex);//人妖
})(proxy2.proxy);
console.log('外部window.sex', window.sex); // 男(不影响外部window)
实现JCSS沙箱-shadowDom
shadowDom
shadowDom会生成一个作用域,使其不会被外部所影响。
- 可以使用和操作常规 DOM 一样的方式来操作 Shadow DOM
- Shadow DOM 内部的元素始终不会影响到它外部的元素
- 外部CSS不会影响到shadowDom内部
一个挂载在imput中的shadow-root
基本使用
//1. 创建一个shadow-host的html元素 用于挂载shadowDom
const hostDiv = document.getElementById('shadowHost')
//2. 给host创建一个shadowDom
// mode:open表示可以操作内部dom
const shadowRoot = shadowHost.attachShadow({mode: 'open'});
//3. 创建一个div 添加到shadowRoot中
// 可以添加内部样式 实现样式隔离
const div = document.createElement('div');
div.setAttribute('class', 'red')
const style = document.createElement('style'); // 创建style标签并添加
style.textContent = `
.red{
color: red;
}
`;
shadowRoot.appendChild(style)
shadowRoot.appendChild(div)
乾坤中的使用思路
- 通过网络请求获取css字符串内容
- 将子应用渲染到shadowDom中
- 将css内容添加进shadowDom中 实现CSS隔离