☰
个人全栈项目实战:从零手搭t3code的工程化之路
2026/10/28 6:12:50 网站建设 项目流程

最近手头在折腾一个代号叫t3code的个人项目,陆陆续续写了差不多两个月,从一开始拿着草稿纸画流程图,到现在跑在测试环境上稳定服务日常请求,中间踩了不少坑,也推翻了好几次设计。趁热把这段实战记录整理出来,给同样在做全栈小项目的朋友一些参考。

"t3"这个前缀其实有两层意思:一是指这个项目已经是我第三次重新写核心模块,前两次都是写到一半觉得架构不对,推倒重来;二来也致敬了一下社区里流行的 T3 技术选型思路——TypeScript 全家桶、类型安全优先、端到端的类型推导贯通。整个项目从零到一,没有用任何重型框架,完全靠手搭,这也让每一步迭代都能清楚地知道瓶颈出在哪里。

这篇文章会从项目定位、技术选型思路、核心模块实现、测试排错,一直讲到上线部署,全程记录真实的选择逻辑和踩坑经历。适合准备做个人全栈项目、想从单体杂烩转向模块化工程化的朋友,看完整套思路,你至少能少走一半弯路。

1. 名字和定位:t3code 到底想做成什么

1.1 为什么我叫它"third code"

先说名字的来历。这个项目最早是我给自己写的一个效率工具,用来管理和检索零散的代码片段。第一版很粗暴,就是一堆 Markdown 文件加一个 Node 脚本做文本匹配,能用但不好用。第二版上了数据库,但前后端耦合得厉害,改一个字段要动三层代码。到第三版,我才下定决心用工程化的方式从头搭,这一版就叫t3code(third code 的谐音)。

t3code 这个名字,在开发阶段也给过我不少提醒。每次想偷懒、想跳过类型定义、想在某个模块里硬塞不相关的逻辑时,我就会盯着项目名里的"3"字想:第三版了,再搞成屎山就说不过去了。这种心理暗示很有用,它让我在最容易松懈的编码细节上保持了足够的克制。

如果你也在起项目代号,建议别起那种特别宏大或者特别空泛的名字。像 t3code 这样带一点时间线、带一点自我约定的名字,反而能在日常开发里不断提醒你——这个项目到底要解决什么问题,你希望它长成什么样子。相比之下,我上一版起的什么"super-tools"之类,每次打开都想自我怀疑一遍。

1.2 核心目标:做自己的代码片段管家

t3code 的产品定位非常简单:一个面向开发者个人的代码片段管理与快捷检索工具。它要解决的痛点也很明确——我手头积累了大量的示例代码、配置文件、脚本片段,散落在本地文件夹、笔记软件和聊天记录里,真要找的时候永远找不到。

于是我把需求收敛成了几件必须做成的事:

  • 支持按语言、标签、关键词多维度的检索;
  • 支持片段内联编辑与版本历史回溯;
  • 支持一键复制和常用片段的"收藏夹"式置顶;
  • 所有数据本地保存,不依赖外部云服务,隐私可控。

这些需求听起来不复杂,但真正做起来,检索的准确率、编辑体验的流畅度、历史版本的存储策略,每一项都有不少细节需要打磨。我也刻意没有加"分享社区""云端同步"这类功能,因为那会迅速吞噬掉一个小项目的全部精力。守住边界,是 t3code 第一原则。

1.3 先定边界,再写代码

在写第一行代码之前,我给 t3code 定了几条硬性约束,这些约束后来证明极其重要:

  • 单用户设计:只服务我自己(最多加上一两个朋友),不做权限体系,不做多租户。这让整个数据模型省掉了一大半复杂度。
  • 本地优先:数据文件就是 SQLite,存放路径固定,备份就是复制文件。不引入远程数据库,免得部署和运维成本失控。
  • 尽量少依赖:能自己写的就不引包,每个引入的依赖都必须能说清楚"它解决了我自己写会解决不了的问题"。最终核心依赖控制在十个左右。

为什么这么在意边界?因为我见过太多个人项目死于"顺手加个功能"。加收藏、加分享、加多人协作、加移动端适配……每一个功能都有理由,每一个都会把时间砸进去,最后做出来的东西不是自己需要的,也永远没法收尾。t3code 宁可功能少而精,也要保持每周都能迭代出一个可感知的版本。

2. 技术骨架:手搭全栈时我是怎么设计的

2.1 为什么没继续用之前的"一把梭"方案

在 t3code 之前,我的小项目基本都是 Node.js Express 后端加静态 HTML 前端,或者干脆用 Next.js 一把梭。这种方式启动很快,但项目复杂度一上来,问题就暴露了:类型不互通,改一个接口要前端后端来回对参数;没有清晰的模块边界,工具函数到处乱塞,到最后自己都找不到业务代码。

所以 t3code 的技术选型决策,第一原则不是"哪个框架最流行",而是"哪种结构能让我在三个月后还能秒懂三个月前的代码"。我最后定下来的是:

  • 前端:React 18 + TypeScript,Vite 做构建。没有选重型 UI 框架,组件自己写,样式用 CSS Modules。
  • 后端:Node.js + Fastify。轻量、原生支持 TypeScript 类型定义、插件体系干净,比 Express 更适合做工程化一点的小服务。
  • 数据库:SQLite + Prisma。单文件部署,Prisma 的 schema 管理和 migration 机制很省心。
  • 通信:JSON over HTTP,配套 zod 做运行时校验。每一步的输入输出都有类型保障。
  • 进程管理:后端服务挂在 PM2 下,前端静态文件用 Nginx 托管并在本地反代。

这是一个相当"保守"的选型组合,没有追任何新版本、新特性。为什么保守?因为个人项目的核心诉求是稳定可维护,而不是炫技。用大家都熟的技术,出问题时搜索引擎都能救你。

2.2 类型安全是 t3code 的生命线

t3code 最大的工程化亮点,是打通了从前端到数据库的端到端类型推导。具体的做法是:所有 API 的请求和响应模型,都在一个共享类型包里定义,前后端分别引用;后端用 zod 做运行时校验,确保进入业务逻辑的数据一定是合法的,前端则直接用类型定义生成请求函数,参数错误在编译期就会被拦下。

这套方案的收益,在联调阶段体现得尤其明显。以前写前后端对接,最痛苦的时刻就是"我记得这个接口返回的字段叫 status,怎么变成了 state"。现在,接口字段只要有一方改动,编译直接报错,根本留不到运行期。

需要说明的是,这种端到端类型方案在当前生态里可能有各自的实现路径,我选择的 zod 共享 schema 方案也只是其中一种,核心思路是让类型定义成为前后端之间可执行的契约。如果你用的是 Next.js 这类全栈框架,内置的 RPC 能力也能达到类似效果。关键不是用什么工具,而是你要在系统里扎下"契约先于实现"这根弦。

2.3 数据模型设计的三个关键决策

t3code 的数据模型并不复杂,核心就三张表:snippets(代码片段)、tags(标签,多对多关联)、versions(片段历史版本)。但就这三张表,我在设计时做了三个在后来看到价值的决策。

第一个决策:标签做成交联表,而不是数组字段。很多小项目图省事,会把标签直接存成 JSON 数组。但一旦你需要"查某个标签下所有片段""统计标签使用频率",JSON 字段会让你痛不欲生。多对多关联表前期只多写了十几行代码,后期省下的写 SQL 的时间是不可估量的。

第二个决策:所有列表查询都用统一的分页和过滤参数。?page=1&pageSize=20&lang=javascript&tag=hooks,这个查询串模式在 t3code 里是通用的,任何列表接口都遵循同一套规范。这意味着前端写一个usePagedQuery钩子,就能适配所有列表页面,后端也只要写一个通用的查询解析器。

第三个决策:版本历史采取"存档快照"而不是"差异补丁"。每次编辑全文存一个快照,字段简单,恢复也快。缺点是多占存储空间,但对个人项目来说,几百个片段、每个几 KB 的快照,几乎可以忽略不计。差异补丁看起来高大上,但合并和回滚的处理成本会显著拖慢开发。

2.4 目录结构:一眼就能看懂的分层

t3code 的项目目录,我用了比较经典的 monorepo 风格,但没上 Nx 或 Turborepo,就单纯靠 npm workspace 搞定:

t3code/ ├── packages/ │ ├── shared/ # 共享类型、zod schema、常量定义 │ ├── api/ # 后端服务(Fastify + Prisma) │ └── web/ # 前端应用(React + Vite) ├── scripts/ # 本地开发和部署脚本 └── docs/ # 项目文档和决策记录

这种分层的好处是:想改前端,永远不需要在api目录里找代码;想改类型定义,只需要打开shared。每个包的依赖边界也清晰,web永远不会直接碰api的内部实现,只能通过shared里约定的接口模型通信。

目录结构做清晰了,开发心态也会跟着变。以前代码一团糟的时候,我常常连打开编辑器都感到抗拒;现在进了每个包,都知道自己该动哪里,写代码的精神负担小了很多。

3. 核心功能拆解:片段的增删改查里藏了多少细节

3.1 检索模块:从关键词模糊匹配到多维查询

t3code 的检索模块是核心功能,我前后做了三个版本。

第一版就是最朴素的 SQLLIKE '%keyword%',能搜但精度低,比如搜 "useState" 会把 "useStateWithInitialState" 这种臃肿的示例全拉出来。

第二版做成了关键词分词匹配:前端把输入拆成多个词,后端对每个词做包含匹配,取交集。这一步把"精确感"提上来了,但标签和语言的过滤还是独立的查询参数,组合起来有点绕。

第三版引入了简单的多维查询语法:支持lang:javascript限定语言、tag:hooks限定标签、is:starred只看收藏,再加上纯文本关键词。前端解析语法生成结构化条件对象,后端用一个统一的buildWhereClause函数把这些条件翻译成 Prisma 查询。整个模块的调用链路非常干净。

这套第三版查询语法,在代码编辑器里也能被聪明的开发者拓展成更复杂的自定义查询语言,但对我目前的使用场景已经绰绰有余了。每天高频操作就是输入一两个关键词加一个语言限定,响应基本在几十毫秒内。

3.2 编辑模块与历史版本:保存时的顺序问题

历史版本模块踩的坑比想象中多。初版设计是"每次编辑就生成一个新版本",结果发现这个逻辑有个时序隐患:如果用户在保存时连续点了两次保存按钮,或者编辑过程中前端组件卸载重挂,会导致重复的快照插入,版本历史里出现连续两条一模一样的记录。

后来我加了一层变更指纹校验:保存时,前端把当前内容做一次简单的哈希摘要,后端对比该片段最新版本的摘要,如果一致就拒绝生成新版本,只更新"最后修改时间"。这个改动很小,但版本历史的可信度一下就上去了。

另一个编辑模块的细节是防抖暂存。t3code 的编辑器支持本地草稿自动暂存到 localStorage,每 2 秒一次。这样即使浏览器崩了,重新打开页面也能恢复未保存的输入。这个功能的实现原理很简单,但用户感知很好,我后来用任何笔记工具,都会下意识期待这个能力。

3.3 片段渲染与代码高亮:自己写就别折腾

代码高亮这个功能,我一开始是自信满满想自己写分词器的。写了两天之后,我果断放弃,换成了开源的代码高亮库。这个决策想特别分享给做类似项目的朋友:高亮分词是典型的吃力不讨好的模块,它属于那种"你做了也看不见明显收益,但占用时间极多"的工作。

代码高亮库的用法也很简单,拿到代码文本,指定语言,就能返回高亮的 token 序列,前端渲染成带class的<span>就完成了。配合一个深色主题,视觉效果立刻拉满。

我还在此基础上加了一个很实用的功能:可点击的行号标记。每行代码前面有个小圆点,点击后可以在当前片段里插入一个"书签"标记,类似"这行代码是这个例子的关键"。这样一个片段存下来,无论是自己回头还是分享给朋友,别人能一眼定位重点。

3.4 收藏夹与常用片段:排序算法比你想的复杂

"收藏夹"功能看着简单,就是加个starred布尔字段。但真正做的时候,发现列表排序的策略才是关键。

最开始是按starred倒排,再按更新时间倒排。用了一阵子发现问题:收藏的片段多了以后,更新频率低的、但很重要的片段会沉到底部。后来我加了一个"最近使用"字段,每次复制片段时都更新时间戳,然后排序规则改成:starred优先,已收藏的按最近使用倒排,未收藏的按创建时间倒排。

这个改动让收藏夹真正变成了"高频工作台",而不是"历史归档室"。每次打开 t3code,最上面永远是我最近在用的片段,这种顺手感是纯按时间排序给不了的。

4. 测试与排错:我从一次奇特的"幽灵字段"事件中学到的

4.1 测试分层:单测、集成、手动验证各管一段

t3code 没做全套的自动化测试,但保持了三个级别的组合:

  • 单元测试:只覆盖共享包里的工具函数,比如查询语法解析器、版本摘要算法、标签规范化。这部分用 Vitest,跑得飞快。
  • 接口集成测试:用 Fastify 自带的inject能力,在不启动真实端口的情况下,直接模拟 HTTP 请求打接口。测试数据库用单独的文件,每次跑之前重建表结构。
  • 手动验收清单:一个三十来项的清单,涵盖主要操作链路。每次发版前过一遍,大概十分钟,但能拦住 90% 的明显回归。

很多个人项目开发者会犯一个极端——要么完全不写测试,要么非要追求 100% 覆盖率。这两条路在个人项目里都走不通。正确的姿势是:只在纯逻辑模块写密集单测,I/O 部分靠集成测试,UI 部分靠手动验收。t3code 的开发节奏是每天写代码,每周跑一轮完整验收,测试带来的安心感非常值得。

4.2 一次"幽灵字段"异常导致的排查链路

这个排错记录值得单独写一小节,因为它展现了一个非常典型的排查链路。

现象:在 t3code 的片段列表页,某个老片段显示的语言标签是空白的,但打开编辑页,语言字段明明有值。

第一反应:以为是前端渲染漏了。查了组件代码、查了 API 返回的数据,结果接口给的就是null,前端没背锅。

第二反应:以为是数据库里那一条数据坏了。直接 SQL 查那条记录,language字段有值,不是数据问题。

第三反应:怀疑 Prisma 的映射有 bug。把 Prisma 查询的 SQL 日志打开,发现了问题——查询语句里根本没有这个字段。但奇怪的是,全新建的片段语言显示正常。于是对比了新老片段在数据库里的差异,发现问题出在:老片段在最初建表时的字段名是lang,后来迁移改成了language,Prisma 的 migration 生成了新列,但某些旧数据因为迁移脚本里的默认值逻辑,新列被填成了 null。

最终修复:补一条针对历史数据的一次性迁移脚本,把lang有值但language为 null 的记录统一回填。整个过程涉及前端组件、接口返回、数据库直查、ORM 映射、迁移脚本五个层面,如果不是一层层往下摸排,很容易误伤。

这次的教训是:ORM 永远比你以为的更加不可靠,尤其当你改过 schema 做过迁移之后,别默认旧数据都符合新约束。这也是为什么我后来坚持所有 schema 变更都要配套写数据回填脚本,而不是加个 nullable 字段就完事。

4.3 状态管理的教训:别再让"全局状态"发光发热了

早期的 t3code 前端用过一段全局状态管理库,把列表数据、选中项、编辑中草稿全放在全局 store 里。结果项目写着写着,我发现一个诡异的问题:有时候改了 A 片段的内容,B 片段显示却出现了 A 的内容。

排查了半天,定位到原因:全局 store 里的某个缓存字段在初始化时被错误地共享了引用,两个组件拿到同一个对象,一个改了,另一个跟着遭殃。这几乎是全局状态的经典通病——你在全局 store 存了哪种引用类型的数据,就要做好它被任何一个组件改动并影响所有消费者的觉悟。

之后我基本把所有非跨页面级的状态都收回到了组件局部,用服务层的请求缓存代替全局 store。经过这次重构,t3code 前端的状态一致性隐患基本清零。做个人全栈项目,尤其是中小工具型产品,能用 URL、能靠服务端数据刷新解决的,就不要引入全局状态层。

5. 性能优化与体验细节:小项目也能有大讲究

5.1 首屏加载:把包体积压下来

t3code 前端在刚做完时,首屏加载居然要 200 多 KB 的 JS 传输量,这个数字对于一个内部工具来说有点过了。我做了三个动作改善:

  • 路由级的代码切割:列表页、详情页、编辑页分别打包,首次只加载列表页需要的部分。
  • 手动分包:把代码高亮库和编辑器等重模块单独拆成一个 chunk,而且只在真正打开编辑页时才加载。
  • 去掉不必要的 polyfill:Vite 构建时默认的 target 调成现代浏览器,省掉了大量老版本兼容代码。

这三板斧下来,首屏 JS 降到了 80KB 上下,换来的主观感受是"秒开"。对于个人项目,这类优化一定要做在真正被使用之后,从"可用"到"轻快"的升级感,使用体验提升非常明显。

5.2 响应速度优化:两条关键链路

t3code 里最常走的链路是"搜索"和"打开编辑页"。这两条链路我都做了针对性优化。

搜索链路:后端是 SQLite 单文件数据库,查询本身很快,真正的瓶颈在序列化响应和网络传输。优化就是只返回列表需要的精简字段(id、标题、语言、标签名列表、更新时间),不把整个代码内容塞进列表响应。这样搜索出来的数据体量小了一个数量级,前端渲染负担也小很多。

打开编辑页链路:按需加载代码内容。列表页完全不加载代码正文,只有点击进入详情或编辑时,才发单独请求拿完整代码。这样即使用户收藏了五百个片段,列表页内存占用也始终稳定。

这两条链路看起来都是不起眼的小设计,但实际用起来的流畅感差距很大。工程化不一定要上微服务、消息队列那一套,把这些常规细节做出极致就够了。

5.3 空状态、加载态和错误提示:不能等出事了才想

体验细节里,最容易被忽略但又最影响感知的,是系统的空状态和错误提示。t3code 里我做了三个时刻的特别设计:

  • 搜索无结果:显示的不是干巴巴的"无结果",而是给出一个建议——"试试减少关键词、或者不加lang:过滤",并展示当前已选的所有过滤条件,方便逐个移除。
  • 加载失败:把接口出错时的原始错误信息收起来,展示一句人话提示和一个"重试"按钮,而不是任由页面白屏。
  • 本地草稿与服务器版本冲突:编辑页打开时如果发现本地有暂存草稿,弹窗让用户选择用草稿继续还是以服务器版本为准,避免用户辛辛苦苦写的半截代码莫名消失。

这些小设计说来都不难,但它们决定了 t3code 是一个"工具"还是"作品"。我自己使用的时候,每次看到精心设计过的空状态提示,都会对这个项目的价值多一分认可。

6. 上线部署与运维基建:本地跑通和跑在生产环境是两回事

6.1 部署环境的搭建与取舍

t3code 的部署架构很简单也足够稳定:

  • 后端 API:跑在一台云主机上,PM2 守护进程,监听 3000 端口。
  • 前端静态资源:Nginx 托管构建产物,并把/api路径反代到本地 3000 端口。
  • 数据库:SQLite 文件放在云主机的一个固定目录下,每个整点用 cron 任务复制一次快照到对象存储服务的一个私有桶里,保留最近 30 份。

所有的环境变量(数据库路径、端口、对象存储配置等)集中在一个.env文件里管理,密钥不进代码仓库。部署脚本是一段 shell 脚本,负责拉代码、装依赖、跑 migration、构建前端、重启 PM2,整个过程大概 20 秒。

有朋友建议我直接上 Docker 和容器编排,但我觉得 t3code 这种单机服务,Docker 属于额外复杂度。只要能保证"十秒回滚"和"每天自动备份",物理机上直接跑完全可以胜任。个人项目的基建原则是:复杂度要永远跟业务体量匹配,不要为了"规范"而引入你根本用不上的复杂度。

6.2 从一次故障中学到的数据完整性教训

t3code 上线第三周,出现过一次小故障:用户刷新详情页时,偶尔会看到"片段不存在"的提示。我心想这不可能,数据明明都在。

查了半天,问题出在 PM2 的集群模式。我为了让 Node 进程跑得稳一点,开了两个实例,它们共享同一个 SQLite 文件。后果是:两个进程同时进行写操作时,SQLite 产生锁竞争异常,偶尔一次插入就失败了——版本保存不成功,详情页自然查不到。

解决方式有两种:一是把 PM2 改成单实例模式,彻底避免并发写;二是换 PostgreSQL 这类支持并发的数据库。对 t3code 的使用场景来说,单实例完全够用,于是选择改部署配置,一行搞定。这也让我以后养成了一个习惯:引入任何集群、负载均衡之类的高可用设计前,先问问系统底层依赖是不是真的支持并发。

6.3 监控与日志:从"不知道坏了"到"提前发现要坏"

个人项目一开始是不做监控的,因为觉得不必要。直到有一次数据库备份脚本因为目录权限问题连续失败了三天,我居然完全没发现。从那以后加了两个简单的观测手段:

  • 健康检查接口:后端暴露一个GET /healthz,定时任务每分钟请求一次,连续三次失败就触发告警(直接推送到我的手机通知)。接口内部还会顺便检查 SQLite 文件能否正常读写,以及磁盘剩余空间是否低于某个阈值。
  • 结构化日志:所有请求和关键操作都输出 JSON 日志,包含时间戳、路径、耗时、状态码、错误信息。排查问题时,只要搜关键词就能快速定位链路。

这两件事做起来成本极低,但价值非常高。监控的意义不在"出了事能看见",而在"出了事能在第一时间发现并快速定位"。对于个人项目这种维护精力有限的场景,这就是最合适的运维基建。

7. 持续迭代与项目复盘:t3code 的下一站在哪里

7.1 回顾:哪些选择帮了大忙,哪些决定拖了后腿

项目走到现在,回过头来复盘,有几个选择是明显正确的:

  • 端到端类型契约:这可能是 t3code 能保持健康迭代的最大功臣。每次改动接口,编译期的报错帮助你避免了大量运行期问题。
  • 极简的部署架构:单机、单实例、SQLite、PM2,这套组合极其省心,让我把精力全花在功能上。
  • 刻意不做功能:当时没做云端同步、没做多人协作、没做浏览器插件,让 t3code 的每个功能都足够扎实。

拖过后腿的也有几个:早期全局状态管理导致的引用混乱,浪费了一整天的排查时间;还有一个是搜索模块第三版的多维查询语法,在构造复杂条件时偶尔会出错需要调试,如果我改用"几个独立输入框"的交互,可能更快也更稳。

7.2 下一步的几个迭代方向

按照"使用驱动开发"的原则,t3code 接下来的迭代方向按优先级排列:

  1. 从代码片段扩展到小项目模板管理:比片段更重一点的、能直接 clone 下来就跑的模板工程。这正好把 t3code 的模型从"短片段"提升到"整个目录"。
  2. 给常用操作加快捷键:搜、建、存、复制、收藏,全套键盘操作,做成一个可以完全离开鼠标的界面。
  3. 导出与分享的更好体验:目前导出格式很朴素,希望做成一个"图片分享卡片"效果,方便在社区里晒代码的时候直接用。
  4. 浏览器插件形式:在浏览器里选中代码就能直接收录进 t3code,这个功能值得单独花时间去打磨。

7.3 给同类个人项目的一点经验

最后分享一点从 t3code 里沉淀出来的个人项目经验,不针对具体技术,更多是节奏和心态层面:

  • 先追求能用,别追求优雅:第一版能跑起来就行,哪怕代码丑一点。你会发现"能用之后产生的真实反馈",会给你接下来所有重构提供最清晰的依据。
  • 写决策记录:每个关键的技术选型,用几行字记下来为什么这么选、当时考虑的取舍是什么。三个月后你想改某个模块,翻一翻这个记录,就能避免推翻你当初深思熟虑的决定。
  • 保持能随时部署和回滚的能力:哪怕项目再小,从第一天就搭好自动部署和备份脚本。这让你在任何时候都敢改代码、敢实验,因为你知道出事能拉回来。
  • 让项目有名字有性格:一个像 "t3code" 这样的名字,会潜移默化地让你以更高的标准对待这个项目,它就不再是"练手的玩具",而是一个值得交付的作品。

t3code 目前还在按两周一个小迭代的节奏往前走。我现在最享受的时刻,是某一刻突然需要一个现成代码片段,打开 t3code 输入几个关键词,那个收藏许久的片段就在最上面等着我。一个人做的小工具,能让自己每天用起来都觉得"真香",这大概就是个人项目最好的状态。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询