关于前端路由中的参数问题的学习(一): params,query, hash(#)

1、Query 与 params的区别是什么?

这是两个层面的概念,容易混是因为框架把两者都封装成了「获取参数」的 API。


一、协议层面的区别

sql 复制代码
GET /user/123?ref=wechat HTTP/1.1
      │      │  │
      │      │  └── Query(查询参数)
      │      └───── 分隔符
      └──────────── Path 参数(也叫 Route/URL 参数)
维度 Path 参数 (/user/123) Query 参数 (?ref=wechat)
在 URL 中的位置 路径的一部分 路径之后,以 ? 开头
协议语义 资源的唯一标识 请求的附加条件
是否可选 通常必须(决定访问哪个资源) 通常可选(不影响资源定位)
服务端路由匹配 /user/:id 匹配 /user/123 路由匹配后额外解析
RESTful 规范 ✅ 标准做法 ⚠️ 用于过滤/分页/搜索

二、代码层面的对比

Express 示例

javascript 复制代码
// 请求: GET /user/123?ref=wechat

app.get('/user/:id', (req, res) => {
  console.log(req.params.id)   // "123"  ← Path 参数
  console.log(req.query.ref)   // "wechat"  ← Query 参数
})

Vue Router 示例

javascript 复制代码
// 路由配置
{ path: '/user/:id', component: User }

// 访问 /user/123?ref=wechat
const route = useRoute()

route.params.id     // "123"      ← Path 参数
route.query.ref     // "wechat"   ← Query 参数

框架把两者都叫做「参数」,但来源完全不同:

来源 框架 API 从哪来
Path 参数 req.params / route.params URL 路径匹配路由定义中的 :xxx
Query 参数 req.query / route.query URL 中 ? 后的解析结果

三、使用场景对比

场景 应该用 原因
查看用户详情页 /user/123 123 是资源的唯一标识
分页 /list?page=2 页码是查询条件,不是资源本身
搜索过滤 /products?category=phone 筛选条件,可选
文章详情 /article/abc123 文章 ID 是资源标识
带来源统计的跳转 /article/abc123?ref=banner ref 是附加追踪信息

四、一个容易混淆的点

有些框架(如 NestJS)把 Path 参数也叫 Param,但和 Query 是分开的装饰器:

typescript 复制代码
@Get('/user/:id')
findOne(
  @Param('id') id: string,      // ← 来自路径 /user/123
  @Query('ref') ref: string     // ← 来自查询 ?ref=wechat
) {
  // ...
}

五、一句话总结

Path 参数是「地址的一部分」,标识「找哪个资源」;Query 参数是「附加条件」,标识「怎么找/要什么格式」。框架把两者都叫做 params 是为了 API 方便,但协议层面完全是两回事。

2、Query 参数不影响资源定位,那是否会导致重新发起请求呢?

Query 参数是 URL 的一部分 ,变化后浏览器会判定为不同的 URL ,所以默认会重新请求


一、直接回答

操作 是否重新请求 原因
地址栏直接改 ?page=1?page=2 回车 ✅ 会 完整 URL 变了,浏览器标准导航
点击 <a href="?page=2"> ✅ 会 同上
history.pushState(null, '', '?page=2') ❌ 不会 JS 伪装,浏览器被欺骗
SPA 框架路由切换(如 router.push({ query: { page: 2 } }) ❌ 不会 内部就是 pushState

二、关键区分

bash 复制代码
URL 变化 ≠ 一定发请求

发不发请求,取决于「浏览器是否认为是导航」:

标准导航(地址栏回车、点击链接)     → 发请求
JS 伪装(pushState/replaceState)     → 不发请求

Query 参数本身不影响这个判定逻辑,它只是 URL 的一部分。


三、缓存层面的影响

ini 复制代码
GET /list?page=1  → 缓存键 A
GET /list?page=2  → 缓存键 B(独立缓存,互不影响)

即使两次请求服务端返回的内容结构一样,浏览器也视为不同资源。


四、一句话总结

Query 参数变化会导致重新请求,不是因为它是「查询条件」,而是因为它是「URL 的一部分」。URL 变了,浏览器默认就当新资源处理------除非被 pushState 拦截。

3、params,query, # 概念总结

维度 Path 参数 (/user/123) Query 参数 (?page=2) Fragment (#section)
URL 位置 路径段 ? 之后 # 之后
协议角色 资源唯一标识 查询/过滤条件 文档内锚点定位
是否发送给服务端 ✅ 是 ✅ 是 ❌ 否
服务端能否获取 req.params req.query ❌ 不能
变化是否触发浏览器请求 ✅ 是(标准导航) ✅ 是(标准导航) ❌ 否(天生不请求)
SPA pushState 后是否请求 ❌ 否 ❌ 否 ❌ 否
缓存键是否包含 ✅ 是 ✅ 是 ❌ 否
是否影响缓存独立性 ✅ 独立条目 ✅ 独立条目 ❌ 同一缓存
RESTful 语义 资源定位(必须) 筛选/分页(可选)
SEO 权重 忽略
SPA 路由实现 History 模式 pushState History 模式 pushState Hash 模式 hashchange
需要服务端配合 必须(回退 index.html) 必须(同左) 不需要
刷新页面风险 404(无回退时) 404(无回退时) 无风险
典型使用场景 /user/123 用户详情 /list?page=2 分页 页面内跳转、Hash 路由
框架获取方式 route.params.id route.query.page location.hash

核心公式

ini 复制代码
浏览器发请求 = URL 变化(Path/Query) + 标准导航(非 pushState)

缓存键 = 完整 URL = Scheme + Host + Path + Query
       (不含 Fragment)

一句话区分

Path 参数告诉服务端「找谁」,Query 参数告诉服务端「怎么找」,Fragment 只告诉浏览器「跳到哪」------服务端根本看不见 Fragment。

4、Fragment 后可以跟多个参数吗?

可以,但不是标准参数格式,需要你自己解析。


一、语法上允许

bash 复制代码
https://example.com/page#section?foo=bar&baz=qux

但这里有个陷阱:?& 在 Fragment 中不会被浏览器特殊处理,它们只是普通字符。


二、实际解析结果

javascript 复制代码
const url = new URL('https://example.com/page#section?foo=bar&baz=qux')

console.log(url.hash)      // "#section?foo=bar&baz=qux"
console.log(url.search)    // ""  ← Query 为空!

浏览器把 # 后的所有内容都当 Fragment ,包括 ?&


三、如果你想"模拟"参数

需要手动解析:

javascript 复制代码
const hash = location.hash.slice(1)  // "section?foo=bar&baz=qux"

// 自己拆
const [anchor, queryString] = hash.split('?')
const params = new URLSearchParams(queryString)

console.log(anchor)        // "section"
console.log(params.get('foo'))  // "bar"

四、对比标准 Query

写法 浏览器解析 用途
?foo=bar#section search="?foo=bar" hash="#section" ✅ 标准,Query 发给服务端
#section?foo=bar hash="#section?foo=bar" search="" ❌ 非标准,全当 Fragment

五、实际应用场景

Hash 路由中的"伪 Query"

javascript 复制代码
// Vue Router Hash 模式
https://app.com/#/user?id=123&tab=profile

// 实际
hash = "#/user?id=123&tab=profile"

// 框架内部解析:
//   path: "/user"
//   query: { id: "123", tab: "profile" }

这是前端框架自己约定的格式,不是浏览器原生支持的。


六、一句话总结

Fragment 后可以跟任意字符(包括 ? &),但浏览器不会把它们当 Query 解析。需要参数时,要么放 ? 之前(标准 Query),要么自己在 Fragment 里约定格式手动拆。

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

URL 是「分层结构」,但浏览器和服务端对分层的「解释权」不同。


一、核心矛盾

scss 复制代码
同一串 URL,三方各自解析:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   HTTP 协议  │     │   浏览器     │     │   服务端     │
│  (RFC 3986) │     │  (地址栏+JS) │     │  (收到请求)  │
├─────────────┤     ├─────────────┤     ├─────────────┤
│ 只认到 #前  │     │ 全都要管     │     │ 只认到 #前  │
│ 后面是客户端 │     │ 包括:       │     │ 后面不存在   │
│ 自己的事    │     │ - 发什么请求 │     │             │
│             │     │ - 历史记录   │     │             │
│             │     │ - 缓存键     │     │             │
│             │     │ - 页面内滚动 │     │             │
└─────────────┘     └─────────────┘     └─────────────┘

Fragment 的设计初衷:服务端返回完整文档,浏览器自己跳转到内部位置。

SPA 的「借用」 :把 Fragment 当成「前端路由状态」,这是协议设计之外的二次发明


二、本质:「谁有权解释 URL」

层级 谁解释 解释范围
协议层 HTTP 标准 # 是边界,后面不发送
浏览器层 浏览器厂商 # 后的行为自己定(滚动、历史、JS 读取)
应用层 前端框架 # 后发明路由语法,自己解析

冲突点 :协议说 # 后是「文档位置」,SPA 把它变成「应用状态」。


三、一句话

Fragment 的本质是「客户端私域」------协议把它划给浏览器,浏览器把它开放给 JS,前端框架就在这片私域里自建了一套路由系统。这是「协议设计的简洁性」与「应用需求的复杂性」之间的张力。

相关推荐
阡陌Jony1 小时前
缓存相关学习笔记(一):Service Worker 缓存
前端
假如让我当三天老蒯1 小时前
前端跨域解决方案(学习用)
前端·javascript·面试
阡陌Jony1 小时前
关于前端路由中的参数问题的学习(二)
前端
IT_陈寒2 小时前
SpringBoot自动配置这个坑,我踩进去又爬出来了
前端·人工智能·后端
runnerdancer11 小时前
LLM是怎么处理messages数组的,提示词缓存又是什么
前端·agent
陈随易12 小时前
VSCode的Copilot扩展支持接入DeepSeek,Kimi了!
前端·后端·程序员
我不是外星人13 小时前
有了 Harness Engineering ,真的还需要研发工程师吗?
前端·后端·ai编程
IT_陈寒16 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
Jackson__17 小时前
分享一个横向滚动案例,带悬停暂停,通用性很强
前端