关于前端路由中的参数问题的学习(二)

1、Hash 路由和 History 路由对缓存的影响是什么?

核心结论:路由模式本身不直接影响 HTTP 缓存,但决定了缓存失效的触发条件和粒度。


一、Hash 路由的缓存特征

bash 复制代码
https://example.com/#/user/profile
维度 表现
HTTP 请求 # 后的内容不会 发送到服务端,每次路由切换只发 GET /,服务端永远看到的是同一个 URL
缓存行为 整站是一个 HTML 入口,一损俱损------HTML 缓存失效时全站刷新
版本控制 通常靠构建时给 index.html 加 hash(如 app.abc123.js),HTML 本身无 hash
典型问题 发版后用户访问的还是旧 HTML,引用的旧 JS/CSS 可能已被 CDN 清理,导致 404

缓存失效难点

html 复制代码
<!-- 发版前 -->
<script src="/app.abc123.js"></script>

<!-- 发版后,用户浏览器缓存了旧的 index.html,还在请求 app.abc123.js -->
<!-- 但 CDN 上只有 app.def456.js,直接白屏 -->

解决 :必须确保 index.html 永不缓存极短缓存Cache-Control: no-cache),让浏览器每次重新获取 HTML 拿到最新资源映射。


二、History 路由的缓存特征

arduino 复制代码
https://example.com/user/profile
维度 表现
HTTP 请求 每个路由路径独立请求(首次访问 /user/profile 会发这个请求到服务端)
服务端配合 需要配置回退到 index.html (Nginx try_files 或服务端路由)
缓存行为 可以按路由粒度配置缓存策略,但通常仍回退到同一个 HTML
SEO/首屏 服务端可针对不同路径做差异化渲染(SSR/SSG)

缓存配置更灵活

nginx 复制代码
# Nginx 示例:API 不缓存,HTML 不缓存,静态资源长期缓存
location /api/ {
    add_header Cache-Control "no-store";
}

location / {
    try_files $uri $uri/ /index.html;
    add_header Cache-Control "no-cache";  # HTML 始终重新验证
}

location ~* \.(js|css|png|webp)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

三、关键差异对比

场景 Hash 路由 History 路由
HTML 缓存策略 必须禁止缓存,否则发版后白屏风险极高 同样需要禁止 HTML 缓存,但可按路径做 SSR 差异化
CDN/边缘缓存 整站一个 URL,无法针对特定路由预热 可预热特定路径(如 /product/detail
服务端感知路由 不能,服务端永远只看到 / 能,可做路径级别的缓存头、SSR、权限校验
资源 404 风险 高(旧 HTML 指向已删除的旧 hash 文件) 同样存在,但可通过服务端路由兜底
多页应用迁移 天然单页,无迁移成本 需确保旧书签/外链能正确回退

四、对实际开发的影响

1. 静态资源命名(两者通用)

无论哪种路由,必须给 JS/CSS 加 content-hash

js 复制代码
// vite/webpack 配置
output: {
  filename: 'js/[name]-[contenthash:8].js'
}

这样 app.js 内容不变则 URL 不变(长期缓存),内容变了 URL 变(强制更新)。

2. HTML 的缓存策略(核心)

http 复制代码
# 这是最关键的响应头,两种路由模式都需要
Cache-Control: no-cache  # 或 no-store,确保每次重新验证

如果 HTML 被强缓存,Hash 和 History 都会发版后用户看不到更新

3. History 路由的特殊陷阱

nginx 复制代码
# 错误配置:把 /user/profile 缓存了 1 小时
location /user/ {
    expires 1h;  # 灾难!用户 1 小时内看不到该路由的更新
}

必须确保所有非文件路径都回退到 index.html 且不缓存


五、一句话总结

路由模式决定的是 URL 形态和服务端参与度,不直接改变浏览器缓存机制。真正决定缓存行为的是 HTTP 响应头(Cache-ControlETag)和资源命名策略。Hash 路由因为服务端无感知,对 HTML 缓存更敏感;History 路由因为服务端可介入,缓存策略可以做得更精细,但也更容易配错。

2、为什么#后的内容不会发送给服务器?

这是 HTTP 协议的规定,# 及之后的内容属于「片段标识符」(Fragment Identifier),设计初衷是定位文档内部位置,不是请求的一部分。


一、协议层面的规定

RFC 3986 对 URI 结构的定义

markdown 复制代码
  https://example.com/path?query=123#section2
  \_____________________/\___________/\______/
           |                   |          |
        请求部分              查询参数   片段标识符
       (发给服务端)         (发给服务端)  (浏览器自用)
  • Scheme (https://) → 协议
  • Authority (example.com) → 主机
  • Path (/path) → 资源路径
  • Query (?query=123) → 查询参数,会发送
  • Fragment (#section2) → 片段,不发送

为什么这样设计?

片段标识符的原始用途是锚点定位

html 复制代码
<!-- 点击跳转到页面内 id="chapter3" 的元素 -->
<a href="https://example.com/doc.html#chapter3">第三章</a>

如果 #chapter3 发给服务端,服务端拿到它没有任何意义------它不知道页面里哪个元素叫 chapter3,这是文档结构层面的信息,只有浏览器在解析 HTML 后才能处理


二、实际验证

浏览器行为

javascript 复制代码
// 你在地址栏输入
location.href = 'https://api.example.com/data#hashvalue'

// 实际发出的 HTTP 请求
GET /data HTTP/1.1        ← 注意:没有 #hashvalue
Host: api.example.com

服务端永远看不到

javascript 复制代码
// Node.js Express
app.get('/data', (req, res) => {
  console.log(req.url);        // 输出: "/data"
  console.log(req.originalUrl); // 输出: "/data"
  // req 对象里没有任何地方能拿到 #hashvalue
})

浏览器内部处理流程

bash 复制代码
用户输入/点击: https://example.com/page#section
        │
        ▼
    解析 URL
        │
        ├── 提取请求部分: https://example.com/page
        │       │
        │       ▼
        │   发送 HTTP 请求到服务端
        │       │
        │       ▼
        │   接收响应 (HTML/JSON/...)
        │
        └── 提取片段: #section
                │
                ▼
            浏览器本地处理
                │
                ├── 页面已加载?→ 滚动到 id="section" 的元素
                │
                └── 页面未加载?→ 等页面渲染完成后滚动

三、SPA 路由的「副作用」

Hash 路由(如 Vue Router 的 createWebHashHistory利用了这个协议特性

javascript 复制代码
// 浏览器地址栏
https://app.example.com/#/user/profile

// 实际 HTTP 请求(每次路由切换)
GET / HTTP/1.1              ← 永远是根路径,服务端无感知
Host: app.example.com

// 路由变化由浏览器本地处理
window.addEventListener('hashchange', () => {
  const route = location.hash.slice(1)  // "/user/profile"
  // 前端自己渲染对应组件,不发新请求
})

好处 :不需要服务端配合,任何静态服务器都能部署。

代价 :SEO 不友好(搜索引擎通常忽略 # 后内容),URL 带 # 不够美观。


四、对比 History 路由

javascript 复制代码
// History 路由(createWebHistory)
https://app.example.com/user/profile

// 路由切换时
history.pushState({}, '', '/user/profile')  // 浏览器地址变了
// 但默认不会发请求!需要前端拦截处理

// 用户刷新页面或首次访问时
GET /user/profile HTTP/1.1    ← 这个会发给服务端!

这里 /user/profile路径部分 ,不是片段,所以必须服务端配置回退

nginx 复制代码
# 否则服务端返回 404,因为磁盘上根本没有 /user/profile/index.html
location / {
    try_files $uri $uri/ /index.html;  # 所有路径都回退到 index.html
}

五、一句话总结

# 是浏览器和文档之间的「私有约定」,从 HTTP 协议诞生起就不是请求的一部分。SPA 的 Hash 路由是「借用」这个特性实现前端路由,而 History 路由则是通过 history.pushState 伪造路径,需要服务端配合兜底。`

3、查询参数,为什么可以发送给服务端?它改变了路由了吗?

查询参数(?key=value没有改变路由 (Path 没变),但它是 HTTP 协议规定的请求组成部分,所以必须发送给服务端。


一、协议层面的区别

bash 复制代码
https://example.com/user?id=123#section
          \____/ \______/ \______/
            │       │        │
           Path   Query    Fragment
           │       │        │
        资源路径   参数      锚点
           │       │        │
        会发送   会发送    不发送
部分 协议角色 发送?
Path (/user) 标识「哪个资源」 ✅ 是
Query (?id=123) 资源的「附加条件」 ✅ 是
Fragment (#section) 文档「内部位置」 ❌ 否

二、为什么 Query 必须发送?

1. 它是资源定位的一部分

bash 复制代码
GET /user?id=123 HTTP/1.1

服务端收到这个请求时,Path 和 Query 共同决定返回什么

javascript 复制代码
// Express 示例
app.get('/user', (req, res) => {
  const id = req.query.id  // "123"
  // 根据 id 查询数据库,返回不同用户数据
  res.json(await db.user.findById(id))
})

没有 Query,服务端无法区分

  • /user?id=123 → 返回用户 123 的资料
  • /user?id=456 → 返回用户 456 的资料

2. 协议设计如此(RFC 3986)

Query 是 "hierarchical data" (层级数据),用于缩小资源范围

Fragment 是 "secondary resource" (次级资源),用于客户端内部定位

简单说:Query 影响「服务端返回什么」,Fragment 影响「浏览器怎么看」。


三、Query 改变路由了吗?

没有。 路由(Path)还是 /user,Query 只是给这个路由附加了条件

地址 Path(路由) Query 是否同一页面
/user?id=123 /user id=123 ✅ 是
/user?id=456 /user id=456 ✅ 是
/user/123 /user/123 ❌ 不是(RESTful 路径参数)

Query vs 路径参数的区别

javascript 复制代码
// Query 方式(条件过滤)
GET /user?id=123        → 在 /user 这个集合里筛选 id=123 的

// 路径参数方式(资源定位)
GET /user/123           → 直接定位到 id=123 这个资源

两者服务端都能收到,但语义不同:

  • Query :可选条件、筛选、搜索(?page=2&keyword=vue
  • Path 参数 :资源唯一标识(/user/123

四、实际场景对比

场景 地址 服务端收到 用途
分页 /list?page=2 /list?page=2 服务端按页码查询
搜索 /search?q=vue /search?q=vue 服务端按关键词检索
筛选 /products?category=phone 同上 服务端按分类过滤
锚点定位 /doc#chapter3 /doc 服务端只返回文档,浏览器自己滚动

五、一句话总结

Query(? 后)是 HTTP 请求的一部分,因为它影响「服务端返回什么内容」;Fragment(# 后)不是,因为它只影响「浏览器怎么展示内容」。Query 不改变路由(Path),只是给同一路由附加了查询条件。

4、?后面的内容变化,会使得浏览器重新发起请求吗?

会,但取决于具体变化方式和浏览器/框架的处理。


一、直接答案

操作 是否重新请求 原因
地址栏直接修改 Query 后回车 ✅ 会 完整导航,浏览器重新加载页面
<a href="?page=2"> 点击 ✅ 会 同上,标准导航行为
history.pushState({page:2}, '', '?page=2') ❌ 不会 仅修改地址栏,不发请求
SPA 框架路由切换(如 Vue Router) ❌ 不会 框架拦截,前端本地处理

二、核心区分:浏览器导航 vs 地址栏伪装

场景 1:浏览器认为「页面变了」→ 重新请求

ini 复制代码
当前: https://example.com/list?page=1
用户: 手动改成 ?page=2 并回车
      或点击 <a href="?page=2">

浏览器行为:
  1. 解析新 URL
  2. 发现 Query 变了 → 判定为不同资源
  3. 发送 GET /list?page=2
  4. 重新加载页面

浏览器判定依据:完整 URL(含 Query)变了 = 新资源。

场景 2:JS 伪装地址 → 不请求

javascript 复制代码
// 纯修改地址栏,不触发导航
history.pushState({page: 2}, '', '?page=2')

// 浏览器行为:
//   - 地址栏显示 ?page=2
//   - 不发送任何 HTTP 请求
//   - 页面不刷新
//   - 但会产生一条新历史记录

SPA 路由的原理就是这个 :用 pushState 骗过地址栏,实际前端自己处理。


三、Query 变化 vs Hash 变化的对比

操作 Query 变化 Hash 变化
直接修改地址栏回车 ✅ 重新请求 ❌ 不请求(页面内锚点跳转)
pushState 修改 ❌ 不请求 ❌ 不请求
<a href="..."> 点击 ✅ 重新请求 ❌ 不请求(触发 hashchange
浏览器前进/后退 按历史记录决定 按历史记录决定

关键差异

  • Hash 变化 :浏览器天生不请求 (协议规定 # 不发送)
  • Query 变化 :浏览器默认会请求,除非被 JS 拦截

四、SPA 框架如何处理 Query?

Vue Router 示例

javascript 复制代码
// 当前: /list?page=1
router.push({ query: { page: 2 } })  // 或点击 <router-link :to="{ query: { page: 2 } }">

// 框架内部行为:
// 1. 调用 history.pushState(null, '', '?page=2')  ← 不请求
// 2. 触发路由守卫 + 组件重新渲染
// 3. 组件内通过 useRoute().query.page 获取新值
// 4. 你可能在 watch 里发 API 请求获取新数据

注意 :框架不发页面请求,但你的代码通常会发 API 请求

vue 复制代码
<script setup>
const route = useRoute()

watch(() => route.query.page, (newPage) => {
  // 这里你自己调 API
  fetchData(newPage)
})
</script>

五、浏览器缓存的影响

即使 Query 变化导致重新请求,缓存策略仍然生效

http 复制代码
GET /list?page=1  → 200,缓存
GET /list?page=2  → 200,新请求(URL 不同,缓存不命中)
GET /list?page=1  → 304 或 from disk cache(URL 相同,缓存命中)

Query 是 URL 的一部分 ,所以 ?page=1?page=2两个独立的缓存条目


六、一句话总结

Query 变化默认会触发浏览器重新请求,因为完整 URL 变了。但 SPA 通过 history.pushState 拦截了这个行为,只改地址栏不发请求,由前端自己处理数据更新。

5、这个问题的根源是什么?本质是啥?

这个问题的根源在于浏览器对 URL 变化的「导航判定」机制


一、本质:浏览器的「导航」vs「状态」

bash 复制代码
┌─────────────────────────────────────────┐
│           URL 结构                      │
│  https://host/path?query#fragment      │
│           │      │      │               │
│           │      │      └─ 浏览器私用    │
│           │      │         (不发送)      │
│           │      │                      │
│           │      └─ 查询条件             │
│           │         (发送,影响服务端)    │
│           │                             │
│           └─ 资源路径                    │
│              (发送,标识资源)             │
└─────────────────────────────────────────┘

浏览器只认一个原则:

「导航」= 地址栏的 URL 变了,且不是 JS 伪装的 → 重新请求资源


二、核心矛盾

层面 设计初衷 被 SPA 借用后的冲突
HTTP 协议 Query 是请求的一部分,变化 = 新资源 SPA 想把它当「前端状态」用
浏览器导航 URL 变化 = 页面跳转 SPA 想做到「无刷新更新」
历史记录 记录用户访问过的「页面」 SPA 想记录「应用状态」

SPA 的解决方案history.pushState ------ 欺骗浏览器

javascript 复制代码
// 浏览器以为:用户在同一个页面内操作(不导航)
// 实际上:前端偷偷改了地址,自己处理一切
history.pushState({}, '', '?page=2')

三、一句话本质

Query 属于 HTTP 请求,变化触发重新请求是浏览器的「默认导航行为」;SPA 用 pushState 拦截了这个行为,把 Query 降级为「前端状态管理工具」,这是「协议设计」与「应用需求」之间的张力。

6、history.pushState({}, '', '?page=2') 这会触发重新向服务器获取资源吗?

不会。 pushState 只改地址栏,不发任何请求。


一、直接验证

javascript 复制代码
// 执行这行代码
history.pushState({}, '', '?page=2')

// 浏览器行为:
//   ✅ 地址栏变成 ?page=2
//   ✅ 产生一条新历史记录(可前进/后退)
//   ❌ 不发送 HTTP 请求
//   ❌ 不重新加载页面
//   ❌ 不触发 popstate 事件(前进/后退才触发)

二、与真正导航的对比

操作 地址栏变化 发请求? 页面刷新? 历史记录?
pushState('?page=2') ❌ 否 ❌ 否 ✅ 新增
location.href = '?page=2' ✅ 是 ✅ 是 ✅ 新增
location.replace('?page=2') ✅ 是 ✅ 是 ❌ 替换
点击 <a href="?page=2"> ✅ 是 ✅ 是 ✅ 新增
浏览器前进/后退 按历史 按历史 ---

三、SPA 路由的核心原理

javascript 复制代码
// Vue Router / React Router 内部大致逻辑:
function navigateTo(query) {
  // 1. 骗过浏览器:只改地址,不请求
  history.pushState({ query }, '', '?' + new URLSearchParams(query))
  
  // 2. 前端自己处理:匹配路由、渲染组件、发 API
  const matchedRoute = matchRoute(location.pathname)
  render(matchedRoute.component)
  fetchData(query)  // 你自己调接口取数据
}

关键pushState 让浏览器「以为」还在当前页,SPA 趁机接管一切。


四、注意一个细节

pushState 本身不触发 popstate,但前进/后退会:

javascript 复制代码
// 用户点击后退按钮
window.addEventListener('popstate', (e) => {
  // 这里能拿到 pushState 时传的第一个参数
  console.log(e.state)  // { query: ... }
  // 前端自己恢复对应状态
})

五、一句话总结

pushState 是浏览器的「地址栏伪装 API」,只操作历史记录和地址显示,完全不涉及网络请求。SPA 路由的无刷新体验,全靠它「欺骗」浏览器实现的。

7、是不是说history路由不会触发新的请求到服务器?

不是。History 路由本身不会触发请求,但用户刷新或直接访问时会触发。


一、区分两个概念

场景 是否发请求 原因
前端 pushState 切换路由 ❌ 不会 JS 只改地址栏,不发请求
用户按 F5 刷新页面 ✅ 会 浏览器重新加载当前 URL
用户直接输入地址访问 ✅ 会 浏览器发起全新导航
点击外部链接进入 ✅ 会 同上

二、History 路由的完整流程

sql 复制代码
用户点击 <router-link to="/user/123">
        │
        ▼
    Vue Router 拦截点击
        │
        ▼
    history.pushState(null, '', '/user/123')
        │
        ├── 地址栏变成 /user/123
        ├── 新增历史记录
        ├── ❌ 不发 HTTP 请求
        │
        ▼
    前端匹配路由 → 渲染 User 组件
        │
        ▼
    组件内调 API: fetch('/api/user/123')  ← 这个请求是你自己发的

注意pushState 那一步确实没请求,但组件加载数据时的 fetch 是真实请求


三、服务端必须配合的原因

当用户刷新 或直接访问 https://app.com/user/123 时:

bash 复制代码
浏览器发送: GET /user/123 HTTP/1.1

服务端收到这个请求,但磁盘上根本没有 /user/123/index.html
        │
        ▼
    必须配置回退:
    
    Nginx: try_files $uri $uri/ /index.html
    Node:  app.get('*', (req, res) => res.sendFile('index.html'))
    
        │
        ▼
    返回 index.html → 浏览器加载 SPA → 前端路由接管 → 渲染 User 组件

如果没有回退配置:服务端返回 404,SPA 根本加载不起来。


四、Hash 路由 vs History 路由对比

场景 Hash 路由 (/#/user/123) History 路由 (/user/123)
pushState/hashchange 切换 ❌ 不发请求 ❌ 不发请求
用户刷新页面 ❌ 不发请求(服务端只看到 / 会发请求 (服务端看到 /user/123
需要服务端配置 不需要 必须配置回退
为什么能直接用 # 后内容不发送,服务端无感知 路径变化服务端能感知,必须配合

五、一句话纠正

History 路由的「前端切换」不会触发请求(靠 pushState),但「用户刷新或直接访问」会触发请求(这是标准浏览器导航)。所以 History 路由必须服务端配置回退到 index.html,否则刷新就 404。

相关推荐
IT_陈寒2 小时前
SpringBoot自动配置这个坑,我踩进去又爬出来了
前端·人工智能·后端
runnerdancer11 小时前
LLM是怎么处理messages数组的,提示词缓存又是什么
前端·agent
陈随易12 小时前
VSCode的Copilot扩展支持接入DeepSeek,Kimi了!
前端·后端·程序员
我不是外星人13 小时前
有了 Harness Engineering ,真的还需要研发工程师吗?
前端·后端·ai编程
IT_陈寒16 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
Jackson__17 小时前
分享一个横向滚动案例,带悬停暂停,通用性很强
前端
MariaH17 小时前
git rebase的使用
前端
_柳青杨17 小时前
深入理解 JavaScript 事件循环
前端·javascript
阡陌Jony17 小时前
关于前端性能优化的一些问题:
前端