前端路由的两种实现:hash 与 history
单页应用切换视图时不重新请求 HTML,但浏览器的地址栏、前进后退、刷新都要正常工作。hash 和 history 是两条不同的实现路径。
hash 模式
地址形如 example.com/#/user/1。# 后面是片段标识符,改变它不会触发页面请求,浏览器只抛出 hashchange 事件。
window.addEventListener('hashchange', () => {
const path = location.hash.slice(1) || '/';
render(path);
});
优点:服务端零配置,刷新永远回到 index.html。缺点:URL 里有个 #,对 SEO 不友好,部分统计工具也会把它截断。
history 模式
用 pushState 修改地址,不触发请求:
history.pushState({ page: 1 }, '', '/user/1');
window.addEventListener('popstate', () => render(location.pathname));
优点:URL 干净,像传统多页站点。缺点:刷新会真的向服务器请求 /user/1,如果服务器没有对应文件,就是 404。
解决方式是在服务端把未匹配的路径统一回退到入口文件:
# Apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Nginx 对应的是 try_files $uri $uri/ /index.html;。
容易忽略的细节
pushState不会触发popstate,前进后退才会。所以路由跳转要手动调用渲染。- 需要拦截站内
<a>点击,否则整页刷新; - 滚动位置恢复要自己记录,浏览器不会自动处理;
- 部署到子目录时,路由前缀和静态资源路径都要相应调整。
怎么选
内部系统、后台管理优先 hash,省掉服务端配置。面向公众、需要被搜索引擎收录的站点用 history,并配好回退规则。本站是静态站点,每篇文章都是真实文件,不涉及这个问题——但理解它对于排查线上 404 仍然必要。