frontend-bug: 主题列表点搜索第一回不生效(路由守卫取消了自家请求)
这个提交包含在:
父节点
81104fc6d9
当前提交
998d90d425
@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
date: 2026-05-13
|
||||||
|
type: frontend-bug
|
||||||
|
module: admin-product-line-list
|
||||||
|
priority: high
|
||||||
|
notify: ["@mmg"]
|
||||||
|
status: pending
|
||||||
|
---
|
||||||
|
|
||||||
|
# 主题列表「点搜索第一回必然不好使,第二回才好使」— 路由守卫自己取消了自己发的请求
|
||||||
|
|
||||||
|
## 现象
|
||||||
|
|
||||||
|
管理后台「产品管理 → 主题列表」(`/product/line`),切换筛选条件(主题类型下拉或关键词)后点「搜索」按钮:
|
||||||
|
|
||||||
|
- **第 1 次点搜索:列表无任何变化**(看起来没反应)
|
||||||
|
- **第 2 次点同一个搜索:列表才刷新**
|
||||||
|
|
||||||
|
DevTools Network 面板可见:每次点搜索都发出 **2 个 URL 完全相同的请求**(如 `list?keyword=&productType=CORE&page=1&pageSize=20`):
|
||||||
|
- 请求 1:`status = (canceled)`,`size = 0.0 kB`,4ms
|
||||||
|
- 请求 2:`status = 200`,`size = 1.2 kB`,61ms
|
||||||
|
|
||||||
|
100% 必现。
|
||||||
|
|
||||||
|
## 根因(纯前端,后端无关)
|
||||||
|
|
||||||
|
文件:`hl-ui/src/views/product/line/index.vue` + 通用的 `hl-ui/src/router/index.js` + `hl-ui/src/utils/request.js`
|
||||||
|
|
||||||
|
`handleSearchSubmit` 里干了两件事(顺序):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleSearchSubmit = debounce(() => {
|
||||||
|
syncToUrl() // 1. 调 router.replace({ query }) 把筛选条件同步到 URL
|
||||||
|
resetAndLoad() // 2. 调 fetchList(),axios 发请求
|
||||||
|
}, 300)
|
||||||
|
```
|
||||||
|
|
||||||
|
而 `router/index.js` 的 `beforeEach` 守卫里每次路由跳转前会调 `clearAllPendingRequests()` 取消所有正在飞的 axios 请求(项目自带的请求清理机制,用来防止路由切换后还在跑的旧请求污染新页面)。
|
||||||
|
|
||||||
|
**时序冲突**:
|
||||||
|
|
||||||
|
1. `syncToUrl()` 触发 `router.replace`,但 `beforeEach` 是**异步**触发的(微任务/下一拍)
|
||||||
|
2. `resetAndLoad()` **同步**立即调 axios 发出请求 A,加入 `pendingRequests` Map
|
||||||
|
3. 这一拍微任务跑到 `beforeEach` → `clearAllPendingRequests()` → **把刚发出的请求 A 取消了**(canceled)
|
||||||
|
4. 但 `watch(route.query)` 或 `useSearchParams` 的 query 同步逻辑监听到 URL 变化,**又补发了请求 B**
|
||||||
|
5. 请求 B 这次没有再触发路由跳转,beforeEach 不再跑,请求 B 顺利 200 → 列表刷新
|
||||||
|
|
||||||
|
**所以用户看到的现象是:点 1 次搜索 → 2 个请求 → 第 1 个被自家路由守卫掐了 → 第 2 个才成功 → 因为防抖 300ms 的存在,用户感觉是"点了没反应",下一次手动再点才好使。**
|
||||||
|
|
||||||
|
实际上每次都是「第 1 个被取消、第 2 个成功」;但视觉上数据更新有延迟 + canceled 的请求让 loading 状态错乱,**用户主观感受就是「第一次不生效」**。
|
||||||
|
|
||||||
|
## 修复方案(前端,2 选 1)
|
||||||
|
|
||||||
|
### 改法 A(推荐,最小改动)— `handleSearchSubmit` 里只发请求,不动 URL
|
||||||
|
|
||||||
|
URL 同步不是必须每次都做,可以从"搜索时同步"改成"组件卸载/页面切换时同步",或者干脆只用 `replaceState` 不触发 vue-router(避免触发 beforeEach):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleSearchSubmit = debounce(() => {
|
||||||
|
// syncToUrl() // ❌ 删掉,或下面替换成静默 replaceState
|
||||||
|
// 静默版(如果还想保留 URL 可分享的能力):
|
||||||
|
// const url = new URL(window.location.href)
|
||||||
|
// Object.entries(searchForm).forEach(([k,v]) => v ? url.searchParams.set(k,v) : url.searchParams.delete(k))
|
||||||
|
// window.history.replaceState({}, '', url) // 绕过 vue-router,不触发 beforeEach
|
||||||
|
resetAndLoad()
|
||||||
|
}, 300)
|
||||||
|
```
|
||||||
|
|
||||||
|
`window.history.replaceState` 不会触发 vue-router 的 `beforeEach`,自然不会触发 `clearAllPendingRequests`。
|
||||||
|
|
||||||
|
### 改法 B — `syncToUrl()` 后等 router.replace 完成的 Promise 再 `resetAndLoad()`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleSearchSubmit = debounce(async () => {
|
||||||
|
await syncToUrl() // 让 router.replace 走完 beforeEach(此时 pendingRequests 是空的,cancel 啥都没影响)
|
||||||
|
resetAndLoad() // 这次发出的请求不会被自己取消
|
||||||
|
}, 300)
|
||||||
|
```
|
||||||
|
|
||||||
|
需要 `syncToUrl()` 返回 `router.replace(...)` 的 Promise。
|
||||||
|
|
||||||
|
### 改法 C(不推荐)— 让 `clearAllPendingRequests` 不在 beforeEach 跑
|
||||||
|
|
||||||
|
这是改通用 request 拦截器,会影响所有页面的路由切换清理逻辑,风险大,**不推荐**。
|
||||||
|
|
||||||
|
## 推荐方案
|
||||||
|
|
||||||
|
**改法 A**:`handleSearchSubmit` 内不用 vue-router 改 URL,直接 `window.history.replaceState` 静默同步。改动局限在 `product/line/index.vue` 一个文件,零副作用。
|
||||||
|
|
||||||
|
## 顺手检查
|
||||||
|
|
||||||
|
同一个团队规律:项目中其他列表页凡是用了「点搜索 → syncToUrl → fetchList」的双步模式都可能踩这个坑。建议 grep `syncToUrl` 找出所有调用点,统一改成方案 A 的写法。常见嫌疑列表:
|
||||||
|
|
||||||
|
- `views/order/list`(订单列表)
|
||||||
|
- `views/order-v2/list`(订单管理 v2)
|
||||||
|
- `views/product-v2/list`(产品 v2 列表)
|
||||||
|
- `views/customer/list`(客户管理)
|
||||||
|
- `views/agency/list`(公司管理)
|
||||||
|
|
||||||
|
如果上述页面也有「点搜索第一次不灵」的反馈,根因 100% 一样。
|
||||||
|
|
||||||
|
## 验证清单(前端改完自测)
|
||||||
|
|
||||||
|
- [ ] 进入「主题列表」,切换主题类型下拉 → 点搜索 1 次 → 列表立即刷新(Network 只有 1 个请求且 200)
|
||||||
|
- [ ] 输入关键词 → 点搜索 1 次 → 列表立即刷新
|
||||||
|
- [ ] 点「重置」按钮 → 列表恢复全量
|
||||||
|
- [ ] URL query 仍能正确反映当前筛选条件(用于分享/刷新保持状态)
|
||||||
|
- [ ] 浏览器前进/后退键能恢复对应筛选条件
|
||||||
|
- [ ] 顺手验证:上面"顺手检查"列出的 5 个嫌疑列表页是否同样存在双请求,如有一并修
|
||||||
|
|
||||||
|
## 后端无需任何改动
|
||||||
|
|
||||||
|
后端 `GET /admin/product-line/list` 接口本身正常,2 个请求带的参数也都正确,问题完全在前端搜索按钮的实现 + 路由守卫的清理时序冲突。这条 changelog 只为通知前端修。
|
||||||
|
|
||||||
|
@mmg 看下 `hl-ui/src/views/product/line/index.vue` 的 `handleSearchSubmit`,按改法 A 改即可,预计 P1 — 影响所有列表页搜索体验。
|
||||||
正在加载...
x
在新工单中引用
屏蔽一个用户