前端路由详解:Hash vs History

作者:mCell日期:2025/12/2

同步至个人站点:前端路由详解

前端路由详解:Hash vs History

背景

在 Web 开发的早期,互联网主要由 多页应用(MPA, Multi-Page Application)组成。那时的路由逻辑非常简单:用户点击一个链接,浏览器向服务器发送请求;服务器接收请求,根据 URL 路径找到对应的 HTML 文件(或通过模板引擎生成),返回给浏览器;浏览器卸载当前页面,重新渲染新页面。

这种模式的缺点显而易见:每次页面切换都需要重新加载资源,出现短暂的“白屏”,用户体验不够流畅。

随着 AJAX 技术的普及,单页应用(SPA, Single-Page Application)开始流行。SPA 的核心理念是:页面初始化时加载必要的 HTML、CSS 和 JavaScript,之后的页面切换不再请求完整的 HTML,而是通过 JS 动态更新页面内容。

这就带来了一个新问题:如何在不刷新页面的前提下,改变 URL 并渲染对应的内容?

这就是前端路由诞生的背景。目前主流的解决方案有两种:Hash 模式History 模式

Hash 模式

如果你看到 URL 中包含一个 # 号,例如 http://www.example.com/#/home,那么这个应用很可能使用的是 Hash 模式。

Hash 的本质

Hash(哈希)原本是用来做页面定位的(锚点)。比如 <a href="#content"> 可以直接跳转到页面 id 为 content 的位置。

Hash 有一个非常重要的特性:URL 中 # 及其后面的内容,虽然会显示在浏览器地址栏,但不会被包含在 HTTP 请求中。

当你访问 http://www.example.com/#/home 时,浏览器向服务器请求的仅仅是 http://www.example.com/。这意味着,无论 # 后面的内容如何变化,服务端都只接收到同一个请求,返回同一个 index.html

实现原理

在浏览器中,我们可以通过 window.location.hash 属性读取或修改 Hash 值。

更关键的是,浏览器提供了一个 hashchange 事件。当 URL 的 Hash 部分发生变化时,就会触发这个事件。

1// 监听 Hash 变化
2window.addEventListener("hashchange", function () {
3  console.log("The hash has changed to: " + location.hash)
4  // 在这里根据 hash 的值,动态更新页面 DOM
5})
6

优缺点分析

  • 优点
    • 兼容性好:支持低版本浏览器(如 IE8)。
    • 无需服务端配置:因为 Hash 不参与 HTTP 请求,服务器只需处理根路径请求,部署极其简单。
  • 缺点
    • URL 不美观# 符号夹在中间,违背了 URL 的语义(Uniform Resource Locator),看起来像个“补丁”。
    • SEO 较差:搜索引擎爬虫虽然在进化,但对 Hash 的支持依然不如纯路径友好。

History 模式

为了解决 Hash 模式 URL 不美观的问题,HTML5 标准在 history 对象上增加了新的 API。这就是 History 模式的基础。

核心 API

在 HTML4 时代,window.history 只能用于前进(forward)、后退(back)和跳转(go)。

HTML5 新增了两个关键方法,允许我们在不刷新页面的情况下修改 URL:

  1. history.pushState(state, title, url):向历史记录堆栈中添加一条新记录。
  2. history.replaceState(state, title, url):修改当前的历史记录。

例如,执行 history.pushState(null, null, '/user/id') 后,浏览器的地址栏会变为 http://www.example.com/user/id,但浏览器不会向服务器发送请求,页面也不会刷新。

实现原理

History 模式的实现比 Hash 稍微复杂一点。我们需要处理两种情况:

  1. 用户点击链接:前端框架会拦截 <a> 标签的点击事件,阻止默认跳转,改用 history.pushState 修改 URL,并手动更新视图。
  2. 用户点击浏览器的前进/后退按钮:这会触发 popstate 事件。我们需要监听这个事件来更新视图。
1// 监听浏览器的前进、后退
2window.addEventListener("popstate", function (event) {
3  console.log(
4    "Location: " + document.location + ", state: " + JSON.stringify(event.state)
5  )
6  // 根据当前 path 更新视图
7})
8

优缺点分析

  • 优点
    • URL 美观:和传统后端路由一样的路径结构,符合 RESTful 规范。
    • 功能更强pushState 可以传递 state 对象,允许在页面跳转时传递复杂数据。
  • 缺点
    • 兼容性:需要 IE10 及以上。
    • 必须服务端配置:这是最大的痛点(详见第五节)。

源码级实战:手写迷你路由

为了彻底理解,我们模仿 Vue Router 写一个简化版。

4.1 HashRouter 实现

1class HashRouter {
2  constructor() {
3    this.routes = {} // 存储路径与回调函数的映射
4    this.currentUrl = ""
5
6    // 绑定 this,防止指向丢失
7    this.refresh = this.refresh.bind(this)
8
9    // 监听 load  hashchange 事件
10    window.addEventListener("load", this.refresh)
11    window.addEventListener("hashchange", this.refresh)
12  }
13
14  // 注册路由
15  route(path, callback) {
16    this.routes[path] = callback || function () {}
17  }
18
19  // 刷新页面逻辑
20  refresh() {
21    // 获取当前 hash,去掉 # 
22    this.currentUrl = location.hash.slice(1) || "/"
23    // 执行对应的回调函数(渲染 UI)
24    if (this.routes[this.currentUrl]) {
25      this.routes[this.currentUrl]()
26    }
27  }
28}
29

HistoryRouter 实现

1class HistoryRouter {
2  constructor() {
3    this.routes = {}
4
5    this.bindPopState()
6    this.initLinkHijack() // 拦截 a 标签
7  }
8
9  route(path, callback) {
10    this.routes[path] = callback || function () {}
11  }
12
13  // 监听浏览器自带的前进后退
14  bindPopState() {
15    window.addEventListener("popstate", (e) => {
16      const path = location.pathname
17      this.updateView(path)
18    })
19  }
20
21  // 拦截全局点击事件,处理 link 跳转
22  initLinkHijack() {
23    document.addEventListener("click", (e) => {
24      const target = e.target
25      if (target.tagName === "A") {
26        e.preventDefault() // 阻止默认跳转
27        const path = target.getAttribute("href")
28        // 手动修改 URL
29        history.pushState(null, null, path)
30        // 更新视图
31        this.updateView(path)
32      }
33    })
34  }
35
36  updateView(path) {
37    if (this.routes[path]) {
38      this.routes[path]()
39    }
40  }
41}
42

部署难题:History 模式下的 404 问题

这是新手最容易遇到的“坑”。

现象复现

你在本地开发时(使用 webpack-dev-servervite),一切正常。但是,当你运行 npm run build 打包,将生成的文件部署到 Nginx 服务器后:

  1. 访问根路径 http://www.site.com/,页面正常显示。
  2. 点击导航进入 http://www.site.com/about,页面正常显示(因为是 JS 动态渲染的)。
  3. 但是,如果你在 /about 页面按下刷新按钮,或者直接在地址栏输入这个地址,页面会显示 404 Not Found

根本原因

这是一个典型的“前端路由与后端路由冲突”问题。

  • 前端逻辑:我认为 /about 是一个视图(Component),属于 index.html 的一部分。
  • 后端逻辑:当浏览器发送 /about 请求时,服务器会去文件系统中查找名为 about 的文件夹或文件。

很显然,你的服务器上只有一个 index.html,并没有 about 这个文件,所以 Nginx 诚实地返回了 404。

解决方案

解决思路很简单:告诉服务器,如果找不到对应的文件,不要报 404,而是统统返回 index.html

只要返回了 index.html,浏览器就会加载 JS,路由插件(Vue Router 等)就会接管 URL,分析路径是 /about,然后渲染出对应的组件。

Nginx 配置示例:

1location / {
2  root   /usr/share/nginx/html;
3  index  index.html index.htm;
4
5  # 核心配置:尝试查找文件,找不到则重定向到 index.html
6  try_files $uri $uri/ /index.html;
7}
8

try_files $uri $uri/ /index.html; 的意思是:

  1. 先看用户请求的是不是一个真实存在的文件($uri)。
  2. 如果不是,再看是不是一个真实存在的目录($uri/)。
  3. 如果都不是,就返回 /index.html

总结与选型指南

最后,我们用一张表格来总结两者的区别,帮助你在项目中做出选择。

特性Hash 模式History 模式
URL 外观example.com/#/aboutexample.com/about
美观度丑,有“#”号干扰美观,符合标准
原理window.location.hashhistory.pushState
兼容性极好(IE8+)较好(IE10+)
服务端配置不需要必须配置
应用场景内部系统、Demo、静态资源服务器正式商业项目、C 端应用

在现代前端开发中,除非你有特殊的兼容性需求或者由于权限问题无法配置服务器,否则强烈建议使用 History 模式。它不仅能提供更好的用户体验,也更符合 Web 标准的发展趋势。

(完)


前端路由详解:Hash vs History》 是转载文章,点击查看原文


相关推荐


git:通过令牌方式访问远程仓库
一点事2025/11/30

文章目录 git:通过令牌方式访问远程仓库一、进入仓库添加令牌二、设置令牌的权限三、保存令牌信息四、使用令牌拉取代码 git:通过令牌方式访问远程仓库 一、进入仓库添加令牌 二、设置令牌的权限 三、保存令牌信息 四、使用令牌拉取代码


C++中不同类型的默认转换详解
码事漫谈2025/11/27

不熟悉默认类型转换,真的很容易写出bug!!! 在C++中,类型转换是一个非常重要的概念。下面详细讲解C++中各种默认类型转换的规则和机制。 1. 算术类型转换 整型提升 (Integral Promotion) char c = 'A'; short s = 100; int i = c + s; // char和short都提升为int 算术转换规则 // 转换优先级:long double > double > float > unsigned long long > long lo


一次性讲清楚常见的软件架构图
uzong2025/11/25

本文是根据 ProcessOn 整理汇总而来,ProcessOn 是一个非常不错的画图软件,功能强大,界面优美。 作者:面汤放盐(微信公众号) 时间:2025.11.24 架构图详细分类 架构图作为一种重要的工具,用于可视化展示软件、系统、应用程序等的体系结构及其组成部分之间的关系。 常用的架构图种类有:业务架构图、应用架构图、系统架构图、技术架构图、部署架构图、数据架构图、产品架构图、功能架构图、信息架构图等。 关于架构图所处位置,可见下图所示: 1. 业务架构图 1.1. 什么是业务架构图


Lua 的 getmetatable 函数
IMPYLH2025/11/23

Lua 的 getmetatable 函数 用于获取指定对象的元表(metatable)。元表是Lua中实现面向对象编程和运算符重载的重要机制。以下是关于getmetatable函数的详细说明: 基本语法: metatable = getmetatable(object) 参数说明: object: 可以是任意Lua值(table、userdata、string等)返回值:如果对象有元表则返回元表,否则返回nil 特性细节: 对不同类型的对象处理方式: 普通table:返回


Lua 的 error 函数
IMPYLH2025/11/21

Lua 的 error 函数是一个用于显式抛出错误的内置函数,它会中断当前程序的正常执行流程。该函数有两种调用形式: error(message, level)error(message) 参数说明: message:字符串类型的错误信息,是必选参数level:可选参数,指定错误发生的位置层级,默认为1(表示调用error的位置) 使用示例: 通过 Shift 运行 function divide(a, b) if b == 0 then error("除数不


使用 Docker 部署 RabbitMQ 的详细指南
s***55812025/11/19

使用 Docker 部署 RabbitMQ 的详细指南 在现代应用程序开发中,消息队列系统是不可或缺的一部分。RabbitMQ 是一个流行的开源消息代理软件,它实现了高级消息队列协议(AMQP)。本文将详细介绍如何使用 Docker 部署 RabbitMQ,并提供一些配置和管理的技巧。 1. 前期准备 在开始之前,请确保您的系统上已经安装了 Docker。如果尚未安装,可以参考 Docker 官方文档 或我写的前面一篇文章 CentOS 上安装 Docker 的详细指南 进行安装。 2.


LVS-NAT 模式负载均衡集群部署与配置
ttthe_MOon2025/11/18

LVS(Linux Virtual Server)是基于 Linux 的负载均衡群集技术,NAT 模式通过地址转换实现内外网通信与负载分发,核心是 LVS 服务器充当网关,将外网请求转发至后端 Web 服务器。 一、核心概念 1. LVS 基础 本质:作用在四层的负载均衡器,不提供网页服务,仅通过算法为后端服务器分流减压。 核心价值:解决单台服务器性能瓶颈,实现高可用(HA)和负载均衡(LB)。 集群定义:3 台以上服务器对外表现为一个整体,提供单一访问入口(IP / 域名)。 2. NAT 模


关于No Chatbot的思考
HeteroCat2025/11/17

前言 近期在大模型圈子里,“No Chatbot”这一概念潇潇浮出江面,它意指不再将智能体局限于传统的对话式问答系统,而是发展为具备主动规划、调用工具和协作能力的智能助理。作为一名长期研究提示词工程和智能体应用的创作者,我想结合最近的思考,与大家探讨这一趋势的内涵与意义。 什么是“No Chatbot” “No Chatbot”并不是不要聊天机器人,而是指下一代智能体需要突破单纯聊天交互的框架,具备更强的自主性和执行力。它主要包含以下特征: 任务导向:关注完成复杂任务,而不是只回答问题。系统会


Spring boot启动原理及相关组件
q***38512025/11/16

优质博文:IT-BLOG-CN 一、Spring Boot应用启动 一个Spring Boot应用的启动通常如下: @SpringBootApplication @Slf4j public class ApplicationMain { public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(ApplicationMain.


UDP服务端绑定INADDR_ANY后,客户端该用什么IP访问?
咸鱼_要_翻身2025/11/15

目录 一、问题 二、详细解释 1、INADDR_ANY 到底是什么? 2、客户端可以使用什么IP访问? 三、为什么要传IP? 1、网络层寻址的需要 2、操作系统协议栈的需要 3、服务端主机区分流量的需要 四、总结 一、问题         在UDP协议中,服务端使用INADDR_ANY了,然后客户端可以使用什么IP可以访问服务端?为什么要传IP?这是一个非常经典且重要的问题。我们来分步拆解和解答。 核心答案:当UDP服务端绑定到 INADDR_ANY (其值通常是

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2025 聚合阅读