1. 项目整体思路t3code 到底要解决什么问题如果你平时经常写技术文章、做代码笔记或者维护自己的文档库一定遇到过这种让人抓狂的场面本地文件夹里堆了几十个.md和.txt里面散落着各种抄来的代码片段有的是从 Stack Overflow 复制的有的是以前做项目时随手留的还有的是聊天记录里别人发的一段修复方案。真到要用的时候根本想不起来这段代码存哪儿了更麻烦的是——就算找到了这代码当时是用来干嘛的、依赖什么环境、配套哪一版语法一概没有标记。于是你只能又一次打开浏览器重新搜索一遍。我刚做 t3code 这个项目的时候目标非常明确做一个以代码片段为核心的本地管理工具把零散的代码块整理成带上下文、带标签、可检索的卡片式清单。它不是一个什么复杂的在线平台不搞协作文档也不做云端同步就是老老实实地把存代码、找代码、复用代码这件事做顺。名字里 T3 其实代表的是 TypeScript Tailwind TanStack 这一套技术组合也是我熟悉的开发栈。之所以选择这个方向是因为我觉得代码片段工具是目前开发工具链里被严重低估的一环。大家很舍得在编辑器插件、CI/CD、监控告警上花钱花时间但最日常、最高频的复制粘贴旧代码这件事却一直靠人工记忆力在硬撑。t3code 想做的就是把这部分效率捡回来。这个项目适合谁呢我觉得分三类人第一类是写技术博客的作者他们需要大量代码示例而且需要反复整理格式第二类是日常维护多个小项目的全栈开发者经常在不同项目间搬代码第三类是刚入行的前端学习者需要一个能让代码积累看得见回馈的工具。对这三类人来说t3code 的卡片式组织方式都能明显压缩找代码的时间。我也可以直接说结论t3code 不是那种炫技的项目技术选型都比较主流没有自己造框架也没有用什么被吹得天花乱坠的新特性。它的价值在于信息组织方式和细节打磨——怎么提取代码信息、怎么呈现上下文、怎么让用户的记忆负担最小化。这也是这篇文章我想重点展开的部分。2. 方案选型为什么是 TypeScript 全家桶2.1 前端框架选型背后的权衡t3code 的核心前端我用的是 React TypeScript。有人可能会问现在 Solid、Svelte、Vue 都挺香的为什么还要选 React我的理由其实很朴素这个项目不追求极致性能但追求生态成熟度和可维护性。代码片段卡片列表这种中等复杂度的交互React 的声明式模型写起来最顺手——数据进来组件渲染状态变化视图更新逻辑链路非常清晰。Tailwind 的选择也类似。我承认 Tailwind 的类名看起来确实有点脏但它在快速迭代 UI 时效率极高。t3code 的卡片列表我改了很多版样式——从纯列表改成网格再从网格改成混合布局——如果每次都要去维护单独的 CSS 文件工作量起码翻一倍。Tailwind 允许我直接在组件里调整布局类名所见即所得尤其适合这种边做边想的个人项目。TanStack 我主要用到了两个库TanStack Query 和 TanStack Table。前者负责管理本地数据的异步状态后者用来做代码清单的表格视图。可能有人会质疑一个纯本地工具用 Query 是不是有点重实际操作下来Query 哪怕不连后端对缓存、刷新、loading 状态的管理也很有帮助尤其是后面我加了全文搜索和标签筛选之后Query 的派生状态机制省了我大量逻辑代码。2.2 本地存储与数据结构设计t3code 的存储层我选了 IndexedDB配了一个非常薄的封装层。没有选 localStorage 是因为代码片段的体量一旦大起来localStorage 那 5MB 的上限很快就撑不住了。实测我存了接近八百个代码片段每个片段带标签、描述、代码正文、来源链接IndexedDB 才用了不到 20MB完全没有压力。数据结构设计上我踩过一次坑。一开始我把标签做成数组直接存进每条记录里结果后续想统计标签频率、做标签推荐时每次都要全表扫描。后来我把数据拆成了两个 store一个是snippets表存代码记录本身另一个是tags表单独维护标签信息两个表通过标签名关联。这个设计很像传统关系型数据库的范式化思路虽然牺牲了一点查询便利性但换来的是统计和筛选的清晰度。每一条 snippet 记录的核心字段大概是这样的interface Snippet { id: string; title: string; description: string; code: string; language: string; tags: string[]; source?: string; createdAt: number; updatedAt: number; favorite: boolean; }这里的language字段不是给用户随便填的字符串而是通过一个受限的下拉框选择。我在实操中发现如果language字段是自由输入马上就会出现js、javascript、JavaScript三种写法并存的情况到时候按语言筛选就形同虚设。所以后来我干脆做成了一个内置几十种常见语言的枚举。3. 核心细节拆解与实操要点3.1 代码块解析从粘贴到结构化t3code 最核心的交互入口就是一个大文本框用户直接把代码粘贴进去系统自动识别语言并生成卡片。这个过程的难点在于语言识别。开头我想用正则去猜语言试了几个常见规则后直接放弃了——正则根本分不清sql和text更别说js和ts这种长得几乎一样的语法。最后我引入了 highlight.js 自带的语言检测能力配合自己的规则兜底。具体做法是用户粘贴代码后先用 highlight.js 跑一遍语言检测如果置信度高就直接选中如果检测结果不明朗就弹出一个小下拉框让用户手动确认。这种方案比我最初设想的全自动识别更靠谱因为实际场景里太多边界情况了靠算法全猜对是不现实的。代码解析的另一个关键细节是缩进处理。从网页上复制代码时经常带有一层父容器的 padding粘贴进来后整个代码块最左侧多了几个空格如果直接存储后面复制出来用的时候就很恶心。我做了个自动去缩进逻辑统计每行开头的空格数取最小值然后整体减去这个最小值。这个处理虽然简单但是特别实用——实测大概有三分之一粘贴进 t3code 的代码都能触发这个规则。3.2 代码渲染与复制反馈卡片渲染的代码高亮我直接用 highlight.js 完成没有上 prism。原因很简单我只需要最基础的高亮能力highlight.js 包体积更小也足够稳定。真正花心思的地方是复制代码这个看似不起眼的操作。我之前用过很多笔记软件最难受的就是复制代码没有反馈——点了按钮以为复制了粘贴的时候才发现根本没选中。t3code 的做法是复制成功后按钮文字变成已复制同时按钮背景色闪一下绿色1.5 秒后恢复原样。这个微交互看着很小但实际使用的时候确实能省掉很多不确定到底复制了没有的迟疑。复制操作本身我用了navigator.clipboard.writeText但这里有个必须注意的坑API 要求页面处于 focus 状态而且必须通过用户手势触发。如果复制按钮是在异步回调里执行 API有些浏览器会直接拒绝。所以我把复制事件绑定在按钮的 click 事件上代码也是同步执行的这样才能稳定工作。3.3 搜索与筛选的信息检索t3code 的搜索功能我做了两层一层是全文字符串搜索另一层是标签筛选。全文搜索就是简单地遍历所有字段做 include 匹配这个方案在最开始够用但数据上几百条之后会感觉到明显的卡顿。后来我加了一个简单的预处理策略每次搜索时先用 id 列表维护一个搜索缓存只有数据变化时才重新建立索引将搜索逻辑和工作量压缩了很多。标签筛选走的是前面提到的tags表。每次点击标签时系统读取该标签关联的 snippet id 列表与当前搜索条件做交集。这个交集运算在浏览器端做就算几千条数据也就几毫秒的事完全没有必要搞正则全文索引或者第三方搜索引擎。我还是那句话个人工具的复杂度控制比功能堆叠重要得多。4. 实操过程完整复现 t3code 的搭建路径4.1 项目初始化与目录设计创建项目的时候我用的是 Vite 的 react-ts 模板而不是 Next.js。原因很直接t3code 是一个纯前端本地应用不需要服务端渲染不需要 API 路由上 Next.js 反而增加构建复杂度和心智负担。Vite 的启动速度和热更新体验让我在开发阶段节省了大量时间。初始化命令没什么特别npm create vitelatest t3code -- --template react-ts cd t3code npm install安装完基础依赖后我又补了这几个包npm install react-router-dom npm install tanstack/react-query npm install tanstack/react-table npm install highlight.js npm install dexie路由我用在标签页和设置页之间切换dexie是 IndexedDB 的操作库API 比原生 IndexedDB 好用太多。核心目录结构我设计为src/ components/ # 纯展示组件 pages/ # 路由页面 stores/ # 数据状态管理 utils/ # 解析和高亮等辅助函数 types/ # TypeScript 类型定义一个原则是组件文件里尽量不直接写数据逻辑统一放在stores里。这个分法刚开始会觉得多了一层但当你写了三百行逻辑代码之后就明白组件里只有 UI 该有多清爽。4.2 核心模块的实现细节数据层数据层我直接用了 Dexie 的 API。定义数据库的代码很简单但有一个隐藏的坑IndexedDB 的版本号升级。如果你改了表结构必须同步递增版本号否则浏览器不会执行新的 schema 定义。我一开始就吃了这个亏增加一个索引字段后忘了更新版本号结果上线后发现新字段根本没有生效。import Dexie from dexie; const db new Dexie(t3code-db); db.version(1).stores({ snippets: id, title, language, favorite, updatedAt, tags: name, count, });Dexie 的链式写法很直白但值得注意的是id 这个字段不需要自动生成用crypto.randomUUID()就好。这个方法在现代浏览器里原生支持不用引任何依赖。状态管理状态管理我本来想用 Redux Toolkit但实际搭建时发现t3code 的全局共享状态就两块当前筛选条件和数据变更标记。Redux 这种重量级方案在这里明显过重。我最终选择了 zustand——一个只有 1KB 左右的轻量状态库写起来比 Redux 简单太多。import { create } from zustand; interface FilterState { activeTag: string | null; searchQuery: string; setActiveTag: (tag: string | null) void; setSearchQuery: (query: string) void; } export const useFilterStore createFilterState((set) ({ activeTag: null, searchQuery: , setActiveTag: (tag) set({ activeTag: tag }), setSearchQuery: (query) set({ searchQuery: query }), }));这里把筛选条件单独抽成一个 store而不是和数据存在一起是有意的。因为筛选条件变化频率非常高而数据本身变化频率很低。如果放在同一个 store 里每次敲一个字母都会让整个列表组件重渲染数据多的时候能明显感觉到输入卡顿。解析模块解析模块是最容易写歪的地方。我的建议是始终保持短暂、可验证的原则——每个解析步骤都拆成独立的纯函数输入输出都确定方便单测。比如代码清洗那段逻辑export function normalizeCode(rawCode: string): string { // 去掉首尾空白 const trimmed rawCode.trim(); // 自动去缩进 const lines trimmed.split(\n); const indentCounts lines .filter((line) line.trim().length 0) .map((line) line.match(/^\s*/)?.[0].length ?? 0); const minIndent Math.min(...indentCounts); return lines.map((line) line.slice(minIndent)).join(\n); }这样一个函数脱离项目也能看懂出问题也容易定位。事实上这个 normalize 逻辑我后来改了至少三版第一版没处理空行、第二版没考虑 tab 和空格混用、第三版才稳定。纯函数配合单测确实让我每次改动时心里有底。4.3 渲染与导出从界面到完整的可用工具列表视图我用 TanStack Table 做了表格模式同时保留卡片模式。两种模式的数据源完全一样只是渲染方式不同。表格模式下列定义支持排序比如按更新时间排序、按语言排序这对我最近改过哪段代码这类查询特别有用。卡片模式则走网格布局每张卡片显示代码预览、标题、标签和语言标识。代码预览做了截断超出四行就折叠避免卡片高度失控。这个看似简单的截断其实也有讲究高亮后的代码是带 HTML 标签的不能直接在 HTML 上做 CSS 截断否则可能会截到标签中间导致样式错乱。我的方案是先用纯文本截断再对截断后的文本做高亮渲染。导出功能是我最早做的一个模块也是我最推荐个人工具都带上的一部分——保证用户永远不会被数据锁死。t3code 支持两条导出路径一条是把所有数据导出成 JSON 文件兼容性最好另一条是导出成 Markdown 文件每个 snippet 生成一个小节带标题、描述和代码块。实测导出的一百个片段直接放进语雀或者 Notion 里都能正常渲染代码块格式没有乱掉。5. 常见问题与排查技巧实录5.1 IndexedDB 打不开或数据丢失这是我收到反馈最多的一个问题。表现是重启浏览器之后数据还在但过一段时间再打开发现什么都空了。后来排查发现大部分原因是浏览器设置了无痕模式或者自动清理站点数据。这是浏览器的默认行为不是 bug。解决思路是首次启动时做一个检测尝试写入一条测试数据如果失败就弹出提示告诉用户当前浏览器环境可能会清理数据建议换用普通窗口。这个提示不能省略因为很多用户并不知道自己的浏览器开了退出时清除历史数据的选项。另外还有一个规律Safari 的 IndexedDB 在某些版本下只要空间不足就会直接清空数据库这一点比 Chrome 激进得多而且没有回调提示。所以我在写入数据时做了 try-catch 包裹一旦捕获到QuotaExceededError就立即提示用户主动导出备份。数据安全这种事宁可多提醒也不能指望浏览器替你兜底。5.2 高亮渲染出现闪烁卡片列表渲染高亮代码时会出现一个很明显的闪烁刚渲染出来是纯文本半秒钟之后才变成带颜色的高亮代码。原因很简单highlight.js 在挂载阶段就执行了高亮逻辑但浏览器还没完成布局绘制高亮前后 DOM 结构差异又大导致闪了一下。解决方法也很直接延迟高亮执行。我把 highlight 放进requestAnimationFrame的回调里浏览器先绘制纯文本帧再绘制高亮帧视觉上就顺滑多了。这个问题其实并不严重但如果你把代码卡片列表做得很多这种闪烁会非常影响观感。5.3 导入大型 JSON 时界面卡死有一次我测试导入一份 3000 条数据的 JSON 备份结果浏览器标签页直接卡了十几秒接近崩溃。定位之后发现问题出在数据导入时的逐条写入——每写一条就触发一次 React 状态更新状态一变整个列表组件就重渲染几百几千条数据累积下来就是灾难。优化思路是批量操作:一次性把数据全部写入 IndexedDB等全部写完再刷新一次列表同时写入过程中用一个 loading 标志挡着界面不触发中间状态的渲染。如果数据量特别大GitHub 等开放平台又长期不可用也可以退回用 Worker 线程做导入——但个人工具一般到不了这个量级先做批处理就够了。5.4 常见问题速查表问题现象根本原因解决方案打开页面数据为空浏览器清理了 IndexedDB检测存储可用性提示用户关闭无痕模式复制按钮没反应clipboard API 被异步调用把 API 调用放在 click 同步事件里高亮代码闪烁高亮在渲染前执行用 rAF 延迟高亮先绘制纯文本帧语言识别错误混淆 js/ts 或 sql/text低置信度时让用户手动确认导入导致卡顿逐条写入触发重渲染改成批量写入完成后刷新一次版本更新后字段失效IndexedDB 版本号未递增修改 schema 时同步递增 version5.5 一条让我印象深刻的排查经历有一次用户反馈说搜索功能失灵关键词明明在代码里但搜不出来。我本地怎么试都正常后来让他打开控制台看报错发现他的浏览器版本太旧不支持String.prototype.replaceAll方法。我在处理搜索高亮时用了 replaceAll而这个 API 是 ES2021 才引入的旧浏览器直接抛异常。从那以后我定了个规矩个人项目也要明确浏览器支持范围并且在代码里避免随手用新 API除非构建工具能自动做 polyfill。特别是做搜索这种和字符串打交道多的功能尽量用最基础的split、join、indexOf代替新语法兼容性会更稳。6. 最后聊点个人实操体会t3code 这个项目从开始做到现在给我最大的一个教训是个人工具最大的敌人不是功能不够多而是功能太多导致维护不过来。最初我计划加代码对比、分享到社交平台、团队协作一堆功能后来一个一个砍掉了。砍到最后剩下的功能就是粘贴、整理、搜索、导出这四件事。恰恰是这四件事让 t3code 在本地环境中变得非常好用。如果在你的工作流里也有代码写过又找不到这种困扰我建议你试一试卡片式的碎片整理办法。你不一定要复刻 t3code 的所有功能哪怕只是在本地建一个带标签的代码片段文件夹配合现在各种支持全文搜索的编辑器效率也能提升不少。从一个小工具出发解决一个真实痛点然后不断打磨它到你每天都在用的地步这种积累带来的满足感比单纯追新技术要扎实得多。我自己的使用习惯也变了以前是写文章时临时翻记录找代码现在每天工作结束前花两分钟把当天写的有价值片段收进 t3code打上标签备注用途。两周之后你再看那个清单会发现自己积累的速度比想象中快得多。所谓工具的价值说到底就是帮你把时间花在真正重要的事情上而不是重复劳动里。