Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: sanbox proxy's bound function bind this at runtime #1518

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,9 @@ export type MicroAppStateActions = {
setGlobalState: (state: Record<string, any>) => boolean;
offGlobalStateChange: () => boolean;
};

export enum WindowType {
Window = '[object Window]',
DOMWindow = '[object DOMWindow]',
global = '[object global]',
}
16 changes: 10 additions & 6 deletions src/sandbox/__tests__/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,23 @@ describe('getTargetValue', () => {
this.field = field;
}
const notConstructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation);
// `this` of not constructable function will be bound automatically, and it can not be changed by calling with special `this`
// `this` of not constructable function will be bound automatically, but it can be changed by calling with special `this`
const result = {};
notConstructableFunction.call(result, '123');
notConstructableFunction('123');
expect(result).toStrictEqual({});
expect(window.field).toEqual('123');

notConstructableFunction.call(result, '456');
expect(result).toStrictEqual({ field: '456' });
// window.field not be affected
expect(window.field).toEqual('123');

prototypeAddedAfterFirstInvocation.prototype.addedFn = () => {};
const constructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation);
// `this` coule be available if it be predicated as a constructable function
// `this` coule also be available when it be predicated as a constructable function
const result2 = {};
constructableFunction.call(result2, '456');
expect(result2).toStrictEqual({ field: '456' });
// window.field not be affected
constructableFunction.call(result2, '789');
expect(result2).toStrictEqual({ field: '789' });
expect(window.field).toEqual('123');
});

Expand Down
22 changes: 20 additions & 2 deletions src/sandbox/__tests__/proxySandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* @since 2020-03-31
*/

import { isBoundedFunction } from '../../utils';
import { getCurrentRunningSandboxProxy } from '../common';
import ProxySandbox from '../proxySandbox';

Expand Down Expand Up @@ -290,7 +289,26 @@ test('bounded function should not be rebounded', () => {

expect(proxy.fn1 === fn).toBeFalsy();
expect(proxy.fn2 === boundedFn).toBeTruthy();
expect(isBoundedFunction(proxy.fn1)).toBeTruthy();
});

test('this should refer correctly', () => {
const proxy = new ProxySandbox('this-refer-test').proxy as any;
const obj = {};
proxy.getTemplate = function getTemplate() {
this.a = 'a';
this.b = 'b';
};
// this refers to global;
const fn = proxy.getTemplate;
fn();
expect(proxy.a).toBe('a');
expect(proxy.b).toBe('b');
expect(window.a).toBeUndefined();
expect(window.b).toBeUndefined();

// this refers to obj;
proxy.getTemplate.apply(obj);
expect(obj).toStrictEqual({ a: 'a', b: 'b' });
});

test('the prototype should be kept while we create a function with prototype on proxy', () => {
Expand Down
18 changes: 16 additions & 2 deletions src/sandbox/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { isBoundedFunction, isCallable, isConstructable } from '../utils';
import { WindowType } from '../interfaces';

let currentRunningSandboxProxy: WindowProxy | null;
export function getCurrentRunningSandboxProxy() {
Expand All @@ -14,14 +15,27 @@ export function setCurrentRunningSandboxProxy(proxy: WindowProxy | null) {
currentRunningSandboxProxy = proxy;
}

const isWindow = (obj: any) => {
if (!obj || typeof obj !== 'object') {
return false;
}
const typeofObj = Object.prototype.toString.call(obj);
if (typeofObj === WindowType.Window || typeofObj === WindowType.DOMWindow || typeofObj === WindowType.global) {
return true;
}
return obj.window === obj;
};

export function getTargetValue(target: any, value: any): any {
/*
仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
@warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
*/
if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {
const boundValue = Function.prototype.bind.call(value, target);
const boundValue = function boundValue(this: any, ...rest: any[]) {
return Function.prototype.apply.call(value, typeof this === 'undefined' || isWindow(this) ? target : this, rest);
kuitos marked this conversation as resolved.
Show resolved Hide resolved
} as Record<string, any>;

// some callable function has custom fields, we need to copy the enumerable props to boundValue. such as moment function.
// use for..in rather than Object.keys.forEach for performance reason
Expand All @@ -32,7 +46,7 @@ export function getTargetValue(target: any, value: any): any {

// copy prototype if bound function not have but target one have
// as prototype is non-enumerable mostly, we need to copy it from target function manually
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
if (value.hasOwnProperty('prototype')) {
// we should not use assignment operator to set boundValue prototype like `boundValue.prototype = value.prototype`
// as the assignment will also look up prototype chain while it hasn't own prototype property,
// when the lookup succeed, the assignment will throw an TypeError like `Cannot assign to read only property 'prototype' of function` if its descriptor configured with writable false or just have a getter accessor
Expand Down
8 changes: 2 additions & 6 deletions src/sandbox/proxySandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,8 @@ export default class ProxySandbox implements SandBox {
}

// 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);
const source = propertiesWithGetter.has(p) ? rawWindow : p in target ? target : rawWindow;
return getTargetValue(source === target ? proxy : rawWindow, (source as any)[p]);
},

// trap in operator
Expand Down