SPA下的路由模式详解

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 变化不会触发页面刷新。

javascript 复制代码
// URL 示例
http://example.com/#/home
http://example.com/#/about
http://example.com/#/user/123

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

实现原理

javascript 复制代码
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 变化。

javascript 复制代码
// 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                       // 监听浏览器前进后退

实现原理

javascript 复制代码
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 复制代码
# Nginx 配置
location / {
    try_files $uri $uri/ /index.html;
}
apache 复制代码
# 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>
javascript 复制代码
// 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

javascript 复制代码
// 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

jsx 复制代码
// 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 实现简单路由

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();  // 初始化

三、路由参数处理

javascript 复制代码
// 动态路由参数解析
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);
  }
}

四、路由守卫实现

javascript 复制代码
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());
});

五、性能优化

javascript 复制代码
// 路由懒加载
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 美观
  • ✅ 可以配置服务器
  • ✅ 面向现代浏览器

实际项目配置示例

javascript 复制代码
// 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)
相关推荐
环信4 小时前
2026年开发者选择即时通讯厂商应注意的几点
前端
卷帘依旧4 小时前
Generator 全面解析 + async/await 深度对比
前端·javascript
yqcoder5 小时前
数据劫持的双雄:深入解析 Object.defineProperty 与 Proxy
开发语言·前端·javascript
lichenyang4535 小时前
鸿蒙聊天 Demo 练习 03:接入 Next.js 后端接口,实现真机前后端联调
前端
小三金5 小时前
EXPO+RN echarts图表库,以及如何使用
前端·javascript·react.js
ZFSS5 小时前
Midjourney Shorten API 的集成与使用
java·前端·数据库·人工智能·ai·midjourney·ai编程
Pu_Nine_96 小时前
IntersectionObserver 详解:封装 Vue 指令实现图片懒加载
前端·javascript·vue.js·性能优化
清灵xmf6 小时前
Web 和 Native 是怎么“对话“的?JSBridge 解答
前端·webview·native·jsbridge·hybrid
jiayong237 小时前
前端面试题库 - ES6+新特性篇
前端·面试·es6