不引路由库也能做单页应用:手写 SPA 路由吃透 hash 和 history
一、hash 模式:最省事的一种
hash 指地址栏里 # 后面的部分,比如 /#/about。改 hash 不会触发页面刷新,但会触发 hashchange 事件,这就是路由的抓手。思路很直白:把路由表写成一个对象,key 是路径,value 是渲染函数,事件一来就取当前路径对应的函数执行。
class Router {
constructor(routes) {
this.routes = routes;
window.addEventListener('hashchange', () => this.render());
this.render();
}
getPath() { return location.hash.slice(1) || '/'; }
go(path) { location.hash = path; }
render() {
const path = this.getPath();
const fn = this.routes[path] || this.routes['*'];
if (fn) fn();
}
}
const router = new Router({
'/': () => app.innerHTML = '<h1>首页</h1>',
'/about': () => app.innerHTML = '<h1>关于我们</h1>',
'*': () => app.innerHTML = '<h1>404</h1>'
});
location.hash.slice(1) 把 # 号去掉,为空就当根路径处理。routes[‘*’] 做兜底,路径对不上时走它,这就是最简单的 404。写好之后点 <a href="#/about"> 就能切页,地址变了,浏览器没刷新。
二、history 模式:地址看起来更正常
带 # 的地址多少有点别扭,history 模式能让路径变成干净好看的 /about。它靠 HTML5 的 History API:history.pushState 改地址且不刷新页面,用户点前进后退时触发 popstate 事件。
function navigate(path) {
history.pushState({}, '', path);
render(path);
}
function render(path) {
const box = document.getElementById('app');
const m = match(path);
if (!m) { box.innerHTML = '<h1>404</h1>'; return; }
m.fn(box, m.params);
}
// 拦截站内链接,别让它整页刷新
document.addEventListener('click', e => {
const a = e.target.closest('a[data-link]');
if (a) { e.preventDefault(); navigate(a.getAttribute('href')); }
});
window.addEventListener('popstate', () => render(location.pathname));
render(location.pathname);
match 负责把 /user/:id 这种动态路由和真实路径对上,顺便取出参数:
function match(path) {
for (const key in routes) {
const ks = key.split('/'), ps = path.split('/');
if (ks.length !== ps.length) continue;
const params = {};
const ok = ks.every((k, i) => {
if (k.startsWith(':')) { params[k.slice(1)] = ps[i]; return true; }
return k === ps[i];
});
if (ok) return { fn: routes[key], params };
}
return null;
}
先用层数过滤掉明显不匹配的,再逐段比对,带冒号的那一段算动态参数,值塞进 params 交给渲染函数。
三、两种模式怎么选
hash 模式不用服务端配合,刷新、分享链接都不会 404,做 demo、静态托管、内部后台最省事。history 模式地址干净,观感和 SEO 都更体面,代价是刷新 /about 时服务器会真去找这个文件,找不到就 404,必须让服务端把所有路径都回退到 index.html(Nginx 配 try_files,本地开发服务器一般也有对应的 history 回退开关)。要是部署时懒得动服务器配置,老实用 hash。
四、踩坑提醒
第一,popstate 只在你自己 pushState 或 replaceState 之后的前进后退才触发,程序里调 navigate 不会自动触发它,所以 navigate 里必须手动补一次 render,这一行漏掉就会出现"点了没反应"。
第二,a 标签一定要拦。不拦截的话点链接浏览器直接整页刷新,单页应用的优势全没了。上面用 data-link 标记需要接管的链接,普通外链保持原样跳转。
第三,切换时该清理的东西别落下:上个页面挂的定时器、事件监听、轮询请求,不取消的话来回切几次内存就悄悄涨上去了。成熟的路由库都提供销毁钩子,自己写就得手动找个地方清理。
第四,这里的动态匹配只处理了单层参数,多层嵌套、可选参数、查询串解析都得自己补。真要做成正式项目还是上成熟路由库,手写的价值在于搞懂"地址变化到内容更新"这条链路。
第五,history 模式部署要服务端配合这条最容易被忘,本地跑得好好的,一上线刷新就白屏。
五、小结
两种模式本质是一回事:改地址 + 渲染对应内容。hash 省事不用配服务器,history 好看但得让服务端回退。七十行写完一个能跑的路由,再回头看路由库的那些配置项,就知道它替你兜了多少事了。
- 点赞
- 收藏
- 关注作者
评论(0)