博主头像
mxd's Blog

以技术为翼,以生活为魂

SPA Router 导航器的三种实现方式

前阵子有个读者私信问我:“老哥,我看网上讲前端路由的文章,要么只贴概念,要么直接甩源码,能不能给几段真正能跑的代码,让我一眼看懂三种实现到底差在哪?”

我说行,今天就写一篇,把我这些年写 SPA 时踩过的坑和用过的方案摊开聊。


Hash 实现:# 号后面的世界

核心就两件事:监听 hashchange,切组件。

// 路由表
const routes = {
  '/home': () => render('Home'),
  '/about': () => render('About'),
  '/user/:id': (params) => render('User', params)
};

// 监听 hash 变化
window.addEventListener('hashchange', () => {
  const path = location.hash.slice(1) || '/home';
  matchRoute(path);
});

// 初始化
matchRoute(location.hash.slice(1) || '/home');

URL 变化过程

操作浏览器地址栏页面刷新?服务器收到请求
初始访问https://example.com/GET /
点击链接https://example.com/#/about不请求
刷新页面https://example.com/#/aboutGET /(# 后不发)
直接输入带 # 的 URLhttps://example.com/#/user/123GET /

关键点# 号及后面的内容不会发给服务器。所以你随便刷新,服务器永远只收到对 / 的请求,根本不会 404。这也是 Hash 模式"零配置"的原因。


History 实现:把 # 号干掉

history.pushState 改路径,监听 popstate 做回退。

// 导航
function navigate(path) {
  history.pushState({ path }, '', path);
  matchRoute(path);
}

// 浏览器前进/后退
window.addEventListener('popstate', (e) => {
  matchRoute(location.pathname);
});

// 拦截 <a> 标签点击(防止默认跳转)
document.addEventListener('click', (e) => {
  if (e.target.tagName === 'A') {
    e.preventDefault();
    navigate(e.target.getAttribute('href'));
  }
});

URL 变化过程

操作浏览器地址栏页面刷新?服务器收到请求
初始访问https://example.com/GET /
JS 导航https://example.com/about不请求
刷新页面https://example.com/aboutGET /about ⚠️
直接输入 URLhttps://example.com/user/123GET /user/123 ⚠️

坑来了:一旦刷新或直接访问子路径,浏览器会真的向服务器请求 /about/user/123。如果服务器上没有对应文件,直接 404

我第一次部署 History 模式的 SPA 时,刷新页面全白,控制台一片红,还以为是前端代码 bug,查了半天才发现是服务器没配 fallback。


Vue Router:一行代码切换两种模式

Vue Router 本质上就是上面两种方案的封装,但做得更完善。

import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';

// History 模式(需服务端支持)
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/user/:id', component: User }
  ]
});

// Hash 模式(零配置,把上面 createWebHistory 换成下面这个)
// history: createWebHashHistory(),

URL 变化对比

模式地址栏示例刷新行为服务端要求
Hash/#/about安全
History/about需 fallback必须配置

Vue Router 还顺手解决了你手写时容易忽略的问题:比如 <router-link> 会自动拦截点击事件,不用你手动 e.preventDefault();比如路由守卫、滚动恢复、懒加载,都是开箱即用。


History 模式的服务器配置大全

这是本文重点。以下配置都是把任意路径回退到 index.html,让前端路由接管。

Nginx

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

try_files 的意思是:先找文件,再找目录,都找不到就回退到 index.html。我现在的项目基本都用 Nginx,这段配置复制粘贴就能用。

Apache

.htaccess 文件(放在项目根目录):

<<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

或者直接在 httpd.conf 里配:

<<Directory "/var/www/html">
    Options -MultiViews
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.html [QSA,L]
</Directory>

记得确保 mod_rewrite 已启用(a2enmod rewrite),这是我当年用 Apache 时漏掉的一步,配了半天没生效。

Caddy

Caddy 配置最简洁,这也是我后来喜欢它的原因:

example.com {
    root * /var/www/html
    file_server
    try_files {path} {path}/ /index.html
}

或者更现代的 Caddyfile v2 写法:

example.com {
    root * /var/www/html
    file_server
    handle {
        rewrite {path} {path}/
        file_server {
            try_files {path} /index.html
        }
    }
}

Lighttpd

server.modules += ( "mod_rewrite" )

url.rewrite-if-not-file = (
    "^/(.*)$" => "/index.html"
)

或者更稳妥的版本(排除真实文件和目录):

url.rewrite-once = (
    "^/(.*\.(js|css|png|jpg|ico|svg|woff|woff2|ttf))$" => "$0",
    "^/(.*)$" => "/index.html"
)

Node.js / Express

如果你用 Node 做静态服务器,别忘了兜底:

const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'dist')));

// 所有非 API 请求回退到 index.html
app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
});

app.listen(3000);

注意:这段 app.get('*') 要放在所有其他路由最后面,否则会把你的 API 接口也吞掉。我排这个 bug 排了半小时。

汇总表

服务器核心思路配置难度
Nginxtry_files 回退
Apachemod_rewrite 规则⭐⭐
Caddytry_filesrewrite
Lighttpdurl.rewrite⭐⭐
Express末尾通配路由

我的建议

Hash 模式:内部工具、快速原型、懒得配服务器的时候用。URL 丑是丑点,但省心。

History 模式:正式项目、对外产品、有 SEO 需求时用。但务必把上面的服务器配置加上,否则用户刷新一次就流失了。

Vue Router:Vue 项目无脑用。需要 Hash 就 createWebHashHistory(),需要 History 就 createWebHistory(),切换成本为零。

SPA Router 导航器的三种实现方式
https://blog.mxdyeah.com/post/spa-router
本文作者 mxdyeah
发布时间 2026-05-18
许可协议 CC BY-NC-SA 4.0
发表新评论

AD: