无界微前端源码解析:路由同步

无界微前端源码解析:路由同步

深入分析主子应用路由同步机制,理解 sync 模式的实现原理。

路由同步原理

无界通过 URL query 参数实现主子应用路由同步:

javascript 复制代码
主应用 URL:
https://main.com/home?vue3=%2Fuser%2F123

子应用实际路由:
/user/123

同步流程

bash 复制代码
┌─────────────────┐     pushState/replaceState     ┌─────────────────┐
│                 │ ─────────────────────────────► │                 │
│   子应用路由     │                                │   主应用 URL    │
│                 │ ◄───────────────────────────── │                 │
└─────────────────┘     浏览器刷新/前进后退         └─────────────────┘

子应用 → 主应用

typescript 复制代码
// packages/wujie-core/src/sync.ts
export function syncUrlToWindow(iframeWindow: Window): void {
  const { sync, id, prefix } = iframeWindow.__WUJIE;
  
  // 解析主应用 URL
  let winUrlElement = anchorElementGenerator(window.location.href);
  const queryMap = getAnchorElementQueryMap(winUrlElement);
  
  // 非同步模式且 URL 上没有当前 id 的参数,直接返回
  if (!sync && !queryMap[id]) return (winUrlElement = null);
  
  // 获取子应用当前路由
  const curUrl = iframeWindow.location.pathname + 
                 iframeWindow.location.search + 
                 iframeWindow.location.hash;
  
  // 处理短路径
  let validShortPath = "";
  if (prefix) {
    Object.keys(prefix).forEach((shortPath) => {
      const longPath = prefix[shortPath];
      // 找出最长匹配路径
      if (curUrl.startsWith(longPath) && 
          (!validShortPath || longPath.length > prefix[validShortPath].length)) {
        validShortPath = shortPath;
      }
    });
  }
  
  // 同步模式:更新参数
  if (sync) {
    queryMap[id] = window.encodeURIComponent(
      validShortPath 
        ? curUrl.replace(prefix[validShortPath], `{${validShortPath}}`) 
        : curUrl
    );
  } else {
    // 非同步模式:清理参数
    delete queryMap[id];
  }
  
  // 构建新 URL
  const newQuery = "?" + Object.keys(queryMap)
    .map((key) => key + "=" + queryMap[key])
    .join("&");
  winUrlElement.search = newQuery;
  
  // 更新主应用 URL
  if (winUrlElement.href !== window.location.href) {
    window.history.replaceState(null, "", winUrlElement.href);
  }
  winUrlElement = null;
}

主应用 → 子应用

typescript 复制代码
// packages/wujie-core/src/sync.ts
export function syncUrlToIframe(iframeWindow: Window): void {
  const { pathname, search, hash } = iframeWindow.location;
  const { id, url, sync, execFlag, prefix, inject } = iframeWindow.__WUJIE;

  // 只在浏览器刷新或第一次渲染时同步
  const idUrl = sync && !execFlag ? getSyncUrl(id, prefix) : url;
  
  // 排除 href 跳转情况
  const syncUrl = (/^http/.test(idUrl) ? null : idUrl) || url;
  const { appRoutePath } = appRouteParse(syncUrl);

  const preAppRoutePath = pathname + search + hash;
  
  // 路由不同则同步
  if (preAppRoutePath !== appRoutePath) {
    iframeWindow.history.replaceState(null, "", inject.mainHostPath + appRoutePath);
  }
}

获取同步 URL

typescript 复制代码
// packages/wujie-core/src/utils.ts
export function getSyncUrl(id: string, prefix?: { [key: string]: string }): string {
  const winUrlElement = anchorElementGenerator(window.location.href);
  const queryMap = getAnchorElementQueryMap(winUrlElement);
  
  let syncUrl = queryMap[id] ? window.decodeURIComponent(queryMap[id]) : "";
  
  // 处理短路径还原
  if (prefix && syncUrl) {
    Object.keys(prefix).forEach((shortPath) => {
      const longPath = prefix[shortPath];
      syncUrl = syncUrl.replace(`{${shortPath}}`, longPath);
    });
  }
  
  return syncUrl;
}

短路径配置

通过 prefix 配置可以缩短 URL:

typescript 复制代码
startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  sync: true,
  prefix: {
    'u': '/user',           // /user/123 → {u}/123
    'p': '/product/detail', // /product/detail/456 → {p}/456
  },
});

效果:

ini 复制代码
原始: ?vue3=%2Fuser%2F123
短路径: ?vue3=%7Bu%7D%2F123

History 劫持

typescript 复制代码
// packages/wujie-core/src/iframe.ts
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;
    
    // 更新 base 标签
    updateBase(iframeWindow, appHostPath, mainHostPath);
    // 同步到主应用
    syncUrlToWindow(iframeWindow);
  };
  
  history.replaceState = function (data: any, title: string, url?: string): void {
    // 类似逻辑...
  };
}

前进后退监听

typescript 复制代码
// packages/wujie-core/src/iframe.ts
export function syncIframeUrlToWindow(iframeWindow: Window): void {
  // hashchange 事件
  iframeWindow.addEventListener("hashchange", () => syncUrlToWindow(iframeWindow));
  
  // popstate 事件
  iframeWindow.addEventListener("popstate", () => {
    syncUrlToWindow(iframeWindow);
  });
}

href 跳转处理

typescript 复制代码
// packages/wujie-core/src/sync.ts
export function processAppForHrefJump(): void {
  window.addEventListener("popstate", () => {
    let winUrlElement = anchorElementGenerator(window.location.href);
    const queryMap = getAnchorElementQueryMap(winUrlElement);
    winUrlElement = null;
    
    Object.keys(queryMap)
      .map((id) => getWujieById(id))
      .filter((sandbox) => sandbox)
      .forEach((sandbox) => {
        const url = queryMap[sandbox.id];
        const iframeBody = rawDocumentQuerySelector.call(sandbox.iframe.contentDocument, "body");
        
        // 前进到 href 跳转的页面
        if (/http/.test(url)) {
          if (sandbox.degrade) {
            renderElementToContainer(sandbox.document.documentElement, iframeBody);
            renderIframeReplaceApp(
              window.decodeURIComponent(url),
              getDegradeIframe(sandbox.id).parentElement,
              sandbox.degradeAttrs
            );
          } else {
            renderIframeReplaceApp(
              window.decodeURIComponent(url),
              sandbox.shadowRoot.host.parentElement,
              sandbox.degradeAttrs
            );
          }
          sandbox.hrefFlag = true;
          
        // 从 href 页面后退
        } else if (sandbox.hrefFlag) {
          if (sandbox.degrade) {
            const { iframe } = initRenderIframeAndContainer(sandbox.id, sandbox.el, sandbox.degradeAttrs);
            patchEventTimeStamp(iframe.contentWindow, sandbox.iframe.contentWindow);
            iframe.contentWindow.onunload = () => sandbox.unmount();
            iframe.contentDocument.appendChild(iframeBody.firstElementChild);
            sandbox.document = iframe.contentDocument;
          } else {
            renderElementToContainer(sandbox.shadowRoot.host, sandbox.el);
          }
          sandbox.hrefFlag = false;
        }
      });
  });
}

清理非激活应用 URL

typescript 复制代码
// packages/wujie-core/src/sync.ts
export function clearInactiveAppUrl(): void {
  let winUrlElement = anchorElementGenerator(window.location.href);
  const queryMap = getAnchorElementQueryMap(winUrlElement);
  
  Object.keys(queryMap).forEach((id) => {
    const sandbox = getWujieById(id);
    if (!sandbox) return;
    
    // 子应用执行过且已失活才需要清除
    if (sandbox.execFlag && sandbox.sync && !sandbox.hrefFlag && !sandbox.activeFlag) {
      delete queryMap[id];
    }
  });
  
  const newQuery = "?" + Object.keys(queryMap)
    .map((key) => key + "=" + window.decodeURIComponent(queryMap[key]))
    .join("&");
  winUrlElement.search = newQuery;
  
  if (winUrlElement.href !== window.location.href) {
    window.history.replaceState(null, "", winUrlElement.href);
  }
  winUrlElement = null;
}

推送 URL

typescript 复制代码
// packages/wujie-core/src/sync.ts
export function pushUrlToWindow(id: string, url: string): void {
  let winUrlElement = anchorElementGenerator(window.location.href);
  const queryMap = getAnchorElementQueryMap(winUrlElement);
  
  // 更新参数
  queryMap[id] = window.encodeURIComponent(url);
  
  const newQuery = "?" + Object.keys(queryMap)
    .map((key) => key + "=" + queryMap[key])
    .join("&");
  winUrlElement.search = newQuery;
  
  // 使用 pushState 添加历史记录
  window.history.pushState(null, "", winUrlElement.href);
  winUrlElement = null;
}

使用示例

typescript 复制代码
// 开启路由同步
startApp({
  name: 'vue3',
  url: 'http://localhost:7300/',
  el: '#container',
  sync: true,  // 开启同步
  prefix: {
    'u': '/user',
  },
});

// 子应用路由变化
// /user/123 → 主应用 URL: ?vue3=%7Bu%7D%2F123

// 刷新浏览器
// 主应用 URL: ?vue3=%7Bu%7D%2F123 → 子应用恢复到 /user/123

小结

无界的路由同步机制:

场景 处理方式
子应用路由变化 通过 history 劫持同步到主应用 URL
浏览器刷新 从主应用 URL 恢复子应用路由
前进后退 监听 popstate 事件同步
href 跳转 特殊处理,重新渲染 iframe
应用切换 清理非激活应用的 URL 参数

核心技巧:

  1. URL Query 存储:子应用路由编码后存入主应用 URL
  2. 短路径优化:通过 prefix 配置缩短 URL
  3. History 劫持:拦截 pushState/replaceState 实现同步
  4. 事件监听:hashchange 和 popstate 处理前进后退

下一篇我们将分析通信机制。


📦 源码版本:wujie v1.0.22

上一篇:JS 隔离

下一篇:通信机制

相关推荐
Aliex_git2 小时前
Vue 错误处理机制源码理解
前端·javascript·vue.js
普通码农2 小时前
PowerShell 神操作:输入「p」直接当「pnpm」用,敲命令速度翻倍!
前端·后端·程序员
Komorebi゛3 小时前
【Vue3+Element Plus】el-dialog弹窗点击遮罩层无法关闭弹窗问题记录
前端·vue.js·elementui
vim怎么退出3 小时前
一次线上样式问题复盘:当你钻进 CSS 牛角尖时,问题可能根本不在 CSS
前端·css
echo_e3 小时前
手搓前端虚拟列表
前端
用泥种荷花3 小时前
【LangChain学习笔记】创建智能体
前端
再吃一根胡萝卜3 小时前
在 Ant Design Vue 的 a-table 中将特定数据行固定在底部
前端
掘金安东尼4 小时前
Vercel:我们为 React2Shell 发起了一项价值 100 万美元的黑客挑战
前端·javascript·github