From a3e67ecab5d5a2ad80422b4b0cc435f92e3926de Mon Sep 17 00:00:00 2001 From: "liangyong9098@cvte.com" Date: Mon, 25 Dec 2023 16:48:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AD=90=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=BE=AA=E7=8E=AF=E5=B5=8C=E5=A5=97iframe=E5=87=BA?= =?UTF-8?q?=E7=8E=B0window=20=E6=8C=87=E5=90=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/main-react/src/pages/React17.js | 21 +- examples/react16/src/App.js | 12 +- examples/react16/src/Communication.js | 16 + examples/react16/src/Iframe.js | 23 + examples/react17/src/Communication.js | 30 +- packages/wujie-core/src/iframe copy.ts--- | 850 ++++++++++++++++++++++ packages/wujie-core/src/iframe.ts | 15 +- 7 files changed, 943 insertions(+), 24 deletions(-) create mode 100644 examples/react16/src/Iframe.js create mode 100644 packages/wujie-core/src/iframe copy.ts--- diff --git a/examples/main-react/src/pages/React17.js b/examples/main-react/src/pages/React17.js index e19f8466e..5a9feeeb9 100644 --- a/examples/main-react/src/pages/React17.js +++ b/examples/main-react/src/pages/React17.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import hostMap from "../hostMap"; import WujieReact from "wujie-react"; import { useNavigate, useLocation } from "react-router-dom"; @@ -7,7 +7,7 @@ export default function React17() { const location = useLocation(); const navigation = useNavigate(); const react17Url = hostMap("//localhost:7100/"); - const path = location.pathname.replace("/react17-sub", "").replace("/react17", "");//// + const path = location.pathname.replace("/react17-sub", "").replace("/react17", ""); //// // 告诉子应用要跳转哪个路由 path && WujieReact.bus.$emit("react17-router-change", path); const props = { @@ -15,15 +15,16 @@ export default function React17() { navigation(`/${name}`); }, }; + + useEffect(() => { + window.addEventListener("message", (e) => { + if (e.data && e.data.type && e.data.type === "react17") { + alert(e.data.msg); + } + }); + }, []); return ( // 保活模式,name相同则复用一个子应用实例,改变url无效,必须采用通信的方式告知路由变化 - + ); } diff --git a/examples/react16/src/App.js b/examples/react16/src/App.js index 46fe42ee3..0085e1375 100644 --- a/examples/react16/src/App.js +++ b/examples/react16/src/App.js @@ -5,6 +5,7 @@ import Location from "./Location"; import Communication from "./Communication"; import React17 from "./nest"; import Font from "./Font"; +import Iframe from "./Iframe"; import logo from "./logo.svg"; import Tag from "antd/es/tag"; import Button from "antd/es/button"; @@ -39,10 +40,10 @@ const Home = () => ( export default function App() { // 在 react16-sub 路由下主动告知主应用路由跳转,主应用也跳到相应路由高亮菜单栏 - const location = useLocation() + const location = useLocation(); useEffect(() => { - window.$wujie?.bus.$emit('sub-route-change', "react16", location.pathname) - }, [location]) + window.$wujie?.bus.$emit("sub-route-change", "react16", location.pathname); + }, [location]); return (
@@ -75,6 +76,11 @@ export default function App() { + + +
); } diff --git a/packages/wujie-core/src/iframe copy.ts--- b/packages/wujie-core/src/iframe copy.ts--- new file mode 100644 index 000000000..d25bb8069 --- /dev/null +++ b/packages/wujie-core/src/iframe copy.ts--- @@ -0,0 +1,850 @@ +import WuJie from "./sandbox"; +import { ScriptObject } from "./template"; +import { renderElementToContainer } from "./shadow"; +import { syncUrlToWindow } from "./sync"; +import { + fixElementCtrSrcOrHref, + isConstructable, + anchorElementGenerator, + isMatchSyncQueryById, + warn, + error, + execHooks, + getCurUrl, + getAbsolutePath, + setAttrsToElement, + setTagToScript, + getTagFromScript, +} from "./utils"; +import { + documentProxyProperties, + rawAddEventListener, + rawRemoveEventListener, + rawDocumentQuerySelector, + mainDocumentAddEventListenerEvents, + mainAndAppAddEventListenerEvents, + appDocumentAddEventListenerEvents, + appDocumentOnEvents, + appWindowAddEventListenerEvents, + appWindowOnEvent, + windowProxyProperties, + windowRegWhiteList, + rawWindowAddEventListener, + rawWindowRemoveEventListener, +} from "./common"; +import type { appAddEventListenerOptions } from "./common"; +import { getJsLoader } from "./plugin"; +import { WUJIE_TIPS_SCRIPT_ERROR_REQUESTED, WUJIE_DATA_FLAG } from "./constant"; +import { ScriptObjectLoader } from "./index"; + +declare global { + interface Window { + // 是否存在无界 + __POWERED_BY_WUJIE__?: boolean; + // 子应用公共加载路径 + __WUJIE_PUBLIC_PATH__: string; + // 原生的querySelector + __WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__: typeof Document.prototype.querySelector; + + // iframe内原生的createElement + __WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__: typeof Document.prototype.createElement; + + // iframe内原生的createTextNode + __WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__: typeof Document.prototype.createTextNode; + + // iframe内原生的head + __WUJIE_RAW_DOCUMENT_HEAD__: typeof Document.prototype.head; + + // 原生的querySelector + __WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__: typeof Document.prototype.querySelectorAll; + // 原生的window对象 + __WUJIE_RAW_WINDOW__: Window; + // 子应用沙盒实例 + __WUJIE: WuJie; + // 记录注册在主应用中的事件 + __WUJIE_EVENTLISTENER__: Set<{ listener: EventListenerOrEventListenerObject; type: string; options: any }>; + // 子应用mount函数 + __WUJIE_MOUNT: () => void; + // 子应用unmount函数 + __WUJIE_UNMOUNT: () => void; + // document type + Document: typeof Document; + // img type + HTMLImageElement: typeof HTMLImageElement; + // node type + Node: typeof Node; + // element type + Element: typeof Element; + // htmlElement typeof + HTMLElement: typeof HTMLElement; + // anchor type + HTMLAnchorElement: typeof HTMLAnchorElement; + // source type + HTMLSourceElement: typeof HTMLSourceElement; + // link type + HTMLLinkElement: typeof HTMLLinkElement; + // script type + HTMLScriptElement: typeof HTMLScriptElement; + // media type + HTMLMediaElement: typeof HTMLMediaElement; + EventTarget: typeof EventTarget; + Event: typeof Event; + ShadowRoot: typeof ShadowRoot; + // 注入对象 + $wujie: { [key: string]: any }; + } + interface HTMLHeadElement { + _cacheListeners: Map; + } + interface HTMLBodyElement { + _cacheListeners: Map; + } + interface Document { + createTreeWalker( + root: Node, + whatToShow?: number, + filter?: NodeFilter | null, + entityReferenceExpansion?: boolean + ): TreeWalker; + } +} + +/** + * 修改window对象的事件监听,只有路由事件采用iframe的事件 + */ +function patchIframeEvents(iframeWindow: Window) { + iframeWindow.__WUJIE_EVENTLISTENER__ = iframeWindow.__WUJIE_EVENTLISTENER__ || new Set(); + iframeWindow.addEventListener = function addEventListener( + type: K, + listener: (this: Window, ev: WindowEventMap[K]) => any, + options?: boolean | appAddEventListenerOptions + ) { + // 运行插件钩子函数 + execHooks(iframeWindow.__WUJIE.plugins, "windowAddEventListenerHook", iframeWindow, type, listener, options); + // 相同参数多次调用 addEventListener 不会导致重复注册,所以用set。 + iframeWindow.__WUJIE_EVENTLISTENER__.add({ type, listener, options }); + const isConfigTargetWindow = typeof options === "object" && options.targetWindow; + + // 处理子应用循环嵌套iframe场景的 window 指向问题 + if (appWindowAddEventListenerEvents.includes(type) || isConfigTargetWindow) { + if (type === "message") { + debugger; + } + const targetWindow = isConfigTargetWindow ? options?.targetWindow : window.__WUJIE_RAW_WINDOW__ || window; + return rawWindowAddEventListener.call(targetWindow, type, listener, options); + } + // 在子应用嵌套场景使用window.window获取真实window + rawWindowAddEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options); + }; + + iframeWindow.removeEventListener = function removeEventListener( + type: K, + listener: (this: Window, ev: WindowEventMap[K]) => any, + options?: boolean | appAddEventListenerOptions + ) { + // 运行插件钩子函数 + execHooks(iframeWindow.__WUJIE.plugins, "windowRemoveEventListenerHook", iframeWindow, type, listener, options); + iframeWindow.__WUJIE_EVENTLISTENER__.forEach((o) => { + // 这里严格一点,确保子应用销毁的时候都能销毁 + if (o.listener === listener && o.type === type && options == o.options) { + iframeWindow.__WUJIE_EVENTLISTENER__.delete(o); + } + }); + const isConfigTargetWindow = typeof options === "object" && options.targetWindow; + if (appWindowAddEventListenerEvents.includes(type) || isConfigTargetWindow) { + const targetWindow = isConfigTargetWindow ? options?.targetWindow : window.__WUJIE_RAW_WINDOW__ || window; + return rawWindowRemoveEventListener.call(targetWindow, type, listener, options); + } + rawWindowRemoveEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options); + }; +} + +function patchIframeVariable(iframeWindow: Window, wujie: WuJie, appHostPath: string): void { + iframeWindow.__WUJIE = wujie; + iframeWindow.__WUJIE_PUBLIC_PATH__ = appHostPath + "/"; + iframeWindow.$wujie = wujie.provide; + iframeWindow.__WUJIE_RAW_WINDOW__ = iframeWindow; +} + +/** + * 对iframe的history的pushState和replaceState进行修改 + * 将从location劫持后的数据修改回来,防止跨域错误 + * 同步路由到主应用 + * @param iframeWindow + * @param appHostPath 子应用的 host path + * @param mainHostPath 主应用的 host path + */ +function patchIframeHistory(iframeWindow: Window, appHostPath: string, mainHostPath: string): void { + const history = iframeWindow.history; + const rawHistoryPushState = history.pushState; + const rawHistoryReplaceState = history.replaceState; + history.pushState = function (data: any, title: string, url?: string): void { + const baseUrl = + mainHostPath + iframeWindow.location.pathname + iframeWindow.location.search + iframeWindow.location.hash; + const mainUrl = getAbsolutePath(url?.replace(appHostPath, ""), baseUrl); + const ignoreFlag = url === undefined; + + rawHistoryPushState.call(history, data, title, ignoreFlag ? undefined : mainUrl); + if (ignoreFlag) return; + updateBase(iframeWindow, appHostPath, mainHostPath); + syncUrlToWindow(iframeWindow); + }; + history.replaceState = function (data: any, title: string, url?: string): void { + const baseUrl = + mainHostPath + iframeWindow.location.pathname + iframeWindow.location.search + iframeWindow.location.hash; + const mainUrl = getAbsolutePath(url?.replace(appHostPath, ""), baseUrl); + const ignoreFlag = url === undefined; + + rawHistoryReplaceState.call(history, data, title, ignoreFlag ? undefined : mainUrl); + if (ignoreFlag) return; + updateBase(iframeWindow, appHostPath, mainHostPath); + syncUrlToWindow(iframeWindow); + }; +} + +/** + * 动态的修改iframe的base地址 + * @param iframeWindow + * @param appHostPath + * @param mainHostPath + */ +function updateBase(iframeWindow: Window, appHostPath: string, mainHostPath: string) { + const baseUrl = new URL(iframeWindow.location.href?.replace(mainHostPath, ""), appHostPath); + const baseElement = rawDocumentQuerySelector.call(iframeWindow.document, "base"); + if (baseElement) baseElement.setAttribute("href", appHostPath + baseUrl.pathname); +} + +/** + * patch iframe window effect + * @param iframeWindow + */ +// TODO 继续改进 +function patchWindowEffect(iframeWindow: Window): void { + // 属性处理函数 + function processWindowProperty(key: string): boolean { + const value = iframeWindow[key]; + try { + if (typeof value === "function" && !isConstructable(value)) { + iframeWindow[key] = window[key].bind(window); + } else { + iframeWindow[key] = window[key]; + } + return true; + } catch (e) { + warn(e.message); + return false; + } + } + Object.getOwnPropertyNames(iframeWindow).forEach((key) => { + // 特殊处理 + if (key === "getSelection") { + Object.defineProperty(iframeWindow, key, { + get: () => iframeWindow.document[key], + }); + return; + } + // 单独属性 + if (windowProxyProperties.includes(key)) { + processWindowProperty(key); + return; + } + // 正则匹配,可以一次处理多个 + windowRegWhiteList.some((reg) => { + if (reg.test(key) && key in iframeWindow.parent) { + return processWindowProperty(key); + } + return false; + }); + }); + // onEvent set + const windowOnEvents = Object.getOwnPropertyNames(window) + .filter((p) => /^on/.test(p)) + .filter((e) => !appWindowOnEvent.includes(e)); + + // 走主应用window + windowOnEvents.forEach((e) => { + const descriptor = Object.getOwnPropertyDescriptor(iframeWindow, e) || { + enumerable: true, + writable: true, + }; + try { + Object.defineProperty(iframeWindow, e, { + enumerable: descriptor.enumerable, + configurable: true, + get: () => window[e], + set: + descriptor.writable || descriptor.set + ? (handler) => { + window[e] = typeof handler === "function" ? handler.bind(iframeWindow) : handler; + } + : undefined, + }); + } catch (e) { + warn(e.message); + } + }); + // 运行插件钩子函数 + execHooks(iframeWindow.__WUJIE.plugins, "windowPropertyOverride", iframeWindow); +} + +/** + * 记录节点的监听事件 + */ +function recordEventListeners(iframeWindow: Window) { + const sandbox = iframeWindow.__WUJIE; + iframeWindow.Node.prototype.addEventListener = function ( + type: string, + handler: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + // 添加事件缓存 + const elementListenerList = sandbox.elementEventCacheMap.get(this); + if (elementListenerList) { + if (!elementListenerList.find((listener) => listener.type === type && listener.handler === handler)) { + elementListenerList.push({ type, handler, options }); + } + } else sandbox.elementEventCacheMap.set(this, [{ type, handler, options }]); + return rawAddEventListener.call(this, type, handler, options); + }; + + iframeWindow.Node.prototype.removeEventListener = function ( + type: string, + handler: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void { + // 清除缓存 + const elementListenerList = sandbox.elementEventCacheMap.get(this); + if (elementListenerList) { + const index = elementListenerList?.findIndex((ele) => ele.type === type && ele.handler === handler); + elementListenerList.splice(index, 1); + } + if (!elementListenerList?.length) { + sandbox.elementEventCacheMap.delete(this); + } + return rawRemoveEventListener.call(this, type, handler, options); + }; +} + +/** + * 恢复节点的监听事件 + */ +export function recoverEventListeners(rootElement: Element | ChildNode, iframeWindow: Window) { + const sandbox = iframeWindow.__WUJIE; + const elementEventCacheMap: WeakMap< + Node, + Array<{ type: string; handler: EventListenerOrEventListenerObject; options: any }> + > = new WeakMap(); + const ElementIterator = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT, null, false); + let nextElement = ElementIterator.currentNode; + while (nextElement) { + const elementListenerList = sandbox.elementEventCacheMap.get(nextElement); + if (elementListenerList?.length) { + elementEventCacheMap.set(nextElement, elementListenerList); + elementListenerList.forEach((listener) => { + nextElement.addEventListener(listener.type, listener.handler, listener.options); + }); + } + nextElement = ElementIterator.nextNode() as HTMLElement; + } + sandbox.elementEventCacheMap = elementEventCacheMap; +} + +/** + * 恢复根节点的监听事件 + */ +export function recoverDocumentListeners( + oldRootElement: Element | ChildNode, + newRootElement: Element | ChildNode, + iframeWindow: Window +) { + const sandbox = iframeWindow.__WUJIE; + const elementEventCacheMap: WeakMap< + Node, + Array<{ type: string; handler: EventListenerOrEventListenerObject; options: any }> + > = new WeakMap(); + const elementListenerList = sandbox.elementEventCacheMap.get(oldRootElement); + if (elementListenerList?.length) { + elementEventCacheMap.set(newRootElement, elementListenerList); + elementListenerList.forEach((listener) => { + newRootElement.addEventListener(listener.type, listener.handler, listener.options); + }); + } + sandbox.elementEventCacheMap = elementEventCacheMap; +} + +/** + * 修复vue绑定事件e.timeStamp < attachedTimestamp 的情况 + */ +export function patchEventTimeStamp(targetWindow: Window, iframeWindow: Window) { + Object.defineProperty(targetWindow.Event.prototype, "timeStamp", { + get: () => { + return iframeWindow.document.createEvent("Event").timeStamp; + }, + }); +} + +/** + * patch document effect + * @param iframeWindow + */ +// TODO 继续改进 +function patchDocumentEffect(iframeWindow: Window): void { + const sandbox = iframeWindow.__WUJIE; + + /** + * 处理 addEventListener和removeEventListener + * 由于这个劫持导致 handler 的this发生改变,所以需要handler.bind(document) + * 但是这样会导致removeEventListener无法正常工作,因为handler => handler.bind(document) + * 这个地方保存callback = handler.bind(document) 方便removeEventListener + */ + const handlerCallbackMap: WeakMap = + new WeakMap(); + const handlerTypeMap: WeakMap> = new WeakMap(); + iframeWindow.Document.prototype.addEventListener = function ( + type: string, + handler: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + if (!handler) return; + let callback = handlerCallbackMap.get(handler); + const typeList = handlerTypeMap.get(handler); + // 设置 handlerCallbackMap + if (!callback) { + callback = typeof handler === "function" ? handler.bind(this) : handler; + handlerCallbackMap.set(handler, callback); + } + // 设置 handlerTypeMap + if (typeList) { + if (!typeList.includes(type)) typeList.push(type); + } else { + handlerTypeMap.set(handler, [type]); + } + + // 运行插件钩子函数 + execHooks(iframeWindow.__WUJIE.plugins, "documentAddEventListenerHook", iframeWindow, type, callback, options); + if (appDocumentAddEventListenerEvents.includes(type)) { + return rawAddEventListener.call(this, type, callback, options); + } + // 降级统一走 sandbox.document + if (sandbox.degrade) return sandbox.document.addEventListener(type, callback, options); + if (mainDocumentAddEventListenerEvents.includes(type)) + return window.document.addEventListener(type, callback, options); + if (mainAndAppAddEventListenerEvents.includes(type)) { + window.document.addEventListener(type, callback, options); + sandbox.shadowRoot.addEventListener(type, callback, options); + return; + } + sandbox.shadowRoot.addEventListener(type, callback, options); + }; + iframeWindow.Document.prototype.removeEventListener = function ( + type: string, + handler: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + const callback: EventListenerOrEventListenerObject = handlerCallbackMap.get(handler); + const typeList = handlerTypeMap.get(handler); + if (callback) { + if (typeList?.includes(type)) { + typeList.splice(typeList.indexOf(type), 1); + if (!typeList.length) { + handlerCallbackMap.delete(handler); + handlerTypeMap.delete(handler); + } + } + + // 运行插件钩子函数 + execHooks(iframeWindow.__WUJIE.plugins, "documentRemoveEventListenerHook", iframeWindow, type, callback, options); + if (appDocumentAddEventListenerEvents.includes(type)) { + return rawRemoveEventListener.call(this, type, callback, options); + } + if (sandbox.degrade) return sandbox.document.removeEventListener(type, callback, options); + if (mainDocumentAddEventListenerEvents.includes(type)) { + return window.document.removeEventListener(type, callback, options); + } + if (mainAndAppAddEventListenerEvents.includes(type)) { + window.document.removeEventListener(type, callback, options); + sandbox.shadowRoot.removeEventListener(type, callback, options); + return; + } + sandbox.shadowRoot.removeEventListener(type, callback, options); + } + }; + // 处理onEvent + const elementOnEvents = Object.keys(iframeWindow.HTMLElement.prototype).filter((ele) => /^on/.test(ele)); + const documentOnEvent = Object.keys(iframeWindow.Document.prototype) + .filter((ele) => /^on/.test(ele)) + .filter((ele) => !appDocumentOnEvents.includes(ele)); + elementOnEvents + .filter((e) => documentOnEvent.includes(e)) + .forEach((e) => { + const descriptor = Object.getOwnPropertyDescriptor(iframeWindow.Document.prototype, e) || { + enumerable: true, + writable: true, + }; + try { + Object.defineProperty(iframeWindow.Document.prototype, e, { + enumerable: descriptor.enumerable, + configurable: true, + get: () => (sandbox.degrade ? sandbox.document[e] : sandbox.shadowRoot.firstElementChild[e]), + set: + descriptor.writable || descriptor.set + ? (handler) => { + const val = typeof handler === "function" ? handler.bind(iframeWindow.document) : handler; + sandbox.degrade ? (sandbox.document[e] = val) : (sandbox.shadowRoot.firstElementChild[e] = val); + } + : undefined, + }); + } catch (e) { + warn(e.message); + } + }); + // 处理属性get + const { + ownerProperties, + modifyProperties, + shadowProperties, + shadowMethods, + documentProperties, + documentMethods, + documentEvents, + } = documentProxyProperties; + modifyProperties.concat(shadowProperties, shadowMethods, documentProperties, documentMethods).forEach((propKey) => { + const descriptor = Object.getOwnPropertyDescriptor(iframeWindow.Document.prototype, propKey) || { + enumerable: true, + writable: true, + }; + try { + Object.defineProperty(iframeWindow.Document.prototype, propKey, { + enumerable: descriptor.enumerable, + configurable: true, + get: () => sandbox.proxyDocument[propKey], + set: undefined, + }); + } catch (e) { + warn(e.message); + } + }); + // 处理document专属事件 + // TODO 内存泄露 + documentEvents.forEach((propKey) => { + const descriptor = Object.getOwnPropertyDescriptor(iframeWindow.Document.prototype, propKey) || { + enumerable: true, + writable: true, + }; + try { + Object.defineProperty(iframeWindow.Document.prototype, propKey, { + enumerable: descriptor.enumerable, + configurable: true, + get: () => (sandbox.degrade ? sandbox : window).document[propKey], + set: + descriptor.writable || descriptor.set + ? (handler) => { + (sandbox.degrade ? sandbox : window).document[propKey] = + typeof handler === "function" ? handler.bind(iframeWindow.document) : handler; + } + : undefined, + }); + } catch (e) { + warn(e.message); + } + }); + // process owner property + ownerProperties.forEach((propKey) => { + Object.defineProperty(iframeWindow.document, propKey, { + enumerable: true, + configurable: true, + get: () => sandbox.proxyDocument[propKey], + set: undefined, + }); + }); + // 运行插件钩子函数 + execHooks(iframeWindow.__WUJIE.plugins, "documentPropertyOverride", iframeWindow); +} + +/** + * patch Node effect + * 1、处理 getRootNode + * 2、处理 appendChild、insertBefore,当插入的节点为 svg 时,createElement 的 patch 会被去除,需要重新 patch + * @param iframeWindow + */ +function patchNodeEffect(iframeWindow: Window): void { + const rawGetRootNode = iframeWindow.Node.prototype.getRootNode; + const rawAppendChild = iframeWindow.Node.prototype.appendChild; + const rawInsertRule = iframeWindow.Node.prototype.insertBefore; + iframeWindow.Node.prototype.getRootNode = function (options?: GetRootNodeOptions): Node { + const rootNode = rawGetRootNode.call(this, options); + if (rootNode === iframeWindow.__WUJIE.shadowRoot) return iframeWindow.document; + else return rootNode; + }; + iframeWindow.Node.prototype.appendChild = function (node: T): T { + const res = rawAppendChild.call(this, node); + patchElementEffect(node, iframeWindow); + return res; + }; + iframeWindow.Node.prototype.insertBefore = function (node: T, child: Node | null): T { + const res = rawInsertRule.call(this, node, child); + patchElementEffect(node, iframeWindow); + return res; + }; +} + +/** + * 修复资源元素的相对路径问题 + * @param iframeWindow + */ +function patchRelativeUrlEffect(iframeWindow: Window): void { + fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLImageElement, "src"); + fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLAnchorElement, "href"); + fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLSourceElement, "src"); + fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLLinkElement, "href"); + fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLScriptElement, "src"); + fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLMediaElement, "src"); +} + +/** + * 初始化base标签 + */ +export function initBase(iframeWindow: Window, url: string): void { + const iframeDocument = iframeWindow.document; + const baseElement = iframeDocument.createElement("base"); + const iframeUrlElement = anchorElementGenerator(iframeWindow.location.href); + const appUrlElement = anchorElementGenerator(url); + baseElement.setAttribute("href", appUrlElement.protocol + "//" + appUrlElement.host + iframeUrlElement.pathname); + iframeDocument.head.appendChild(baseElement); +} + +/** + * 初始化iframe的dom结构 + * @param iframeWindow + * @param wujie + * @param mainHostPath + * @param appHostPath + */ +function initIframeDom(iframeWindow: Window, wujie: WuJie, mainHostPath: string, appHostPath: string): void { + const iframeDocument = iframeWindow.document; + const newDoc = window.document.implementation.createHTMLDocument(""); + const newDocumentElement = iframeDocument.importNode(newDoc.documentElement, true); + iframeDocument.documentElement + ? iframeDocument.replaceChild(newDocumentElement, iframeDocument.documentElement) + : iframeDocument.appendChild(newDocumentElement); + iframeWindow.__WUJIE_RAW_DOCUMENT_HEAD__ = iframeDocument.head; + iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__ = iframeWindow.Document.prototype.querySelector; + iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__ = iframeWindow.Document.prototype.querySelectorAll; + iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__ = iframeWindow.Document.prototype.createElement; + iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__ = iframeWindow.Document.prototype.createTextNode; + initBase(iframeWindow, wujie.url); + patchIframeHistory(iframeWindow, appHostPath, mainHostPath); + patchIframeEvents(iframeWindow); + if (wujie.degrade) recordEventListeners(iframeWindow); + syncIframeUrlToWindow(iframeWindow); + + patchWindowEffect(iframeWindow); + patchDocumentEffect(iframeWindow); + patchNodeEffect(iframeWindow); + patchRelativeUrlEffect(iframeWindow); +} + +/** + * 防止运行主应用的js代码,给子应用带来很多副作用 + */ +// TODO 更加准确抓取停止时机 +function stopIframeLoading(iframeWindow: Window) { + const oldDoc = iframeWindow.document; + return new Promise((resolve) => { + function loop() { + setTimeout(() => { + let newDoc; + try { + newDoc = iframeWindow.document; + } catch (err) { + newDoc = null; + } + // wait for document ready + if (!newDoc || newDoc == oldDoc) { + loop(); + } else { + iframeWindow.stop ? iframeWindow.stop() : iframeWindow.document.execCommand("Stop"); + resolve(); + } + }, 1); + } + loop(); + }); +} + +export function patchElementEffect( + element: (HTMLElement | Node | ShadowRoot) & { _hasPatch?: boolean }, + iframeWindow: Window +): void { + const proxyLocation = iframeWindow.__WUJIE.proxyLocation as Location; + if (element._hasPatch) return; + try { + Object.defineProperties(element, { + baseURI: { + configurable: true, + get: () => proxyLocation.protocol + "//" + proxyLocation.host + proxyLocation.pathname, + set: undefined, + }, + ownerDocument: { + configurable: true, + get: () => iframeWindow.document, + }, + _hasPatch: { get: () => true }, + }); + } catch (error) { + console.warn(error); + } + execHooks(iframeWindow.__WUJIE.plugins, "patchElementHook", element, iframeWindow); +} + +/** + * 子应用前进后退,同步路由到主应用 + * @param iframeWindow + */ +export function syncIframeUrlToWindow(iframeWindow: Window): void { + iframeWindow.addEventListener("hashchange", () => syncUrlToWindow(iframeWindow)); + iframeWindow.addEventListener("popstate", () => { + syncUrlToWindow(iframeWindow); + }); +} + +/** + * iframe插入脚本 + * @param scriptResult script请求结果 + * @param iframeWindow + * @param rawElement 原始的脚本 + */ +export function insertScriptToIframe( + scriptResult: ScriptObject | ScriptObjectLoader, + iframeWindow: Window, + rawElement?: HTMLScriptElement +) { + const { src, module, content, crossorigin, crossoriginType, async, attrs, callback, onload } = + scriptResult as ScriptObjectLoader; + const scriptElement = iframeWindow.document.createElement("script"); + const nextScriptElement = iframeWindow.document.createElement("script"); + const { replace, plugins, proxyLocation } = iframeWindow.__WUJIE; + const jsLoader = getJsLoader({ plugins, replace }); + let code = jsLoader(content, src, getCurUrl(proxyLocation)); + // 添加属性 + attrs && + Object.keys(attrs) + .filter((key) => !Object.keys(scriptResult).includes(key)) + .forEach((key) => scriptElement.setAttribute(key, String(attrs[key]))); + + // 内联脚本 + if (content) { + // patch location + if (!iframeWindow.__WUJIE.degrade && !module) { + code = `(function(window, self, global, location) { + ${code} +}).bind(window.__WUJIE.proxy)( + window.__WUJIE.proxy, + window.__WUJIE.proxy, + window.__WUJIE.proxy, + window.__WUJIE.proxyLocation, +);`; + } + const descriptor = Object.getOwnPropertyDescriptor(scriptElement, "src"); + // 部分浏览器 src 不可配置 取不到descriptor表示无该属性,可写 + if (descriptor?.configurable || !descriptor) { + // 解决 webpack publicPath 为 auto 无法加载资源的问题 + try { + Object.defineProperty(scriptElement, "src", { get: () => src || "" }); + } catch (error) { + console.warn(error); + } + } + } else { + src && scriptElement.setAttribute("src", src); + crossorigin && scriptElement.setAttribute("crossorigin", crossoriginType); + } + module && scriptElement.setAttribute("type", "module"); + scriptElement.textContent = code || ""; + nextScriptElement.textContent = + "if(window.__WUJIE.execQueue && window.__WUJIE.execQueue.length){ window.__WUJIE.execQueue.shift()()}"; + + const container = rawDocumentQuerySelector.call(iframeWindow.document, "head"); + const execNextScript = () => !async && container.appendChild(nextScriptElement); + const afterExecScript = () => { + onload?.(); + execNextScript(); + }; + + // 错误情况处理 + if (/^ { + if (!iframeWindow.__WUJIE) { + patchIframeVariable(iframeWindow, sandbox, appHostPath); + } + initIframeDom(iframeWindow, sandbox, mainHostPath, appHostPath); + /** + * 如果有同步优先同步,非同步从url读取 + */ + if (!isMatchSyncQueryById(iframeWindow.__WUJIE.id)) { + iframeWindow.history.replaceState(null, "", mainHostPath + appRoutePath); + } + }); + return iframe; +} diff --git a/packages/wujie-core/src/iframe.ts b/packages/wujie-core/src/iframe.ts index ee7807898..b36e64920 100644 --- a/packages/wujie-core/src/iframe.ts +++ b/packages/wujie-core/src/iframe.ts @@ -123,8 +123,14 @@ function patchIframeEvents(iframeWindow: Window) { execHooks(iframeWindow.__WUJIE.plugins, "windowAddEventListenerHook", iframeWindow, type, listener, options); // 相同参数多次调用 addEventListener 不会导致重复注册,所以用set。 iframeWindow.__WUJIE_EVENTLISTENER__.add({ type, listener, options }); - if (appWindowAddEventListenerEvents.includes(type) || (typeof options === "object" && options.targetWindow)) { - const targetWindow = typeof options === "object" && options.targetWindow ? options?.targetWindow : iframeWindow; + const isConfigTargetWindow = typeof options === "object" && options.targetWindow; + + // 处理子应用循环嵌套iframe场景的 window 指向问题 + if (appWindowAddEventListenerEvents.includes(type) || isConfigTargetWindow) { + const targetWindow = + typeof options === "object" && options.targetWindow + ? options?.targetWindow + : window.__WUJIE_RAW_WINDOW__ || window; return rawWindowAddEventListener.call(targetWindow, type, listener, options); } // 在子应用嵌套场景使用window.window获取真实window @@ -144,8 +150,9 @@ function patchIframeEvents(iframeWindow: Window) { iframeWindow.__WUJIE_EVENTLISTENER__.delete(o); } }); - if (appWindowAddEventListenerEvents.includes(type) || (typeof options === "object" && options.targetWindow)) { - const targetWindow = typeof options === "object" && options.targetWindow ? options?.targetWindow : iframeWindow; + const isConfigTargetWindow = typeof options === "object" && options.targetWindow; + if (appWindowAddEventListenerEvents.includes(type) || isConfigTargetWindow) { + const targetWindow = isConfigTargetWindow ? options?.targetWindow : window.__WUJIE_RAW_WINDOW__ || window; return rawWindowRemoveEventListener.call(targetWindow, type, listener, options); } rawWindowRemoveEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options); From a596d55d1c8830d9f35ab2bd8945be9e9df93c1d Mon Sep 17 00:00:00 2001 From: "liangyong9098@cvte.com" Date: Mon, 25 Dec 2023 16:57:06 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/wujie-core/src/iframe copy.ts--- | 850 ---------------------- 1 file changed, 850 deletions(-) delete mode 100644 packages/wujie-core/src/iframe copy.ts--- diff --git a/packages/wujie-core/src/iframe copy.ts--- b/packages/wujie-core/src/iframe copy.ts--- deleted file mode 100644 index d25bb8069..000000000 --- a/packages/wujie-core/src/iframe copy.ts--- +++ /dev/null @@ -1,850 +0,0 @@ -import WuJie from "./sandbox"; -import { ScriptObject } from "./template"; -import { renderElementToContainer } from "./shadow"; -import { syncUrlToWindow } from "./sync"; -import { - fixElementCtrSrcOrHref, - isConstructable, - anchorElementGenerator, - isMatchSyncQueryById, - warn, - error, - execHooks, - getCurUrl, - getAbsolutePath, - setAttrsToElement, - setTagToScript, - getTagFromScript, -} from "./utils"; -import { - documentProxyProperties, - rawAddEventListener, - rawRemoveEventListener, - rawDocumentQuerySelector, - mainDocumentAddEventListenerEvents, - mainAndAppAddEventListenerEvents, - appDocumentAddEventListenerEvents, - appDocumentOnEvents, - appWindowAddEventListenerEvents, - appWindowOnEvent, - windowProxyProperties, - windowRegWhiteList, - rawWindowAddEventListener, - rawWindowRemoveEventListener, -} from "./common"; -import type { appAddEventListenerOptions } from "./common"; -import { getJsLoader } from "./plugin"; -import { WUJIE_TIPS_SCRIPT_ERROR_REQUESTED, WUJIE_DATA_FLAG } from "./constant"; -import { ScriptObjectLoader } from "./index"; - -declare global { - interface Window { - // 是否存在无界 - __POWERED_BY_WUJIE__?: boolean; - // 子应用公共加载路径 - __WUJIE_PUBLIC_PATH__: string; - // 原生的querySelector - __WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__: typeof Document.prototype.querySelector; - - // iframe内原生的createElement - __WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__: typeof Document.prototype.createElement; - - // iframe内原生的createTextNode - __WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__: typeof Document.prototype.createTextNode; - - // iframe内原生的head - __WUJIE_RAW_DOCUMENT_HEAD__: typeof Document.prototype.head; - - // 原生的querySelector - __WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__: typeof Document.prototype.querySelectorAll; - // 原生的window对象 - __WUJIE_RAW_WINDOW__: Window; - // 子应用沙盒实例 - __WUJIE: WuJie; - // 记录注册在主应用中的事件 - __WUJIE_EVENTLISTENER__: Set<{ listener: EventListenerOrEventListenerObject; type: string; options: any }>; - // 子应用mount函数 - __WUJIE_MOUNT: () => void; - // 子应用unmount函数 - __WUJIE_UNMOUNT: () => void; - // document type - Document: typeof Document; - // img type - HTMLImageElement: typeof HTMLImageElement; - // node type - Node: typeof Node; - // element type - Element: typeof Element; - // htmlElement typeof - HTMLElement: typeof HTMLElement; - // anchor type - HTMLAnchorElement: typeof HTMLAnchorElement; - // source type - HTMLSourceElement: typeof HTMLSourceElement; - // link type - HTMLLinkElement: typeof HTMLLinkElement; - // script type - HTMLScriptElement: typeof HTMLScriptElement; - // media type - HTMLMediaElement: typeof HTMLMediaElement; - EventTarget: typeof EventTarget; - Event: typeof Event; - ShadowRoot: typeof ShadowRoot; - // 注入对象 - $wujie: { [key: string]: any }; - } - interface HTMLHeadElement { - _cacheListeners: Map; - } - interface HTMLBodyElement { - _cacheListeners: Map; - } - interface Document { - createTreeWalker( - root: Node, - whatToShow?: number, - filter?: NodeFilter | null, - entityReferenceExpansion?: boolean - ): TreeWalker; - } -} - -/** - * 修改window对象的事件监听,只有路由事件采用iframe的事件 - */ -function patchIframeEvents(iframeWindow: Window) { - iframeWindow.__WUJIE_EVENTLISTENER__ = iframeWindow.__WUJIE_EVENTLISTENER__ || new Set(); - iframeWindow.addEventListener = function addEventListener( - type: K, - listener: (this: Window, ev: WindowEventMap[K]) => any, - options?: boolean | appAddEventListenerOptions - ) { - // 运行插件钩子函数 - execHooks(iframeWindow.__WUJIE.plugins, "windowAddEventListenerHook", iframeWindow, type, listener, options); - // 相同参数多次调用 addEventListener 不会导致重复注册,所以用set。 - iframeWindow.__WUJIE_EVENTLISTENER__.add({ type, listener, options }); - const isConfigTargetWindow = typeof options === "object" && options.targetWindow; - - // 处理子应用循环嵌套iframe场景的 window 指向问题 - if (appWindowAddEventListenerEvents.includes(type) || isConfigTargetWindow) { - if (type === "message") { - debugger; - } - const targetWindow = isConfigTargetWindow ? options?.targetWindow : window.__WUJIE_RAW_WINDOW__ || window; - return rawWindowAddEventListener.call(targetWindow, type, listener, options); - } - // 在子应用嵌套场景使用window.window获取真实window - rawWindowAddEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options); - }; - - iframeWindow.removeEventListener = function removeEventListener( - type: K, - listener: (this: Window, ev: WindowEventMap[K]) => any, - options?: boolean | appAddEventListenerOptions - ) { - // 运行插件钩子函数 - execHooks(iframeWindow.__WUJIE.plugins, "windowRemoveEventListenerHook", iframeWindow, type, listener, options); - iframeWindow.__WUJIE_EVENTLISTENER__.forEach((o) => { - // 这里严格一点,确保子应用销毁的时候都能销毁 - if (o.listener === listener && o.type === type && options == o.options) { - iframeWindow.__WUJIE_EVENTLISTENER__.delete(o); - } - }); - const isConfigTargetWindow = typeof options === "object" && options.targetWindow; - if (appWindowAddEventListenerEvents.includes(type) || isConfigTargetWindow) { - const targetWindow = isConfigTargetWindow ? options?.targetWindow : window.__WUJIE_RAW_WINDOW__ || window; - return rawWindowRemoveEventListener.call(targetWindow, type, listener, options); - } - rawWindowRemoveEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options); - }; -} - -function patchIframeVariable(iframeWindow: Window, wujie: WuJie, appHostPath: string): void { - iframeWindow.__WUJIE = wujie; - iframeWindow.__WUJIE_PUBLIC_PATH__ = appHostPath + "/"; - iframeWindow.$wujie = wujie.provide; - iframeWindow.__WUJIE_RAW_WINDOW__ = iframeWindow; -} - -/** - * 对iframe的history的pushState和replaceState进行修改 - * 将从location劫持后的数据修改回来,防止跨域错误 - * 同步路由到主应用 - * @param iframeWindow - * @param appHostPath 子应用的 host path - * @param mainHostPath 主应用的 host path - */ -function patchIframeHistory(iframeWindow: Window, appHostPath: string, mainHostPath: string): void { - const history = iframeWindow.history; - const rawHistoryPushState = history.pushState; - const rawHistoryReplaceState = history.replaceState; - history.pushState = function (data: any, title: string, url?: string): void { - const baseUrl = - mainHostPath + iframeWindow.location.pathname + iframeWindow.location.search + iframeWindow.location.hash; - const mainUrl = getAbsolutePath(url?.replace(appHostPath, ""), baseUrl); - const ignoreFlag = url === undefined; - - rawHistoryPushState.call(history, data, title, ignoreFlag ? undefined : mainUrl); - if (ignoreFlag) return; - updateBase(iframeWindow, appHostPath, mainHostPath); - syncUrlToWindow(iframeWindow); - }; - history.replaceState = function (data: any, title: string, url?: string): void { - const baseUrl = - mainHostPath + iframeWindow.location.pathname + iframeWindow.location.search + iframeWindow.location.hash; - const mainUrl = getAbsolutePath(url?.replace(appHostPath, ""), baseUrl); - const ignoreFlag = url === undefined; - - rawHistoryReplaceState.call(history, data, title, ignoreFlag ? undefined : mainUrl); - if (ignoreFlag) return; - updateBase(iframeWindow, appHostPath, mainHostPath); - syncUrlToWindow(iframeWindow); - }; -} - -/** - * 动态的修改iframe的base地址 - * @param iframeWindow - * @param appHostPath - * @param mainHostPath - */ -function updateBase(iframeWindow: Window, appHostPath: string, mainHostPath: string) { - const baseUrl = new URL(iframeWindow.location.href?.replace(mainHostPath, ""), appHostPath); - const baseElement = rawDocumentQuerySelector.call(iframeWindow.document, "base"); - if (baseElement) baseElement.setAttribute("href", appHostPath + baseUrl.pathname); -} - -/** - * patch iframe window effect - * @param iframeWindow - */ -// TODO 继续改进 -function patchWindowEffect(iframeWindow: Window): void { - // 属性处理函数 - function processWindowProperty(key: string): boolean { - const value = iframeWindow[key]; - try { - if (typeof value === "function" && !isConstructable(value)) { - iframeWindow[key] = window[key].bind(window); - } else { - iframeWindow[key] = window[key]; - } - return true; - } catch (e) { - warn(e.message); - return false; - } - } - Object.getOwnPropertyNames(iframeWindow).forEach((key) => { - // 特殊处理 - if (key === "getSelection") { - Object.defineProperty(iframeWindow, key, { - get: () => iframeWindow.document[key], - }); - return; - } - // 单独属性 - if (windowProxyProperties.includes(key)) { - processWindowProperty(key); - return; - } - // 正则匹配,可以一次处理多个 - windowRegWhiteList.some((reg) => { - if (reg.test(key) && key in iframeWindow.parent) { - return processWindowProperty(key); - } - return false; - }); - }); - // onEvent set - const windowOnEvents = Object.getOwnPropertyNames(window) - .filter((p) => /^on/.test(p)) - .filter((e) => !appWindowOnEvent.includes(e)); - - // 走主应用window - windowOnEvents.forEach((e) => { - const descriptor = Object.getOwnPropertyDescriptor(iframeWindow, e) || { - enumerable: true, - writable: true, - }; - try { - Object.defineProperty(iframeWindow, e, { - enumerable: descriptor.enumerable, - configurable: true, - get: () => window[e], - set: - descriptor.writable || descriptor.set - ? (handler) => { - window[e] = typeof handler === "function" ? handler.bind(iframeWindow) : handler; - } - : undefined, - }); - } catch (e) { - warn(e.message); - } - }); - // 运行插件钩子函数 - execHooks(iframeWindow.__WUJIE.plugins, "windowPropertyOverride", iframeWindow); -} - -/** - * 记录节点的监听事件 - */ -function recordEventListeners(iframeWindow: Window) { - const sandbox = iframeWindow.__WUJIE; - iframeWindow.Node.prototype.addEventListener = function ( - type: string, - handler: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ): void { - // 添加事件缓存 - const elementListenerList = sandbox.elementEventCacheMap.get(this); - if (elementListenerList) { - if (!elementListenerList.find((listener) => listener.type === type && listener.handler === handler)) { - elementListenerList.push({ type, handler, options }); - } - } else sandbox.elementEventCacheMap.set(this, [{ type, handler, options }]); - return rawAddEventListener.call(this, type, handler, options); - }; - - iframeWindow.Node.prototype.removeEventListener = function ( - type: string, - handler: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions - ): void { - // 清除缓存 - const elementListenerList = sandbox.elementEventCacheMap.get(this); - if (elementListenerList) { - const index = elementListenerList?.findIndex((ele) => ele.type === type && ele.handler === handler); - elementListenerList.splice(index, 1); - } - if (!elementListenerList?.length) { - sandbox.elementEventCacheMap.delete(this); - } - return rawRemoveEventListener.call(this, type, handler, options); - }; -} - -/** - * 恢复节点的监听事件 - */ -export function recoverEventListeners(rootElement: Element | ChildNode, iframeWindow: Window) { - const sandbox = iframeWindow.__WUJIE; - const elementEventCacheMap: WeakMap< - Node, - Array<{ type: string; handler: EventListenerOrEventListenerObject; options: any }> - > = new WeakMap(); - const ElementIterator = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT, null, false); - let nextElement = ElementIterator.currentNode; - while (nextElement) { - const elementListenerList = sandbox.elementEventCacheMap.get(nextElement); - if (elementListenerList?.length) { - elementEventCacheMap.set(nextElement, elementListenerList); - elementListenerList.forEach((listener) => { - nextElement.addEventListener(listener.type, listener.handler, listener.options); - }); - } - nextElement = ElementIterator.nextNode() as HTMLElement; - } - sandbox.elementEventCacheMap = elementEventCacheMap; -} - -/** - * 恢复根节点的监听事件 - */ -export function recoverDocumentListeners( - oldRootElement: Element | ChildNode, - newRootElement: Element | ChildNode, - iframeWindow: Window -) { - const sandbox = iframeWindow.__WUJIE; - const elementEventCacheMap: WeakMap< - Node, - Array<{ type: string; handler: EventListenerOrEventListenerObject; options: any }> - > = new WeakMap(); - const elementListenerList = sandbox.elementEventCacheMap.get(oldRootElement); - if (elementListenerList?.length) { - elementEventCacheMap.set(newRootElement, elementListenerList); - elementListenerList.forEach((listener) => { - newRootElement.addEventListener(listener.type, listener.handler, listener.options); - }); - } - sandbox.elementEventCacheMap = elementEventCacheMap; -} - -/** - * 修复vue绑定事件e.timeStamp < attachedTimestamp 的情况 - */ -export function patchEventTimeStamp(targetWindow: Window, iframeWindow: Window) { - Object.defineProperty(targetWindow.Event.prototype, "timeStamp", { - get: () => { - return iframeWindow.document.createEvent("Event").timeStamp; - }, - }); -} - -/** - * patch document effect - * @param iframeWindow - */ -// TODO 继续改进 -function patchDocumentEffect(iframeWindow: Window): void { - const sandbox = iframeWindow.__WUJIE; - - /** - * 处理 addEventListener和removeEventListener - * 由于这个劫持导致 handler 的this发生改变,所以需要handler.bind(document) - * 但是这样会导致removeEventListener无法正常工作,因为handler => handler.bind(document) - * 这个地方保存callback = handler.bind(document) 方便removeEventListener - */ - const handlerCallbackMap: WeakMap = - new WeakMap(); - const handlerTypeMap: WeakMap> = new WeakMap(); - iframeWindow.Document.prototype.addEventListener = function ( - type: string, - handler: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ): void { - if (!handler) return; - let callback = handlerCallbackMap.get(handler); - const typeList = handlerTypeMap.get(handler); - // 设置 handlerCallbackMap - if (!callback) { - callback = typeof handler === "function" ? handler.bind(this) : handler; - handlerCallbackMap.set(handler, callback); - } - // 设置 handlerTypeMap - if (typeList) { - if (!typeList.includes(type)) typeList.push(type); - } else { - handlerTypeMap.set(handler, [type]); - } - - // 运行插件钩子函数 - execHooks(iframeWindow.__WUJIE.plugins, "documentAddEventListenerHook", iframeWindow, type, callback, options); - if (appDocumentAddEventListenerEvents.includes(type)) { - return rawAddEventListener.call(this, type, callback, options); - } - // 降级统一走 sandbox.document - if (sandbox.degrade) return sandbox.document.addEventListener(type, callback, options); - if (mainDocumentAddEventListenerEvents.includes(type)) - return window.document.addEventListener(type, callback, options); - if (mainAndAppAddEventListenerEvents.includes(type)) { - window.document.addEventListener(type, callback, options); - sandbox.shadowRoot.addEventListener(type, callback, options); - return; - } - sandbox.shadowRoot.addEventListener(type, callback, options); - }; - iframeWindow.Document.prototype.removeEventListener = function ( - type: string, - handler: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ): void { - const callback: EventListenerOrEventListenerObject = handlerCallbackMap.get(handler); - const typeList = handlerTypeMap.get(handler); - if (callback) { - if (typeList?.includes(type)) { - typeList.splice(typeList.indexOf(type), 1); - if (!typeList.length) { - handlerCallbackMap.delete(handler); - handlerTypeMap.delete(handler); - } - } - - // 运行插件钩子函数 - execHooks(iframeWindow.__WUJIE.plugins, "documentRemoveEventListenerHook", iframeWindow, type, callback, options); - if (appDocumentAddEventListenerEvents.includes(type)) { - return rawRemoveEventListener.call(this, type, callback, options); - } - if (sandbox.degrade) return sandbox.document.removeEventListener(type, callback, options); - if (mainDocumentAddEventListenerEvents.includes(type)) { - return window.document.removeEventListener(type, callback, options); - } - if (mainAndAppAddEventListenerEvents.includes(type)) { - window.document.removeEventListener(type, callback, options); - sandbox.shadowRoot.removeEventListener(type, callback, options); - return; - } - sandbox.shadowRoot.removeEventListener(type, callback, options); - } - }; - // 处理onEvent - const elementOnEvents = Object.keys(iframeWindow.HTMLElement.prototype).filter((ele) => /^on/.test(ele)); - const documentOnEvent = Object.keys(iframeWindow.Document.prototype) - .filter((ele) => /^on/.test(ele)) - .filter((ele) => !appDocumentOnEvents.includes(ele)); - elementOnEvents - .filter((e) => documentOnEvent.includes(e)) - .forEach((e) => { - const descriptor = Object.getOwnPropertyDescriptor(iframeWindow.Document.prototype, e) || { - enumerable: true, - writable: true, - }; - try { - Object.defineProperty(iframeWindow.Document.prototype, e, { - enumerable: descriptor.enumerable, - configurable: true, - get: () => (sandbox.degrade ? sandbox.document[e] : sandbox.shadowRoot.firstElementChild[e]), - set: - descriptor.writable || descriptor.set - ? (handler) => { - const val = typeof handler === "function" ? handler.bind(iframeWindow.document) : handler; - sandbox.degrade ? (sandbox.document[e] = val) : (sandbox.shadowRoot.firstElementChild[e] = val); - } - : undefined, - }); - } catch (e) { - warn(e.message); - } - }); - // 处理属性get - const { - ownerProperties, - modifyProperties, - shadowProperties, - shadowMethods, - documentProperties, - documentMethods, - documentEvents, - } = documentProxyProperties; - modifyProperties.concat(shadowProperties, shadowMethods, documentProperties, documentMethods).forEach((propKey) => { - const descriptor = Object.getOwnPropertyDescriptor(iframeWindow.Document.prototype, propKey) || { - enumerable: true, - writable: true, - }; - try { - Object.defineProperty(iframeWindow.Document.prototype, propKey, { - enumerable: descriptor.enumerable, - configurable: true, - get: () => sandbox.proxyDocument[propKey], - set: undefined, - }); - } catch (e) { - warn(e.message); - } - }); - // 处理document专属事件 - // TODO 内存泄露 - documentEvents.forEach((propKey) => { - const descriptor = Object.getOwnPropertyDescriptor(iframeWindow.Document.prototype, propKey) || { - enumerable: true, - writable: true, - }; - try { - Object.defineProperty(iframeWindow.Document.prototype, propKey, { - enumerable: descriptor.enumerable, - configurable: true, - get: () => (sandbox.degrade ? sandbox : window).document[propKey], - set: - descriptor.writable || descriptor.set - ? (handler) => { - (sandbox.degrade ? sandbox : window).document[propKey] = - typeof handler === "function" ? handler.bind(iframeWindow.document) : handler; - } - : undefined, - }); - } catch (e) { - warn(e.message); - } - }); - // process owner property - ownerProperties.forEach((propKey) => { - Object.defineProperty(iframeWindow.document, propKey, { - enumerable: true, - configurable: true, - get: () => sandbox.proxyDocument[propKey], - set: undefined, - }); - }); - // 运行插件钩子函数 - execHooks(iframeWindow.__WUJIE.plugins, "documentPropertyOverride", iframeWindow); -} - -/** - * patch Node effect - * 1、处理 getRootNode - * 2、处理 appendChild、insertBefore,当插入的节点为 svg 时,createElement 的 patch 会被去除,需要重新 patch - * @param iframeWindow - */ -function patchNodeEffect(iframeWindow: Window): void { - const rawGetRootNode = iframeWindow.Node.prototype.getRootNode; - const rawAppendChild = iframeWindow.Node.prototype.appendChild; - const rawInsertRule = iframeWindow.Node.prototype.insertBefore; - iframeWindow.Node.prototype.getRootNode = function (options?: GetRootNodeOptions): Node { - const rootNode = rawGetRootNode.call(this, options); - if (rootNode === iframeWindow.__WUJIE.shadowRoot) return iframeWindow.document; - else return rootNode; - }; - iframeWindow.Node.prototype.appendChild = function (node: T): T { - const res = rawAppendChild.call(this, node); - patchElementEffect(node, iframeWindow); - return res; - }; - iframeWindow.Node.prototype.insertBefore = function (node: T, child: Node | null): T { - const res = rawInsertRule.call(this, node, child); - patchElementEffect(node, iframeWindow); - return res; - }; -} - -/** - * 修复资源元素的相对路径问题 - * @param iframeWindow - */ -function patchRelativeUrlEffect(iframeWindow: Window): void { - fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLImageElement, "src"); - fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLAnchorElement, "href"); - fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLSourceElement, "src"); - fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLLinkElement, "href"); - fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLScriptElement, "src"); - fixElementCtrSrcOrHref(iframeWindow, iframeWindow.HTMLMediaElement, "src"); -} - -/** - * 初始化base标签 - */ -export function initBase(iframeWindow: Window, url: string): void { - const iframeDocument = iframeWindow.document; - const baseElement = iframeDocument.createElement("base"); - const iframeUrlElement = anchorElementGenerator(iframeWindow.location.href); - const appUrlElement = anchorElementGenerator(url); - baseElement.setAttribute("href", appUrlElement.protocol + "//" + appUrlElement.host + iframeUrlElement.pathname); - iframeDocument.head.appendChild(baseElement); -} - -/** - * 初始化iframe的dom结构 - * @param iframeWindow - * @param wujie - * @param mainHostPath - * @param appHostPath - */ -function initIframeDom(iframeWindow: Window, wujie: WuJie, mainHostPath: string, appHostPath: string): void { - const iframeDocument = iframeWindow.document; - const newDoc = window.document.implementation.createHTMLDocument(""); - const newDocumentElement = iframeDocument.importNode(newDoc.documentElement, true); - iframeDocument.documentElement - ? iframeDocument.replaceChild(newDocumentElement, iframeDocument.documentElement) - : iframeDocument.appendChild(newDocumentElement); - iframeWindow.__WUJIE_RAW_DOCUMENT_HEAD__ = iframeDocument.head; - iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__ = iframeWindow.Document.prototype.querySelector; - iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__ = iframeWindow.Document.prototype.querySelectorAll; - iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__ = iframeWindow.Document.prototype.createElement; - iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__ = iframeWindow.Document.prototype.createTextNode; - initBase(iframeWindow, wujie.url); - patchIframeHistory(iframeWindow, appHostPath, mainHostPath); - patchIframeEvents(iframeWindow); - if (wujie.degrade) recordEventListeners(iframeWindow); - syncIframeUrlToWindow(iframeWindow); - - patchWindowEffect(iframeWindow); - patchDocumentEffect(iframeWindow); - patchNodeEffect(iframeWindow); - patchRelativeUrlEffect(iframeWindow); -} - -/** - * 防止运行主应用的js代码,给子应用带来很多副作用 - */ -// TODO 更加准确抓取停止时机 -function stopIframeLoading(iframeWindow: Window) { - const oldDoc = iframeWindow.document; - return new Promise((resolve) => { - function loop() { - setTimeout(() => { - let newDoc; - try { - newDoc = iframeWindow.document; - } catch (err) { - newDoc = null; - } - // wait for document ready - if (!newDoc || newDoc == oldDoc) { - loop(); - } else { - iframeWindow.stop ? iframeWindow.stop() : iframeWindow.document.execCommand("Stop"); - resolve(); - } - }, 1); - } - loop(); - }); -} - -export function patchElementEffect( - element: (HTMLElement | Node | ShadowRoot) & { _hasPatch?: boolean }, - iframeWindow: Window -): void { - const proxyLocation = iframeWindow.__WUJIE.proxyLocation as Location; - if (element._hasPatch) return; - try { - Object.defineProperties(element, { - baseURI: { - configurable: true, - get: () => proxyLocation.protocol + "//" + proxyLocation.host + proxyLocation.pathname, - set: undefined, - }, - ownerDocument: { - configurable: true, - get: () => iframeWindow.document, - }, - _hasPatch: { get: () => true }, - }); - } catch (error) { - console.warn(error); - } - execHooks(iframeWindow.__WUJIE.plugins, "patchElementHook", element, iframeWindow); -} - -/** - * 子应用前进后退,同步路由到主应用 - * @param iframeWindow - */ -export function syncIframeUrlToWindow(iframeWindow: Window): void { - iframeWindow.addEventListener("hashchange", () => syncUrlToWindow(iframeWindow)); - iframeWindow.addEventListener("popstate", () => { - syncUrlToWindow(iframeWindow); - }); -} - -/** - * iframe插入脚本 - * @param scriptResult script请求结果 - * @param iframeWindow - * @param rawElement 原始的脚本 - */ -export function insertScriptToIframe( - scriptResult: ScriptObject | ScriptObjectLoader, - iframeWindow: Window, - rawElement?: HTMLScriptElement -) { - const { src, module, content, crossorigin, crossoriginType, async, attrs, callback, onload } = - scriptResult as ScriptObjectLoader; - const scriptElement = iframeWindow.document.createElement("script"); - const nextScriptElement = iframeWindow.document.createElement("script"); - const { replace, plugins, proxyLocation } = iframeWindow.__WUJIE; - const jsLoader = getJsLoader({ plugins, replace }); - let code = jsLoader(content, src, getCurUrl(proxyLocation)); - // 添加属性 - attrs && - Object.keys(attrs) - .filter((key) => !Object.keys(scriptResult).includes(key)) - .forEach((key) => scriptElement.setAttribute(key, String(attrs[key]))); - - // 内联脚本 - if (content) { - // patch location - if (!iframeWindow.__WUJIE.degrade && !module) { - code = `(function(window, self, global, location) { - ${code} -}).bind(window.__WUJIE.proxy)( - window.__WUJIE.proxy, - window.__WUJIE.proxy, - window.__WUJIE.proxy, - window.__WUJIE.proxyLocation, -);`; - } - const descriptor = Object.getOwnPropertyDescriptor(scriptElement, "src"); - // 部分浏览器 src 不可配置 取不到descriptor表示无该属性,可写 - if (descriptor?.configurable || !descriptor) { - // 解决 webpack publicPath 为 auto 无法加载资源的问题 - try { - Object.defineProperty(scriptElement, "src", { get: () => src || "" }); - } catch (error) { - console.warn(error); - } - } - } else { - src && scriptElement.setAttribute("src", src); - crossorigin && scriptElement.setAttribute("crossorigin", crossoriginType); - } - module && scriptElement.setAttribute("type", "module"); - scriptElement.textContent = code || ""; - nextScriptElement.textContent = - "if(window.__WUJIE.execQueue && window.__WUJIE.execQueue.length){ window.__WUJIE.execQueue.shift()()}"; - - const container = rawDocumentQuerySelector.call(iframeWindow.document, "head"); - const execNextScript = () => !async && container.appendChild(nextScriptElement); - const afterExecScript = () => { - onload?.(); - execNextScript(); - }; - - // 错误情况处理 - if (/^ { - if (!iframeWindow.__WUJIE) { - patchIframeVariable(iframeWindow, sandbox, appHostPath); - } - initIframeDom(iframeWindow, sandbox, mainHostPath, appHostPath); - /** - * 如果有同步优先同步,非同步从url读取 - */ - if (!isMatchSyncQueryById(iframeWindow.__WUJIE.id)) { - iframeWindow.history.replaceState(null, "", mainHostPath + appRoutePath); - } - }); - return iframe; -}