24
头图

Preface

Since the micro-front-end framework micro-app open sourced, many small partners have been very interested and asked me how to achieve it, but this is not a few words to explain. In order to clarify the principle, I will implement a simple micro front-end framework from scratch. Its core functions include: rendering, JS sandbox, style isolation, and data communication. Because there is too much content, it will be divided into four articles to explain according to the function. This is the second article in the series: the sandbox article.

Through these articles, you can understand the specific principles and implementation methods of the micro front-end framework, which will be of great help when you use the micro-front-end framework or write a set of the micro-front-end framework yourself. If this article is helpful to you, please like and leave a comment.

related suggestion

Start

In the previous article, we have completed the rendering of the micro front end. Although the page has been rendered normally, the base application and the sub-application are executed under the same window. This may cause some problems, such as global variable conflicts. , Global event monitoring and unbinding.

Below we list two specific problems, and then solve them by creating a sandbox.

Examples of questions

1. The sub-application adds a global variable to the window: globalStr='child' . If the base application also has the same global variable: globalStr='parent' at this time, a variable conflict occurs, and the base application variable will be overwritten.

2. After the sub-application is rendered, a global monitoring event is added scroll

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

When the sub-application is uninstalled, the monitoring function is not unbound, and the monitoring of page scrolling always exists. If the sub-application is rendered twice, the listener function will be bound twice, which is obviously wrong.

Next, we will create a JS sandbox environment for the micro front end to isolate the JS of the base application and the sub-application to solve these two typical problems.

Create a sandbox

Since each sub-application needs an independent sandbox, we create a class through class: SandBox. When a new sub-application is created, a new sandbox is created to bind to it.

// /src/sandbox.js
export default class SandBox {
  active = false // 沙箱是否在运行
  microWindow = {} // // 代理的对象
  injectedKeys = new Set() // 新添加的属性,在卸载时清空

  constructor () {}

  // 启动
  start () {}

  // 停止
  stop () {}
}

We use Proxy for proxy operations, and the proxy object is an empty object microWindow . Thanks to the powerful functions of Proxy, sandboxing becomes simple and efficient.

Perform proxy-related operations in constructor microWindow , set the get , set , and deleteProperty . At this time, the sub-application can basically cover the operation of the window.

// /src/sandbox.js
export 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
      },
    })
  }

  ...
}

After creating the agent, we then perfect the start and stop , the implementation is also very simple, as follows:

// /src/sandbox.js
export 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()
    }
  }
}

The prototype of the above sandbox is complete, let's try it to see if it works.

Use sandbox

In src/app.js introduced into the sandbox, in CreateApp creating sandbox instance constructor and mount execution start method sandbox method, unmount performing stop method sandbox process.

// /src/app.js
import 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)
    }
  }
}

We created a sandbox instance above and started the sandbox, will the sandbox take effect?

Obviously it won't work, we also need to wrap the js of the sub-application through a with function, modify the js scope, and point the window of the sub-application to the proxy object. The form is like:

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

bindScope in the sandbox and modify the js scope:

// /src/sandbox.js

export 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);`
  }
}

Then add the use bindScope in the mount method

// /src/app.js

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

At this point, the sandbox really works. Let's verify the first problem in the problem example.

Close the sandbox first. Because the sub-application covers the base application's global variable globalStr , when we access this variable in the base, the value obtained is: child , indicating that the variable conflicts.

After opening the sandbox, print globalStr in the base application again, and the value obtained is: parent , indicating that the variable conflict problem has been resolved and the sandbox is running correctly.

The first problem has been solved, we started to solve the second problem: global monitoring of events.

Override global events

Let’s review the second problem again. The reason for the error is that the event listener is not cleared when the sub-application is uninstalled. If the child application knows that it will be uninstalled and actively clears the event listener, this problem can be avoided, but this is an ideal situation. The application does not know when it will be uninstalled. Second, many third-party libraries also have some global monitoring events, and the sub-applications cannot be fully controlled. Therefore, we need to automatically clear the remaining global monitoring events of the sub-application when the sub-application is uninstalled.

window.addEventListener and window.removeEventListener in the sandbox, record all global monitoring events, and clear them if there are residual global monitoring events when the application is uninstalled.

Create a effect function, perform specific operations here

// /src/sandbox.js

// 记录addEventListener、removeEventListener原生方法
const rawWindowAddEventListener = window.addEventListener
const 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()
    }
  }
}

Execute the effect method in the sandbox's constructor to get the uninstall hook function releaseEffect, and execute the uninstall operation when the sandbox is closed, that is, execute the releaseEffect function in the stop method

// /src/sandbox.js

export 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()
    }
  }
}

In this way, the operations of rewriting global events and uninstalling are basically completed. Let's verify whether it is running normally.

First, close the sandbox and verify the existence of the second problem: After uninstalling the sub-application, scroll the page and still print the scroll, indicating that the event has not been uninstalled.

After opening the sandbox, uninstall the sub-application and scroll the page. At this time, scroll no longer prints, indicating that the event has been uninstalled.

As can be seen from the screenshots, in addition to the scroll event we actively monitor, there are error , unhandledrejection etc. These events are bound by third parties such as frameworks, construction tools, etc. If they are not cleared, it will cause memory failure. Recycling, causing a memory leak.

The sandbox function is basically completed at this point, and both problems have been resolved. Of course, the problems that the sandbox needs to solve are far more than these, but the basic architecture ideas remain the same.

Concluding remarks

The core of the JS sandbox is to modify the js scope and rewrite the window. Its usage scenarios are not limited to the micro front end, but can also be used in other places. For example, we can use the sandbox when we provide external components or introduce third-party components. avoid confrontation.

In the next article, we will complete the style isolation of the micro front end.


cangdu
1.1k 声望281 粉丝

hurry up