当前位置: 首页 > news >正文

【JavaScript】前端两种路由模式,Hash路由,History 路由

从后端视角看前端路由:Hash 与 History 的原理、差异与部署要点

为什么后端也要懂前端路由

在单页应用(SPA)模式下,页面结构与切换逻辑主要在前端完成,但是否能“刷新不 404”、是否“可 SEO”、是否“URL 优雅”、以及“生产环境如何配置服务端”都直接依赖后端。
不了解前端路由机制,常见线上事故包括:用户刷新子路由直接 404静态资源被错误回退到 index.htmlAPI 404 被 “吃掉” 等。

注解:SPA 的核心是“前端拦截导航”。URL 会变,但不发起整页请求;前端根据 URL 渲染对应组件。问题在于“刷新/直达”时浏览器仍会向后端请求该路径,这正是 History 路由与后端需配合的根源。


前端路由是什么

  • 传统 MPA:每个页面一个 HTML,URL 变化 => 浏览器发起新请求 => 服务器返回新文档。

  • SPA:只有一个 index.html 作为壳;URL 变化由前端接管,前端在当前文档内渲染“新页面”

  • 两种实现路径

    • Hash 路由:利用 # 片段(location.hash),通过 hashchange 事件驱动渲染。
    • History 路由:利用 HTML5 的 history.pushState/replaceStatepopstate 事件。

注解:Hash 变更不会触发浏览器向服务器发请求History 直达/刷新会请求对应路径,因此 History 需要服务端兜底回 index.html


Hash 路由(# 路由)详解

核心原理

  • URL 形如:https://example.com/#/orders/123
  • 修改 # 后的片段不会触发整页请求;前端监听 hashchange 自行渲染。

极简实现(示意):

const routes = {'/': () => renderHome(),'/orders': () => renderOrders(),'/orders/:id': (id) => renderOrderDetail(id),
};function parseHash() {const path = location.hash.slice(1) || '/';// 省略:简单参数匹配matchAndRender(routes, path);
}window.addEventListener('hashchange', parseHash);
window.addEventListener('DOMContentLoaded', parseHash);

优缺点

  • 优点

    • 无需后端特殊配置,刷新永不 404(请求始终落到 index.html)。
    • 兼容性强(历史包袱大的环境更稳)。
  • 缺点

    • URL 带 # 不够优雅,SEO 普遍较差(片段不参与标准 HTTP 请求与响应)。
    • 统计、监控对 # 片段的采集可能需要额外处理。

注解:OAuth Implicit Flow 常把 access_token 放在 # 片段里返回(避免泄露给服务端),Hash 模式天然契合这类回调处理。

适用场景

  • 内部系统、对 SEO 不敏感、快速上线、后端不想配任何额外规则时。

History 路由(HTML5 路由)详解

核心原理

  • URL 形如:https://example.com/orders/123
  • 前端通过 history.pushState() 修改地址栏、拦截点击;浏览器 后退/前进 触发 popstate,页面在前端渲染。
  • 刷新/直达:浏览器会向服务端请求 /orders/123。若服务端无此物理文件且不回退到 index.html,就会 404

极简实现(示意):

function goto(path) {history.pushState({}, '', path);render(path); // pushState 不会触发 popstate,需要主动渲染
}window.addEventListener('popstate', () => render(location.pathname));
window.addEventListener('DOMContentLoaded', () => render(location.pathname));// 拦截站内链接点击
document.addEventListener('click', e => {const a = e.target.closest('a[href^="/"]');if (!a) return;e.preventDefault();goto(a.getAttribute('href'));
});

优缺点

  • 优点

    • URL 优雅、更接近传统网站体验;对 SEO 更友好(配合 SSR/预渲染最佳)。
    • 与服务端日志、统计、A/B 平台兼容性好(URL 即路径)。
  • 缺点

    • 需要后端配置回退到 index.html(且要避免误伤 /api 与静态资源)。
    • 需要现代浏览器(IE9+ 基本可用,但历史环境要评估)。

注解:pushState/replaceState 不会触发 popstate;只有“后退/前进”触发。因此手动导航后要主动渲染。


Hash vs History:对照速览

维度Hash 路由History 路由
URL 形态/#!/users/#/users/users
刷新行为不发起新页面请求请求 /users(需服务端兜底)
服务端配置基本不需要必须 try_files ... /index.html
SEO较差较好(配合 SSR/预渲染最佳)
兼容性旧环境稳现代环境 OK
统计/监控需处理 # 片段开箱即用
OAuth 隐式回调天然适配也可,但更常见在 Hash 场景
复杂度中(服务端+CDN 需要细化规则)

注解:有些项目“双模”:开发/预发用 Hash(省配置),生产切 History(SEO 友好、URL 优雅),迁移时注意服务端路由与静态资源路径。


生产部署要点

Nginx

server {listen 80;server_name example.com;root /var/www/app;      # SPA 构建产物目录,含 index.htmlindex index.html;# 先放 API,避免被 SPA 回退“吃掉”location /api/ {proxy_pass http://127.0.0.1:8080;}# 静态资源:强缓存 + 不回退location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {expires 1y;add_header Cache-Control "public, immutable";try_files $uri =404;}# 仅对 HTML 导航进行回退location / {try_files $uri $uri/ /index.html;}
}

注解:顺序非常重要/api/ 必须在前;静态资源必须 =404,否则 404 资源会被错误回退到 index.html,导致前端报 Unexpected token <(拿到 HTML 当 JS/CSS 解析)。

进一步精细化(只对 Accept=html 的请求回退)
map $http_accept $is_html {default 0;"~*text/html" 1;
}location / {if ($is_html) {try_files $uri $uri/ /index.html;}# 非 HTML(如 XHR 请求、文件下载)按正常 404 处理try_files $uri =404;
}

Caddy(更现代的配置体验)

example.com {root * /var/www/appencode zstd gzip@api path /api/*reverse_proxy @api http://127.0.0.1:8080@static {path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.ico *.woff *.woff2}header @static Cache-Control "public, max-age=31536000, immutable"file_server@spa {not path /api/*not path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.ico *.woff *.woff2}rewrite @spa /index.html
}

Go 原生 net/http

mux := http.NewServeMux()// 1) API 放在前面
mux.Handle("/api/", http.StripPrefix("/api", apiHandler()))// 2) 静态资源:保持原样
fs := http.FileServer(http.Dir("dist"))
mux.Handle("/assets/", fs) // 例如构建产物里的静态目录// 3) SPA 回退:只对 HTML 导航生效
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 命中真实文件则直接返回if _, err := os.Stat(filepath.Join("dist", r.URL.Path)); err == nil {http.ServeFile(w, r, filepath.Join("dist", r.URL.Path))return}// 仅对 GET + Accept: text/html 回退到 index.htmlif r.Method == http.MethodGet && strings.Contains(r.Header.Get("Accept"), "text/html") {http.ServeFile(w, r, "dist/index.html")return}http.NotFound(w, r)
})log.Fatal(http.ListenAndServe(":80", mux))

Gin

r := gin.Default()// API
r.Any("/api/*path", apiHandler)// 静态资源
r.Static("/assets", "./dist/assets")// SPA 回退
r.NoRoute(func(c *gin.Context) {accept := c.GetHeader("Accept")if c.Request.Method == http.MethodGet &&strings.Contains(accept, "text/html") &&!strings.HasPrefix(c.Request.URL.Path, "/api/") {c.File("./dist/index.html")return}c.Status(http.StatusNotFound)
})r.Run(":80")

GoFrame

s := g.Server()// API
s.Group("/api", func(g *ghttp.RouterGroup) {g.ALL("/*", ApiHandler)
})// 静态资源
s.SetServerRoot("dist")// SPA 回退(仅对 HTML 导航)
s.BindHookHandler("/*", ghttp.HookBeforeServe, func(r *ghttp.Request) {p := r.URL.Path// 命中文件则直接返回if _, err := os.Stat(filepath.Join("dist", p)); err == nil {return}// 仅对 HTML 导航回退if r.Method == "GET" &&strings.Contains(r.Header.Get("Accept"), "text/html") &&!strings.HasPrefix(p, "/api/") {r.Response.ServeFile("dist/index.html")r.ExitAll()}
})
s.Run()

注解:不要用“全局 404 回 index.html” 的偷懒做法;会把 /api 的真实 404 也吃掉,导致客户端难以发现接口错误。


部署细节与易踩坑

  1. 子目录部署与 <base> 标签
    若应用部署在子路径 /app,History 模式需在 index.html 中设置:

    <base href="/app/">
    

    并确保服务端回退到 /app/index.html,静态资源路径也以 /app 为前缀。

    注解:漏配 <base> 会导致相对路径资源 404、路由跳转错位。

  2. 资源缓存策略

    • 构建产物文件名带 hash(如 app.83f2.js),静态资源可 immutable 强缓存。
    • index.html 不建议长缓存(随发布变更,需要被及时拉取)。
  3. 仅对 HTML 导航回退

    • 根据 Accept: text/htmlGET 方法判断是否为“页面导航”。
    • 避免把 XHR、fetch()、下载等请求回退到 HTML。
  4. Service Worker 与路由

    • SW 也可拦截导航并回退 index.html,但建议 先把服务端兜底配好,再引入 SW 以降低复杂度。
  5. 滚动与定位

    • History 模式可使用 history.scrollRestoration = 'manual' 自行控制滚动。
    • Hash 模式的 #id 会触发原生锚点滚动,注意与前端路由滚动逻辑的耦合。
  6. State 体积

    • pushStatestate 对象会被历史栈保存,不要塞大对象(可能影响性能与稳定)。

选型与决策清单

  • 需要优雅 URL、SEO、与日志/风控/埋点天然对齐History,但你必须能配好反向代理/网关/CDN 路由与缓存细则。
  • 内部系统、赶进度、环境复杂或历史包袱重Hash,免配置、可快速上线。
  • 分阶段上线:测试/预发用 Hash;生产切 History(逐步完善服务端兜底与监控)。

常见问题

症状可能原因快速定位解决
刷新子路由 404History 未做兜底直接访问 /users按上文配置 try_files ... /index.html
JS/CSS 报 Unexpected token <把静态资源 404 回退到 index.html网络面板看返回体是 HTML静态资源 try_files $uri =404,不要回退
API 返回的是 HTML/api 被 SPA 回退匹配访问 /api/xxx 看返回先匹配 /api 再做 SPA 回退
子目录部署资源 404缺少 <base> 或路由前缀查看请求路径前缀<base href="/app/"> + 服务端路由前缀
SEO 无效果纯 CSR 渲染观察首屏 HTML引入 SSR/预渲染(仅 History 真正受益明显)

对比

维度HashHistory
服务端改造必须兜底到 index.html,并做 API/静态资源排除
刷新/直达永不 404会请求真实路径(必须兜底)
URL 美观一般(含 #优雅(类传统站点)
SEO/SSR强(配 SSR/预渲染)
风险点统计与回调处理细节配置错误最易出线上事故
迁移难度中(含基础设施协同)
典型用途内部系统、低成本上线面向 C 端、品牌/SEO 要求高

总结

  • Hash 路由:前端自给自足,后端几乎零配合;代价是 URL 与 SEO。
  • History 路由:用户体验与 SEO 友好,但刷新/直达必须有 服务端兜底,并和 API、静态资源做好“分流”。
  • 对 Go 后端来说,掌握本文几段 Nginx/Caddy/Gin/GoFrame 样例配置,就能把前端 History 路由稳定落地,避免 404 与静态资源回退等典型坑。

注解:真正上线前,务必按“直接打开深链 / 刷新 / 后退前进 / 断网重试 / 缓存与版本回滚”五类场景做灰度验证;把静态资源 404 与 API 404 留在它们该在的地方,把 index.html 兜底只用于“HTML 导航”。

http://www.dtcms.com/a/362904.html

相关文章:

  • UBUNTU之Onvif开源服务器onvif_srvd:2、测试
  • @Value注解底层原理(二)
  • 云端职达:你的AI求职专属猎头,颠覆传统招聘模式
  • 哈尔滨云前沿服务器托管与租用服务
  • STM32——串口
  • 在windows上使用ROS2 kilted
  • Pytorch Yolov11目标检测+window部署+推理封装 留贴记录
  • LeetCode算法日记 - Day 30: K 个一组翻转链表、两数之和
  • Unity核心概率④:MonoBehavior
  • @Hadoop 介绍部署使用详细指南
  • 从 WPF 到 Avalonia 的迁移系列实战篇6:ControlTheme 和 Style区别
  • R 语言科研绘图第 71 期 --- 散点图-边际
  • 小白也能看懂!“找不到 msvcp140.dll无法继续执行代码” 的6种简易解决方法,5 分钟快速修复
  • Watt Toolkit下载安装并加速GitHub
  • C# 原型模式(C#中的克隆)
  • 基因表达数据的K-M生存曲线的数据处理及绘制
  • Anaconda安装与使用详细教程
  • 服务器CPU飙高?排查步骤与工具推荐
  • 深入探索 HarmonyOS Stage 模型与 ArkUI:构建现代化、高性能应用
  • 【NestJS】HTTP 接口传参的 5 种方式(含前端调用与后端接收)
  • 面试新纪元:无声胜有声,让AI成为你颈上的智慧伙伴
  • 基于YOLO8的番茄成熟度检测系统(数据集+源码+文章)
  • 利用飞算Java打造电商系统核心功能模块的设计与实现
  • Controller返回CompletableFuture到底是怎么样的
  • 【DSP28335 入门教程】定时器中断:为你的系统注入精准的“心跳”
  • 在windows平台oracle 23ai 数据库上使用bbed
  • zephyr设备树的硬件描述转换为c语言
  • 梳理一下 @types/xxx
  • 【Python语法基础学习笔记】竞赛常用标准库
  • 数据库的锁级别