双框架实战:用 Vue 和 React 做同一个文章搜索器
学完两套语法后,最好的巩固方式不是再背一遍,而是让它们面对相同需求:显示文章、输入关键词、只看收藏、切换收藏,以及处理无匹配结果。需求不变,差异才看得清。
先运行,再阅读
本仓库 examples/frontend 提供独立 Vite 多页面练习,不改变知识库主站的框架。两个入口分别是 /vue.html 和 /react.html,都使用本地样例数据,没有真实 API 或管理员写入。
cd examples/frontend
npm ci
npm run dev
使用 Node 22.12+ 或满足工具要求的较新版本,按终端给出的地址分别打开两个入口。npm test 检查共享数据逻辑与异步竞态模型,npm run typecheck 检查两个实现,npm run build 验证生产产物。
共享什么,不共享什么
两个实现共用 Article 类型、样例数据和纯过滤函数。输入、复选框和收藏状态分别放在各自组件里。这样 Vue 的 ref 与 computed、React 的 useState 与渲染计算都能自然表达,不需要为了“统一”而包一层自创框架。
过滤函数先对查询 trim、转小写,再按标题和收藏条件筛选。空查询表示不过滤标题;“没有匹配”是成功的空结果,不是异常。收藏通过稳定 ID 更新,不能依赖列表下标,因为过滤后位置会变化。
按这个顺序读源码
先看 shared/model.ts,它完全不依赖 UI 框架。再看 vue/App.vue,找原始状态和 computed。然后看 react/App.tsx,找每轮渲染的派生结果。最后比较事件处理:Vue 可以修改响应式值,React 使用 setter 并返回新数组。
每个卡片的收藏按钮提供 aria-pressed,筛选项有 label,结果数量使用状态提示。样式相同、数据相同、行为相同,避免把审美差异误当成框架差异。
故意改坏一次
把 React 的数组更新改成原地修改再传回旧数组,观察更新是否符合预期;然后恢复不可变更新。把 Vue 的 query 改为普通字符串,观察为何失去自动更新。再给列表加可编辑输入并改变排序,比较稳定 ID 和下标 key。
这些实验在本地练习副本里进行,不要直接修改生产知识库。发现“没更新”时先解释状态机制,再补代码,别通过强制刷新掩盖问题。
从练习走向真实系统
下一阶段把本地数据换成接口:增加运行时响应解析、loading/error/empty、取消与旧响应过滤,再把可分享的搜索条件写到 URL。最后加入鉴权和保存冲突处理。不要一次把所有库都接上,否则不知道是哪一步引入了问题。
练习项目故意不包含登录、服务端缓存、数据库持久化和 SSR。刷新会恢复本地初始收藏,这属于明确的练习边界。它证明组件与过滤行为,不代表完成了生产文章系统。
验收任务与参考
分别在两种实现中搜索 React、搜索不存在的词、清空、开启只看收藏、切换收藏。两边结果应一致,键盘能完成操作,窄屏不应把整页撑宽。
从零基础版本走到进阶版本
新增的基础课入口提供原生 HTML/JavaScript、Vue、React 三份相同的三条文章搜索器。它们先只有搜索、计数、空状态和清空,便于逐行对比;本章两个进阶入口再加入收藏筛选,初始数据为两条。不要把两组示例的预期数量混用。
先读原生版本的 render:它显式替换列表和计数。Vue 版本用模板声明结构,computed 保存筛选关系;React 版本在组件渲染中计算结果,通过 JSX 描述结构。三者都调用类似的字符串和数组方法,所以框架没有替代 JavaScript 的计算能力。
接下来亲手实现一个需求:显示“已收藏 X 篇”。原生版在统一渲染函数计算;Vue 版可以用 computed;React 版直接根据当轮 articles 计算。三种实现都不应另存一份容易过期的计数状态。
完成后说明每个文件的职责、刷新为何恢复初始数据、用户输入怎样到达筛选函数、界面由谁更新。能独立讲清这四个问题,再往网络请求与持久化推进,会比直接复制整套后台模板扎实。
