微前端乾坤框架 CSS JS沙箱隔离环境原理

1,810 阅读5分钟

前言

大家好这里是阳九,一个中途转行的野路子码农,热衷于研究和手写前端工具.

我的宗旨就是 万物皆可手写

新手创作不易,有问题欢迎指出和轻喷,谢谢

本文章适合有一定前端开发经验,使用过qiankun框架,对webpack有一定了解的前端攻城狮,如果没有请绕道恶补基础知识)

本文章内的代码只显示了关键思路,无法真实运行

沙箱隔离环境

首先 多个前端项目运行在同一个基座中可能会遇到的最大问题有两个

  1. JS代码的污染
  2. CSS的污染

这时我们就需要实现沙箱环境 , 也就是一个windows隔离环境

在乾坤中,每个微应用是运行在单个JS沙箱内的,在构建微应用时会创建一个沙箱,并将代码运行在该沙箱内。

实现JS沙箱

关键点: 对浏览器端window环境进行隔离 乾坤中根据场景提供了三种不同的沙箱:

快照沙箱(snapshotSandbox)

优缺点: snapshotSandbox会污染全局window,但是可以支持不兼容Proxy的浏览器。

基本思路: 在利用快照,赋值和还原window属性,达到进出沙箱的效果

  1. 激活沙箱,记录window的快照windowSnapshot 将上一个沙箱修改过的属性赋值给window(还原修改)
  2. 使用该沙箱,操作全局window
  3. 退出沙箱 找出修改了的属性存入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会生成一个作用域,使其不会被外部所影响。

  1. 可以使用和操作常规 DOM 一样的方式来操作 Shadow DOM
  2. Shadow DOM 内部的元素始终不会影响到它外部的元素
  3. 外部CSS不会影响到shadowDom内部

一个挂载在imput中的shadow-root

image.png

基本使用

//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)

乾坤中的使用思路

  1. 通过网络请求获取css字符串内容
  2. 将子应用渲染到shadowDom中
  3. 将css内容添加进shadowDom中 实现CSS隔离