diff --git a/dev/main-react16/src/pages/react16/react16.js b/dev/main-react16/src/pages/react16/react16.js index dd788aeb4..4f6cd20a9 100644 --- a/dev/main-react16/src/pages/react16/react16.js +++ b/dev/main-react16/src/pages/react16/react16.js @@ -180,7 +180,7 @@ export default class App extends React.Component { onAfterhidden={this.handleAfterhidden} onDataChange={this.handleDataChange} baseRoute='/micro-app/demo/react16' - keep-alive + // keep-alive // destroy // inline // disableSandbox diff --git a/package.json b/package.json index 4721eafe8..bf449dea7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@micro-zoe/micro-app", - "version": "0.6.2", + "version": "0.7.0", "description": "A lightweight, efficient and powerful micro front-end framework", "private": false, "main": "lib/index.min.js", diff --git a/src/__tests__/create_app.test.ts b/src/__tests__/create_app.test.ts index face963eb..310542581 100644 --- a/src/__tests__/create_app.test.ts +++ b/src/__tests__/create_app.test.ts @@ -2,7 +2,7 @@ import { commonStartEffect, releaseAllEffect, ports } from './common/initial' import { appInstanceMap } from '../create_app' import { appStates, keepAliveStates } from '../constants' -import microApp, { unmountApp, unmountAllApps } from '..' +import microApp, { unmountApp, unmountAllApps, getActiveApps } from '..' describe('create_app', () => { let appCon: Element @@ -457,4 +457,10 @@ describe('create_app', () => { }) }) }) + + // 测试getActiveApps方法 + test('test getActiveApps method', async () => { + // 因为上面已经执行unmountAllApps卸载了所有应用,所以这里长度为0 + expect(getActiveApps(true).length).toBe(0) + }) }) diff --git a/src/__tests__/demos/common/defer.js b/src/__tests__/demos/common/defer.js index e69de29bb..3684963e5 100644 --- a/src/__tests__/demos/common/defer.js +++ b/src/__tests__/demos/common/defer.js @@ -0,0 +1 @@ +console.error('my defer') diff --git a/src/__tests__/demos/dynamic/errorjs.html b/src/__tests__/demos/dynamic/errorjs.html new file mode 100644 index 000000000..603e0ccb9 --- /dev/null +++ b/src/__tests__/demos/dynamic/errorjs.html @@ -0,0 +1,13 @@ + + + + + + + dynamic + + + + + + diff --git a/src/__tests__/demos/special-html/notexistdefer.html b/src/__tests__/demos/special-html/notexistdefer.html new file mode 100644 index 000000000..40e4e9e34 --- /dev/null +++ b/src/__tests__/demos/special-html/notexistdefer.html @@ -0,0 +1,14 @@ + + + + + + + Document + + + + + + + diff --git a/src/__tests__/demos/special-html/onlydefer.html b/src/__tests__/demos/special-html/onlydefer.html new file mode 100644 index 000000000..75252fb00 --- /dev/null +++ b/src/__tests__/demos/special-html/onlydefer.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + + + + diff --git a/src/__tests__/sandbox/index.test.ts b/src/__tests__/sandbox/index.test.ts index 7227ae473..0aade2880 100644 --- a/src/__tests__/sandbox/index.test.ts +++ b/src/__tests__/sandbox/index.test.ts @@ -125,6 +125,13 @@ describe('sandbox', () => { } }) + Object.defineProperty(window, 'qqqqqqqqqq', { + value: 11, + configurable: false, + enumerable: true, + writable: true, + }) + // proxyWindow 无法继承原window上被隔离的变量 expect(proxyWindow.scopeProperty1).toBeUndefined() expect(proxyWindow.scopeProperty2).toBeUndefined() @@ -136,7 +143,8 @@ describe('sandbox', () => { const proxyScopedPro1Desc = Object.getOwnPropertyDescriptor(proxyWindow, 'scopeProperty1') expect(proxyScopedPro1Desc?.configurable).toBeTruthy() - proxyWindow.notScopedProperty = 'not-ScopedProperty' + proxyWindow.notScopedProperty = 'not-ScopedProperty-value' + proxyWindow.qqqqqqqqqq = 11 const proxyNotScopedProDesc = Object.getOwnPropertyDescriptor(proxyWindow, 'notScopedProperty') expect(proxyNotScopedProDesc?.configurable).toBeFalsy() }) @@ -228,6 +236,7 @@ describe('sandbox', () => { value: 'value3', configurable: true, writable: true, + enumerable: true, }, }) @@ -342,8 +351,8 @@ describe('sandbox', () => { proxyWindow['descriptor-key2'] = 'new-descriptor-key2' expect(proxyWindow['descriptor-key1']).toBe('new-descriptor-key1') - // writable为false,赋值无效 - expect(proxyWindow['descriptor-key2']).toBe('descriptor-key2') + // 原生window有相同值,且writable为false,vlaue依然可以定义到proxyWindow上 + expect(proxyWindow['descriptor-key2']).toBe('new-descriptor-key2') }) // 将原window上的_babelPolyfill设置为false @@ -368,4 +377,47 @@ describe('sandbox', () => { expect(proxyWindow.boundFunction.name).toBe('bound willbind') expect(proxyWindow.boundFunction).toBe(boundFunction) }) + + // 测试重写eval、Image方法 + test('test set eval & Image', () => { + const sandbox = new Sandbox('test-set-eval&Image', `http://127.0.0.1:${ports.sandbox}/common/`) + sandbox.start('') + const proxyWindow: any = sandbox.proxyWindow + + proxyWindow.eval = 'neweval' + expect(proxyWindow.eval).toBe('neweval') + + proxyWindow.Image = 'newimage' + expect(proxyWindow.Image).toBe('newimage') + }) + + // 分支覆盖 proxyWindow getter方法 + test('coverage: proxyWindow getter', () => { + const sandbox = new Sandbox('test-coverage-proxy-getter', `http://127.0.0.1:${ports.sandbox}/common/`) + sandbox.start('') + const proxyWindow: any = sandbox.proxyWindow + + Object.defineProperties(window, { + 'key1-for-proxy-getter': { + get () { + return 'key1-proxy-getter-value' + }, + set () {}, + configurable: true, + enumerable: true, + }, + }) + + proxyWindow['key1-for-proxy-getter'] = 'set value to proxyWindow' + expect(Object.getOwnPropertyDescriptor(proxyWindow, 'key1-for-proxy-getter')?.writable).toBeTruthy() + }) + + // 分支覆盖: 在createDescriptorFormicroAppWindow 获取descriptor为空 + test('coverage: empty descriptor in createDescriptorFormicroAppWindow ', () => { + // @ts-ignore + delete window.parent + const sandbox = new Sandbox('empty-descriptor-createDescriptorFormicroAppWindow', `http://127.0.0.1:${ports.sandbox}/common/`) + sandbox.start('') + expect(Object.getOwnPropertyDescriptor(window, 'parent')).toBeUndefined() + }) }) diff --git a/src/__tests__/source/scripts.test.ts b/src/__tests__/source/scripts.test.ts index 368c711cc..30d1c7006 100644 --- a/src/__tests__/source/scripts.test.ts +++ b/src/__tests__/source/scripts.test.ts @@ -10,12 +10,18 @@ describe('source scripts', () => { commonStartEffect(ports.source_scripts) microApp.start({ plugins: { - global: [{ - loader (code, _url, _options) { - // console.log('全局插件', _url) - return code - } - }], + global: [ + { + loader (code, _url, _options) { + // console.log('全局插件', _url) + return code + } + }, + { + loader: 'invalid loader' as any, + }, + 'invalid plugin' as any, + ], modules: { 'test-app1': [ { @@ -190,7 +196,7 @@ describe('source scripts', () => { await new Promise((reslove) => { microappElement9.addEventListener('mounted', () => { setTimeout(() => { - expect(console.error).toHaveBeenLastCalledWith('[micro-app] app test-app9:', expect.any(Error)) + expect(console.error).toHaveBeenLastCalledWith('[micro-app] app test-app9:', expect.any(Object)) reslove(true) }, 100) }, false) @@ -302,4 +308,34 @@ describe('source scripts', () => { }, false) }) }) + + // 初始化渲染时,某个js文件报错 + test('an error occurs at the first rendering', async () => { + const microappElement15 = document.createElement('micro-app') + microappElement15.setAttribute('name', 'test-app15') + microappElement15.setAttribute('url', `http://127.0.0.1:${ports.source_scripts}/dynamic/errorjs.html`) + + appCon.appendChild(microappElement15) + await new Promise((reslove) => { + microappElement15.addEventListener('mounted', () => { + expect(console.error).toHaveBeenLastCalledWith('[micro-app from runScript] app test-app15: ', expect.any(Error)) + reslove(true) + }, false) + }) + }) + + // 分支覆盖:初始化获取 defer js 文件失败 + test('coverage: failed to get defer js file', async () => { + const microappElement16 = document.createElement('micro-app') + microappElement16.setAttribute('name', 'test-app16') + microappElement16.setAttribute('url', `http://127.0.0.1:${ports.source_scripts}/special-html/notexistdefer.html`) + + appCon.appendChild(microappElement16) + await new Promise((reslove) => { + microappElement16.addEventListener('mounted', () => { + expect(console.error).toHaveBeenLastCalledWith('[micro-app] app test-app16:', expect.any(Object)) + reslove(true) + }, false) + }) + }) }) diff --git a/src/libs/utils.ts b/src/libs/utils.ts index c248c0725..c0970ee53 100644 --- a/src/libs/utils.ts +++ b/src/libs/utils.ts @@ -68,6 +68,7 @@ export function isShadowRoot (target: unknown): boolean { export const rawDefineProperty = Object.defineProperty export const rawDefineProperties = Object.defineProperties +export const rawHasOwnProperty = Object.prototype.hasOwnProperty /** * format error log diff --git a/src/sandbox/index.ts b/src/sandbox/index.ts index 9c97c2a99..ad9a09d52 100644 --- a/src/sandbox/index.ts +++ b/src/sandbox/index.ts @@ -14,6 +14,7 @@ import { rawDefineProperty, rawDefineProperties, isFunction, + rawHasOwnProperty, } from '../libs/utils' import microApp from '../micro_app' import bindFunctionToRawWindow from './bind_function' @@ -74,12 +75,12 @@ export default class SandBox implements SandBoxInterface { constructor (appName: string, url: string) { // get scopeProperties and escapeProperties from plugins this.getScopeProperties(appName) - // Rewrite global event listener & timeout - Object.assign(this, effect(this.microAppWindow)) // create proxyWindow with Proxy(microAppWindow) this.proxyWindow = this.createProxyWindow() // inject global properties this.initMicroAppWindow(this.microAppWindow, appName, url) + // Rewrite global event listener & timeout + Object.assign(this, effect(this.microAppWindow)) } start (baseroute: string): void { @@ -175,6 +176,7 @@ export default class SandBox implements SandBoxInterface { // create proxyWindow with Proxy(microAppWindow) private createProxyWindow () { const rawWindow = globalEnv.rawWindow + const descriptorTargetMap = new Map() // window.xxx will trigger proxy return new Proxy(this.microAppWindow, { get: (target: microAppWindowType, key: PropertyKey): unknown => { @@ -193,8 +195,9 @@ export default class SandBox implements SandBoxInterface { if (escapeSetterKeyList.includes(key)) { Reflect.set(rawWindow, key, value) } else if ( - !target.hasOwnProperty(key) && - rawWindow.hasOwnProperty(key) && + // target.hasOwnProperty has been rewritten + !rawHasOwnProperty.call(target, key) && + rawHasOwnProperty.call(rawWindow, key) && !this.scopeProperties.includes(key) ) { const descriptor = Object.getOwnPropertyDescriptor(rawWindow, key) @@ -233,23 +236,36 @@ export default class SandBox implements SandBoxInterface { }, // Object.getOwnPropertyDescriptor(window, key) getOwnPropertyDescriptor: (target: microAppWindowType, key: PropertyKey): PropertyDescriptor|undefined => { - if (target.hasOwnProperty(key)) { + if (rawHasOwnProperty.call(target, key)) { + descriptorTargetMap.set(key, 'target') return Object.getOwnPropertyDescriptor(target, key) } - if (rawWindow.hasOwnProperty(key)) { - // console, alert ... - return Object.getOwnPropertyDescriptor(rawWindow, key) + if (rawHasOwnProperty.call(rawWindow, key)) { + descriptorTargetMap.set(key, 'rawWindow') + const descriptor = Object.getOwnPropertyDescriptor(rawWindow, key) + if (descriptor && !descriptor.configurable) { + descriptor.configurable = true + } + return descriptor } return undefined }, + // Object.defineProperty(window, key, Descriptor) + defineProperty: (target: microAppWindowType, key: PropertyKey, value: PropertyDescriptor): boolean => { + const from = descriptorTargetMap.get(key) + if (from === 'rawWindow') { + return Reflect.defineProperty(rawWindow, key, value) + } + return Reflect.defineProperty(target, key, value) + }, // Object.getOwnPropertyNames(window) ownKeys: (target: microAppWindowType): Array => { return unique(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target))) }, deleteProperty: (target: microAppWindowType, key: PropertyKey): boolean => { - if (target.hasOwnProperty(key)) { + if (rawHasOwnProperty.call(target, key)) { this.injectedKeys.has(key) && this.injectedKeys.delete(key) this.escapeKeys.has(key) && Reflect.deleteProperty(rawWindow, key) return Reflect.deleteProperty(target, key) @@ -274,7 +290,7 @@ export default class SandBox implements SandBoxInterface { microAppWindow.rawWindow = globalEnv.rawWindow microAppWindow.rawDocument = globalEnv.rawDocument microAppWindow.removeDomScope = removeDomScope - microAppWindow.hasOwnProperty = (key: PropertyKey) => Object.prototype.hasOwnProperty.call(microAppWindow, key) || Object.prototype.hasOwnProperty.call(globalEnv.rawWindow, key) + microAppWindow.hasOwnProperty = (key: PropertyKey) => rawHasOwnProperty.call(microAppWindow, key) || rawHasOwnProperty.call(globalEnv.rawWindow, key) this.setMappingPropertiesWithRawDescriptor(microAppWindow) this.setHijackProperties(microAppWindow, appName) } diff --git a/src/source/scripts.ts b/src/source/scripts.ts index d5534bb17..0cd1f3e1b 100644 --- a/src/source/scripts.ts +++ b/src/source/scripts.ts @@ -217,18 +217,6 @@ export function execScripts ( initedHook.errorCount === deferScriptPromise.length ) }) - // Promise.all(deferScriptPromise).then((res: string[]) => { - // res.forEach((code, index) => { - // const [url, info] = deferScriptInfo[index] - // info.code = info.code || code - // runScript(url, app, info, false, initedHook) - // !info.module && initedHook(false) - // }) - // initedHook(isUndefined(initedHook.moduleCount)) - // }).catch((err) => { - // logError(err, app.name) - // initedHook(true) - // }) } else { initedHook(true) }