无界微前端源码解析:JS 隔离

38 阅读2分钟

无界微前端源码解析: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代理 windowlocation 劫持、self/window 返回代理
proxyDocument代理 documentDOM 查询代理到 shadowRoot
proxyLocation代理 location域名替换、href 跳转处理

核心技巧:

  1. IIFE 包装:脚本执行时替换全局变量
  2. 属性分类:不同属性从不同来源获取
  3. 元素 patch:修正 baseURI 和 ownerDocument
  4. 降级方案:不支持 Proxy 时使用 defineProperty

下一篇我们将分析路由同步机制。


📦 源码版本:wujie v1.0.22

上一篇:CSS 隔离

下一篇:路由同步