不引路由库也能做单页应用:手写 SPA 路由吃透 hash 和 history

举报
茉莉风铃 发表于 2026/09/19 17:38:41 2026/09/19
【摘要】 手写 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 好看但得让服务端回退。七十行写完一个能跑的路由,再回头看路由库的那些配置项,就知道它替你兜了多少事了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。