diff --git a/.eslintrc.js b/.eslintrc.js index fccef056..081a761b 100755 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,5 +39,6 @@ module.exports = { 'comma-dangle': ['error', 'only-multiline'], 'no-eval': ['error', { allowIndirect: true }], 'no-new-func': 0, + 'space-before-function-paren': 0, } } diff --git a/Contact.md b/Contact.md index 1754b434..f1a409f0 100644 --- a/Contact.md +++ b/Contact.md @@ -1,18 +1,7 @@ 欢迎小伙伴们加入micro-app微信群交流^ ^ -![image](https://github.com/user-attachments/assets/35f37851-32e1-4751-aa80-2cfc28741988) - - - - - - - - - - - +![IMG_0607](https://github.com/user-attachments/assets/a976245d-2d5e-460e-8bcb-421dbc31e494) diff --git a/dev/children/vite4/src/main.js b/dev/children/vite4/src/main.js index 17d1cb71..8dba7f49 100644 --- a/dev/children/vite4/src/main.js +++ b/dev/children/vite4/src/main.js @@ -380,25 +380,28 @@ if (process.env.NODE_ENV !== 'production') { /* ---------------------- 插件相关 --------------------- */ // vite环境下无法设置window指向proxyWindow,其值依然是iframeWindow,所以插件无法使用 -// window.escapeKey1 = 'escapeKey1' // 无效,只定义在iframeWindow上 -// window.escapeKey2 = 'escapeKey2' // 无效,只定义在iframeWindow上 -// if (window.__MICRO_APP_ENVIRONMENT__) { -// window.__MICRO_APP_PROXY_WINDOW__.escapeKey3 = 'escapeKey3' // 逃逸到rawWindow上 -// window.__MICRO_APP_PROXY_WINDOW__.escapeKey4 = 'escapeKey4' // 逃逸到rawWindow上 -// } - - -// console.log('scopeProperties scopeKeySpe: ', scopeKeySpe) -// console.log('scopeProperties window.scopeKeySpe: ', window.scopeKeySpe) - -// console.log('scopeProperties Vue: ', Vue) -// console.log('scopeProperties window.Vue: ', window.Vue) - -// window.Vue = Vue ? Vue : 'child Vue' - -// console.log('scopeProperties Vue: ', Vue) -// console.log('scopeProperties window.Vue: ', window.Vue) - +if (window.__MICRO_APP_ENVIRONMENT__) { + // ----------------------- scope相关---------------------开始 + console.assert(window['scopeKey-vite-1'] === undefined, `window['scopeKey-vite-1'] 在主应用上存在,但不会兜底到主应用查找`) + console.assert(('scopeKey-vite-1' in window) === false , 'scopeKey-vite-1 in window 应该为false') + // ----------------------- scope相关---------------------结束 + + + // ----------------------- escape相关--------------------开始 + console.assert(window['escapeKey-vite-1'] !== undefined, `window['escapeKey-vite-1'] 兜底到主应用,且在主应用存在`) + console.assert(window['escapeKey-vite-2'] === undefined, `window['escapeKey-vite-2'] 兜底到主应用,但主应用上不存在`) + console.assert(escapeKey1 !== undefined, `escapeKey1 由全局插件配置,兜底到主应用,且在主应用存在`) + + window['escapeKey-vite-2'] = 'escapeKey-vite-2 from child' + console.assert(window.rawWindow['escapeKey-vite-2'] === undefined, `window['escapeKey-vite-2']=xxx的设置不会兜底到主应用上`) + console.assert(window['escapeKey-vite-func']() === window.rawWindow, `兜底函数的this指向原生window`) + // ----------------------- escape相关--------------------结束 + + console.assert(window.Vue === undefined, 'window.Vue 应该为false') + console.assert(('Vue' in window) === false, 'Vue in window 应该为false') + window.Vue = '子应用内部自定义Vue' + console.assert(window.Vue === '子应用内部自定义Vue', 'window.Vue 应该为子应用内部自定义Vue') +} /* ---------------------- 特殊操作 --------------------- */ // window.document.domain = 'localhost'; diff --git a/dev/main-react16/src/global.jsx b/dev/main-react16/src/global.jsx index d7f6aa07..1b1e8716 100644 --- a/dev/main-react16/src/global.jsx +++ b/dev/main-react16/src/global.jsx @@ -60,7 +60,14 @@ const prefetchConfig = [ // microApp.preFetch(prefetchConfig) +window['escapeKey1'] = 'escapeKey1 from base app by global plugin' window['escapeKey3'] = 'escapeKey3 from base app' +window['scopeKey-vite-1'] = 'scopeKey-vite-1 from base app' +window['escapeKey-vite-1'] = 'escapeKey-vite-1 from base app' +window['escapeKey-vite-func'] = function () { + console.log('escapeKey-vite-func', this) + return this +} window.Vue = { tip: 'Vue from base' } microApp.start({ @@ -147,6 +154,10 @@ microApp.start({ vite2: [{ escapeProperties: ['escapeKey3', 'escapeKey4'], }], + vite4: [{ + scopeProperties: ['scopeKey-vite-1', 'scopeKey-vite-2'], + escapeProperties: ['escapeKey-vite-1', 'escapeKey-vite-2', 'escapeKey-vite-func'], + }], } }, /** diff --git a/dev/main-react16/src/pages/react16/react16.js b/dev/main-react16/src/pages/react16/react16.js index 2f1445dd..a3ac3ec7 100644 --- a/dev/main-react16/src/pages/react16/react16.js +++ b/dev/main-react16/src/pages/react16/react16.js @@ -448,7 +448,7 @@ export default class App extends React.Component { // fiber // ssr // clear-data - // iframe + iframe > ) diff --git a/docs/zh-cn/changelog.md b/docs/zh-cn/changelog.md index 6a5bae9f..3757275c 100644 --- a/docs/zh-cn/changelog.md +++ b/docs/zh-cn/changelog.md @@ -8,6 +8,19 @@ --- +### 1.0.0-rc.9 + +`2024-9-10` + +- **Feature** + - 支持 cesiumjs及其生态, [issue 1351](https://github.com/micro-zoe/micro-app/issues/1351) +- **Bug Fix** + - 🐞 修复 vite环境下iframe沙箱对于插件系统`escapeProperties`不支持的问题。 + - 🐞 修复 iframe沙箱下jquery通过`$('body').append`创建元素逃逸的问题,[issue 1339](https://github.com/micro-zoe/micro-app/issues/1339)。 + - 🐞 修复 iframe沙箱部分场景下执行`instanceOf`导致死循环的问题,[issue 1341](https://github.com/micro-zoe/micro-app/issues/1341)。 + - 🐞 修复 子应用css样式缺少闭合标签导致解析失败的问题,[issue 1342](https://github.com/micro-zoe/micro-app/issues/1342)。 + + ### 1.0.0-rc.8 `2024-8-20` diff --git a/docs/zh-cn/plugins.md b/docs/zh-cn/plugins.md index 58100d2c..6302dc4f 100644 --- a/docs/zh-cn/plugins.md +++ b/docs/zh-cn/plugins.md @@ -13,9 +13,15 @@ microApp.start({ plugins: { // 全局插件,作用于所有子应用的js文件 global?: Array<{ - // 可选,强隔离的全局变量(默认情况下子应用无法找到的全局变量会兜底到主应用中,scopeProperties可以禁止这种情况) + /** + * 可选,强隔离的全局变量(with沙箱才会生效) + * 默认情况下with沙箱子应用无法找到的全局变量会兜底到主应用中查找,设置scopeProperties后,全局变量只会在子应用内部查找,阻止兜底查找操作 + */ scopeProperties?: string[], - // 可选,可以逃逸到外部的全局变量(escapeProperties中的变量会同时赋值到子应用和外部真实的window上) + /** + * 可选,可以逃逸的全局变量 + * 默认情况下iframe沙箱子应用的全局变量只会在内部查找,不会兜底到主应用查找,设置escapeProperties后,无法查找的全局变量会兜底到主应用查找。 + */ escapeProperties?: string[], // 可选,如果函数返回 `true` 则忽略 script 和 link 标签的创建 excludeChecker?: (url: string) => boolean @@ -33,9 +39,15 @@ microApp.start({ modules?: { // appName为应用的名称,这些插件只会作用于指定的应用 [appName: string]: Array<{ - // 可选,强隔离的全局变量(默认情况下子应用无法找到的全局变量会兜底到主应用中,scopeProperties可以禁止这种情况) + /** + * 可选,强隔离的全局变量(with沙箱才会生效) + * 默认情况下with沙箱子应用无法找到的全局变量会兜底到主应用中查找,设置scopeProperties后,全局变量只会在子应用内部查找,阻止兜底查找操作 + */ scopeProperties?: string[], - // 可选,可以逃逸到外部的全局变量(escapeProperties中的变量会同时赋值到子应用和外部真实的window上) + /** + * 可选,可以逃逸的全局变量 + * 默认情况下iframe沙箱子应用的全局变量只会在内部查找,不会兜底到主应用查找,设置escapeProperties后,无法查找的全局变量会兜底到主应用查找。 + */ escapeProperties?: string[], // 可选,如果函数返回 `true` 则忽略 script 和 link 标签的创建 excludeChecker?: (url: string) => boolean diff --git a/package.json b/package.json index 96498c3d..cb70e936 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@micro-zoe/micro-app", - "version": "1.0.0-rc.8", + "version": "1.0.0-rc.9", "description": "A lightweight, efficient and powerful micro front-end framework", "private": false, "main": "lib/index.min.js", diff --git a/src/libs/global_env.ts b/src/libs/global_env.ts index 3409e32a..e9790e4c 100644 --- a/src/libs/global_env.ts +++ b/src/libs/global_env.ts @@ -14,6 +14,7 @@ import { import { updateElementInfo, } from '../sandbox/adapter' +import '../proxies/worker' declare global { interface Node { diff --git a/src/libs/utils.ts b/src/libs/utils.ts index 63479ef1..65d7fb64 100644 --- a/src/libs/utils.ts +++ b/src/libs/utils.ts @@ -26,7 +26,7 @@ export const globalThis = (typeof global !== 'undefined') ) ) -export const noop = () => {} +export const noop = () => { } export const noopFalse = () => false // Array.isArray @@ -43,57 +43,57 @@ export const rawHasOwnProperty = Object.prototype.hasOwnProperty export const toTypeString = (value: unknown): string => rawToString.call(value) // is Undefined -export function isUndefined (target: unknown): target is undefined { +export function isUndefined(target: unknown): target is undefined { return target === undefined } // is Null -export function isNull (target: unknown): target is null { +export function isNull(target: unknown): target is null { return target === null } // is String -export function isString (target: unknown): target is string { +export function isString(target: unknown): target is string { return typeof target === 'string' } // is Boolean -export function isBoolean (target: unknown): target is boolean { +export function isBoolean(target: unknown): target is boolean { return typeof target === 'boolean' } // is Number -export function isNumber (target: unknown): target is Number { +export function isNumber(target: unknown): target is Number { return typeof target === 'number' } // is function -export function isFunction (target: unknown): target is Function { +export function isFunction(target: unknown): target is Function { return typeof target === 'function' } // is PlainObject -export function isPlainObject > (target: unknown): target is T { +export function isPlainObject>(target: unknown): target is T { return toTypeString(target) === '[object Object]' } // is Object -export function isObject (target: unknown): target is Object { +export function isObject(target: unknown): target is Object { return !isNull(target) && typeof target === 'object' } // is Promise -export function isPromise (target: unknown): target is Promise { +export function isPromise(target: unknown): target is Promise { return toTypeString(target) === '[object Promise]' } // is bind function -export function isBoundFunction (target: unknown): boolean { +export function isBoundFunction(target: unknown): boolean { return isFunction(target) && target.name?.indexOf('bound ') === 0 && !target.hasOwnProperty('prototype') } // is constructor function -export function isConstructor (target: unknown): boolean { +export function isConstructor(target: unknown): boolean { if (isFunction(target)) { const targetStr = target.toString() return ( @@ -107,70 +107,86 @@ export function isConstructor (target: unknown): boolean { } // is ShadowRoot -export function isShadowRoot (target: unknown): target is ShadowRoot { +export function isShadowRoot(target: unknown): target is ShadowRoot { return typeof ShadowRoot !== 'undefined' && target instanceof ShadowRoot } -export function isURL (target: unknown): target is URL { +export function isURL(target: unknown): target is URL { return target instanceof URL || !!(target as URL)?.href } // iframe element not instanceof base app Element, use tagName instead -export function isElement (target: unknown): target is Element { +export function isElement(target: unknown): target is Element { return target instanceof Element || isString((target as Element)?.tagName) } // iframe node not instanceof base app Node, use nodeType instead -export function isNode (target: unknown): target is Node { +export function isNode(target: unknown): target is Node { return target instanceof Node || isNumber((target as Node)?.nodeType) } -export function isLinkElement (target: unknown): target is HTMLLinkElement { +export function isCanvasElement(target: unknown): target is HTMLAnchorElement { + return toTypeString(target) === '[object HTMLCanvasElement]' +} + +export function isAnchorElement(target: unknown): target is HTMLAnchorElement { + return toTypeString(target) === '[object HTMLAnchorElement]' +} + +export function isAudioElement(target: unknown): target is HTMLAnchorElement { + return toTypeString(target) === '[object HTMLAudioElement]' +} + +export function isVideoElement(target: unknown): target is HTMLAnchorElement { + return toTypeString(target) === '[object HTMLVideoElement]' +} + +export function isLinkElement(target: unknown): target is HTMLLinkElement { return toTypeString(target) === '[object HTMLLinkElement]' } -export function isStyleElement (target: unknown): target is HTMLStyleElement { +export function isStyleElement(target: unknown): target is HTMLStyleElement { return toTypeString(target) === '[object HTMLStyleElement]' } -export function isScriptElement (target: unknown): target is HTMLScriptElement { +export function isScriptElement(target: unknown): target is HTMLScriptElement { return toTypeString(target) === '[object HTMLScriptElement]' } -export function isIFrameElement (target: unknown): target is HTMLIFrameElement { +export function isIFrameElement(target: unknown): target is HTMLIFrameElement { return toTypeString(target) === '[object HTMLIFrameElement]' } -export function isDivElement (target: unknown): target is HTMLDivElement { +export function isDivElement(target: unknown): target is HTMLDivElement { return toTypeString(target) === '[object HTMLDivElement]' } -export function isImageElement (target: unknown): target is HTMLImageElement { +export function isImageElement(target: unknown): target is HTMLImageElement { return toTypeString(target) === '[object HTMLImageElement]' } -export function isBaseElement (target: unknown): target is HTMLBaseElement { +export function isBaseElement(target: unknown): target is HTMLBaseElement { return toTypeString(target) === '[object HTMLBaseElement]' } -export function isDocumentFragment (target: unknown): target is DocumentFragment { +export function isDocumentFragment(target: unknown): target is DocumentFragment { return toTypeString(target) === '[object DocumentFragment]' } -export function isMicroAppBody (target: unknown): target is HTMLElement { +export function isMicroAppBody(target: unknown): target is HTMLElement { return isElement(target) && target.tagName.toUpperCase() === 'MICRO-APP-BODY' } -export function isMicroAppHead (target: unknown): target is HTMLElement { +export function isMicroAppHead(target: unknown): target is HTMLElement { return isElement(target) && target.tagName.toUpperCase() === 'MICRO-APP-HEAD' } // is ProxyDocument -export function isProxyDocument (target: unknown): target is Document { +export function isProxyDocument(target: unknown): target is Document { return toTypeString(target) === '[object ProxyDocument]' } -export function isTargetExtension (path: string, suffix: string): boolean { +export function isTargetExtension(path: string, suffix: string): boolean { try { return createURL(path).pathname.split('.').pop() === suffix } catch { @@ -178,7 +194,7 @@ export function isTargetExtension (path: string, suffix: string): boolean { } } -export function includes (target: unknown[], searchElement: unknown, fromIndex?: number): boolean { +export function includes(target: unknown[], searchElement: unknown, fromIndex?: number): boolean { if (target == null) { throw new TypeError('includes target is null or undefined') } @@ -204,7 +220,7 @@ export function includes (target: unknown[], searchElement: unknown, fromIndex?: * @param msg message * @param appName app name, default is null */ -export function logError ( +export function logError( msg: unknown, appName: string | null = null, ...rest: unknown[] @@ -222,7 +238,7 @@ export function logError ( * @param msg message * @param appName app name, default is null */ -export function logWarn ( +export function logWarn( msg: unknown, appName: string | null = null, ...rest: unknown[] @@ -240,7 +256,7 @@ export function logWarn ( * @param fn callback * @param args params */ -export function defer (fn: Func, ...args: unknown[]): void { +export function defer(fn: Func, ...args: unknown[]): void { Promise.resolve().then(fn.bind(null, ...args)) } @@ -249,7 +265,7 @@ export function defer (fn: Func, ...args: unknown[]): void { * @param fn callback * @param args params */ -export function macro (fn: Func, delay = 0, ...args: unknown[]): void { +export function macro(fn: Func, delay = 0, ...args: unknown[]): void { setTimeout(fn.bind(null, ...args), delay) } @@ -257,7 +273,7 @@ export function macro (fn: Func, delay = 0, ...args: unknown[]): void { * create URL as MicroLocation */ export const createURL = (function (): (path: string | URL, base?: string) => MicroLocation { - class Location extends URL {} + class Location extends URL { } return (path: string | URL, base?: string): MicroLocation => { return (base ? new Location('' + path, base) : new Location('' + path)) as MicroLocation } @@ -267,7 +283,7 @@ export const createURL = (function (): (path: string | URL, base?: string) => Mi * Add address protocol * @param url address */ -export function addProtocol (url: string): string { +export function addProtocol(url: string): string { return url.startsWith('//') ? `${globalThis.location.protocol}${url}` : url } @@ -277,7 +293,7 @@ export function addProtocol (url: string): string { * 1. micro-app -> attributeChangedCallback * 2. preFetch */ -export function formatAppURL (url: string | null, appName: string | null = null): string { +export function formatAppURL(url: string | null, appName: string | null = null): string { if (!isString(url) || !url) return '' try { @@ -304,7 +320,7 @@ export function formatAppURL (url: string | null, appName: string | null = null) * 5. plugins * 6. router api (push, replace) */ -export function formatAppName (name: string | null): string { +export function formatAppName(name: string | null): string { if (!isString(name) || !name) return '' return name.replace(/(^\d+)|([^\w\d-_])/gi, '') } @@ -315,7 +331,7 @@ export function formatAppName (name: string | null): string { * 2. https://domain/xx to https://domain/xx/ * @param url app.url */ -export function getEffectivePath (url: string): string { +export function getEffectivePath(url: string): string { const { origin, pathname } = createURL(url) if (/\.(\w+)$/.test(pathname)) { const pathArr = `${origin}${pathname}`.split('/') @@ -331,7 +347,7 @@ export function getEffectivePath (url: string): string { * @param path address * @param baseURI base url(app.url) */ -export function CompletionPath (path: string, baseURI: string): string { +export function CompletionPath(path: string, baseURI: string): string { if ( !path || /^((((ht|f)tps?)|file):)?\/\//.test(path) || @@ -346,7 +362,7 @@ export function CompletionPath (path: string, baseURI: string): string { * which is used to complete the relative address in the css * @param linkPath full link address */ -export function getLinkFileDir (linkPath: string): string { +export function getLinkFileDir(linkPath: string): string { const pathArr = linkPath.split('/') pathArr.pop() return addProtocol(pathArr.join('/') + '/') @@ -359,7 +375,7 @@ export function getLinkFileDir (linkPath: string): string { * @param errorCb failed callback * @param finallyCb finally callback */ -export function promiseStream ( +export function promiseStream( promiseList: Array | T>, successCb: CallableFunction, errorCb: CallableFunction, @@ -367,7 +383,7 @@ export function promiseStream ( ): void { let finishedNum = 0 - function isFinished () { + function isFinished() { if (++finishedNum === promiseList.length && finallyCb) finallyCb() } @@ -388,18 +404,18 @@ export function promiseStream ( } // Check whether the browser supports module script -export function isSupportModuleScript (): boolean { +export function isSupportModuleScript(): boolean { const s = document.createElement('script') return 'noModule' in s } // Create a random symbol string -export function createNonceSrc (): string { +export function createNonceSrc(): string { return 'inline-' + Math.random().toString(36).substr(2, 15) } // Array deduplication -export function unique (array: any[]): any[] { +export function unique(array: any[]): any[] { return array.filter(function (this: Record, item) { return item in this ? false : (this[item] = true) }, Object.create(null)) @@ -412,7 +428,7 @@ export const requestIdleCallback = globalThis.requestIdleCallback || return setTimeout(function () { fn({ didTimeout: false, - timeRemaining () { + timeRemaining() { return Math.max(0, 50 - (Date.now() - lastTime)) }, }) @@ -423,7 +439,7 @@ export const requestIdleCallback = globalThis.requestIdleCallback || * Wrap requestIdleCallback with promise * Exec callback when browser idle */ -export function promiseRequestIdle (callback: CallableFunction): Promise { +export function promiseRequestIdle(callback: CallableFunction): Promise { return new Promise((resolve) => { requestIdleCallback(() => { callback(resolve) @@ -435,16 +451,16 @@ export function promiseRequestIdle (callback: CallableFunction): Promise { * Record the currently running app.name */ let currentAppName: string | null = null -export function setCurrentAppName (appName: string | null): void { +export function setCurrentAppName(appName: string | null): void { currentAppName = appName } // get the currently running app.name -export function getCurrentAppName (): string | null { +export function getCurrentAppName(): string | null { return currentAppName } -export function throttleDeferForSetAppName (appName: string): void { +export function throttleDeferForSetAppName(appName: string): void { if (currentAppName !== appName && !getPreventSetState()) { setCurrentAppName(appName) defer(() => { @@ -455,15 +471,15 @@ export function throttleDeferForSetAppName (appName: string): void { // only for iframe document.body(head).querySelector(querySelectorAll) let iframeCurrentAppName: string | null = null -export function setIframeCurrentAppName (appName: string | null) { +export function setIframeCurrentAppName(appName: string | null) { iframeCurrentAppName = appName } -export function getIframeCurrentAppName (): string | null { +export function getIframeCurrentAppName(): string | null { return iframeCurrentAppName } -export function throttleDeferForIframeAppName (appName: string): void { +export function throttleDeferForIframeAppName(appName: string): void { if (iframeCurrentAppName !== appName && !getPreventSetState()) { setIframeCurrentAppName(appName) defer(() => { @@ -474,7 +490,7 @@ export function throttleDeferForIframeAppName (appName: string): void { // prevent set app name let preventSetState = false -export function getPreventSetState (): boolean { +export function getPreventSetState(): boolean { return preventSetState } @@ -485,7 +501,7 @@ export function getPreventSetState (): boolean { * -----> element scope point to base app <----- * removeDomScope(false) */ -export function removeDomScope (force?: boolean): void { +export function removeDomScope(force?: boolean): void { if (force !== false) { setCurrentAppName(null) setIframeCurrentAppName(null) @@ -501,14 +517,14 @@ export function removeDomScope (force?: boolean): void { } // is safari browser -export function isSafari (): boolean { +export function isSafari(): boolean { return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) } /** * Create pure elements */ -export function pureCreateElement (tagName: K, options?: ElementCreationOptions): MicroAppElementTagNameMap[K] { +export function pureCreateElement(tagName: K, options?: ElementCreationOptions): MicroAppElementTagNameMap[K] { const element = (window.rawDocument || document).createElement(tagName, options) if (element.__MICRO_APP_NAME__) delete element.__MICRO_APP_NAME__ element.__PURE_ELEMENT__ = true @@ -516,13 +532,13 @@ export function pureCreateElement (ta } // is invalid key of querySelector -export function isInvalidQuerySelectorKey (key: string): boolean { +export function isInvalidQuerySelectorKey(key: string): boolean { if (__TEST__) return !key || /(^\d)|([^\w\d-_$])/gi.test(key) return !key || /(^\d)|([^\w\d-_\u4e00-\u9fa5])/gi.test(key) } // unique element -export function isUniqueElement (key: string): boolean { +export function isUniqueElement(key: string): boolean { return ( /^body$/i.test(key) || /^head$/i.test(key) || @@ -537,18 +553,18 @@ export type RootContainer = HTMLElement & MicroAppElementInterface * get micro-app element * @param target app container */ -export function getRootContainer (target: HTMLElement | ShadowRoot): RootContainer { +export function getRootContainer(target: HTMLElement | ShadowRoot): RootContainer { return (isShadowRoot(target) ? (target as ShadowRoot).host : target) as RootContainer } /** * trim start & end */ -export function trim (str: string): string { +export function trim(str: string): string { return str ? str.replace(/^\s+|\s+$/g, '') : '' } -export function isFireFox (): boolean { +export function isFireFox(): boolean { return navigator.userAgent.indexOf('Firefox') > -1 } @@ -557,7 +573,7 @@ export function isFireFox (): boolean { * @param search - search string to parse * @returns a query object */ -export function parseQuery (search: string): LocationQueryObject { +export function parseQuery(search: string): LocationQueryObject { const result: LocationQueryObject = {} const queryList = search.split('&') @@ -586,7 +602,7 @@ export function parseQuery (search: string): LocationQueryObject { * @param queryObject - query object to stringify * @returns query string without the leading `?` */ -export function stringifyQuery (queryObject: LocationQueryObject): string { +export function stringifyQuery(queryObject: LocationQueryObject): string { let result = '' for (const key in queryObject) { @@ -611,10 +627,10 @@ export function stringifyQuery (queryObject: LocationQueryObject): string { /** * Register or unregister callback/guard with Set */ -export function useSetRecord () { +export function useSetRecord() { const handlers: Set = new Set() - function add (handler: T): () => boolean { + function add(handler: T): () => boolean { handlers.add(handler) return (): boolean => { if (handlers.has(handler)) return handlers.delete(handler) @@ -631,10 +647,10 @@ export function useSetRecord () { /** * record data with Map */ -export function useMapRecord () { +export function useMapRecord() { const data: Map = new Map() - function add (key: PropertyKey, value: T): () => boolean { + function add(key: PropertyKey, value: T): () => boolean { data.set(key, value) return (): boolean => { if (data.has(key)) return data.delete(key) @@ -652,7 +668,7 @@ export function useMapRecord () { } } -export function getAttributes (element: Element): AttrsType { +export function getAttributes(element: Element): AttrsType { const attr = element.attributes const attrMap: AttrsType = new Map() for (let i = 0; i < attr.length; i++) { @@ -667,7 +683,7 @@ export function getAttributes (element: Element): AttrsType { * @param fiberTasks fiber task list * @param callback action callback */ -export function injectFiberTask (fiberTasks: fiberTasks, callback: CallableFunction): void { +export function injectFiberTask(fiberTasks: fiberTasks, callback: CallableFunction): void { if (fiberTasks) { fiberTasks.push(() => promiseRequestIdle((resolve: PromiseConstructor['resolve']) => { callback() @@ -682,7 +698,7 @@ export function injectFiberTask (fiberTasks: fiberTasks, callback: CallableFunct * serial exec fiber task of link, style, script * @param tasks task array or null */ -export function serialExecFiberTasks (tasks: fiberTasks): Promise | null { +export function serialExecFiberTasks(tasks: fiberTasks): Promise | null { return tasks?.reduce((pre, next) => pre.then(next), Promise.resolve()) || null } @@ -690,7 +706,7 @@ export function serialExecFiberTasks (tasks: fiberTasks): Promise | null { * inline script start with inline-xxx * @param address source address */ -export function isInlineScript (address: string): boolean { +export function isInlineScript(address: string): boolean { return address.startsWith('inline-') } @@ -700,7 +716,7 @@ export function isInlineScript (address: string): boolean { * @param appName app.name * @param args arguments */ -export function execMicroAppGlobalHook ( +export function execMicroAppGlobalHook( fn: Func | null, appName: string, hookName: string, @@ -717,7 +733,7 @@ export function execMicroAppGlobalHook ( * remove all childNode from target node * @param $dom target node */ -export function clearDOM ($dom: HTMLElement | ShadowRoot | Document): void { +export function clearDOM($dom: HTMLElement | ShadowRoot | Document): void { while ($dom?.firstChild) { $dom.removeChild($dom.firstChild) } @@ -750,7 +766,7 @@ export function instanceOf unknown>( * @param appName app name */ const formatEventList = ['mounted', 'unmount'] -export function formatEventType (type: string, appName: string): string { +export function formatEventType(type: string, appName: string): string { return formatEventList.includes(type) ? `${type}-${appName}` : type } @@ -758,6 +774,13 @@ export function formatEventType (type: string, appName: string): string { * Is the object empty * target maybe number, string, array ... */ -export function isEmptyObject (target: unknown): boolean { +export function isEmptyObject(target: unknown): boolean { return isPlainObject(target) ? !Object.keys(target).length : true } + +/** + * + * @param {string} url input url + * @returns {boolean} is relative path + */ +export const isRelativePath = (url: string) => !/^https?:\/\//i.test(url) && !/^\/\//i.test(url) diff --git a/src/proxies/worker.ts b/src/proxies/worker.ts new file mode 100644 index 00000000..957c2b44 --- /dev/null +++ b/src/proxies/worker.ts @@ -0,0 +1,81 @@ + +interface WorkerOptions { + name?: string; + type?: 'classic' | 'module'; + credentials?: 'omit' | 'same-origin' | 'include'; +} + +interface WorkerInstance extends EventTarget { + postMessage(message: any, transfer?: Transferable[]): void; + terminate(): void; +} +interface Worker { + new(url: string | URL, options?: WorkerOptions): WorkerInstance; +} + +// 重写 Worker 构造函数的类型 +const originalWorker = window.Worker + +function isSameOrigin(url: string | URL): boolean { + if (url instanceof URL && url.protocol === 'blob:') { + // 如果 url 是 Blob URL,直接返回 true + return true + } + + // 检查 URL 是否与当前页面在同一个源 + try { + const parsedUrl = new URL(url as string) + return ( + parsedUrl.protocol === window.location.protocol && + parsedUrl.hostname === window.location.hostname && + parsedUrl.port === window.location.port + ) + } catch (error) { + return false + } +} + +function urlFromScript(script: string) { + let blob + try { + blob = new Blob([script], { + type: 'application/javascript' + }) + } catch (e) { + const BlobBuilder = + // @ts-ignore + window.BlobBuilder || + // @ts-ignore + window.WebKitBlobBuilder || + // @ts-ignore + window.MozBlobBuilder || + // @ts-ignore + window.MSBlobBuilder + const blobBuilder = new BlobBuilder() + blobBuilder.append(script) + blob = blobBuilder.getBlob('application/javascript') + } + + const URL = window.URL || window.webkitURL + return URL.createObjectURL(blob) +} + +// @ts-ignore +const WorkerProxy = new Proxy(originalWorker, { + construct(Target, args): WorkerInstance { + const [scriptURL, options] = args + if (!isSameOrigin(scriptURL)) { + // 如果 scriptURL 是跨域的,使用 Blob URL 加载并执行 worker + const script = `import "${scriptURL}";` + const workerPath = urlFromScript(script) + options.type = 'module' + return new Target(workerPath, options) as WorkerInstance + } else { + // 如果 scriptURL 是同源的,直接使用原生的 Worker 构造函数 + return new Target(scriptURL, options) as WorkerInstance + } + }, +}) + +// @ts-ignore +window.Worker = WorkerProxy diff --git a/src/sandbox/adapter.ts b/src/sandbox/adapter.ts index 804e770c..6d002fb7 100644 --- a/src/sandbox/adapter.ts +++ b/src/sandbox/adapter.ts @@ -10,12 +10,18 @@ import { isMicroAppBody, getPreventSetState, throttleDeferForIframeAppName, + isAnchorElement, + isRelativePath, + isImageElement, + isVideoElement, + isAudioElement, } from '../libs/utils' import { appInstanceMap, isIframeSandbox, } from '../create_app' import microApp from '../micro_app' +import { AppManager } from '../app_manager' export class BaseSandbox implements BaseSandboxType { constructor (appName: string, url: string) { @@ -143,14 +149,37 @@ export function updateElementInfo (node: T, appName: string | null): T { * 1. 测试baseURI和ownerDocument在with沙箱中是否正确 * 经过验证with沙箱不能重写ownerDocument,否则react点击事件会触发两次 */ - rawDefineProperties(node, { + const props: {[kye:string]:any} = { __MICRO_APP_NAME__: { configurable: true, enumerable: true, writable: true, value: appName, }, - }) + } + if (isAnchorElement(node)) { + // a 标签 + const microApp = AppManager.getInstance().get(appName) + if (microApp) { + let originalHref = node.href + props.href = { + get() { + if (isRelativePath(originalHref)) { + return `${microApp.url}${originalHref}` + } + return originalHref + }, + set(value: string) { + originalHref = value + } + } + } + } + if (isImageElement(node) || isVideoElement(node) || isAudioElement(node)) { + // @ts-ignore + node.crossOrigin = 'anonymous' + } + rawDefineProperties(node, props) /** * In FireFox, iframe Node.prototype will point to native Node.prototype after insert to document diff --git a/src/sandbox/iframe/index.ts b/src/sandbox/iframe/index.ts index 1e81338d..cdc550a7 100644 --- a/src/sandbox/iframe/index.ts +++ b/src/sandbox/iframe/index.ts @@ -68,8 +68,6 @@ export default class IframeSandbox { private removeHistoryListener!: CallableFunction // Properties that can be escape to rawWindow public escapeProperties: PropertyKey[] = [] - // Properties escape to rawWindow, cleared when unmount - public escapeKeys = new Set() public deleteIframeElement: () => void public iframe!: HTMLIFrameElement | null // Promise used to mark whether the sandbox is initialized @@ -227,11 +225,6 @@ export default class IframeSandbox { if (!umdMode || destroy) { this.deleteIframeElement() - this.escapeKeys.forEach((key: PropertyKey) => { - Reflect.deleteProperty(globalEnv.rawWindow, key) - }) - this.escapeKeys.clear() - this.clearHijackUmdHooks() } diff --git a/src/sandbox/iframe/window.ts b/src/sandbox/iframe/window.ts index dbb96a7c..8eba5601 100644 --- a/src/sandbox/iframe/window.ts +++ b/src/sandbox/iframe/window.ts @@ -22,7 +22,6 @@ import { import { escape2RawWindowKeys, escape2RawWindowRegExpKeys, - // hijackInstanceOfWindowRegExpKeys, } from './special_key' /** @@ -37,7 +36,7 @@ export function patchWindow ( microAppWindow: microAppWindowType, sandbox: IframeSandbox, ): CommonEffectHook { - patchWindowProperty(appName, microAppWindow) + patchWindowProperty(appName, microAppWindow, sandbox) createProxyWindow(microAppWindow, sandbox) return patchWindowEffect(microAppWindow) } @@ -49,6 +48,7 @@ export function patchWindow ( function patchWindowProperty ( appName: string, microAppWindow: microAppWindowType, + sandbox: IframeSandbox, ):void { const rawWindow = globalEnv.rawWindow @@ -96,27 +96,11 @@ function patchWindowProperty ( configurable: true, enumerable: false, value (target: unknown): boolean { - return target instanceof rawWindow[key] || instanceOf(target, microAppWindow[key]) + return instanceOf(target, rawWindow[key]) || instanceOf(target, microAppWindow[key]) }, }) } - // hijackInstanceOfWindowRegExpKeys.some((reg: RegExp) => { - // if (reg.test(key) && key in rawWindow) { - // rawDefineProperty(microAppWindow[key], Symbol.hasInstance, { - // configurable: true, - // enumerable: false, - // value: (target: unknown) => { - // return target instanceof rawWindow[key] - // ? true - // : instanceOf(target, microAppWindow[key]) - // }, - // }) - // return true - // } - // return false - // }) - return /^on/.test(key) && !SCOPE_WINDOW_ON_EVENT_OF_IFRAME.includes(key) }) .forEach((eventName: string) => { @@ -137,6 +121,24 @@ function patchWindowProperty ( logWarn(e, appName) } }) + + /** + * In esmodule(vite) proxyWindow will not take effect, + * escapeProperties should define to microAppWindow + */ + sandbox.escapeProperties.forEach((key: PropertyKey) => { + let rawValue = microAppWindow[key] + rawDefineProperty(microAppWindow, key, { + enumerable: true, + configurable: true, + get () { + return rawValue ?? bindFunctionToRawTarget(rawWindow[key], rawWindow) + }, + set (value: unknown) { + rawValue = value + } + }) + }) } /** @@ -179,7 +181,7 @@ function createProxyWindow ( * 2. window.key in module app(vite), fall into microAppWindow(iframeWindow), escapeProperties will not take effect * 3. if (key)... --> fall into microAppWindow(iframeWindow), escapeProperties will not take effect */ - if (includes(sandbox.escapeProperties, key) && !Reflect.has(target, key)) { + if (includes(sandbox.escapeProperties, key) && !Reflect.get(target, key)) { return bindFunctionToRawTarget(Reflect.get(rawWindow, key), rawWindow) } @@ -194,19 +196,14 @@ function createProxyWindow ( customProperties.add(key) } + // sandbox.escapeProperties will not set to rawWindow from rc.9 Reflect.set(target, key, value) - if (includes(sandbox.escapeProperties, key)) { - !Reflect.has(rawWindow, key) && sandbox.escapeKeys.add(key) - Reflect.set(rawWindow, key, value) - } - return true }, has: (target: microAppWindowType, key: PropertyKey) => key in target, deleteProperty: (target: microAppWindowType, key: PropertyKey): boolean => { if (Reflect.has(target, key)) { - sandbox.escapeKeys.has(key) && Reflect.deleteProperty(rawWindow, key) return Reflect.deleteProperty(target, key) } return true diff --git a/src/sandbox/scoped_css.ts b/src/sandbox/scoped_css.ts index 183a58ca..07612754 100644 --- a/src/sandbox/scoped_css.ts +++ b/src/sandbox/scoped_css.ts @@ -75,7 +75,7 @@ class CSSParser { // reset scopecssDisableNextLine this.scopecssDisableNextLine = false - if (!selectors) return parseError('selector missing', this.linkPath) + if (!selectors) return this.printError('selector missing', this.linkPath) this.recordResult(selectors) @@ -132,11 +132,11 @@ class CSSParser { // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration private styleDeclarations (): boolean | void { - if (!this.matchOpenBrace()) return parseError("Declaration missing '{'", this.linkPath) + if (!this.matchOpenBrace()) return this.printError("Declaration missing '{'", this.linkPath) this.matchAllDeclarations() - if (!this.matchCloseBrace()) return parseError("Declaration missing '}'", this.linkPath) + if (!this.matchCloseBrace()) return this.printError("Declaration missing '}'", this.linkPath) return true } @@ -169,7 +169,7 @@ class CSSParser { // reset scopecssDisableNextLine this.scopecssDisableNextLine = false - if (!this.cssText) return + if (!this.cssText.length) return // extract comments in declarations if (this.cssText.charAt(0) === '/') { @@ -222,18 +222,18 @@ class CSSParser { private keyframesRule (): boolean | void { if (!this.commonMatch(/^@([-\w]+)?keyframes\s*/)) return false - if (!this.commonMatch(/^[^{]+/)) return parseError('@keyframes missing name', this.linkPath) + if (!this.commonMatch(/^[^{]+/)) return this.printError('@keyframes missing name', this.linkPath) this.matchComments() - if (!this.matchOpenBrace()) return parseError("@keyframes missing '{'", this.linkPath) + if (!this.matchOpenBrace()) return this.printError("@keyframes missing '{'", this.linkPath) this.matchComments() while (this.keyframeRule()) { this.matchComments() } - if (!this.matchCloseBrace()) return parseError("@keyframes missing '}'", this.linkPath) + if (!this.matchCloseBrace()) return this.printError("@keyframes missing '}'", this.linkPath) this.matchLeadingSpaces() @@ -295,7 +295,7 @@ class CSSParser { this.matchRules() - if (!this.matchCloseBrace()) return parseError('@layer missing \'}\'', this.linkPath) + if (!this.matchCloseBrace()) return this.printError('@layer missing \'}\'', this.linkPath) this.matchLeadingSpaces() @@ -324,13 +324,13 @@ class CSSParser { return () => { if (!this.commonMatch(reg)) return false - if (!this.matchOpenBrace()) return parseError(`${name} missing '{'`, this.linkPath) + if (!this.matchOpenBrace()) return this.printError(`${name} missing '{'`, this.linkPath) this.matchComments() this.matchRules() - if (!this.matchCloseBrace()) return parseError(`${name} missing '}'`, this.linkPath) + if (!this.matchCloseBrace()) return this.printError(`${name} missing '}'`, this.linkPath) this.matchLeadingSpaces() @@ -350,11 +350,11 @@ class CSSParser { // common handler for @font-face, @page private commonHandlerForAtRuleWithSelfRule (name: string): boolean | void { - if (!this.matchOpenBrace()) return parseError(`@${name} missing '{'`, this.linkPath) + if (!this.matchOpenBrace()) return this.printError(`@${name} missing '{'`, this.linkPath) this.matchAllDeclarations() - if (!this.matchCloseBrace()) return parseError(`@${name} missing '}'`, this.linkPath) + if (!this.matchCloseBrace()) return this.printError(`@${name} missing '}'`, this.linkPath) this.matchLeadingSpaces() @@ -377,7 +377,7 @@ class CSSParser { i += 2 if (this.cssText.charAt(i - 1) === '') { - return parseError('End of comment missing', this.linkPath) + return this.printError('End of comment missing', this.linkPath) } // get comment content @@ -443,6 +443,12 @@ class CSSParser { this.result += strFragment } } + + private printError (msg: string, linkPath?: string): void { + if (this.cssText.length) { + parseError(msg, linkPath) + } + } } /** diff --git a/src/source/patch.ts b/src/source/patch.ts index 04ab0f87..2014d6cd 100644 --- a/src/source/patch.ts +++ b/src/source/patch.ts @@ -632,7 +632,7 @@ export function patchElementAndDocument (): void { }, set (code: string) { globalEnv.rawInnerHTMLDesc.set.call(this, code) - const currentAppName = this.__MICRO_APP_NAME__ || getCurrentAppName() + const currentAppName = this.__MICRO_APP_NAME__ || getIframeCurrentAppName() || getCurrentAppName() Array.from(this.children).forEach((child) => { if (isElement(child) && currentAppName) { updateElementInfo(child, currentAppName)