第 25 课:给学习笔记页加上搜索、标签筛选和 URL 同步
这一课,我们不再只做"测试结构重构",而是回到一个非常真实的前端页面需求:
让笔记页从静态展示升级成可搜索、可按标签筛选、可同步到地址栏的页面。
这一步很重要。
因为真正的后台页面,几乎很少只是把数据"摆出来"。
它们更常见的形态是:
- 有输入框
- 有筛选条件
- 有空状态
- 有结果摘要
- 有 URL query 同步
所以这一课,其实是在把笔记页推进到更接近真实业务页面的状态。
这一课一句话在做什么
这一课我们完成了 4 件事:
- 给笔记页加了关键字搜索
- 给笔记页加了标签筛选
- 把
keyword和tag同步到了 URL - 为这套逻辑补了单元测试和 E2E 测试
也就是说,这一课不是只改 UI,而是把:
- 页面交互
- 页面状态
- 路由状态
- 自动化验证
一起补齐了。
这一课新增了什么文件
1. 新增笔记页组合式函数
文件:
src/composables/useNotesPage.ts
这是这一课最核心的新增文件。
它把下面这几类逻辑从 NotesView.vue 里迁了出去:
- 关键字状态
- 标签状态
- 筛选结果
- 结果摘要
- 空状态文案
- 地址栏 query 同步
- 清空筛选行为
这说明一个很重要的工程思路:
当页面开始出现"多状态 + 多派生结果 + 多个行为函数"时,就很适合抽成 composable。
2. 新增组合式函数单元测试
文件:
src/composables/__tests__/useNotesPage.spec.ts
这一课我们不只改功能,还专门给它补了单元测试。
测试覆盖了这些核心点:
- 首次从 URL 回填筛选状态
- 非法 tag 参数自动清理
- 关键字同步走防抖
- 标签筛选立即同步
- 清空筛选恢复默认状态
这能让你第一次更完整地感受到:
- 页面功能不是"写完就算"
- 组合式函数也应该能被独立验证
3. 扩展笔记页 Page Object
文件:
e2e/pages/NotesPage.ts
这一课我们继续扩展了 NotesPageObject,新增了:
- 输入关键字
- 选择标签
- 清空筛选
- 断言筛选摘要
- 断言 URL query
- 断言空状态
这说明 Page Object 不是写完一次就不动了。
当页面能力变强时,页面对象也应该一起成长。
这节课对 NotesView.vue 做了什么
文件:
src/views/NotesView.vue
这次最大的变化是:
NotesView.vue 不再自己手写全部筛选逻辑,而是专注于"渲染 UI + 绑定事件"。
它现在主要做的是:
- 调用
useNotesPage() - 取出页面需要的状态
- 绑定关键字输入框
- 绑定标签下拉框
- 渲染筛选结果摘要
- 在无结果时渲染空状态
这就是典型的:
- 视图层负责展示
- composable 负责页面逻辑
为什么这节课要把逻辑抽进 useNotesPage
你完全可以把这些逻辑直接写进 NotesView.vue。
比如:
const keyword = ref('')const activeTag = ref('全部')const filteredNotes = computed(...)watch(route.query, ...)
这些都能直接写在页面里。
但问题是,一旦都塞进页面,文件很快就会变成:
- 状态一堆
- computed 一堆
- watch 一堆
- handler 一堆
然后页面会越来越难读。
所以这节课我们把它抽出来,是为了让你体会:
页面复杂度上来以后,最先该拆出去的,通常不是模板,而是页面逻辑。
useNotesPage 里到底封装了哪些东西
文件:
src/composables/useNotesPage.ts
1. 页面状态
keywordactiveTag
这两个是用户直接操作出来的原始状态。
2. 派生状态
availableTagsnoteCountuniqueTagCountfilteredNotesfilteredNoteCounthasActiveFiltersemptyDescriptionfilterSummary
这些都不是用户直接输入的,而是:
- 根据原始状态推导出来的结果
这正是 computed 最适合做的事情。
3. 行为函数
handleKeywordChangehandleTagChangeclearFilters
这些函数负责:
- 改状态
- 触发 URL 同步
它们是页面逻辑层的"动作接口"。
4. 地址栏同步状态
isKeywordQuerySyncPendingkeywordQuerySyncDelay
这两个值是为了让用户能看到:
- 当前关键字已经变了
- 但系统还在等待防抖结束后再写入地址栏
这是一种很典型的"同步中可视化"设计。
为什么关键字同步要防抖,标签同步却立即执行
这是这一课一个非常值得记住的设计点。
关键字为什么要防抖
因为关键字输入是高频动作。
用户可能会连续输入:
PPiPinPinia
如果每次都立刻改 URL,就会产生大量无意义的路由 replace。
所以关键字更适合:
- 先更新页面状态
- 再延迟一点同步到地址栏
标签为什么适合立刻同步
因为标签选择通常是低频动作。
用户不会像打字那样一秒改很多次。
所以标签变化直接:
- 立刻更新状态
- 立刻写回 URL
更自然,也更简单。
这说明一个很重要的前端设计思路:
不同交互频率的输入,不一定要使用同一种同步策略。
为什么 URL 同步这么重要
很多初学者会觉得:
- 页面里筛选一下就好了,为什么还要同步地址栏?
真实项目里,URL 同步至少有 4 个实际价值。
1. 刷新页面后能恢复状态
如果你当前筛选的是:
- 关键字:
Pinia - 标签:
store
刷新后还能恢复,就说明这个页面状态是可持久的。
2. 可以分享给别人
别人只要打开同一个链接,就能看到相同筛选结果。
3. 浏览器前进后退更自然
筛选变化也会成为浏览历史的一部分。
4. 更像真实后台系统
很多中后台页面都依赖 query 参数保存筛选状态。
所以你越早练这个模式,越接近真实项目开发。
这一课新增了哪些界面能力
文件:
src/views/NotesView.vue
1. 关键字输入框
它现在支持搜索:
- 标题
- 摘要
- 标签
这比只搜标题更接近真实使用场景。
2. 标签下拉筛选
可选标签不是手写死的,而是根据当前笔记数据自动生成。
这说明页面没有把标签选项写死在模板里,而是通过数据推导出来。
3. 筛选结果摘要
例如:
当前共 4 篇笔记,正在展示全部内容。当前共 4 篇笔记,已匹配 1 篇。
这个摘要非常重要。
因为筛选页面如果没有结果反馈,用户会不知道:
- 到底有没有生效
- 是 0 条结果,还是还没加载出来
4. 空状态提示
当你输入一个不存在的关键字,比如:
Nuxt
页面会明确告诉你:
没有找到符合当前筛选条件的学习笔记。
这比空白一片强得多。
5. 清空筛选
当页面存在筛选条件时,会显示:
清空筛选
这是非常常见且实用的交互细节。
这一课对自动化测试带来了什么变化
文件:
e2e/app.spec.ts
我们新增了 3 条笔记页相关用例:
- 关键字搜索并同步 query
- 标签筛选并清空筛选
- 无结果时显示空状态
这 3 条测试让笔记页不再只有"静态展示是否成功"的验证,而是开始覆盖:
- 页面真实交互
- 路由同步结果
- 边界状态
NotesPageObject 这次为什么继续扩展
文件:
e2e/pages/NotesPage.ts
上一课 NotesPageObject 只覆盖了:
- 页面标题
- 统计卡片
- 笔记卡片
- 建议区块
这一课它继续长出了:
fillKeywordselectTagclearFiltersexpectFilterSummaryexpectQueryValueexpectQueryMissingexpectNoteHiddenexpectEmptyState
这说明 Page Object 的成长方式应该是:
- 跟着页面能力一起增长
而不是一开始就预设成一个巨大的对象。
这节课最值得你理解的工程边界
到这里,你要开始能区分 4 层东西:
1. mock 数据
文件:
src/mock/notes.ts
负责:
- 提供笔记原始数据
2. composable
文件:
src/composables/useNotesPage.ts
负责:
- 页面状态
- 页面派生结果
- 页面行为
- 页面与 URL 的同步
3. view
文件:
src/views/NotesView.vue
负责:
- 把状态渲染成界面
- 把交互事件绑定回 composable
4. tests
文件:
src/composables/__tests__/useNotesPage.spec.tse2e/app.spec.ts
负责:
- 单元测试验证逻辑边界
- E2E 测试验证真实页面行为
这节课你应该真正学会什么
如果你只记住"给笔记页加了个搜索框",那就太浅了。
你真正应该学会的是下面这 6 点:
- 页面功能一复杂,优先考虑把页面逻辑抽成 composable
computed适合表达筛选结果、摘要、空状态这类派生数据watch(route.query)适合处理地址栏回填页面状态- 高频输入和低频筛选,不一定使用同一种 query 同步策略
- Page Object 要跟着页面能力一起演进
- 新功能上线时,单元测试和 E2E 测试都应该跟上
这节课改了哪些文件
src/composables/useNotesPage.tssrc/composables/__tests__/useNotesPage.spec.tssrc/views/NotesView.vuee2e/pages/NotesPage.tse2e/app.spec.tsdocs/25-notes-search-filter-and-query-sync.mddocs/README.md
这一课的验证结果
这一课相关改动已经通过:
npm run test:unit -- --runnpm run test:e2e -- --project=chromiumnpm run lintnpm run type-check
这很关键。
因为你现在做的已经不是"小玩具页面",而是:
- 有页面逻辑
- 有测试
- 有路由同步
- 有类型约束
的工程化前端页面。