第 11 课:把筛选条件同步到 URL

第 11 课:把筛选条件同步到 URL

这一课开始让任务页更像真实后台系统。

因为你会发现,很多管理后台在你筛选列表时,地址栏都会跟着变。

比如:

text 复制代码
/tasks?keyword=Vue&status=待评审&priority=高

这不是"顺手为之",而是一个非常常见、非常有价值的页面设计。

所以这节课要解决的核心问题是:

为什么列表页要把筛选条件写进 URL,以及在 Vue 里应该怎么做?


先讲结论

你先记住一句最重要的话:

当一个列表页的筛选条件值得被刷新、分享、回退恢复时,它就很适合同步到 URL 查询参数

这句话里有 3 个关键词:

  1. 刷新后还能保留
  2. 可以复制链接发给别人
  3. 浏览器前进后退时能恢复状态

如果一个筛选条件满足这些需求,

那它通常就不该只待在组件的本地 ref 里。


这次我们做了什么

这一轮新增了:

  • src/composables/useTaskFilterQuerySync.ts
  • src/composables/__tests__/useTaskFilterQuerySync.spec.ts

更新了:

  • src/views/TasksView.vue
  • src/components/tasks/TaskPageHeader.vue

这说明这次不是只讲原理,

而是真的把"任务页筛选条件 <-> 地址栏查询参数"的同步做出来了。


先看这次解决了什么体验问题

现在任务页有 3 个筛选条件:

  • 关键字
  • 状态
  • 优先级

如果这些状态只存在于页面本地变量里,就会有几个明显问题:

1. 刷新页面后筛选条件会丢

你明明刚筛到一个很具体的结果,

一刷新就回到默认状态了。


2. 不能把当前筛选结果分享给别人

你想把"只看高优先级 + 待评审"的结果发给同事,

但地址栏里没有这些信息,对方打开后看到的还是默认列表。


3. 浏览器回退体验会很怪

用户改了几次筛选条件,再点浏览器后退,

页面状态可能完全对不上地址栏历史。


4. 重新进入页面时不能恢复上下文

很多后台系统最重要的体验之一就是:

我离开再回来,最好还能接着刚才的筛选上下文继续看

URL 查询参数正好很适合承载这种"页面级上下文"。


为什么这次单独新建了一个 composable

这次没有把逻辑继续塞进 useTasksPage.ts

而是新建了:

这是一个很值得你学的边界判断。

因为这组逻辑的核心问题已经不是:

  • 任务怎么加载
  • 列表怎么筛选
  • 任务怎么新增

而是:

任务页筛选状态如何和路由查询参数保持一致

这是一组新的职责。

它和"任务页业务逻辑"相关,

但它本身更偏:

路由同步逻辑

所以把它单独抽成一个 composable,会比继续堆到 useTasksPage() 里更清楚。

这正好也是第 9 课讲过的边界思路的一个真实应用。


当前这次同步的到底是什么

这次我们同步的是 3 个查询参数:

  • keyword
  • status
  • priority

也就是说:

页面状态

  • keyword
  • statusFilter
  • priorityFilter

地址栏状态

  • route.query.keyword
  • route.query.status
  • route.query.priority

现在这两组状态会互相保持一致。


这次最关键的能力:双向同步

这次你一定要理解一个概念:

不是单向写 URL,而是 URL 和页面状态双向同步

也就是说:

方向 1:页面状态变化,写回 URL

比如你在输入框里输入关键字,

或者切换状态筛选,地址栏会跟着更新。


方向 2:URL 变化,回填页面状态

比如你直接打开一个深链接:

text 复制代码
/tasks?keyword=Pinia&status=待开始&priority=中

页面也会自动把这 3 个筛选条件恢复出来。

这一步非常关键。

因为如果你只做"页面改了写 URL",

却不做"URL 改了回填页面",

那它其实还不算完整同步。


为什么 URL 用 query 参数而不是 params

你现在可以先这样记:

params

更适合"资源定位"

比如:

text 复制代码
/tasks/18

它表示"第 18 条任务"。


query

更适合"页面条件"

比如:

text 复制代码
/tasks?status=待评审&priority=高

它表示"任务列表当前用什么条件在看"。

所以筛选条件放进 query,是非常自然的选择。


为什么这次用 router.replace,而不是 router.push

这是这一课非常重要的一个细节。

如果用户每敲一个字都触发一次筛选变化,

你如果每次都用:

ts 复制代码
router.push(...)

浏览器历史记录会爆炸式增长。

结果就是:

  • 用户输入 5 个字
  • 浏览器可能多出 5 条历史记录
  • 后退键体验会很糟

所以这次我们用了:

ts 复制代码
router.replace(...)

它的意义是:

更新当前这条地址记录,而不是追加一条新历史

这通常更符合"筛选条件变化"这种场景。


为什么默认值不写进 URL

现在的实现里:

  • 关键字为空,不写进 URL
  • 状态是"全部",不写进 URL
  • 优先级是"全部",不写进 URL

所以默认状态下,地址会更干净。

这背后的思路是:

URL 只保留真正有信息量的状态

这样有两个好处:

1. 地址更短、更可读

不会出现:

text 复制代码
/tasks?status=全部&priority=全部&keyword=

这种冗余地址。


2. 更容易区分"默认页"和"带条件页"

只要地址栏里出现查询参数,

你就知道这个列表页现在不是默认状态。


为什么这次还做了"非法参数清理"

现在的 useTaskFilterQuerySync.ts 不会盲目信任地址栏参数。

比如有人手动输:

text 复制代码
/tasks?status=未知状态

页面不会直接把这个非法值塞进状态里。

而是会:

  1. 先检查这个值是不是合法选项
  2. 如果不合法,就回退到默认值
  3. 再把地址栏清理成标准格式

这一步非常像真实项目。

因为:

地址栏是外部输入,所有外部输入都应该被校验

这和表单校验、本地存储校验、本质上是同一种思维。


这次最值得你学的设计点:侦听器不是乱写的

这次的核心实现其实是两组 watch

第一个 watch

监听:

ts 复制代码
route.query

作用:

把地址栏状态回填进页面状态


第二个 watch

监听:

  • keyword
  • statusFilter
  • priorityFilter

作用:

把页面状态写回地址栏


为什么这里会有"循环更新"风险

这是这一课最需要你提高警惕的地方。

因为如果你不加控制:

  1. 地址栏变化
  2. 回填页面状态
  3. 页面状态变化
  4. 又写回地址栏
  5. 地址栏再变化

就可能进入重复同步。

所以这次实现里专门用了一个标记:

  • isApplyingRouteQuery

它的作用就是:

告诉第二个 watch:这次状态变化不是用户输入造成的,而是地址栏回填造成的,不要再反向写回

这就是"副作用控制"的典型写法。


为什么任务页要先同步 URL,再加载数据

现在 TasksView.vue 里先调用了:

  • useTaskFilterQuerySync(...)

然后页面挂载后才执行:

  • loadTasks()

这个顺序很合理。

因为如果用户一打开就是:

text 复制代码
/tasks?status=待评审

那页面应该先恢复"待评审"这个筛选条件,

再去加载并显示最终筛选结果。

这会让页面一进入就处在正确上下文里。


这次为什么还专门补了测试

这次新增了:

它验证了 4 件很关键的事:

  1. 首次进入页面时,查询参数会正确回填到页面筛选状态
  2. 页面筛选状态变化后,会写回 URL
  3. 默认值不会冗余地留在 URL 里
  4. 非法查询参数会被自动清理

这很重要,因为带 watch 的逻辑往往很容易写出"看起来能跑,但边界有问题"的代码。

能测住它,说明你的同步边界更清楚了。


真实项目里为什么很多后台都这么做

你现在可以用下面这 4 个理由去理解:

1. 可分享

把当前筛选结果直接发给别人。


2. 可恢复

刷新页面、重新打开页面后,仍然保留原来的条件。


3. 可回退

用户通过浏览器前进后退,也能恢复到之前的筛选上下文。


4. 可调试

开发时你只看地址栏就知道当前页面到底处在什么筛选状态。

这对排查问题非常有帮助。


这节课最容易犯的 6 个错误

1. 只会把状态写进 URL,不会从 URL 恢复状态

这不叫完整同步。


2. 不校验 query 参数是否合法

这会让页面状态被脏数据污染。


3. 把默认值也全部写进 URL

会让地址又长又脏。


4. 用 router.push 处理每一次筛选变化

会让浏览器历史记录非常难用。


5. 忘记处理循环同步问题

很容易造成重复 replace,甚至让状态流变得难以理解。


6. 把这类逻辑无脑塞进页面主 composable

会让 useTasksPage() 的职责越来越散。


你现在应该能回答的 10 个问题

  1. 为什么列表页的筛选条件很适合同步到 URL 查询参数?
  2. 为什么这类信息适合放在 query,而不是 params
  3. 为什么这次要单独新建 useTaskFilterQuerySync.ts
  4. 为什么同步必须是双向的,而不是单向写 URL?
  5. 为什么筛选变化通常更适合用 router.replace
  6. 为什么默认值不应该冗余地写进地址栏?
  7. 为什么地址栏里的查询参数也要做合法性校验?
  8. 为什么这种实现里很容易出现循环更新问题?
  9. isApplyingRouteQuery 这个标记解决了什么问题?
  10. 为什么要先恢复 URL 状态,再加载任务数据?

这节课的动手练习

练习 1

自己手动在浏览器地址栏试这几种地址:

  1. /tasks?status=待评审
  2. /tasks?priority=高
  3. /tasks?keyword=Vue&status=进行中

观察:

  • 页面筛选控件会不会自动恢复
  • 列表结果会不会跟着变化

目的:

把"地址栏就是页面状态的一部分"这件事真正体会到。


练习 2

打开 useTaskFilterQuerySync.ts,自己用一句话解释下面这 3 个函数分别在干什么:

  • getSingleQueryValue
  • buildTaskFilterQuery
  • isTaskFilterQuerySynced

目的:

训练你读懂一个 composable 内部的"辅助函数分工"。


练习 3

试着自己再加一个筛选条件:

  • 只看"我负责的任务"

然后先回答:

  • 它应不应该同步到 URL
  • 应该对应哪个 query key
  • 默认值是不是要写进 URL

目的:

训练你判断"什么状态值得进地址栏,什么状态不值得"。


这节课的复习结论

把这一课压缩成 8 句话:

  1. 列表页筛选条件如果需要被刷新、分享和回退恢复,就很适合同步到 URL。
  2. 这类"页面条件"通常放在 query,而不是 params
  3. 这次的新 composable 本质上解决的是"筛选状态与路由状态同步"问题。
  4. 完整同步必须同时支持"页面写 URL"和"URL 回填页面"。
  5. 默认值不应该冗余地出现在地址栏里。
  6. 地址栏参数和表单输入一样,都是外部输入,必须做合法性校验。
  7. router.replacerouter.push 更适合高频筛选变化。
  8. watch 的同步逻辑最关键的不是写出来,而是防止它进入循环更新。
相关推荐
专注VB编程开发20年2 小时前
VBA/VB6 ADO数据库查询jet+只读更快
开发语言·数据库·ado·vb
曹牧2 小时前
MantisBT
开发语言
彳亍走的猪2 小时前
Android 全局防抖/防重复点击
android·java·开发语言
Mintopia2 小时前
性能优化的错觉:你优化的,可能根本不是瓶颈
前端
Mintopia2 小时前
一次讲清"慢"的本质:CPU、IO、网络到底谁在拖后腿
javascript
05Nuyoah2 小时前
第一阶段:HTML的笔记
前端·笔记·html
小白学大数据2 小时前
Python 爬取图片攻略:告别水印,批量保存高清图片
开发语言·python