同步至个人站点:前端路由详解
前端路由详解: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 的支持依然不如纯路径友好。
- URL 不美观:
History 模式
为了解决 Hash 模式 URL 不美观的问题,HTML5 标准在 history 对象上增加了新的 API。这就是 History 模式的基础。
核心 API
在 HTML4 时代,window.history 只能用于前进(forward)、后退(back)和跳转(go)。
HTML5 新增了两个关键方法,允许我们在不刷新页面的情况下修改 URL:
history.pushState(state, title, url):向历史记录堆栈中添加一条新记录。history.replaceState(state, title, url):修改当前的历史记录。
例如,执行 history.pushState(null, null, '/user/id') 后,浏览器的地址栏会变为 http://www.example.com/user/id,但浏览器不会向服务器发送请求,页面也不会刷新。
实现原理
History 模式的实现比 Hash 稍微复杂一点。我们需要处理两种情况:
- 用户点击链接:前端框架会拦截
<a>标签的点击事件,阻止默认跳转,改用history.pushState修改 URL,并手动更新视图。 - 用户点击浏览器的前进/后退按钮:这会触发
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-server 或 vite),一切正常。但是,当你运行 npm run build 打包,将生成的文件部署到 Nginx 服务器后:
- 访问根路径
http://www.site.com/,页面正常显示。 - 点击导航进入
http://www.site.com/about,页面正常显示(因为是 JS 动态渲染的)。 - 但是,如果你在
/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; 的意思是:
- 先看用户请求的是不是一个真实存在的文件(
$uri)。 - 如果不是,再看是不是一个真实存在的目录(
$uri/)。 - 如果都不是,就返回
/index.html。
总结与选型指南
最后,我们用一张表格来总结两者的区别,帮助你在项目中做出选择。
| 特性 | Hash 模式 | History 模式 |
|---|---|---|
| URL 外观 | example.com/#/about | example.com/about |
| 美观度 | 丑,有“#”号干扰 | 美观,符合标准 |
| 原理 | window.location.hash | history.pushState |
| 兼容性 | 极好(IE8+) | 较好(IE10+) |
| 服务端配置 | 不需要 | 必须配置 |
| 应用场景 | 内部系统、Demo、静态资源服务器 | 正式商业项目、C 端应用 |
在现代前端开发中,除非你有特殊的兼容性需求或者由于权限问题无法配置服务器,否则强烈建议使用 History 模式。它不仅能提供更好的用户体验,也更符合 Web 标准的发展趋势。
(完)
《前端路由详解:Hash vs History》 是转载文章,点击查看原文。
