前言

自从微前端框架micro-app开源后,很多小伙伴都十分感兴趣,问我是如何实现的,但这并不是几句话能够说明确的。为了讲清楚其中的原理,我会从零开始实现一个繁难的微前端框架,它的外围性能包含:渲染、JS沙箱、款式隔离、数据通信。因为内容太多,会依据性能分成四篇文章进行解说,这是系列文章的第二篇:沙箱篇。

通过这些文章,你能够理解微前端框架的具体原理和实现形式,这在你当前应用微前端或者本人写一套微前端框架时会有很大的帮忙。如果这篇文章对你有帮忙,欢送点赞留言。

相干举荐

  • micro-app仓库地址
  • simple-micro-app仓库地址
  • 从零开始写一个微前端框架-渲染篇
  • 从零开始写一个微前端框架-沙箱篇
  • 从零开始写一个微前端框架-款式隔离篇
  • 从零开始写一个微前端框架-数据通信篇
  • micro-app介绍

开始

前一篇文章中,咱们曾经实现了微前端的渲染工作,尽管页面曾经失常渲染,然而此时基座利用和子利用是在同一个window下执行的,这有可能产生一些问题,如全局变量抵触、全局事件监听和解绑。

上面咱们列出了两个具体的问题,而后通过创立沙箱来解决。

问题示例

1、子利用向window上增加一个全局变量:globalStr='child',如果此时基座利用也有一个雷同的全局变量:globalStr='parent',此时就产生了变量抵触,基座利用的变量会被笼罩。

2、子利用渲染后通过监听scroll增加了一个全局监听事件

window.addEventListener('scroll', () => {  console.log('scroll')})

当子利用被卸载时,监听函数却没有解除绑定,对页面滚动的监听始终存在。如果子利用二次渲染,监听函数会绑定两次,这显然是谬误的。

接下来咱们就通过给微前端创立一个JS沙箱环境,隔离基座利用和子利用的JS,从而解决这两个典型的问题,

创立沙箱

因为每个子利用都须要一个独立的沙箱,所以咱们通过class创立一个类:SandBox,当一个新的子利用被创立时,就创立一个新的沙箱与其绑定。

// /src/sandbox.jsexport default class SandBox {  active = false // 沙箱是否在运行  microWindow = {} // // 代理的对象  injectedKeys = new Set() // 新增加的属性,在卸载时清空  constructor () {}  // 启动  start () {}  // 进行  stop () {}}

咱们应用Proxy进行代理操作,代理对象为空对象microWindow,得益于Proxy弱小的性能,实现沙箱变得简略且高效。

constructor中进行代理相干操作,通过Proxy代理microWindow,设置getsetdeleteProperty三个拦截器,此时子利用对window的操作基本上能够笼罩。

// /src/sandbox.jsexport default class SandBox {  active = false // 沙箱是否在运行  microWindow = {} // // 代理的对象  injectedKeys = new Set() // 新增加的属性,在卸载时清空  constructor () {    this.proxyWindow = new Proxy(this.microWindow, {      // 取值      get: (target, key) => {        // 优先从代理对象上取值        if (Reflect.has(target, key)) {          return Reflect.get(target, key)        }        // 否则兜底到window对象上取值        const rawValue = Reflect.get(window, key)        // 如果兜底的值为函数,则须要绑定window对象,如:console、alert等        if (typeof rawValue === 'function') {          const valueStr = rawValue.toString()          // 排除构造函数          if (!/^function\s+[A-Z]/.test(valueStr) && !/^class\s+/.test(valueStr)) {            return rawValue.bind(window)          }        }        // 其它状况间接返回        return rawValue      },      // 设置变量      set: (target, key, value) => {        // 沙箱只有在运行时能够设置变量        if (this.active) {          Reflect.set(target, key, value)          // 记录增加的变量,用于后续清空操作          this.injectedKeys.add(key)        }        return true      },      deleteProperty: (target, key) => {        // 以后key存在于代理对象上时才满足删除条件        if (target.hasOwnProperty(key)) {          return Reflect.deleteProperty(target, key)        }        return true      },    })  }  ...}

创立完代理后,咱们接着欠缺startstop两个办法,实现形式也非常简单,具体如下:

// /src/sandbox.jsexport default class SandBox {  ...  // 启动  start () {    if (!this.active) {      this.active = true    }  }  // 进行  stop () {    if (this.active) {      this.active = false      // 清空变量      this.injectedKeys.forEach((key) => {        Reflect.deleteProperty(this.microWindow, key)      })      this.injectedKeys.clear()    }  }}

下面一个沙箱的雏形就实现了,咱们尝试一下,看看是否无效。

应用沙箱

src/app.js中引入沙箱,在CreateApp的构造函数中创立沙箱实例,并在mount办法中执行沙箱的start办法,在unmount办法中执行沙箱的stop办法。

// /src/app.jsimport loadHtml from './source'+ import Sandbox from './sandbox'export default class CreateApp {  constructor ({ name, url, container }) {    ...+    this.sandbox = new Sandbox(name)  }  ...  mount () {    ...+    this.sandbox.start()    // 执行js    this.source.scripts.forEach((info) => {      (0, eval)(info.code)    })  }  /**   * 卸载利用   * @param destory 是否齐全销毁,删除缓存资源   */  unmount (destory) {    ...+    this.sandbox.stop()    // destory为true,则删除利用    if (destory) {      appInstanceMap.delete(this.name)    }  }}

咱们在下面创立了沙箱实例并启动沙箱,这样沙箱就失效了吗?

显然是不行的,咱们还须要将子利用的js通过一个with函数包裹,批改js作用域,将子利用的window指向代理的对象。模式如:

(function(window, self) {  with(window) {    子利用的js代码  }}).call(代理对象, 代理对象, 代理对象)

在sandbox中增加办法bindScope,批改js作用域:

// /src/sandbox.jsexport default class SandBox {  ...  // 批改js作用域  bindScope (code) {    window.proxyWindow = this.proxyWindow    return `;(function(window, self){with(window){;${code}\n}}).call(window.proxyWindow, window.proxyWindow, window.proxyWindow);`  }}

而后在mount办法中增加对bindScope的应用

// /src/app.jsexport default class CreateApp {  mount () {    ...    // 执行js    this.source.scripts.forEach((info) => {-      (0, eval)(info.code)+      (0, eval)(this.sandbox.bindScope(info.code))    })  }}

到此沙箱才真正起作用,咱们验证一下问题示例中的第一个问题。

先敞开沙箱,因为子利用笼罩了基座利用的全局变量globalStr,当咱们在基座中拜访这个变量时,失去的值为:child,阐明变量产生了抵触。

开启沙箱后,从新在基座利用中打印globalStr的值,失去的值为:parent,阐明变量抵触的问题曾经解决,沙箱正确运行。

第一个问题曾经解决,咱们开始解决第二个问题:全局监听事件。

重写全局事件

再来回顾一下第二个问题,谬误的起因是在子利用卸载时没有清空事件监听,如果子利用晓得本人将要被卸载,被动清空事件监听,这个问题能够防止,但这是现实状况,一是子利用不晓得本人何时被卸载,二是很多第三方库也有一些全局的监听事件,子利用无奈全副管制。所以咱们须要在子利用卸载时,主动将子利用残余的全局监听事件进行清空。

咱们在沙箱中重写window.addEventListenerwindow.removeEventListener,记录所有全局监听事件,在利用卸载时如果有残余的全局监听事件则进行清空。

创立一个effect函数,在这里执行具体的操作

// /src/sandbox.js// 记录addEventListener、removeEventListener原生办法const rawWindowAddEventListener = window.addEventListenerconst rawWindowRemoveEventListener = window.removeEventListener/** * 重写全局事件的监听和解绑 * @param microWindow 原型对象 */ function effect (microWindow) {  // 应用Map记录全局事件  const eventListenerMap = new Map()  // 重写addEventListener  microWindow.addEventListener = function (type, listener, options) {    const listenerList = eventListenerMap.get(type)    // 以后事件非第一次监听,则增加缓存    if (listenerList) {      listenerList.add(listener)    } else {      // 以后事件第一次监听,则初始化数据      eventListenerMap.set(type, new Set([listener]))    }    // 执行原生监听函数    return rawWindowAddEventListener.call(window, type, listener, options)  }  // 重写removeEventListener  microWindow.removeEventListener = function (type, listener, options) {    const listenerList = eventListenerMap.get(type)    // 从缓存中删除监听函数    if (listenerList?.size && listenerList.has(listener)) {      listenerList.delete(listener)    }    // 执行原生解绑函数    return rawWindowRemoveEventListener.call(window, type, listener, options)  }  // 清空残余事件  return () => {    console.log('须要卸载的全局事件', eventListenerMap)    // 清空window绑定事件    if (eventListenerMap.size) {      // 将残余的没有解绑的函数顺次解绑      eventListenerMap.forEach((listenerList, type) => {        if (listenerList.size) {          for (const listener of listenerList) {            rawWindowRemoveEventListener.call(window, type, listener)          }        }      })      eventListenerMap.clear()    }  }}

在沙箱的构造函数中执行effect办法,失去卸载的钩子函数releaseEffect,在沙箱敞开时执行卸载操作,也就是在stop办法中执行releaseEffect函数

// /src/sandbox.jsexport default class SandBox {  ...  // 批改js作用域  constructor () {    // 卸载钩子+   this.releaseEffect = effect(this.microWindow)    ...  }  stop () {    if (this.active) {      this.active = false      // 清空变量      this.injectedKeys.forEach((key) => {        Reflect.deleteProperty(this.microWindow, key)      })      this.injectedKeys.clear()            // 卸载全局事件+      this.releaseEffect()    }  }}

这样重写全局事件及卸载的操作根本实现,咱们验证一下是否失常运行。

首先敞开沙箱,验证问题二的存在:卸载子利用后滚动页面,仍然在打印scroll,阐明事件没有被卸载。

开启沙箱后,卸载子利用,滚动页面,此时scroll不再打印,阐明事件曾经被卸载。

从截图中能够看出,除了咱们被动监听的scroll事件,还有errorunhandledrejection等其它全局事件,这些事件都是由框架、构建工具等第三方绑定的,如果不进行清空,会导致内存无奈回收,造成内存透露。

沙箱性能到此就根本实现了,两个问题都曾经解决。当然沙箱须要解决的问题远不止这些,但根本架构思路是不变的。

结语

JS沙箱的外围在于批改js作用域和重写window,它的应用场景不限于微前端,也能够用于其它中央,比方在咱们向内部提供组件或引入第三方组件时都能够应用沙箱来防止抵触。

下一篇文章咱们会实现微前端的款式隔离。