The micro front end has become a hot topic in the front end field. In terms of technology, the micro front end has always been a topic that can not be avoided is the front end sandbox

What is a sandbox

Sandboxie is a virtual system application that allows you to run a browser or other application in a sandbox environment, so the changes you make can be deleted later. It creates a sandboxed environment in which programs run without permanent impact on the hard drive. In network security, a sandbox is a tool used to test behavior such as untrusted files or applications in an isolated environment

To put it simply, sandbox is an environment isolated from the outside world. The inside and outside environment do not affect each other, and the outside world cannot modify any information in the environment. The things in the sandbox belong to a world alone.

The JavaScript sandbox

For JavaScript, a sandbox is not a traditional sandbox, but a syntactic Hack. It is a security mechanism that keeps untrusted code running inside the sandbox, preventing it from accessing code outside the sandbox. When parsing or executing untrusted JavaScript, isolating the execution environment of the code being executed, and limiting the accessible objects in the executing code, JavaScript closures that handle module dependencies can often be called sandboxes at first.

JavaScript sandbox implementation

We can roughly divide the implementation of the sandbox into two parts:

  • Build a closure environment
  • Emulate native browser objects

Build the closure environment

We know that in JavaScript, there are only global scopes, function scopes, and since ES6, block-level scopes. If you want to isolate the definition of variables and functions in a piece of code, you can only encapsulate the code into a Function due to JavaScript’s control over scope, and use Function scope to achieve scope isolation. It is also because of the need for this way of using functions to achieve scope isolation that IIFE (Call function expressions immediately), a design pattern known as self-executing anonymous functions, is created

(function foo(){ var a = 1; console.log(a); }) (); // The variable name console.log(a) cannot be accessed from the outside // "Uncaught ReferenceError: A is not defined"Copy the code

When a function becomes a function expression that executes immediately, the variables in the expression cannot be accessed externally and have their own lexical scope. Not only does it avoid external access to variables in IIFE, but it also does not pollute the global scope, which makes up for JavaScript’s shortcomings in scope. This is common when writing plug-ins and class libraries, such as the sandbox in JQuery

(function (window) { var jQuery = function (selector, context) { return new jQuery.fn.init(selector, context); } jquery.fn = jquery.prototype = function () {jquery.fn = jquery.fn; window.jQeury = window.$ = jQuery; })(window);})(window);})(window);Copy the code

When IIFE is assigned to a variable, it does not store the IIFE itself, but the result returned by the IIFE execution.

Var result = (function () {var name = ""; return name; }) (); console.log(result); // "/"Copy the code

Emulation of native browser objects

The purpose of emulating native browser objects is to prevent closure environments from manipulating native objects. Tampering and polluting the native environment; Before we finish emulating browser objects we need to focus on a few less commonly used apis.

eval

The eval function converts a string into code execution and returns one or more values

Var b = eval (" ({name: 'Joe'}) ") to the console. The log (b.n ame);Copy the code

Because the code executed by Eval has access to closures and global scopes, this creates a security problem for code injection, because the code can go up the scope chain and tamper with global variables, which we don’t want

new Function

The Function constructor creates a new Function object. Call this constructor directly to create the function on the fly

grammar

new Function ([arg1[, arg2[, ...argN]],] functionBody)

arg1, arg2, … ArgN The name of the argument used by the function must be legally named. The parameter name is a string of valid JavaScript identifiers, or a comma-separated list of valid strings; For example, “×”, “theValue”, or “a,b”.

FunctionBody A string containing JavaScript statements that include function definitions.

const sum = new Function('a'.'b'.'return a + b');

console.log(sum(1.2));/ / 3
Copy the code

You will also encounter security issues similar to eval and relatively minor performance issues.

var a = 1; function sandbox() { var a = 2; return new Function('return a; '); } var f = sandbox(); console.log(f())Copy the code

Unlike Eval, Function creates functions that can only be run in global scope. They are always created in the global environment, so at run time they can only access global variables and their local variables, not variables in the scope in which they were created by the Function constructor; However, it still has access to the global scope. New Function() is a better alternative to eval(). It has excellent performance and security, but it still doesn’t solve the problem of accessing the whole world.

with

With is a JavaScript keyword that extends the scope chain of a statement. It allows semi-sandbox execution. What is half sandbox? Statement adds an object to the top of the scope chain. If there is an unused variable in the sandbox with the same name as an attribute in the scope chain, the variable points to that attribute value. If there is no attribute with the same name, a ReferenceError is raised.

      function sandbox(o) {
            with (o){
                //a=5; 
                c=2;
                d=3;
                console.log(a,b,c,d); // 0,1,2,3 // each variable is first considered to be a local variable. If the local variable has the same name as an obj object property, the local variable points to the obj object property.}}var f = {
            a:0.b:1
        }
        sandbox(f);       
        console.log(f);
        console.log(c,d); // 2,3 c and d are leaked to the window object
Copy the code

Internally, with uses the in operator. For each variable access within the block, it evaluates the variable under sandbox conditions. If the condition is true, it retrieves the variable from the sandbox. Otherwise, the variable is looked up globally. But the with statement causes the program to first look up a variable’s value in the specified object. So variables that are not originally properties of the object can be slow to find and are not suitable for performance-sensitive programs (JavaScript engines perform several performance optimizations at compile time). Some of these optimizations rely on being able to statically analyze the code against its lexology and pre-determine where all variables and functions are defined so that identifiers can be found quickly during execution.) . With also causes data leakage (in non-strict mode, a global variable is automatically created in global scope)

The in operator.

The IN operator detects whether the left-hand operand is a member of the right-hand operand. Where the left-hand operand is a string, or an expression that can be converted to a string, and the right-hand operand is an object or array.

var o = { a : 1, b : function() {} } console.log("a" in o); //true console.log("b" in o); //true console.log("c" in o); //false console.log("valueOf" in o); // Return true, inheriting Object's prototype method console.log("constructor" in o); // Returns true, inheriting the prototype properties of ObjectCopy the code

with + new Function

The use of with slightly limits the scope of the sandbox, providing the object to be looked up from the current with, but still fetching from above if it cannot be found, contaminating or tampering with the global environment.

function sandbox (src) {
    src = 'with (sandbox) {' + src + '} '
    return new Function('sandbox', src)
}
var str = 'let a = 1; Window. The name = "* *"; console.log(a); console.log(b)'
var b = 2
sandbox(str)({});
console.log(window.name);/ / 'zhang'
Copy the code

ProxySandbox Implementation

Consider from the previous section, if you can use with to restrict access to each variable in the block to the sandbox condition of calculating variables, variables from the sandbox. Is it possible to solve the JavaScript sandbox perfectly?

Implement the JavaScript sandbox using with plus proxy

ES6 Proxy is used to modify the default behavior of certain operations, which is equivalent to making changes at the language level. It is a kind of meta programming.

function sandbox(code) {
    code = 'with (sandbox) {' + code + '} '
    const fn = new Function('sandbox', code)

    return function (sandbox) {
        const sandboxProxy = new Proxy(sandbox, {
            has(target, key) {
                return true}})return fn(sandboxProxy)
    }
}
var a = 1;
var code = 'console.log(a)' // TypeError: Cannot read property 'log' of undefined
sandbox(code)({})
Copy the code

We mentioned earlier that with internally uses the IN operator to evaluate variables, and if the condition is true, it retrieves the variables from the sandbox. Ideally, there are no problems, but there are always special cases, such as Symbol. Unscopables.

Symbol.unscopables

The Symbol. Unscopables property of an object, pointing to an object. This object specifies which attributes are excluded from the with environment when the with keyword is used.

Array.prototype[Symbol.unscopables]
/ / {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// keys: true
// }

Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
Copy the code

As the code above shows, the array has six attributes that are excluded by the with command.

So our code needs to be modified as follows:

function sandbox(code) {
    code = 'with (sandbox) {' + code + '} '
    const fn = new Function('sandbox', code)

    return function (sandbox) {
        const sandboxProxy = new Proxy(sandbox, {
            has(target, key) {
                return true
            },
            get(target, key) {
                if (key === Symbol.unscopables) return undefined
                return target[key]
            }
        })
        return fn(sandboxProxy)
    }
}
var test = {
    a: 1.log(){
        console.log('11111')}}var code = 'log(); console.log(a)' // 1111,TypeError: Cannot read property 'log' of undefined
sandbox(code)(test)
Copy the code

Symbol. Unscopables Defines the unusable properties of an object. The Unscopeable attribute is never retrieved from the sandbox object in the with statement, but is retrieved directly from a closure or global scope.

SnapshotSandbox

The source code of the Qiankun snapshotSandbox is as follows.

        function iter(obj, callbackFn) {
            for (const prop in obj) {
                if(obj.hasOwnProperty(prop)) { callbackFn(prop); }}}/** * Diff - based sandbox for older browsers that do not support Proxy */
        class SnapshotSandbox {
            constructor(name) {
                this.name = name;
                this.proxy = window;
                this.type = 'Snapshot';
                this.sandboxRunning = true;
                this.windowSnapshot = {};
                this.modifyPropsMap = {};
                this.active();
            }
            / / activation
            active() {
                // Record the current snapshot
                this.windowSnapshot = {};
                iter(window.(prop) = > {
                    this.windowSnapshot[prop] = window[prop];
                });

                // Restore the previous changes
                Object.keys(this.modifyPropsMap).forEach((p) = > {
                    window[p] = this.modifyPropsMap[p];
                });

                this.sandboxRunning = true;
            }
            / / reduction
            inactive() {
                this.modifyPropsMap = {};

                iter(window.(prop) = > {
                    if (window[prop] ! = =this.windowSnapshot[prop]) {
                        // Record the changes and restore the environment
                        this.modifyPropsMap[prop] = window[prop];
                      
                        window[prop] = this.windowSnapshot[prop]; }});this.sandboxRunning = false; }}let sandbox = new SnapshotSandbox();
        //test
        ((window) = > {
            window.name = 'Joe'
            window.age = 18
            console.log(window.name, window.age) / / zhang SAN, 18
            sandbox.inactive() / / reduction
            console.log(window.name, window.age) //	undefined,undefined
            sandbox.active() / / activation
            console.log(window.name, window.age) / / zhang SAN, 18
        })(sandbox.proxy);
Copy the code

The implementation of snapshotSandbox is relatively simple, and it is mainly used in browsers of earlier versions that do not support Proxy. The principle is based on diff. When activating or uninstalling sub-applications, the sandbox is recorded or restored in the form of snapshots to achieve the sandbox, which will pollute the global window.

legacySandBox

Proxy sandbox was implemented in Singular mode. In order to facilitate understanding, some codes were simplified and annotated here.

//legacySandBox
const callableFnCacheMap = new WeakMap(a);function isCallable(fn) {
  if (callableFnCacheMap.has(fn)) {
    return true;
  }
  const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
  const callable = naughtySafari ? typeof fn === 'function' && typeoffn ! = ='undefined' : typeof fn ===
    'function';
  if (callable) {
    callableFnCacheMap.set(fn, callable);
  }
  return callable;
};

function isPropConfigurable(target, prop) {
  const descriptor = Object.getOwnPropertyDescriptor(target, prop);
  return descriptor ? descriptor.configurable : true;
}

function setWindowProp(prop, value, toDelete) {
  if (value === undefined && toDelete) {
    delete window[prop];
  } else if (isPropConfigurable(window, prop) && typeofprop ! = ='symbol') {
    Object.defineProperty(window, prop, {
      writable: true.configurable: true
    });
    window[prop] = value; }}function getTargetValue(target, value) {
  /* Bind isCallable &&! isBoundedFunction && ! IsConstructable function objects, such as window.console, window.atob, etc. There is no perfect way to check if prototype still has enumerable extension methods @warning Because some edge cases may be raised (such as security exceptions in the context of iframe that may be raised by calling the Top Window object in lodash.isFunction) */
  if(isCallable(value) && ! isBoundedFunction(value) && ! isConstructable(value)) {const boundValue = Function.prototype.bind.call(value, target);
    for (const key in value) {
      boundValue[key] = value[key];
    }
    if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
      Object.defineProperty(boundValue, 'prototype', {
        value: value.prototype,
        enumerable: false.writable: true
      });
    }

    return boundValue;
  }

  return value;
}

/** * Proxy based implementation of the sandbox */
class SingularProxySandbox {
  /** New global variable */ added during sandbox
  addedPropsMapInSandbox = new Map(a);/** Global variables updated during sandbox */
  modifiedPropsOriginalValueMapInSandbox = new Map(a);/** Keeps a map of updated (new and modified) global variables, used to do snapshot */ at any time
  currentUpdatedPropsValueMap = new Map(a); name; proxy; type ='LegacyProxy';

  sandboxRunning = true;

  latestSetProp = null;

  active() {
    if (!this.sandboxRunning) {
      this.currentUpdatedPropsValueMap.forEach((v, p) = > setWindowProp(p, v));
    }

    this.sandboxRunning = true;
  }

  inactive() {
    // console.log(' this.modifiedPropsOriginalValueMapInSandbox', this.modifiedPropsOriginalValueMapInSandbox)
    // console.log(' this.addedPropsMapInSandbox', this.addedPropsMapInSandbox)
    // Delete added attributes and modify existing attributes
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) = > setWindowProp(p, v));
    this.addedPropsMapInSandbox.forEach((_, p) = > setWindowProp(p, undefined.true));

    this.sandboxRunning = false;
  }

  constructor(name) {
    this.name = name;
    const {
      addedPropsMapInSandbox,
      modifiedPropsOriginalValueMapInSandbox,
      currentUpdatedPropsValueMap
    } = this;

    const rawWindow = window;
    // object.create (null) passes in an Object with no prototype chain
    const fakeWindow = Object.create(null); 

    const proxy = new Proxy(fakeWindow, {
      set: (_, p, value) = > {
        if (this.sandboxRunning) {
          if(! rawWindow.hasOwnProperty(p)) { addedPropsMapInSandbox.set(p, value); }else if(! modifiedPropsOriginalValueMapInSandbox.has(p)) {// If the property exists in the current Window object and is not recorded in the Record map, the initial value of the property is recorded
            const originalValue = rawWindow[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }

          currentUpdatedPropsValueMap.set(p, value);
          // The window object must be reset to get updated data the next time you get it
          rawWindow[p] = value;

          this.latestSetProp = p;

          return true;
        }

        // In strict-mode, Proxy handler.set returns false and raises TypeError, which should be ignored in sandbox unload cases
        return true;
      },

      get(_, p) {
        // Avoid using window.window or window.self to escape the sandbox environment and trigger the real environment
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }
        const value = rawWindow[p];
        return getTargetValue(rawWindow, value);
      },

      has(_, p) { / / returns a Boolean
        return p in rawWindow;
      },

      getOwnPropertyDescriptor(_, p) {
        const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
        If a property does not exist as a property of the target object itself, it cannot be set to unconfigurable
        if(descriptor && ! descriptor.configurable) { descriptor.configurable =true;
        }
        returndescriptor; }});this.proxy = proxy; }}let sandbox = new SingularProxySandbox();

((window) = > {
  window.name = 'Joe';
  window.age = 18;
  window.sex = 'male';
  console.log(window.name, window.age,window.sex) // Zhang SAN,18, male
  sandbox.inactive() / / reduction
  console.log(window.name, window.age,window.sex) / / zhang SAN, undefined undefined
  sandbox.active() / / activation
  console.log(window.name, window.age,window.sex) // Zhang SAN,18, male
})(sandbox.proxy); //test
Copy the code

LegacySandBox still operates on the Window object, but it does this by returning the state of the application when it is activated and the state of the master application when it is uninstalled. It also polles the Window, but performs better than the snapshot sandbox and does not need to iterate over the Window object.

ProxySandbox (several sandboxes)

In proxySandbox, the object fakeWindow was proxyed using createFakeWindow. This method makes a copy of the Document, location, top, window, etc properties of the window and gives it to fakeWindow.

Source code display:


function createFakeWindow(global: Window) {
  // map always has the fastest performance in has check scenario
  // see https://jsperf.com/array-indexof-vs-set-has/23
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;

  /* copy the non-configurable property of global to fakeWindow see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object. */
  Object.getOwnPropertyNames(global)
    .filter((p) = > {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      return! descriptor? .configurable; }) .forEach((p) = > {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

        /* make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return. see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get > The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property. */
        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          descriptor.configurable = true;
          /* The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was Example: Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false} Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false} */
          if(! hasGetter) { descriptor.writable =true; }}if (hasGetter) propertiesWithGetter.set(p, true);

        // freeze the descriptor to avoid being modified by zone.js
        // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor)); }});return {
    fakeWindow,
    propertiesWithGetter,
  };
}
Copy the code

ProxySandbox copies a copy of fakeWindow, does not pollute the global window, and allows multiple sub-applications to be loaded simultaneously. See proxySandbox for the source code

About CSS Isolation

Common ones are:

  • CSS Module
  • namespace
  • Dynamic StyleSheet
  • css in js
  • Shadow DOM

We will not repeat the common ones here, but we will focus on Shadow DO.

Shadow DOM

Shadow DOM allows you to attach a hidden DOM tree to a regular DOM tree — it starts with the Shadow root node as the root node, and below this root node can be any element, just like normal DOM elements.