SPA下的路由模式详解

30 阅读4分钟

HashRouter 和 HistoryRouter(通常称 hash 模式与 history 模式)是前端单页应用(SPA)中实现客户端路由的两种机制,核心区别在于如何管理 URL 且不触发页面刷新。

  • HashRouter(哈希模式):基于 URL 中 `` 后面的部分(如 //about),通过监听 hashchange 事件实现路由切换。hash 不会发送给服务器,因此无需后端配置,兼容所有浏览器(包括 IE8+),但 URL 不够美观,且可能干扰锚点定位。
  • HistoryRouter(history 模式):利用 HTML5 History API(pushState() / replaceState() + popstate 事件)创建“干净”URL(如 /about),外观与传统服务端路由一致,更利于 SEO 和用户体验;但刷新或直接访问深层路径时,若服务器未配置 fallback(如将所有请求重定向至 index.html),会返回 404。

两者都不真正发起 HTTP 请求到服务器(除首次加载),由前端 JS 动态渲染视图。Vue Router 默认用 hash 模式;React Router 的 BrowserRouter 对应 history 模式,HashRouter 对应 hash 模式。部署时:hash 模式开箱即用,history 模式需后端配合(如 Nginx 的 try_files 或 Express 的 history-api-fallback)。

SPA 模式下的路由模式详解

SPA(Single Page Application,单页应用)的路由模式核心原理是:在前端控制页面的切换,不向服务器发送请求,通过 URL 的变化来渲染不同的组件/页面。

一、两种核心路由模式

1. Hash 模式(默认)

工作原理

使用 URL 中的 # 符号(hash)作为路由标识,hash 变化不会触发页面刷新。

// URL 示例
http://example.com/#/home
http://example.com/#/about
http://example.com/#/user/123

// hash 部分变化
location.hash = '#/home'     // 不会刷新页面
location.hash = '#/about'    // 不会刷新页面

实现原理

class HashRouter {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
    
    // 监听 hash 变化事件
    window.addEventListener('hashchange', () => {
      this.loadRoute();
    });
    
    // 监听页面加载
    window.addEventListener('load', () => {
      this.loadRoute();
    });
  }
  
  // 注册路由
  route(path, callback) {
    this.routes[path] = callback;
  }
  
  // 加载当前路由对应的组件
  loadRoute() {
    // 获取 hash(去掉 # 号)
    this.currentUrl = location.hash.slice(1) || '/';
    
    // 执行对应的回调函数
    if (this.routes[this.currentUrl]) {
      this.routes[this.currentUrl]();
    }
  }
  
  // 跳转路由
  push(path) {
    location.hash = path;
  }
}

// 使用示例
const router = new HashRouter();

router.route('/home', () => {
  document.getElementById('app').innerHTML = '<h1>首页</h1>';
});

router.route('/about', () => {
  document.getElementById('app').innerHTML = '<h1>关于</h1>';
});

// 跳转
router.push('/about');

优缺点

优点缺点
✅ 兼容性好(支持 IE8+)❌ URL 带有 #,不够美观
✅ 无需服务器配置❌ SEO 不友好(爬虫忽略 # 后内容)
✅ 实现简单,不会刷新页面❌ 微信分享等场景可能丢失参数

2. History 模式(HTML5)

工作原理

利用 HTML5 的 History API 来管理路由,实现无刷新的 URL 变化。

// URL 示例(更美观)
http://example.com/home
http://example.com/about
http://example.com/user/123

// 核心 API
history.pushState(state, title, url)    // 添加历史记录
history.replaceState(state, title, url) // 替换当前历史记录
window.onpopstate                       // 监听浏览器前进后退

实现原理

class HistoryRouter {
  constructor() {
    this.routes = {};
    
    // 监听 popstate(浏览器前进/后退)
    window.addEventListener('popstate', (event) => {
      this.loadRoute(location.pathname);
    });
  }
  
  // 注册路由
  route(path, callback) {
    this.routes[path] = callback;
  }
  
  // 加载路由
  loadRoute(path) {
    if (this.routes[path]) {
      this.routes[path]();
    } else {
      // 404 处理
      this.routes['/404'] && this.routes['/404']();
    }
  }
  
  // 跳转路由
  push(path) {
    // 修改 URL 并添加历史记录
    history.pushState({}, '', path);
    this.loadRoute(path);
  }
  
  // 替换路由(不添加历史记录)
  replace(path) {
    history.replaceState({}, '', path);
    this.loadRoute(path);
  }
  
  // 前进/后退
  go(n) {
    history.go(n);
  }
}

// 使用示例
const router = new HistoryRouter();

router.route('/home', () => {
  document.getElementById('app').innerHTML = '<h1>首页</h1>';
});

router.route('/about', () => {
  document.getElementById('app').innerHTML = '<h1>关于</h1>';
});

// 跳转
router.push('/about');

服务器配置(关键点)

History 模式必须配置服务器,否则刷新页面会 404:

# Nginx 配置
location / {
    try_files $uri $uri/ /index.html;
}
# Apache 配置
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.html [L]
</IfModule>
// Node.js/Express 配置
const express = require('express');
const app = express();

app.use(express.static('dist'));

app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
});

优缺点

优点缺点
✅ URL 美观,无 #❌ 需要服务器配置支持
✅ SEO 友好❌ 兼容性稍差(IE10+)
✅ 符合 Web 标准❌ 实现相对复杂

二、框架中的实现

1. Vue Router

// Vue 3 + Vue Router 4
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'

// Hash 模式
const hashRouter = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

// History 模式
const historyRouter = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

// 使用
app.use(historyRouter)

2. React Router

// React Router v6
import { BrowserRouter, HashRouter, Routes, Route } from 'react-router-dom'

// Hash 模式
function HashModeApp() {
  return (
    <HashRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </HashRouter>
  )
}

// History 模式
function HistoryModeApp() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}

3. 原生 JavaScript 实现简单路由

// 完整的 SPA 路由实现
class SPARouter {
  constructor(options = {}) {
    this.mode = options.mode || 'hash';  // 'hash' 或 'history'
    this.routes = [];
    this.currentView = null;
    
    this.init();
  }
  
  init() {
    if (this.mode === 'hash') {
      window.addEventListener('hashchange', () => this.handleRoute());
      window.addEventListener('load', () => this.handleRoute());
    } else {
      window.addEventListener('popstate', () => this.handleRoute());
      // 拦截所有链接点击
      document.addEventListener('click', (e) => {
        const link = e.target.closest('a');
        if (link && link.getAttribute('data-router') !== 'false') {
          e.preventDefault();
          const href = link.getAttribute('href');
          if (href && !href.startsWith('http')) {
            this.push(href);
          }
        }
      });
    }
  }
  
  handleRoute() {
    let path;
    
    if (this.mode === 'hash') {
      path = window.location.hash.slice(1) || '/';
    } else {
      path = window.location.pathname;
    }
    
    const route = this.routes.find(r => r.path === path);
    
    if (route) {
      this.currentView = route.component;
      this.render();
    } else if (this.routes.find(r => r.path === '/404')) {
      this.currentView = this.routes.find(r => r.path === '/404').component;
      this.render();
    } else {
      console.error('路由未找到');
    }
  }
  
  push(path) {
    if (this.mode === 'hash') {
      window.location.hash = path;
    } else {
      history.pushState({}, '', path);
      this.handleRoute();
    }
  }
  
  replace(path) {
    if (this.mode === 'hash') {
      const url = window.location.href.split('#')[0] + '#' + path;
      window.location.replace(url);
    } else {
      history.replaceState({}, '', path);
      this.handleRoute();
    }
  }
  
  render() {
    const app = document.getElementById('app');
    if (app && this.currentView) {
      app.innerHTML = this.currentView;
    }
  }
  
  addRoute(path, component) {
    this.routes.push({ path, component });
  }
}

// 使用
const router = new SPARouter({ mode: 'history' });

router.addRoute('/', '<h1>首页</h1><a href="/about">关于</a>');
router.addRoute('/about', '<h1>关于</h1><a href="/">返回首页</a>');
router.addRoute('/404', '<h1>404 页面未找到</h1>');

router.handleRoute();  // 初始化

三、路由参数处理

// 动态路由参数解析
class RouteParser {
  // 解析路由参数
  static parseRoute(routePattern, currentPath) {
    const patternParts = routePattern.split('/');
    const pathParts = currentPath.split('/');
    
    if (patternParts.length !== pathParts.length) {
      return null;
    }
    
    const params = {};
    
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i].startsWith(':')) {
        // 动态参数
        const paramName = patternParts[i].slice(1);
        params[paramName] = pathParts[i];
      } else if (patternParts[i] !== pathParts[i]) {
        return null;
      }
    }
    
    return params;
  }
  
  // 查询参数解析
  static parseQuery(queryString) {
    const params = {};
    const search = queryString.startsWith('?') ? queryString.slice(1) : queryString;
    
    if (!search) return params;
    
    search.split('&').forEach(param => {
      const [key, value] = param.split('=');
      params[decodeURIComponent(key)] = decodeURIComponent(value || '');
    });
    
    return params;
  }
  
  // 构建查询字符串
  static buildQuery(params) {
    const query = Object.entries(params)
      .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
      .join('&');
    
    return query ? `?${query}` : '';
  }
}

// 使用示例
class Router extends SPARouter {
  pushWithParams(path, params = {}, query = {}) {
    let finalPath = path;
    
    // 替换路径参数
    Object.entries(params).forEach(([key, value]) => {
      finalPath = finalPath.replace(`:${key}`, value);
    });
    
    // 添加查询参数
    const queryString = RouteParser.buildQuery(query);
    finalPath += queryString;
    
    this.push(finalPath);
  }
  
  getCurrentParams(routePattern) {
    const currentPath = this.mode === 'hash' 
      ? window.location.hash.slice(1) 
      : window.location.pathname;
    
    return RouteParser.parseRoute(routePattern, currentPath);
  }
  
  getCurrentQuery() {
    const search = window.location.search;
    return RouteParser.parseQuery(search);
  }
}

四、路由守卫实现

class RouterWithGuard extends HistoryRouter {
  constructor() {
    super();
    this.beforeHooks = [];
    this.afterHooks = [];
  }
  
  // 注册前置守卫
  beforeEach(callback) {
    this.beforeHooks.push(callback);
  }
  
  // 注册后置守卫
  afterEach(callback) {
    this.afterHooks.push(callback);
  }
  
  // 重写 push 方法,添加守卫
  async push(path) {
    // 执行前置守卫
    for (const hook of this.beforeHooks) {
      const result = await hook(path);
      
      if (result === false) {
        console.log('导航被取消');
        return;
      }
      
      if (typeof result === 'string') {
        // 重定向
        return super.push(result);
      }
    }
    
    // 执行路由跳转
    super.push(path);
    
    // 执行后置守卫
    for (const hook of this.afterHooks) {
      hook(path);
    }
  }
}

// 使用守卫
const router = new RouterWithGuard();

// 登录验证守卫
router.beforeEach((to) => {
  const isLoggedIn = localStorage.getItem('token');
  
  if (to === '/admin' && !isLoggedIn) {
    alert('请先登录');
    return '/login';
  }
  
  return true;
});

// 日志守卫
router.afterEach((to) => {
  console.log(`路由跳转到: ${to}`, new Date());
});

五、性能优化

// 路由懒加载
const router = new Router();

// 方式1:动态导入
router.addRoute('/about', async () => {
  const module = await import('./pages/About.js');
  return module.default;
});

// 方式2:组件缓存
class LazyRouter extends Router {
  constructor() {
    super();
    this.componentCache = new Map();
  }
  
  async loadComponent(path, loader) {
    // 检查缓存
    if (this.componentCache.has(path)) {
      return this.componentCache.get(path);
    }
    
    // 加载组件
    const component = await loader();
    this.componentCache.set(path, component);
    
    return component;
  }
}

// 路由预加载
class PreloadRouter extends Router {
  preload(paths) {
    paths.forEach(path => {
      const route = this.routes.find(r => r.path === path);
      if (route && route.preload) {
        route.preload();  // 提前加载组件
      }
    });
  }
}

// 鼠标悬停时预加载
document.querySelectorAll('a[data-router]').forEach(link => {
  link.addEventListener('mouseenter', () => {
    const path = link.getAttribute('href');
    router.preload([path]);
  });
});

六、选择建议

使用 Hash 模式的场景

  • ✅ 项目不需要 SEO
  • ✅ 无需服务器配置(如 GitHub Pages)
  • ✅ 需要兼容老旧浏览器
  • ✅ 内部管理系统

使用 History 模式的场景

  • ✅ 需要 SEO 优化
  • ✅ 追求 URL 美观
  • ✅ 可以配置服务器
  • ✅ 面向现代浏览器

实际项目配置示例

// vue.config.js (Vue CLI)
module.exports = {
  publicPath: '/',
  devServer: {
    historyApiFallback: true  // 开发环境支持 History 模式
  }
}

// 生产环境 nginx 配置
server {
    listen 80;
    server_name example.com;
    root /var/www/dist;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

总结

对比项Hash 模式History 模式
URL/#/about/about
原理hashchange 事件History API + popstate
服务器配置不需要需要
SEO不友好友好
兼容性IE8+IE10+
实现复杂度简单中等
适用场景管理系统、工具类应用官网、博客、C端产品

最佳实践

  1. 新项目优先考虑 History 模式(配置好服务器即可)
  2. 无法配置服务器或临时项目用 Hash 模式
  3. 开发环境两种模式都支持,通过配置切换
  4. 注意处理刷新 404 问题(History 模式必须配置 fallback)