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,前端框架就在这片私域里自建了一套路由系统。这是「协议设计的简洁性」与「应用需求的复杂性」之间的张力。