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.