了解微前端中 History 模式下「路由拦截」的底层实现逻辑,这是微前端路由隔离的核心技术点。我会从「核心原理→分步实现→主流框架(Wujie/Qiankun)实战→关键细节」层层拆解,结合你熟悉的 Wujie 场景(Vue2 主应用 + Vue3 子应用),让你彻底理解拦截的本质和实现方式。
一、先明确:路由拦截的核心目标
微前端中对 History 模式的路由拦截,核心不是 "禁止" 路由操作,而是 **"劫持并改写" 子应用的路由行为 **,实现两个关键目标:
- 路由隔离 :子应用的路由操作(如跳
/proOrder)会被自动拼接「子应用前缀」(如/vue3-app),最终全局 URL 是/vue3-app/proOrder,主应用能识别; - 历史记录统一:主 / 子应用的路由跳转都写入浏览器的全局历史记录,回退 / 前进按钮正常工作;
- 无侵入性:子应用无需修改自身路由逻辑,感知不到主应用的存在。
二、路由拦截的核心实现步骤(通用逻辑)
微前端框架(如 Wujie、Qiankun)对 History 路由的拦截,本质是「沙箱化重写 + 事件劫持」,共 5 个核心步骤,下面用伪代码 + 通俗解释拆解:
步骤 1:前置约定 ------ 子应用路由前缀
主应用先为每个子应用约定唯一的路由前缀(如你的 /vue3-app),这是拦截的 "基准":
javascript
// 主应用配置:子应用路由前缀映射
const subAppRoutePrefix = {
'vue3-app': '/vue3-app', // Vue3子应用前缀
'react-app': '/react-app' // React子应用前缀
};
步骤 2:沙箱化 ------ 重写子应用的 History API
子应用挂载时,框架会创建「路由沙箱」,重写子应用全局的 history.pushState、history.replaceState 方法(核心拦截点),逻辑如下:
javascript
// 伪代码:重写子应用的 pushState 方法
function hijackHistory(subAppName) {
// 1. 保存原生 History API
const rawPushState = window.history.pushState;
const rawReplaceState = window.history.replaceState;
// 2. 重写 pushState(核心:拼接前缀)
window.history.pushState = function(state, title, url) {
// 子应用要跳转的路径(如子应用调用 pushState('', '', '/proOrder'))
const subAppPath = url;
// 拼接主应用约定的前缀(/vue3-app + /proOrder = /vue3-app/proOrder)
const globalPath = subAppRoutePrefix[subAppName] + subAppPath;
// 调用原生 pushState,更新全局 URL
return rawPushState.call(window.history, state, title, globalPath);
};
// 3. 重写 replaceState(逻辑和 pushState 一致,只是替换历史记录)
window.history.replaceState = function(state, title, url) {
const subAppPath = url;
const globalPath = subAppRoutePrefix[subAppName] + subAppPath;
return rawReplaceState.call(window.history, state, title, globalPath);
};
}
✅ 关键:子应用内部调用 history.pushState('', '', '/proOrder') 时,实际执行的是「拼接前缀后的全局路径」,但子应用自身感知不到 ------ 它以为自己跳的是 /proOrder,实际全局 URL 是 /vue3-app/proOrder。
步骤 3:劫持 popstate 事件 ------ 统一处理回退 / 前进
浏览器的「回退 / 前进」按钮会触发 popstate 事件,框架需要拦截这个事件,区分是主应用还是子应用的路由变化:
javascript
// 伪代码:劫持 popstate 事件
function hijackPopState(subAppName) {
// 保存原生事件监听方法
const rawAddEventListener = window.addEventListener;
// 重写 addEventListener,拦截 popstate 监听
window.addEventListener = function(type, listener) {
if (type === 'popstate') {
// 包装子应用的 popstate 监听函数
const wrappedListener = function(e) {
// 1. 获取全局 URL(如 /vue3-app/proOrder)
const globalPath = window.location.pathname;
// 2. 剥离前缀,得到子应用内部路径(/vue3-app/proOrder → /proOrder)
const subAppPath = globalPath.replace(subAppRoutePrefix[subAppName], '');
// 3. 构造"子应用视角"的事件对象(让子应用以为自己的路径是 /proOrder)
const subAppEvent = {
...e,
target: window,
currentTarget: window,
pathname: subAppPath // 关键:给子应用返回剥离前缀后的路径
};
// 4. 执行子应用的监听函数(传递子应用视角的事件)
listener.call(window, subAppEvent);
};
// 注册包装后的监听函数
return rawAddEventListener.call(window, type, wrappedListener);
}
// 非 popstate 事件,直接调用原生方法
return rawAddEventListener.call(window, type, listener);
};
}
✅ 关键:当用户点击回退按钮,全局 URL 从 /vue3-app/proOrder 变回 /vue3-app/home 时,框架会剥离前缀,给子应用传递 /home,子应用的路由逻辑会正常响应(渲染 Home 组件)。
步骤 4:主应用路由守卫 ------ 识别子应用并加载
主应用自身的路由守卫(如 Vue Router 的 beforeEach)会拦截全局 URL 变化,根据前缀识别要加载的子应用:
javascript
// 主应用(Vue2)路由守卫示例
router.beforeEach((to, from, next) => {
// 1. 检查目标路径是否包含子应用前缀
const subAppName = Object.keys(subAppRoutePrefix).find(name => {
return to.path.startsWith(subAppRoutePrefix[name]);
});
if (subAppName) {
// 2. 若匹配到子应用前缀,加载对应的子应用
loadSubApp(subAppName);
// 3. 剥离前缀,告诉子应用要渲染的内部路由(如 /vue3-app/proOrder → /proOrder)
const subAppPath = to.path.replace(subAppRoutePrefix[subAppName], '');
setSubAppRoute(subAppName, subAppPath);
next();
} else {
// 无匹配子应用,走主应用自身路由
next();
}
});
步骤 5:子应用路由适配 ------ 禁用原生监听
子应用挂载时,框架会禁用其路由的原生 popstate 监听(避免和主应用冲突),这也是你之前 Vue3 主 ts 中 router.history.listen = () => {} 的底层原因:
javascript
// 子应用(Vue3)挂载时的适配逻辑
function adaptSubAppRouter(router) {
// 禁用子应用路由的 popstate 监听(由主应用统一处理)
if (router.history?.listen) {
router.history.listen = () => {};
}
// 或更通用的方式:阻止子应用的 popstate 事件冒泡
window.addEventListener('popstate', (e) => {
e.stopPropagation();
}, true);
}
三、Wujie 框架的实际实现(贴合你的场景)
你使用的 Wujie 作为轻量级微前端框架,其路由拦截逻辑和上述通用步骤一致,但做了更优雅的封装,核心点:
1. 路由沙箱(Sandbox)
Wujie 会为每个子应用创建独立的「全局对象沙箱」,其中就包含对 history 对象的重写:
- 子应用的
window.history是 Wujie 包装后的对象,而非浏览器原生对象; - 子应用调用
pushState/replaceState时,Wujie 自动拼接prefix(你配置的/vue3-app)到 URL 前;
2. 前缀自动处理
你在 WujieVue2 组件中配置的 prefix="/vue3-app",就是告诉 Wujie:这个子应用的路由前缀是 /vue3-app,拦截时自动拼接 / 剥离。
3. 事件隔离
Wujie 会重写子应用的 addEventListener/removeEventListener,确保:
- 子应用的
popstate监听只响应 "自身前缀的路由变化"; - 子应用的路由事件不会冒泡到主应用,避免冲突。
4. 核心代码(Wujie 简化版)
javascript
// Wujie 中重写子应用 history 的核心逻辑(简化)
class WujieHistory {
constructor(subAppPrefix) {
this.prefix = subAppPrefix;
this.rawHistory = window.history;
}
pushState(state, title, url) {
const fullUrl = this.prefix + url;
this.rawHistory.pushState(state, title, fullUrl);
// 记录子应用的路由历史,用于回退/前进
this.recordHistory(fullUrl);
}
replaceState(state, title, url) {
const fullUrl = this.prefix + url;
this.rawHistory.replaceState(state, title, fullUrl);
}
// 其他方法(go/back/forward)同理,统一处理前缀
}
// 子应用挂载时,替换其 window.history
function mountSubApp(subAppName) {
const subAppPrefix = subAppRoutePrefix[subAppName];
window.history = new WujieHistory(subAppPrefix);
}
四、关键细节:避免踩坑的核心点
1. 历史记录栈的统一管理
框架会维护一个「微前端路由历史栈」,记录每个子应用的路由变化,避免:
- 回退时直接从子应用跳回主应用初始页面;
- 多个子应用的历史记录混乱。
2. 子应用卸载时的恢复
子应用卸载时,框架会恢复原生的 history 对象和 popstate 监听,避免内存泄漏:
javascript
// 子应用卸载时恢复原生 API
function unmountSubApp() {
window.history = rawHistory; // 恢复原生 history
window.addEventListener = rawAddEventListener; // 恢复原生事件监听
}
3. 跨子应用路由跳转
主应用跳转到另一个子应用时,框架会:
- 卸载当前子应用;
- 拼接目标子应用的前缀,更新全局 URL;
- 加载目标子应用,并传递剥离前缀后的内部路由。
总结(核心关键点)
- 拦截本质 :微前端的 History 路由拦截 = 重写子应用的
history.pushState/replaceState(拼接前缀) + 劫持popstate事件(剥离前缀) + 主应用路由守卫(识别子应用); - 核心目标:让子应用的路由操作 "看似独立",实则被主应用统一管理,实现 URL 全局唯一、历史记录统一;
- Wujie 适配 :你配置的
prefix是 Wujie 拦截的核心标识,框架自动完成前缀拼接 / 剥离,子应用无需修改路由逻辑; - 关键保障 :子应用禁用原生
popstate监听,避免和主应用的事件冲突,这也是你之前修复router.history.listen报错的底层原因。
理解这个逻辑后,你就能明白:为什么你的 Vue3 子应用跳 /proOrder,全局 URL 会变成 /vue3-app/proOrder,且回退 / 前进能正常工作 ------ 这都是路由拦截在背后起作用。