preface

Since the micro front-end framework micro-App became open source, many friends are very interested and ask me how to achieve it, but it is not a few words can be understood. To illustrate how this works, I’m going to implement a simple micro front-end framework from scratch. Its core features include rendering, JS sandbox, style isolation, and data communication. This is the second in a series of articles: Sandbox, which is divided into four articles based on features.

Through these articles, you will learn how micro front end frameworks work and how they are implemented, which will be of great help if you use them later or write your own. If this post helped you, feel free to like it and leave a comment.

Related to recommend

  • Micro-app Warehouse address
  • Simple-micro-app Warehouse address
  • Write a micro front end frame – render from scratch
  • Write a micro front end frame – sandbox from scratch
  • Write a micro front end frame – style isolation section from scratch
  • Write a micro front end framework from scratch – data communication article
  • Micro – app is introduced

start

In the previous article, we finished rendering the micro front end, and although the page was rendered normally, the base application and the child application were executing in the same window, which could cause problems such as global variable collisions, global event listening and unbinding.

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

Problem of the sample

If the child application adds a global variable to the window: globalStr=’child’, if the base application has the same global variable: globalStr=’parent’, then the base application’s variable will be overwritten.

2. The child application adds a global listener event through the listener Scroll after rendering

window.addEventListener('scroll'.() = > {
  console.log('scroll')})Copy the code

When the child application is uninstalled, the listener function is not unbound, and the listener for page scrolling is always present. If the child applies a second rendering, the listener will bind twice, which is obviously wrong.

Next, we solved these two typical problems by creating a JS sandbox environment for the micro front to isolate the JS for the base application and the sub-application.

Create a sandbox

Since each child application requires a separate SandBox, we create a class with a class: SandBox. When a new child application is created, a new SandBox is created to bind to it.

// /src/sandbox.js
export default class SandBox {
  active = false // Whether the sandbox is running
  microWindow = {} // // Object of the proxy
  injectedKeys = new Set(a)// The newly added properties will be cleared during uninstallation

  constructor () {}

  / / start
  start () {}

  / / stop
  stop () {}
}
Copy the code

We use Proxy for Proxy operation, and the Proxy object is the empty object microWindow. Thanks to the powerful function of Proxy, it becomes simple and efficient to implement sandbox.

Perform agent-related operations in constructor, Proxy microWindow through Proxy, set get, set, deleteProperty three interceptors, at this time child application operations on window can basically override.

// /src/sandbox.js
export default class SandBox {
  active = false // Whether the sandbox is running
  microWindow = {} // // Object of the proxy
  injectedKeys = new Set(a)// The newly added properties will be cleared during uninstallation

  constructor () {
    this.proxyWindow = new Proxy(this.microWindow, {
      / / value
      get: (target, key) = > {
        // The proxy object is preferred
        if (Reflect.has(target, key)) {
          return Reflect.get(target, key)
        }

        // Otherwise, use the window object
        const rawValue = Reflect.get(window, key)

        // If the bottom value is a function, you need to bind the window object, such as console, alert, etc
        if (typeof rawValue === 'function') {
          const valueStr = rawValue.toString()
          // Exclude constructors
          if (!/^function\s+[A-Z]/.test(valueStr) && !/^class\s+/.test(valueStr)) {
            return rawValue.bind(window)}}// In other cases, return directly
        return rawValue
      },
      // Set variables
      set: (target, key, value) = > {
        // Sandbox can only set variables at run time
        if (this.active) {
          Reflect.set(target, key, value)

          // Record the added variables for subsequent cleanup operations
          this.injectedKeys.add(key)
        }

        return true
      },
      deleteProperty: (target, key) = > {
        // The deletion condition is met only when the current key exists on the proxy object
        if (target.hasOwnProperty(key)) {
          return Reflect.deleteProperty(target, key)
        }
        return true}})}... }Copy the code

After creating the agent, we continue to improve the start and stop methods, the implementation is very simple, as follows:

// /src/sandbox.js
export default class SandBox {.../ / start
  start () {
    if (!this.active) {
      this.active = true}}/ / stop
  stop () {
    if (this.active) {
      this.active = false

      // Clear the variables
      this.injectedKeys.forEach((key) = > {
        Reflect.deleteProperty(this.microWindow, key)
      })
      this.injectedKeys.clear()
    }
  }
}
Copy the code

We’ve got a prototype of the sandbox above, so let’s try it out and see if it works.

Use the sandbox

Introduce the sandbox in SRC /app.js, create an instance of the sandbox in the CreateApp constructor, and execute the sandbox start method in the mount method, and the sandbox stop method in the unmount method.

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

  /** * Uninstall application *@param Destory Whether to completely destroy and delete cache resources */
  unmount (destory) {
    ...
+    this.sandbox.stop()
    // If deStory is true, the application is deleted
    if (destory) {
      appInstanceMap.delete(this.name)
    }
  }
}
Copy the code

We created the sandbox instance above and started the sandbox, so the sandbox works?

Obviously not, we need to wrap the child application’s JS with a function, and modify the JS scope so that the child application’s window points to the proxy object. Forms such as:

(function(window, self) {
  with(window){subapplication js code}}).call(proxy object, proxy object, proxy object)Copy the code

Add the method bindScope to the sandbox and change the js scope:

// /src/sandbox.js

export default class SandBox {...// Modify the js scope
  bindScope (code) {
    window.proxyWindow = this.proxyWindow
    return `; (function(window, self){with(window){;${code}\n}}).call(window.proxyWindow, window.proxyWindow, window.proxyWindow); `}}Copy the code

Then add the use of 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))
    })
  }
}
Copy the code

This is where the sandbox really kicks in. Let’s verify the first question in the problem example.

Close the sandbox first because the child application overrides the base application’s global variablesglobalStr, when we access this variable in the base, we get the value:child, indicating that variables conflict.

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

Now that the first problem has been resolved, let’s move on to the second problem: global listener events.

Overwriting global events

To review again the second question, the reason for the error is in the child application uninstall without emptying the event listeners, if the child know they will be unloaded, active empty event listeners, this problem can be avoided, but it is an ideal situation, one is the son don’t know when their application is unloaded, the second is a lot of third-party libraries also have some of the global monitoring events, Sub-applications cannot all be controlled. Therefore, we need to automatically clear the residual global listening events of the child application when the child application is uninstalled.

We rewrite the window in the sandbox. AddEventListener and window. The removeEventListener, record all global event monitoring, at the time of application uninstall if there are residual global monitoring events are cleared.

Create an effect function where the action is performed

// /src/sandbox.js

// Record the addEventListener and removeEventListener native methods
const rawWindowAddEventListener = window.addEventListener
const rawWindowRemoveEventListener = window.removeEventListener

/** * Override global event listening and unbinding *@param MicroWindow prototype object */
 function effect (microWindow) {
  // Use Map to record global events
  const eventListenerMap = new Map(a)/ / rewrite addEventListener
  microWindow.addEventListener = function (type, listener, options) {
    const listenerList = eventListenerMap.get(type)
    // If the current event is not listened for the first time, add cache
    if (listenerList) {
      listenerList.add(listener)
    } else {
      // The first time the current event is listened on, the data is initialized
      eventListenerMap.set(type, new Set([listener]))
    }
    // Execute the native listener function
    return rawWindowAddEventListener.call(window, type, listener, options)
  }

  / / rewrite removeEventListener
  microWindow.removeEventListener = function (type, listener, options) {
    const listenerList = eventListenerMap.get(type)
    // Remove the listener function from the cache
    if(listenerList? .size && listenerList.has(listener)) { listenerList.delete(listener) }// Execute the native unbinding function
    return rawWindowRemoveEventListener.call(window, type, listener, options)
  }

  // Clear the remaining events
  return () = > {
    console.log('Global events to uninstall', eventListenerMap)
    // Clear the window binding event
    if (eventListenerMap.size) {
      // Unbind the remaining unbound functions one by one
      eventListenerMap.forEach((listenerList, type) = > {
        if (listenerList.size) {
          for (const listener of listenerList) {
            rawWindowRemoveEventListener.call(window, type, listener)
          }
        }
      })
      eventListenerMap.clear()
    }
  }
}
Copy the code

The effect function is executed in the constructor of the sandbox to get releaseEffect, the hook function that is uninstalled. When the sandbox is closed, the releaseEffect function is executed in the stop method

// /src/sandbox.js

export default class SandBox {...// Modify the js scope
  constructor () {
    // Uninstall the hook
+   this.releaseEffect = effect(this.microWindow)
    ...
  }

  stop () {
    if (this.active) {
      this.active = false

      // Clear the variables
      this.injectedKeys.forEach((key) = > {
        Reflect.deleteProperty(this.microWindow, key)
      })
      this.injectedKeys.clear()
      
      // Uninstall global events
+      this.releaseEffect()
    }
  }
}
Copy the code

This overwrites the global event and the uninstall operation is basically complete, let’s verify whether the normal operation.

First, close the sandbox to verify the existence of problem 2: After the child application is uninstalled, scroll is still printed, indicating that the event is not uninstalled.

After the sandbox is opened, the child application is uninstalled and the page is scrolled. If the Scroll is not printed, the event is uninstalled.

As can be seen from the screenshots, in addition to the Scroll event that we actively listen to, there are other global events such as Error and unhandledrejection. These events are bound by frameworks, construction tools and other third parties. If you do not empty them, memory cannot be recycled and memory leaks will occur.

The sandbox feature is almost complete here, and both problems have been solved. Of course there are many more problems that sandboxes have to solve, but the basic architectural idea remains the same.

conclusion

The core of the JS sandbox is to modify the JS scope and rewrite the window. It is not limited to the micro front end, but can also be used elsewhere, such as when we provide components to the outside or introduce third party components to avoid conflicts.

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