无界微前端源码解析:JS 隔离
深入分析 Proxy 代理 window、document、location 的实现原理。
代理架构
无界通过 Proxy 将 iframe 中的 DOM 操作代理到 Shadow DOM:
┌─────────────────────────────────────────────────────────┐
│ iframe (JS 运行) │
│ │
│ window.xxx → proxyWindow.xxx │
│ document.xxx → proxyDocument.xxx → shadowRoot │
│ location.xxx → proxyLocation.xxx │
│ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Shadow DOM (DOM 渲染) │
│ │
│ 实际的 DOM 操作在这里执行 │
│ │
└─────────────────────────────────────────────────────────┘
proxyWindow
// packages/wujie-core/src/proxy.ts
const proxyWindow = new Proxy(iframe.contentWindow, {
get: (target: Window, p: PropertyKey): any => {
// 1. location 劫持
if (p === "location") {
return target.__WUJIE.proxyLocation;
}
// 2. self/window 返回代理
if (p === "self" || (p === "window" && Object.getOwnPropertyDescriptor(window, "window").get)) {
return target.__WUJIE.proxy;
}
// 3. 保留原生方法
if (p === "__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__" || p === "__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__") {
return target[p];
}
// 4. 不可配置且不可写的属性直接返回
const descriptor = Object.getOwnPropertyDescriptor(target, p);
if (descriptor?.configurable === false && descriptor?.writable === false) {
return target[p];
}
// 5. 修正 this 指向
return getTargetValue(target, p);
},
set: (target: Window, p: PropertyKey, value: any) => {
checkProxyFunction(target, value);
target[p] = value;
return true;
},
has: (target: Window, p: PropertyKey) => p in target,
});
getTargetValue 处理函数绑定:
// packages/wujie-core/src/utils.ts
export function getTargetValue(target: any, p: PropertyKey): any {
const value = target[p];
// 函数需要绑定正确的 this
if (typeof value === "function" && !isConstructable(value)) {
const bindTarget = value.name.startsWith("bound ") ? target : target;
return value.bind(bindTarget);
}
return value;
}
proxyDocument
// packages/wujie-core/src/proxy.ts
const proxyDocument = new Proxy({}, {
get: function (_fakeDocument, propKey) {
const document = window.document;
const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
// iframe 初始化完成但 webcomponent 未挂载,中止执行
if (!shadowRoot) stopMainAppRun();
const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
// 1. createElement / createTextNode
if (propKey === "createElement" || propKey === "createTextNode") {
return new Proxy(document[propKey], {
apply(_createElement, _ctx, args) {
const rawCreateMethod = propKey === "createElement" ? rawCreateElement : rawCreateTextNode;
const element = rawCreateMethod.apply(iframe.contentDocument, args);
patchElementEffect(element, iframe.contentWindow);
return element;
},
});
}
// 2. documentURI / URL
if (propKey === "documentURI" || propKey === "URL") {
return (proxyLocation as Location).href;
}
// 3. getElementsByTagName / getElementsByClassName / getElementsByName
if (propKey === "getElementsByTagName" || propKey === "getElementsByClassName" || propKey === "getElementsByName") {
return new Proxy(shadowRoot.querySelectorAll, {
apply(querySelectorAll, _ctx, args) {
let arg = args[0];
if (_ctx !== iframe.contentDocument) {
return _ctx[propKey].apply(_ctx, args);
}
// script 标签从 iframe 获取
if (propKey === "getElementsByTagName" && arg === "script") {
return iframe.contentDocument.scripts;
}
// 转换选择器
if (propKey === "getElementsByClassName") arg = "." + arg;
if (propKey === "getElementsByName") arg = `[name="${arg}"]`;
return querySelectorAll.call(shadowRoot, arg);
},
});
}
// 4. getElementById
if (propKey === "getElementById") {
return new Proxy(shadowRoot.querySelector, {
apply(target, ctx, args) {
if (ctx !== iframe.contentDocument) {
return ctx[propKey]?.apply(ctx, args);
}
// 先从 shadowRoot 查找,再从 iframe 查找
return (
target.call(shadowRoot, `[id="${args[0]}"]`) ||
iframe.contentWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__.call(
iframe.contentWindow.document,
`#${args[0]}`
)
);
},
});
}
// 5. querySelector / querySelectorAll
if (propKey === "querySelector" || propKey === "querySelectorAll") {
return new Proxy(shadowRoot[propKey], {
apply(target, ctx, args) {
if (ctx !== iframe.contentDocument) {
return ctx[propKey]?.apply(ctx, args);
}
// 优先 shadowRoot,其次 iframe(排除 base)
return (
target.apply(shadowRoot, args) ||
(args[0] === "base" ? null : iframe.contentWindow[rawPropMap[propKey]].call(iframe.contentWindow.document, args[0]))
);
},
});
}
// 6. documentElement / scrollingElement
if (propKey === "documentElement" || propKey === "scrollingElement") {
return shadowRoot.firstElementChild;
}
// 7. forms / images / links
if (propKey === "forms") return shadowRoot.querySelectorAll("form");
if (propKey === "images") return shadowRoot.querySelectorAll("img");
if (propKey === "links") return shadowRoot.querySelectorAll("a");
// 8. 其他属性分类处理
const { ownerProperties, shadowProperties, shadowMethods, documentProperties, documentMethods } =
documentProxyProperties;
// 从 shadowRoot 获取
if (ownerProperties.concat(shadowProperties).includes(propKey.toString())) {
if (propKey === "activeElement" && shadowRoot.activeElement === null) {
return shadowRoot.body;
}
return shadowRoot[propKey];
}
// shadowRoot 方法
if (shadowMethods.includes(propKey.toString())) {
return getTargetValue(shadowRoot, propKey) ?? getTargetValue(document, propKey);
}
// 从 window.document 获取
if (documentProperties.includes(propKey.toString())) {
return document[propKey];
}
if (documentMethods.includes(propKey.toString())) {
return getTargetValue(document, propKey);
}
},
});
proxyLocation
// packages/wujie-core/src/proxy.ts
const proxyLocation = new Proxy({}, {
get: function (_fakeLocation, propKey) {
const location = iframe.contentWindow.location;
// 1. 域名相关属性从 urlElement 获取
if (propKey === "host" || propKey === "hostname" || propKey === "protocol" ||
propKey === "port" || propKey === "origin") {
return urlElement[propKey];
}
// 2. href 需要替换域名
if (propKey === "href") {
return location[propKey].replace(mainHostPath, appHostPath);
}
// 3. 禁用 reload
if (propKey === "reload") {
warn(WUJIE_TIPS_RELOAD_DISABLED);
return () => null;
}
// 4. replace 需要替换路径
if (propKey === "replace") {
return new Proxy(location[propKey], {
apply(replace, _ctx, args) {
return replace.call(location, args[0]?.replace(appHostPath, mainHostPath));
},
});
}
return getTargetValue(location, propKey);
},
set: function (_fakeLocation, propKey, value) {
// href 跳转需要特殊处理
if (propKey === "href") {
return locationHrefSet(iframe, value, appHostPath);
}
iframe.contentWindow.location[propKey] = value;
return true;
},
ownKeys: function () {
return Object.keys(iframe.contentWindow.location).filter((key) => key !== "reload");
},
});
location.href 跳转处理
// packages/wujie-core/src/proxy.ts
function locationHrefSet(iframe: HTMLIFrameElement, value: string, appHostPath: string): boolean {
const { shadowRoot, id, degrade, document, degradeAttrs } = iframe.contentWindow.__WUJIE;
// 处理相对路径
let url = value;
if (!/^http/.test(url)) {
let hrefElement = anchorElementGenerator(url);
url = appHostPath + hrefElement.pathname + hrefElement.search + hrefElement.hash;
}
// 标记 href 跳转
iframe.contentWindow.__WUJIE.hrefFlag = true;
// 重新渲染
if (degrade) {
const iframeBody = rawDocumentQuerySelector.call(iframe.contentDocument, "body");
renderElementToContainer(document.documentElement, iframeBody);
renderIframeReplaceApp(window.decodeURIComponent(url), getDegradeIframe(id).parentElement, degradeAttrs);
} else {
renderIframeReplaceApp(url, shadowRoot.host.parentElement, degradeAttrs);
}
// 同步路由
pushUrlToWindow(id, url);
return true;
}
降级模式代理
不支持 Proxy 时使用简单对象代理:
// packages/wujie-core/src/proxy.ts
export function localGenerator(
iframe: HTMLIFrameElement,
urlElement: HTMLAnchorElement,
mainHostPath: string,
appHostPath: string
): { proxyDocument: Object; proxyLocation: Object } {
const proxyDocument = {};
const sandbox = iframe.contentWindow.__WUJIE;
// 特殊处理
Object.defineProperties(proxyDocument, {
createElement: {
get: () => function (...args) {
const element = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__.apply(
iframe.contentDocument, args
);
patchElementEffect(element, iframe.contentWindow);
return element;
},
},
getElementById: {
get() {
return function (...args) {
const id = args[0];
return (
sandbox.document.getElementById(id) ||
iframe.contentWindow.__WUJIE_RAW_DOCUMENT_HEAD__.querySelector(`#${id}`)
);
};
},
},
// ...其他属性
});
// 代理 location
const proxyLocation = {};
const location = iframe.contentWindow.location;
// 常量属性
["host", "hostname", "port", "protocol"].forEach((key) => {
proxyLocation[key] = urlElement[key];
});
Object.defineProperties(proxyLocation, {
href: {
get: () => location.href.replace(mainHostPath, appHostPath),
set: (value) => locationHrefSet(iframe, value, appHostPath),
},
reload: {
get() {
warn(WUJIE_TIPS_RELOAD_DISABLED);
return () => null;
},
},
});
return { proxyDocument, proxyLocation };
}
Document 属性分类
// packages/wujie-core/src/common.ts
export const documentProxyProperties = {
// 从 shadowRoot 获取的属性
ownerProperties: ["head", "body"],
shadowProperties: [
"activeElement", "childElementCount", "children", "firstElementChild",
"lastElementChild", "pictureInPictureElement", "pointerLockElement",
"styleSheets",
],
shadowMethods: [
"append", "contains", "getSelection", "elementFromPoint",
"elementsFromPoint", "getAnimations", "prepend", "replaceChildren",
],
// 从 window.document 获取的属性
documentProperties: [
"characterSet", "compatMode", "contentType", "cookie", "currentScript",
"defaultView", "designMode", "dir", "doctype", "domain", "fullscreen",
"fullscreenEnabled", "hidden", "implementation", "lastModified",
"readyState", "referrer", "title", "visibilityState",
],
documentMethods: [
"adoptNode", "close", "createAttribute", "createComment",
"createDocumentFragment", "createEvent", "createExpression",
"createNodeIterator", "createNSResolver", "createProcessingInstruction",
"createRange", "createTreeWalker", "evaluate", "execCommand",
"exitFullscreen", "exitPictureInPicture", "exitPointerLock",
"getSelection", "hasFocus", "importNode", "open", "queryCommandEnabled",
"queryCommandState", "queryCommandSupported", "write", "writeln",
],
};
元素 patch
// packages/wujie-core/src/iframe.ts
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 返回代理的 location
baseURI: {
configurable: true,
get: () => proxyLocation.protocol + "//" + proxyLocation.host + proxyLocation.pathname,
},
// ownerDocument 返回 iframe 的 document
ownerDocument: {
configurable: true,
get: () => iframeWindow.document,
},
_hasPatch: { get: () => true },
});
} catch (error) {
console.warn(error);
}
// 运行插件钩子
execHooks(iframeWindow.__WUJIE.plugins, "patchElementHook", element, iframeWindow);
}
小结
无界的 JS 隔离通过三层代理实现:
| 代理对象 | 作用 | 关键处理 |
|---|---|---|
| proxyWindow | 代理 window | location 劫持、self/window 返回代理 |
| proxyDocument | 代理 document | DOM 查询代理到 shadowRoot |
| proxyLocation | 代理 location | 域名替换、href 跳转处理 |
核心技巧:
- IIFE 包装:脚本执行时替换全局变量
- 属性分类:不同属性从不同来源获取
- 元素 patch:修正 baseURI 和 ownerDocument
- 降级方案:不支持 Proxy 时使用 defineProperty
下一篇我们将分析路由同步机制。
📦 源码版本:wujie v1.0.22
上一篇:CSS 隔离
下一篇:路由同步