Reference in qiankun JS sandbox part of the code code, code.

As well as a lot of reference, copy the CONTENT and pictures of PPT

background

In the micro front end, in order to prevent the JS of the project from influencing each other and conflict, resulting in the project can not work normally, so a simple sandbox is implemented to isolate the JS operating environment between various projects

The principle of structure

In our sandbox implementation, we will have two environments:

Global Env: Refers to the Global environment in which your framework application runs, and represents the external parts.

The child application should actually run in an internal sandbox when loaded, as shown in this image.

Simply put, child applications can read the global environment and not modify it, and all changes are kept in the internal environment to limit its impact.

The sandbox type

According to the functional logic of sandbox implementation, there are two types

1. Snapshot sandbox

One of the most classic practical ideas is actually the snapshot sandbox.

The snapshot sandbox is used to record the snapshot when the application sandbox is mounted and unmounted, and to restore the environment according to the snapshot when the application is switched.

Here’s an example:

Apply A to start = Take A snapshot inside application Awindow.a1 = 10Apply A uninstall = to restore snapshot Windows. a1 =undefined
Copy the code

There are two ways to do this

1. Run two for loops to compare and take snapshots and restore snapshots

function iter(obj: typeof window, callbackFn: (prop: any) => void) {
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const prop in obj) {
    // patch for clearInterval for compatible reason, see #1490
    if (obj.hasOwnProperty(prop) || prop === 'clearInterval') { callbackFn(prop); }}}active() {
    // Record the current snapshot
    this.windowSnapshot = {} as Window;
    iter(window.(prop) = > {
      this.windowSnapshot[prop] = window[prop];
    });

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

    this.sandboxRunning = true;
  }

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]; }});if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] The ${this.name}origin window restore... `.Object.keys(this.modifyPropsMap));
    }

    this.sandboxRunning = false;
  }
Copy the code

2. Use Proxy to record changes

A proxy can set a hook for an Object. When an Object is get/set, the hook is triggered

By hijacking the window, we can hijack some of the child application’s changes to the global environment. As the child application attaches, modifies, and deletes things to the window, we can record the action. When we restore to the global environment outside, we just need to reverse the previous operation. For example, we set a new variable window.a = 123 inside the sandbox. So on the way out, we just delete the A variable.

This is the core code

const proxy = new Proxy(fakeWindow, {
      set: (_: Window, p: PropertyKey, value: any): boolean= > {
        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 as any)[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
          // eslint-disable-next-line no-param-reassign
          (rawWindow as any)[p] = value;

          this.latestSetProp = p;

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}! `);
        }

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

      get(_: Window, p: PropertyKey): any {
        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
        // or use window.top to check if an iframe context
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }

        const value = (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      // trap in operator
      // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
      has(_: Window, p: string | number | symbol): boolean {
        return p in rawWindow;
      },

      getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined {
        const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
        // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
        if(descriptor && ! descriptor.configurable) { descriptor.configurable =true;
        }
        returndescriptor; }});Copy the code

The downside is that you can’t support multiple instances, which means that I can’t have two sandboxes active at the same time. I can’t mount two applications at the same time, or they’ll fight each other and cause errors.

2. Proxy sandbox

Another way to think about it is to completely isolate the environment inside the child application from the environment outside it. As shown in the figure, our APPLICATION A lives in the sandbox of application A, and application B lives in the sandbox of application B. There is no interference between the two. The idea of sandbox is also realized through ES6 proxy and proxy feature.

An object is stored inside the sandbox to record changes made inside the sandbox so that they do not affect the global environment. Records are read first, not Windows

The main code is as follows

set: (target: FakeWindow, p: PropertyKey, value: any): boolean= > {
        if (this.sandboxRunning) {
          // We must kept its description while the property existed in rawWindow before
          if(! target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
            const{ writable, configurable, enumerable } = descriptor! ;if (writable) {
              Object.defineProperty(target, p, { configurable, enumerable, writable, value, }); }}else {
            // @ts-ignore
            target[p] = value;
          }

          if(variableWhiteList.indexOf(p) ! = = -1) {
            // @ts-ignore
            rawWindow[p] = value;
          }

          updatedValueSet.add(p);

          this.latestSetProp = p;

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}! `);
        }

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

      get(target: FakeWindow, p: PropertyKey): any {
        if (p === Symbol.unscopables) return unscopables;

        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'window' || p === 'self') {
          return proxy;
        }

        // hijack global accessing with globalThis keyword
        if (p === 'globalThis') {
          return proxy;
        }

        if (
          p === 'top' ||
          p === 'parent' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))) {// if your master app in an iframe context, allow these props escape the sandbox
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }
          return (rawWindow as any)[p];
        }

        // proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        // mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher
        if (p === 'document' || p === 'eval') {
          setCurrentRunningSandboxProxy(proxy);
          // FIXME if you have any other good ideas
          // remove the mark in next tick, thus we can identify whether it in micro app or not
          // this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
          nextTick(() = > setCurrentRunningSandboxProxy(null));
          switch (p) {
            case 'document':
              return document;
            case 'eval':
              // eslint-disable-next-line no-eval
              return eval;
            // no default}}// eslint-disable-next-line no-nested-ternary
        const value = propertiesWithGetter.has(p)
          ? (rawWindow as any)[p]
          : p in target
          ? (target as any)[p]
          : (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },
Copy the code

How to proxy Window

Let’s go back to the problem of how to proxy the Window object, and imagine a few simple cases

  1. Specify the case of window directly
function foo(){
    window.a1 = 10
}
foo()
Copy the code

PS: HOW did I remember that the window could not be redefined before, but now it does not report an error, embarrassing

We cannot proxy the Window directly by redefining the Window object as something else

But we can use a self-executing function and give it a window variable, like this

fakeWindow = {}
!function(window){
  function foo(){
      window.a1 = 10
  }
  foo()
}(fakeWindow)
console.log(fakeWindow)

{a1: 10}
Copy the code

What if instead of saying window, it just says variable name?

fakeWindow = {a1 : 5}!function(window){
  function foo(){
      a1 = 10
  }
  foo()
}(fakeWindow)
Copy the code

You could write it this way

fakeWindow = {a1 : 5}!function(window){
with(window){
  function foo(){
      a1 = 10
  }
  foo()
}
}(fakeWindow)

// fakeWindow => (a1 : 10)

Copy the code

There is a with statement that extends the scope chain of a statement.

In short, it expands the current search

Such as:

fakeWindow = {a : 10}
with(fakeWindow){
    a=20; // fakeWindow = {a : 20}
}
Copy the code

But with only works for lookup

It doesn’t affect the writing of a new variable

For example, this is no good!

fakeWindow = {a : 10}
with(fakeWindow){
    b=20; // fakeWindow = {a : 10}
}
// fakeWindow = {a : 10}
Copy the code

Like var, they are not sandboxed, but they are natively isolated, limited to their scope, and do not affect other projects

This function is implemented by import-html-entry

The core code is as follows:

function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
	const sourceUrl = isInlineCode(scriptSrc) ? ' ' : `//# sourceURL=${scriptSrc}\n`;

	// Get the global window in this way. Since script is also run in the global scope, we must ensure that the window.proxy binding is bound to the global window as well
	// In nested cases, window.proxy is set to the inner application's Window, while the code actually runs in the global scope. This will cause the window.proxy in the closure to take the outermost microapplication's proxy
	const globalWindow = (0.eval) ('window');
	globalWindow.proxy = proxy;
	/ / TODO strictGlobal way switch switch with closures, stay with way pit flat after the merger
	return strictGlobal
		? `; (function(window, self){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy); `
		: `; (function(window, self){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy); `;
Copy the code

The escape

  1. 0 in non-strict mode,eval(“window”)

2; Local functions in non-strict mode

(Please tell me if there is something wrong, maybe I don’t understand it quite right)