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

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

深入分析主子应用路由同步机制,理解 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 隔离

下一篇:通信机制

相关推荐
我只会写Bug啊4 分钟前
复制可用!纯前端基于 Geolocation API 实现经纬度获取与地图可视化
前端·高德地图·地图·百度地图·经纬度
刘一说11 分钟前
Vue3 模块语法革命:移除过滤器(Filters)的深度解析与迁移指南
前端·vue.js·js
lkbhua莱克瓦2444 分钟前
JavaScript核心语法
开发语言·前端·javascript·笔记·html·ecmascript·javaweb
Trae1ounG44 分钟前
这是什么dom
前端·javascript·vue.js
比老马还六1 小时前
Bipes项目二次开发/扩展积木功能(八)
前端·javascript
易营宝1 小时前
全球建站SaaS平台能提升SEO评分吗?是否值得切换?
大数据·前端·人工智能
513495921 小时前
在Vue.js项目中使用docx和file-saver实现Word文档导出
前端·vue.js·word
AC赳赳老秦2 小时前
Prometheus + DeepSeek:自动生成巡检脚本与告警规则配置实战
前端·javascript·爬虫·搜索引擎·prometheus·easyui·deepseek
接着奏乐接着舞。2 小时前
前端大数据渲染性能优化:Web Worker + 分片处理 + 渐进式渲染
大数据·前端·性能优化
Beginner x_u2 小时前
CSS 中的高度、滚动与溢出:从 height 到 overflow 的完整理解
前端·css·overflow·min-height