手写路由的最小模型
URL 变化怎样成为可订阅的数据?
- 浏览器前后退: 监听 popstate 后重新读取地址,通知 React 刷新匹配结果;
- 程序导航: pushState 本身不会派发 popstate,封装导航时必须主动通知订阅者,不能试图监听一个叫 pushState 的浏览器事件;
- 范围约束: 以下只用于客户端同源路径的教学模型,不覆盖生产路由的参数、加载、重定向和滚动恢复;
如何用外部订阅连接浏览器地址?
import { useSyncExternalStore } from "react";
const navigationEvent = "demo:navigate";
function subscribe(callback) {
window.addEventListener("popstate", callback);
window.addEventListener(navigationEvent, callback);
return () => {
window.removeEventListener("popstate", callback);
window.removeEventListener(navigationEvent, callback);
};
}
function getSnapshot() {
return window.location.pathname;
}
function navigate(to) {
window.history.pushState(null, "", to);
window.dispatchEvent(new Event(navigationEvent));
}
function Route({ path, children }) {
const pathname = useSyncExternalStore(subscribe, getSnapshot);
return pathname === path ? children : null;
}
自定义链接怎样保留浏览器原有操作?
- 拦截条件: 只接管未被阻止、同源、普通左键且没有修饰键的当前窗口导航,其他行为交给浏览器;
- 完整 href: 即使使用客户端导航,也保留真实链接地址,使复制和打开新标签继续有效;
function Link({ to, children }) {
return (
<a
href={to}
onClick={(event) => {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
)
return;
const url = new URL(to, window.location.href);
if (url.origin !== window.location.origin) return;
event.preventDefault();
navigate(url.pathname + url.search + url.hash);
}}
>
{children}
</a>
);
}
为什么这个模型不能直接充当生产路由?
- 缺失能力: 只匹配 pathname 完全相等,不实现嵌套、参数、查询订阅、错误恢复和服务端初值;
- 工程选择: 理解通知与订阅即可,真实项目使用成熟路由实现,避免重复实现浏览器边界;