无界微前端源码解析:路由同步
深入分析主子应用路由同步机制,理解 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 参数 |
核心技巧:
- URL Query 存储:子应用路由编码后存入主应用 URL
- 短路径优化:通过 prefix 配置缩短 URL
- History 劫持:拦截 pushState/replaceState 实现同步
- 事件监听:hashchange 和 popstate 处理前进后退
下一篇我们将分析通信机制。
📦 源码版本:wujie v1.0.22
上一篇:JS 隔离
下一篇:通信机制