React + TypeScript + Vite 打造个人热词记录小工具

发布时间:2026/10/18 9:12:30

React + TypeScript + Vite 打造个人热词记录小工具 隔三差五就会在群里看到有人问“这个新词到底啥意思”问的人一脸真诚答的人其实也刚查过。我受够了这种循环干脆用 React 写了一个自用的热词记录工具项目代号就叫 rea。说白了它是一套极简的“知识卡片”应用遇到没看懂的网络热词随手记下来补充含义、例句和出处打上标签之后想复习时打开页面搜一下就回来了。整个项目只有纯前端数据存在浏览器本地不需要后端服务适合自己日常使用也很适合拿来练手巩固 React 相关技术。这篇就把我从需求到落地、再到踩坑修坑的完整过程写下来想自己折腾类似小工具的同学可以直接抄作业。1. 项目定义与设计思路拆解1.1 为什么会想到做 rea这个念头不是拍脑袋来的。信息过载的时代网络热词的半衰期越来越短今天还挂在热搜上下周就可能成了“过期梗”。更难受的是很多词你当时查过、看懂了不代表你记住了。我经常遇到这种情况刷到一个词觉得眼熟点进去一看发现三个月前自己明明查过但因为当时只是随手划过去完全没留下印象。于是我想能不能给自己做一个“第二大脑”不需要很复杂只要能快速记录、方便检索、偶尔回顾就行。rea 最初的定位就是给自己用的“热词备忘卡片”后来用着用着发现它其实也能当成通用的阅读卡片工具记录书摘、观影笔记、临时灵感都没问题只是我主要用来存网络热词而已。功能边界一旦想清楚后面写代码就不会飘。1.2 技术选型为什么是 React TypeScript Vite很多人在技术选型上纠结很久我反而觉得这个项目恰恰说明了一个道理小工具的核心不是“用什么”而是“怎么用”。我选 React 主要是两个原因一是平时业务代码里写得多熟二是热词卡片这种界面太适合组件化了——每张卡片就是一个独立组件筛选和搜索是典型的声明式渲染场景React 的模型天然匹配。TypeScript 则是我强烈建议加的哪怕是小项目也要加。数据模型一旦有了类型约束改字段、做迁移的时候心里才有底不然光凭记忆去猜数据结构早晚翻车。至于 Vite它启动快、热更新快、配置少相比早期 webpack 那套繁琐配置省下的时间可以用来改功能而不是等编译。这不算什么颠覆性决策但确实是当前最适合小前端项目的组合。1.3 MVP 范围哪些先做、哪些坚决不做我给自己定了一条规矩第一版只做闭环不做扩展。所谓闭环就是“记录→检索→复习”这条主链路能跑通其他全部往后排。第一版必须做的只有四件事新增卡片、编辑删除、标签筛选、关键词搜索。另外我加了一个“导出备份”因为数据存在浏览器本地万一清缓存就全没了导出 JSON 文件算是一份保险。坚决不做的倒是列得更多账号体系不做云端同步不做富文本编辑不做多人协作不做浏览器插件不做。这些功能每一个听起来都很能提升“想象力”但以一个人下班后的业余时间来说任何一个拿出来都足以让项目烂尾。先把一版能用的东西跑起来再谈扩展这个顺序我觉得更适合个人项目。2. 核心细节解析与实操要点2.1 初始化与工程目录初始化用的是 Vite 官方的 React TypeScript 模板命令一行搞定pnpm create vite rea --template react-ts cd rea pnpm install pnpm dev这里我特意用了 pnpm 而不是 npm倒不是因为它性能多强而是它省磁盘空间、依赖安装快个人项目装包次数不多但每次等待少一点体验就好一点。依赖方面我很克制第一版几乎只用了 react、react-dom 和 typescript 相关的东西。后面为了存更多数据才额外加了 IndexedDB 的封装库。很多同学开局就装一堆 UI 框架、状态管理库、网络请求库结果项目本身没写几行代码依赖倒是越来越重这个习惯我不太推荐。工程目录我最终整理成下面这样一个小项目也可以有清晰的分层rea/ ├── index.html ├── vite.config.ts ├── src/ │ ├── main.tsx │ ├── App.tsx │ ├── types.ts │ ├── hooks/ │ │ └── useCards.ts │ ├── utils/ │ │ └── storage.ts │ ├── components/ │ │ ├── HeaderBar.tsx │ │ ├── SearchBox.tsx │ │ ├── TagFilter.tsx │ │ ├── CardList.tsx │ │ ├── CardItem.tsx │ │ ├── CardForm.tsx │ │ └── StatsBar.tsx │ └── styles/ │ ├── global.css │ └── variables.csstypes.ts 放类型定义hooks 放业务逻辑components 只放展示组件utils 放工具函数。这种分法不是哪本书教的而是我踩过“所有东西堆在 App.tsx 里”的坑之后琢磨出来的。App.tsx 一旦超过五百行改一个功能就心惊胆战早点拆开省心得多。2.2 数据模型是地基写本地存储之前我先把数据模型定下来。热词卡片最终长这样// src/types.ts export interface HotWordCard { id: string; term: string; meaning: string; example: string; source: string; tags: string[]; createdAt: number; updatedAt: number; reviewCount: number; lastReviewedAt?: number; }每个字段都不是随手加的。id 用crypto.randomUUID()生成而不是自增数字因为 UUID 在合并数据、导入导出时基本不会冲突。term 是词本身meaning 是含义解释example 是例句或上下文source 记录出处——比如“某个评论区”或“某篇文章”这个字段后期回头看很有价值。tags 是标签数组用来做分类筛选。createdAt 和 updatedAt 用时间戳数字存储比字符串好排序、好比较。reviewCount 和 lastReviewedAt 是给复习功能用的虽然第一版只显示“复习次数”但字段先留好后面做间隔复习就不用动数据结构了。这里有个很重要的设计习惯不要把原始对象直接塞进 localStorage最好在外面包一层带版本的壳比如interface StorageSchema { version: number; cards: HotWordCard[]; }将来如果字段结构变了只要版本号 1读取时写个迁移函数就能平滑升级。很多个人项目数据丢就丢在“没想好种子结构”后面一改代码直接崩这个壳能让你少流一次泪。2.3 组件拆分与状态管理怎么拿捏小项目该不该用什么状态管理库我现在的答案很明确不需要除非你明显感觉到 props 层层传递很痛苦。rea 的状态其实只有一个核心卡片数组。这个数组放在 App 里通过自定义 hook 暴露增删改方法子组件只负责消费数据和触发方法完全够用。给大家看下我最终封装出来的 hook 长什么样// src/hooks/useCards.ts import { useState, useCallback } from react; import type { HotWordCard } from ../types; import { useStoredCards } from ./useStoredCards; export function useCards() { const [cards, setCards] useStoredCardsHotWordCard[](rea:cards:v1, []); const addCard useCallback((input: OmitHotWordCard, id | createdAt | updatedAt | reviewCount) { const now Date.now(); const card: HotWordCard { ...input, id: crypto.randomUUID(), createdAt: now, updatedAt: now, reviewCount: 0, }; setCards(prev [card, ...prev]); }, [setCards]); const updateCard useCallback((id: string, patch: PartialHotWordCard) { setCards(prev prev.map(card card.id id ? { ...card, ...patch, updatedAt: Date.now() } : card )); }, [setCards]); const removeCard useCallback((id: string) { setCards(prev prev.filter(card card.id ! id)); }, [setCards]); return { cards, addCard, updateCard, removeCard }; }组件拆分我给自己定的规则是“一个组件只做一件事”卡片列表只管渲染不管增删改的业务逻辑表单只管收集输入校验通过后把数据抛给上层标签筛选栏只管从卡片数组里算出有哪些标签。各司其职出了问题定位也快。3. 实操过程与核心环节实现3.1 存储层封装从 localStorage 到 IndexedDB第一版我用的是 localStorage因为简单代码量最少。封装的时候我做了一个看起来很小的决定所有读写在 try/catch 里进行读取失败就返回默认数组。这个习惯救了我一次——有一次调试时我不小心往 localStorage 里写入了损坏的 JSON如果没有 try/catch整个应用启动就会白屏有了伪装保护应用能正常打开只是数据暂时为空而已。// src/hooks/useStoredCards.ts import { useEffect, useState } from react; export function useStoredCardsT(key: string, initialValue: T) { const [value, setValue] useStateT(() { try { const raw localStorage.getItem(key); return raw ? JSON.parse(raw) as T : initialValue; } catch { return initialValue; } }); useEffect(() { try { localStorage.setItem(key, JSON.stringify(value)); } catch { // 存储空间不足时静默失败但可以打日志方便排查 console.warn([rea] 写入 localStorage 失败); } }, [key, value]); return [value, setValue] as const; }后来卡片攒到大概两千张的时候明显感觉到每次增删改都要序列化整个数组页面开始卡顿而且 localStorage 有 5MB 的硬限制长期用必然不够。这时候我把存储层换成了 IndexedDB并且用了一个封装库里常见的写法import Dexie from dexie; class ReaDB extends Dexie { cards: Dexie.TableHotWordCard, string; constructor() { super(rea-db); this.version(1).stores({ cards: id, term, tags, createdAt, updatedAt, }); } } export const db new ReaDB();IndexedDB 的优势是没有存量的硬性限制按索引查询也比全量遍历快得多。代价是 API 原始且啰嗦所以多数人会借助封装库。迁移的步骤也不复杂先把旧数据通过导出功能备份成 JSON 文件再写一个一次性脚本把 JSON 导入 IndexedDB跑通之后再把入口逻辑切过去。这个过程中导出备份机制起到了关键作用所以我很推荐任何本地存储应用都留一个导出后门。3.2 卡片 CRUD 和筛选搜索的具体实现筛选和搜索是 rea 的核心交互。我的实现逻辑是全部在前端用useMemo实时计算不在存储层做过滤因为卡片量级对纯前端计算来说完全扛得住。const filteredCards useMemo(() { const keyword query.trim().toLowerCase(); return cards.filter(card { const matchTag activeTag 全部 || card.tags.includes(activeTag); if (!keyword) return matchTag; const matchTerm card.term.toLowerCase().includes(keyword); const matchMeaning card.meaning.toLowerCase().includes(keyword); const matchExample card.example.toLowerCase().includes(keyword); return matchTag (matchTerm || matchMeaning || matchExample); }); }, [cards, query, activeTag]);这个写法没什么门槛但有两个细节要注意。一是toLowerCase()要在循环里调用尽量让每张卡片的字段只转换一次而不是写成整数组先 map 一遍——后者会多一轮遍历。二是搜索要同时覆盖词条、含义、例句三个字段只搜词条名的话用户记得意思忘了词条怎么写就搜不到了。标签筛选的标签列表我也没有单独维护而是每次从卡片数组里动态计算出来const allTags useMemo(() { const set new Setstring(); cards.forEach(card card.tags.forEach(tag set.add(tag))); return [全部, ...set]; }, [cards]);这样免去了标签和卡片之间的同步问题也不会出现“标签存在但卡片被删光”的脏数据。新增和编辑卡片时表单校验只做了两件事词条不能为空词条重复时给个提示但不强制拦截。我特意没做“完全禁止重复”因为同一个词在不同语境下含义可能不一样没必要卡得那么死。导出备份的代码也给需要的同学参考function exportCards() { const blob new Blob([JSON.stringify(cards, null, 2)], { type: application/json, }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download rea-${new Date().toISOString().slice(0, 10)}.json; a.click(); URL.revokeObjectURL(url); }URL.revokeObjectURL这一步经常有人忘短时间大量导出时会占用内存记得用完后释放。3.3 构建、预览与部署rea 是纯前端应用构建部署非常简单。开发完执行pnpm build pnpm previewpreview会本地启动一个服务预览构建产物我先在这个环节跑一遍主流程确认没问题再部署。这里有一个我踩过的坑Vite 默认打包时资源路径是绝对路径/assets/...直接把 dist 目录丢到某个子目录下部署页面会白屏。解决办法是在vite.config.ts里设置 baseimport { defineConfig } from vite; import react from vitejs/plugin-react; export default defineConfig({ plugins: [react()], base: ./, });设置成./之后资源的引用路径会变成相对路径部署到任意子目录都能正常访问。这个小配置我建议所有不需要特殊路由的项目都直接写上省得部署时才发现问题。至于最终部署到哪我选了最朴素的方式打包成静态文件放到一台支持 HTTPS 的静态托管服务上没有用任何后端。这个方案的好处是零维护、零成本打开浏览器就能用。4. 常见问题与排查技巧实录4.1 踩坑记录速查表这是我给 rea 项目整理的一份踩坑速查表都是实际遇到过的问题贴出来给大家直接抄现象根本原因解决办法刷新页面后数据丢失初始化时未处理 localStorage 里的空值和异常值读取时包 try/catch并给默认值中文输入时搜索框疯狂触发没处理输入法 composition 事件用isComposing拦截 防抖卡片数量多了之后列表卡顿列表项没有 memo每张卡片都重复渲染用React.memo包裹卡片组件key 保持稳定部署到子目录后白屏Vite 默认资源路径为绝对路径配置base: ./IndexedDB 打开后立刻读写报错数据库升级还没完成就发起了事务先await db.open()再执行查询更新卡片后页面不刷新直接修改了 state 数组里的对象引用每次 setState 都返回新数组和新对象这里重点说下第 3 条。React 的重新渲染是组件粒度的父组件状态一变默认所有子组件都会重新 render。卡片列表尤其明显因为卡片数量一旦上百每次搜索输入都会导致全量列表渲一遍。我在CardItem上加了React.memo并且保证每张卡片的key用的是稳定且唯一的 id实测下来搜索输入流畅很多。做前端列表永远要记得这两个动作memo和稳定key。4.2 中文输入法那些让人上头的细节中文搜索框是个人项目里最容易“看着正常、用着别扭”的地方。最典型的问题用户用拼音输入法打“jiao”还没选好字搜索框就已经按 “jiao” 过滤了一遍等选完字又按完整拼音过滤一遍页面结果来回跳非常干扰。解决思路分两步。第一步是识别输入法组合输入状态浏览器输入框会触发composition事件组合输入过程中e.nativeEvent.isComposing为 true这时候不要触发实际搜索。第二步才是防抖等用户停止输入一段时间后再真正更新查询词let timer: number | undefined; function handleSearchInput(e: React.ChangeEventHTMLInputElement) { if (e.nativeEvent.isComposing) return; window.clearTimeout(timer); timer window.setTimeout(() { setQuery(e.target.value); }, 300); }防抖的 300ms 不是随便定的。太短起不到防抖效果太长会觉得搜索有延迟。简单需求 300ms 是个比较稳妥的起点数据量大或者远程搜索时可以再放大到 500ms。4.3 从 localStorage 迁到 IndexedDB 的教训这次迁移是我在 rea 上经历的最大一次变更教训也最有价值。最核心的一条是永远先备份再动数据结构。迁移前我先用导出功能把所有卡片存成 JSON这一步保证了下限——就算迁移代码写错了数据也丢不了。迁移过程中还碰到一个非常隐蔽的问题IndexedDB 的事务是异步的初次创建数据库时如果页面加载后立刻调用查询接口数据库升级事务还没完成此时查询会因为“数据库不存在”报错。正确做法是页面启动时先初始化数据库连接async function init() { await db.open(); const count await db.cards.count(); // 如果 count 为 0尝试执行一次性导入 }跑通之后再在入口处调用init()。对动态数据里的类型错误我还在导入逻辑里加了字段校验导入卡片时逐字段检查类型不匹配的直接跳过并统计数量。这样即使备份文件里有脏数据也不会把坏数据灌进新库。整个迁移其实只花了半天但因为提前备份加校验过程非常平稳数据一条没丢。5. 后续还能怎么扩展rea 现在还在持续迭代顺手把想法列一下把复习次数和上次复习时间结合做简单间隔复习提醒加一个“分享卡片图”的功能把一张热词卡片生成一张带卡片的图片方便发到群里给朋友解释导出格式从 JSON 扩展到 Markdown再加 PWA 离线能力手机浏览器存到桌面用起来就更接近原生应用了。在做这个小项目的过程中我最大的体会是工具不一定要大但一定要顺手。rea 一开始只是抵挡“词到用时方恨忘”的小本子结果用着用着变成了我练习 React 组件拆分、状态管理、数据迁移的试验田。有时候完成比完美重要先做一个只有三个功能的版本你才有可能在真实使用中知道下一个功能该做什么。
延伸阅读

更多相关文章

2026/10/18 9:07:30

纯JS魔方代码:前端三维渲染与交互实现的完整拆解

简介:一款使用原生JavaScript开发的三维魔方模拟程序,适合前端初学者、算法爱好者以及正在准备前端面试的开发者阅读,借助真实项目理解交互逻辑、动画调度与状态管理。资源以rar压缩包形式发布,整体大小仅255KB,压缩包…

2026/10/18 9:07:30

Java数据结构课设:通讯录模拟与24点游戏双模块拆解

简介:数据结构课程设计资源包内含两个经典Java项目:手机通讯录模拟与24点扑克牌游戏,覆盖链表、数组、哈希表、排序等数据结构核心应用,适合高校学生完成课程设计、备战考试或巩固Java编程能力。压缩包共73个文件,整体…

2026/10/18 9:07:30

Java数据结构课设:通讯录顺序表与24点递归穷举完整方案

简介:这是面向高校计算机相关专业学生的数据结构课程设计参考项目,覆盖手机通讯录模拟与二十四点扑克牌游戏两大经典应用。通讯录部分借助ArrayList、LinkedList实现联系人动态管理,并用HashMap优化按姓名查找;二十四点游戏则基于…

2026/10/18 10:22:36

专科生论文能用AI写吗?8类AI工具从开题到答辩全流程指南

最近好几个专科的学弟学妹跑来问我同一个问题:“论文真的能靠AI写吗?”。我的回答一直是:能,但你要把它当助手,而不是替身。这篇稿子不藏私,我把这些年自己写论文、帮别人改论文时真正用过、也确实解决问题…

2026/10/18 10:22:36

DeepSeek R1实战指南:访问路径、提示词技巧与自动化脚本

简介:这份PDF指南聚焦DeepSeek R1的完整使用路径,面向需要借助大模型提升数据分析、内容创作与工作效率的开发者、数据科学家、设计师等群体。内容系统覆盖七大获取方式:官网及App、硅基流动、秘塔搜索、Cursor、Groq、国家超算中心&#xff…

2026/10/18 10:22:36

银联银行卡BIN数据实战:支付路由与风控画像的底层逻辑

简介:这份资源为银联官方2020年4月25日发布的银行卡BIN数据整理包,面向支付系统开发、金融风控建模、银行卡识别与清算对账等场景的工程师与数据分析人员。原始数据已由作者整理为可直接导入的SQL文件,共9868条记录,字段涵盖银行卡…

2026/10/18 10:22:36

通信对抗核心机制:干信比、处理增益与干扰容限详解

简介:面向通信对抗原理课程学习者,这份西电通信对抗原理作业PDF是围绕课程核心知识整理的复习与参考材料,适合需要梳理“通信对抗”概念框架、备战期末或完成综合作业的本科生及自学读者。内容系统覆盖了通信对抗的定义与主被动分类&#xff…

2026/10/18 10:17:36

AI日报制作全流程:从信息采集到结构化压缩的工程实践

1. 一份“AI 日报”到底在记录什么每天早上打开信息流,铺天盖地的“某模型又刷新榜单”“某工具一夜爆火”“某大厂开源新框架”,看多了会发现一个尴尬的事实:信息量很大,但真正能沉淀下来的判断很少。我做 AI 领域的内容整理差不…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑