A brief analysis of micro front-end sandbox

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 iframeBut why don’t we use it directly?

iframeThe 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.

  1. 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.
  2. 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..
  3. 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.
  4. 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 iframenative 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 windowenvironment. When switching micro-applications, only windowthe 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 proxycan be implemented windowto store data varBoxin without affecting the original windowvalue 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 activeand inactivesolutions are added to activate or uninstall the sandbox. The core function proxyis created in the constructor.
The principle is similar to the above-mentioned simple demoimplementation, but there is no direct interception window, but creation of one fakeWindow, which leads us to Talking about
multi-instance sandbox

multiple instances

We fakeWindowuse it and put the variables used by the micro-application fakeWindowinto , and the shared variables are read windowfrom .

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 windowthe variables on , save as windowSnapshot
when , deactivatetraverse windowthe variables on again, windowSnapshotcompare them with respectively, save the different ones in modifyMap, and windowrestore the variables
of when the application is switched again, you can modifyMaprestore 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 = 123this 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 iframein 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, iframethe following features can be achieved using the sandbox:

  • Global variable isolation, such as , setTimeoutdifferent version isolationlocationreact
  • 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 iframeis better, but there are still the following problems:

  1. Compatibility issues: There may be differences in implementation solutions between different browsers, which may lead to compatibility issues.
  2. additional performance overhead
  3. Compared with other solutions, communication methods between applications are more troublesome

Sandbox based on ShadowRealm

ShadowRealmThe proposal provides a new mechanism to execute code in the context of new global objects and JavaScriptbuilt-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, ShadowRealmthere 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 Chromeversion 117.0.5938.48, this API is not supported, and we still need polyfillto use it.

Based on VM sandbox

VM sandboxing uses a module similar nodeto 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.

vmAlthough nodeit 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.

Quote