Contents
Preface
In large-scale projects, micro-frontend is a common optimization method. This article explains the mechanism and principle of sandbox in micro-frontend.
First, what is a micro frontend?
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently . — Micro Frontends
The front-end is a technical means and method strategy for multiple teams to jointly build modern web applications by publishing functions independently.
Common micro front-end implementation mechanisms
iframe
If you still don’t understand what a micro frontend is, then just think of it as a kind of iframe
But why don’t we use it directly?
iframe
The biggest feature is that it provides a browser-native hard isolation solution, which can perfectly solve problems such as style isolation and js isolation. But its biggest problem is that its isolation cannot be broken, resulting in the inability to share context between applications, resulting in development experience and product experience problems.
- The url is out of sync. When the browser refreshes the iframe url, the status is lost and the back and forward buttons cannot be used.
- The UI is not synchronized and the DOM structure is not shared. Imagine that there is a pop-up box with a mask layer in the iframe 1/4 of the lower right corner of the screen. At the same time, we require that the pop-up box be displayed in the center of the browser and automatically centered when the browser resizes..
- The global context is completely isolated and memory variables are not shared. For communication, data synchronization and other requirements within and outside the iframe system, the cookies of the main application must be transparently transmitted to sub-applications with different root domain names to achieve the login-free effect.
- slow. Each time a sub-application is entered, it is a process of rebuilding the browser context and reloading resources.
Some of the problems are easier to solve (Question 1), some we can turn a blind eye to (Question 4), but some are difficult to solve (Question 3) or even impossible (Question 2) , and these unsolvable problems will bring very serious experience problems to the product, which ultimately led us to abandon the iframe solution.
Taken from the article: Why Not Iframe
Micro frontend sandbox
In the micro front-end scenario, since multiple independent applications are organized together, without similar iframe
native isolation, conflicts are bound to occur, such as global variable conflicts and style conflicts. These conflicts may lead to abnormal application styles or even functional failures. use.
At this time we need an independent running environment, and this environment is called a sandbox, that is sandbox
.
The first step in implementing a sandbox is to create a scope. This scope will not contain global property objects.
First, we need to isolate the browser’s native objects, but how to isolate them and establish a sandbox environment?
Proxy-based sandbox
Assuming that there is only one micro-application running on the current page, it can monopolize the entire window
environment. When switching micro-applications, only window
the environment can be restored to ensure the next use.
This is the single instance scenario .
single instance
A simplest implementation demo
:
const varBox = {}; const fakeWindow = new Proxy ( window , { get ( target, key ) { return varBox[key] || window [key]; }, set ( target, key, value ) { varBox[key] = value; return true ; }, }); window .test = 1 ;
A simple agent proxy
can be implemented window
to store data varBox
in without affecting the original window
value of
In some articles, he implemented the sandbox more specifically, and also had enabling and deactivating functions:
// Modify the global object window method const setWindowProp = ( prop, value, isDel ) => { if (value === undefined || isDel) { delete window [prop]; } else { window [prop] = value; } } class Sandbox { name; proxy = null ; //Global variables added during the sandbox addedPropsMap = new Map (); //Global variables updated during sandbox modifiedPropsOriginalValueMap = new Map (); //Continuously record the map of updated (new and modified) global variables, used for sandbox activation at any time currentUpdatedPropsValueMap = new Map (); // The application sandbox is activated active () { // Re-modify the properties of the window based on the previously modified records, that is, restore the state before the sandbox this . currentUpdatedPropsValueMap . forEach ( ( v, p ) => setWindowProp (p, v)); } // The application sandbox is uninstalled inactive () { // 1 Restore the properties modified during the sandbox to the original properties this . modifiedPropsOriginalValueMap . forEach ( ( v, p ) => setWindowProp (p, v)); // 2 Restore the global variables added during the sandbox Eliminate this . addedPropsMap . forEach ( ( _, p ) => setWindowProp (p, undefined , true )); } constructor ( name ) { this . name = name; const fakeWindow = Object . create ( null ); // Create an empty object with a prototype of null const { addedPropsMap, modifiedPropsOriginalValueMap, currentUpdatedPropsValueMap } = this ; const proxy = new Proxy (fakeWindow, { set ( _, prop, value ) { if (! window . hasOwnProperty (prop)) { // If there is no property on the window, record it in the new property addedPropsMap. set (prop, value); } else if (!modifiedPropsOriginalValueMap. has (prop)) { // If the current window object has this property and has not been updated, record the initial value of the property on the window const originalValue = window [prop]; modifiedPropsOriginalValueMap.set ( prop, originalValue); } // Record modified properties and modified values currentUpdatedPropsValueMap. set (prop, value); //Set the value to the global window setWindowProp (prop,value); console . log ( 'window.prop' , window [prop]); return true ; }, get ( target, prop ) { return window [prop]; }, }); this .proxy = proxy; } } // Initialize a sandbox const newSandBox = new Sandbox ( 'app1' ); const proxyWindow = newSandBox. proxy ; proxyWindow. test = 1 ; console . log ( window . test , proxyWindow. test ) // 1 1; // Close the sandbox newSandBox. inactive (); console . log ( window . test , proxyWindow. test ); // undefined undefined; // Restart the sandbox newSandBox. active (); console . log ( window . test , proxyWindow. test ) // 1 1 ;
The sandbox active
and inactive
solutions are added to activate or uninstall the sandbox. The core function proxy
is created in the constructor.
The principle is similar to the above-mentioned simple demo
implementation, but there is no direct interception window
, but creation of one fakeWindow
, which leads us to Talking about
multi-instance sandbox
multiple instances
We fakeWindow
use it and put the variables used by the micro-application fakeWindow
into , and the shared variables are read window
from .
class Sandbox { name; constructor ( name, context = {} ) { this . name = name; const fakeWindow = Object . create ({}); return new Proxy (fakeWindow, { set ( target, name, value ) { if ( Object . keys (context). includes (name)) { context[name] = value; } target[name] = value; }, get ( target, name ) { // Prioritize using shared objects if ( Object . keys (context). includes (name)) { return context[name]; } if ( typeof target[name] === 'function' && /^[az]/ . test (name)) { return target[name]. bind && target[name]. bind (target); } else { return target[name]; } } }); } // ... } /** * Note that the context here is very important, because our fakeWindow is an empty object, and there are no attributes on the window. * In actual projects, the context here should contain a large number of window attributes. */ // Initialize 2 sandboxes, share document and a global variable const context = { document : window . document , globalData : 'abc' }; const newSandBox1 = new Sandbox ( 'app1' , context); const newSandBox2 = new Sandbox ( 'app2' , context); newSandBox1. test = 1 ; newSandBox2. test = 2 ; window . test = 3 ; /** * Private properties of each environment are isolated */ console . log (newSandBox1. test , newSandBox2. test , window . test ); // 1 2 3; /** * The shared properties are shared by the sandbox, and the globalData in the newSandBox2 environment has also been changed. */ newSandBox1. globalData = '123' ; console . log (newSandBox1. globalData , newSandBox2. globalData ); // 123 123;
diff-based sandbox
It is also called snapshot sandbox . As the name suggests, it takes a snapshot of the current running environment at a certain stage, and then restores the snapshot when needed to achieve isolation.
Similar to the SL method of playing games, save it at a certain moment, then load it again after the operation is completed, and return to the previous state.
Its implementation can be said to be a simplified version of a single instance, divided into two parts: activation and uninstallation.
active () { // Cache the active state of the sandbox this . windowSnapshot = {}; for ( const item in window ) { this . windowSnapshot [item] = window [item]; } Object . keys ( this . modifyMap ). forEach ( p => { window [p] = this . modifyMap [p]; }) }
inactive () { for ( const item in window ) { if ( this . windowSnapshot [item] !== window [item]) { // Record changes this . modifyMap [item] = window [item]; // Restore window window [item] ] = this . windowSnapshot [item]; } } }
When activate
, traverse window
the variables on , save as windowSnapshot
when , deactivate
traverse window
the variables on again, windowSnapshot
compare them with respectively, save the different ones in modifyMap
, and window
restore the variables
of when the application is switched again, you can modifyMap
restore the variables of on the window to achieve A sandbox switch.
class Sandbox { private windowSnapshot private modifyMap activate : () => void ; deactivate : () => void ; } const sandbox = new Sandbox (); sandbox.activate ( ); // Execute arbitrary code sandbox.deactivate ( );
This solution is much more complicated to implement in actual projects, and its comparison algorithm needs to consider many situations. For example, for window.a.b.c = 123
this modification or the modification of the prototype chain, it is impossible to roll back to the global state before the application is loaded. Therefore, this solution is generally not the first choice. It is a downgrade treatment for old browsers.
This downgrade scheme is also available in qiankun
, and is called SnapshotSandbox
.
iframe based sandbox
As mentioned above, as an implementation method of micro front-end, it also has its unique role iframe
in the sandbox .iframe
const iframe = document . createElement ( 'iframe' , { url : 'about:blank' }); const sandboxGlobal = iframe.contentWindow ; sandbox ( sandboxGlobal);
Note: Only iframes in the same domain can retrieve the corresponding contentWindow. Therefore, it is necessary to provide an empty same-domain URL of the host application as the initial loading URL of this iframe. According to the HTML specification, this URL uses about:blank to ensure that the same domain is guaranteed, and resource loading will not occur.
class SandboxWindow { constructor ( options, context, frameWindow ) { return new Proxy (frameWindow, { set ( target, name, value ) { if ( Object . keys (context). includes (name)) { context[name] = value; } target[name] = value; }, get ( target, name ) { // Prioritize using shared objects if ( Object . keys (context). includes (name)) { return context[name]; } if ( typeof target[name] === 'function' && /^[az]/ . test (name)) { return target[name]. bind && target[name]. bind (target); } else { return target[name]; } } }); } // ... } const iframe = document . createElement ( 'iframe' , { url : 'about:blank' }); document . body . appendChild (iframe); const sandboxGlobal = iframe. contentWindow ; // Variables that need to be shared globally const context = { document : window . document , history : window . history }; const newSandBoxWindow = new SandboxWindow ({}, context, sandboxGlobal); // newSandBoxWindow.history global object // newSandBoxWindow.abc is 'abc' sandbox environment global variable // window .abc is undefined
To summarize, iframe
the following features can be achieved using the sandbox:
- Global variable isolation, such as ,
setTimeout
different version isolationlocation
react
- Routing isolation, applications can implement independent routing or share global routing
- Multi-instance, multiple independent micro-applications can run simultaneously
- Security policy can configure micro-application restrictions on resource loading
Cookie
.localStorage
The sandbox solution iframe
is better, but there are still the following problems:
- Compatibility issues: There may be differences in implementation solutions between different browsers, which may lead to compatibility issues.
- additional performance overhead
- Compared with other solutions, communication methods between applications are more troublesome
Sandbox based on ShadowRealm
ShadowRealm
The proposal provides a new mechanism to execute code in the context of new global objects and JavaScript
built-in assemblies .JavaScript
const sr = new ShadowRealm (); // Sets a new global within the ShadowRealm only sr.evaluate( 'globalThis.x = "my shadowRealm"' ); globalThis. x = "root" ; // const srx = sr.evaluate( 'globalThis.x' ); srx; // "my shadowRealm" x; // "root"
In addition to directly pointing to the string code, you can also reference the file for execution:
const sr = new ShadowRealm (); const redAdd = await sr. importValue ( './inside-code.js' , 'add' ); let result = redAdd ( 2 , 3 ); console . assert (result === 5 );
Click here to view detailed introduction
Back to the topic, ShadowRealm
there are many restrictions on security, and there is a lack of some information interaction methods. Finally, its compatibility is also a major pain point:
As of the current Chrome
version 117.0.5938.48
, this API is not supported, and we still need polyfill
to use it.
Based on VM sandbox
VM sandboxing uses a module similar node
to vm
, by creating a sandbox and then passing in the code that needs to be executed.
const vm = require ( 'node:vm' ); const x = 1 ; const context = { x : 2 }; vm. createContext (context); // Contextify the object. const code = 'x += 40; var y = 17;' ; // `x` and `y` are global variables in the context. // Initially, x has the value 2 because that is the value of context.x . vm. runInContext (code, context); console . log (context. x ); // 42 console . log (context. y ); // 17 console . log (x); // 1; y is not defined.
vm
Although node
it has been implemented in sandbox
, it does not play a big role in the micro-front-end implementation of front-end projects.
Summarize
This article lists a variety of sandbox implementation solutions. In the current front-end field, there are various sandbox implementations. Currently, there is no perfect solution. It is more about adopting suitable solutions in suitable scenarios.