
t3code 这个名字听着像某个开源项目的临时代号其实是我自己维护的一套项目初始化与代码生成工具代号里的 t3 是 third generation 的缩写。前两代踩了不少坑到第三代才真正把“能用的模板”做成了“能自我进化的工具”。简单说t3code 是一套基于 Node.js TypeScript 的命令行脚手架用来解决一个特别朴素的痛点每次开新项目从目录规划、框架选型、环境变量到部署脚本重复劳动太多了。它更像团队内部的“工程模板管理员”你告诉它项目叫什么、技术栈用什么、需不需要后端和数据库它就在本地生成一整套可直接运行的前后端代码还能在后续开发里按约定生成业务模块、一键打包部署。这篇文章适合经常开新项目的前端或全栈同学、想给团队做工程化提效的人以及任何对“写一个自己的 CLI 工具”感兴趣的人。我会把 t3code 的完整设计思路、核心实现和实操流程拆开讲也会把我这一年维护过程中踩过的坑一并列出来。1. 从“复制粘贴模板”到 t3code我为什么在第三轮才做对1.1 第一代模板能用但改不动最早那会儿我所谓的“模板”其实就是一堆打包好的项目文件夹。每开一个项目从旧仓库里把整个目录复制一份然后全局替换项目名、改掉多余的代码再删掉旧的 Git 历史。这套流程在今天看来非常原始但当时确实能省掉从零写 webpack 配置的时间。问题出在维护上。第一代“复制粘贴”式的模板散落在各个项目里根本没有一个统一源头。今天在这个项目里修了一个鉴权 bug明天另一个项目里依然用着有 bug 的旧代码。你不可能把几十个项目挨个同步一遍哪怕你现在想改一个公共组件的样式都得思考“当初复制的是哪个版本”。更可怕的是复制过来的项目里还残留上一家客户的域名、数据库连接串、版权文案上线前才发现要清理的东西比写的功能还多。这也是第一代模板最大的教训模板不是一次性拷贝它应当是一个长期维护的资产。复制粘贴虽然快但一旦失去统一版本管理维护成本会指数级上升。1.2 第二代模板能改但记不住后来我吸取教训把模板单独拉成了一个 Git 仓库团队里每个新项目都从这个仓库拉代码。这算是第二代模板。好处很明显修 bug 可以直接改模板仓库再发一个 tag大家重新拉取就能拿到最新版本。至少不再存在“每个项目一个版本”的失控状态。但新的问题又出现了模板仓库是死的不同项目的需求却五花八门。有人要微前端有人只需要一个简单的管理后台有人用 pnpm有人还在用 npm有人要接 MySQL有人只需要 SQLite。由于模板把所有这些都揉在一起拉下来之后第一件事就是对照 README 里的说明手动删掉不需要的模块、改包管理器、改环境变量。这份 README 大约有 300 行写着“如果不需要 xxx请删除以下文件”。实际操作中大家经常漏改一两处最后项目跑不起来还得花半天排查。我印象最深的一次是同事拿着从模板拉出来的项目问我“为什么启动后一直报数据库连不上” 我一看他压根不需要数据库但模板默认初始化了数据库模块连接配置默认指向内网的一个实例。他按照文档删了前端几个页面却漏掉了后端的初始化代码。这种“删减式”的模板本质上还是在让使用者为模板的通用性买单。1.3 第三代 t3code 的设计目标到第三代也就是现在的 t3code我的思路完全变了不再给你一个装满各种功能的模板而是让程序根据你的回答现场生成一个刚好的项目。这就是“代码生成器”和“模板仓库”的本质区别。t3code 给自己定了几个明确的设计目标按需生成不要的东西绝不生成减少大量“用不到的代码”干扰。可读可改生成出来的代码和手写的一样自然没有奇怪的黑魔法方便二次修改。本地优先所有模板和依赖缓存在本地不强制联网也不会上传任何使用数据。约定大于配置目录结构遵循固定约定减少项目之间的随意性让团队切换项目时不至于迷路。幂等可靠同一命令重复执行不会产生重复文件不会把项目搞坏。听起来可能有点抽象打个比方第一代像复印课本第二代像买配套参考书再自己划重点第三代则像是拿到一份点菜单选什么就上什么而且上菜的人就是厨师本人。2. t3code 核心模块拆解init、gen、deploy 是怎么工作的2.1 技术栈选型为什么用 Node TypeScript在做 t3code 之前我认真考虑过用 Shell 脚本或者 Python 来写。Shell 适合做系统层面的文件操作但对跨平台支持、交互式问答、依赖管理都不太友好Python 虽然跨平台但让前端团队去维护一个 Python 工具心理门槛还是有点高。最终选了 Node.js TypeScript理由很直接和前端生态同构用前端项目的开发体验维护这套脚手架团队成员都能上手不需要额外学一门语言。跨平台天然支持Node 的 fs/path 模块在不同操作系统上行为统一配合cross-env和shelljs脚本也好写。类型系统撑着复杂度t3code 涉及的配置项、模板变量、文件映射非常多TypeScript 能帮我提前发现很多低级错误。发布方便打包成 npm 包之后团队内部一条npm i -g t3code就能装好和装其他 Node 工具没区别。至于为什么不用市面上现成的脚手架工具比如 create-react-app 或 create-vite是因为它们解决的是“某个框架的初始化”问题而 t3code 要解决的是“整个项目的工程规范”问题。我需要把团队内部的代码规范、目录约定、部署脚本、环境变量体系全部搓成一个工具这属于“自定义工程化能力”直接用现成脚手架反而要绕很多路。2.2 init用一场“填空问答”代替 300 行文档t3code 的第一个核心命令是t3 init负责初始化一个全新的项目。它采用命令行交互问答的方式逐项收集信息。我把问答设计成了几组项目基础信息项目名、包名、描述、Git 仓库地址。技术栈选择前端框架目前内置 React、Vue是否包含 Node 后端。后端相关开发端口、是否接入数据库、数据库类型SQLite/MySQL。工程能力是否需要 Docker 部署脚本是否需要 CI 配置文件包管理器选哪种。每一步交互背后都对应着一组模板变量。比如用户填的项目名会经过几个处理函数转成合法的 npm 包名、转成变量名风格、转成路由前缀。这套转换逻辑我单独抽成了模块后面gen命令也要复用避免同一套命名规则在两个地方出现分歧。初始化时最核心的逻辑是“根据答案动态选择模板文件”。我维护着一个文件清单每个文件都声明了自己的适用条件比如backend/src/index.ts只在选了“包含 Node 后端”时生成docker-compose.yml只在选择了 Docker 部署时生成database/sqlite.ts只在选择 SQLite 时生成。这样生成的目录里不会出现一份你根本不需要的文件。此事听起来简单实际实现时最难的已经不是“文件加不加”而是“引用的依赖怎么对得上”。选 SQLite 就要装配套的 ORM 依赖选 MySQL 则是另一套连接配置。我是靠一份“能力矩阵”来解决的——每种能力定义了自己需要哪些依赖、需要引入哪些文件、需要在主文件里追加哪行初始化代码最后统一合并到生成的 package.json 和入口文件里。2.3 gen按约定生成业务模块如果说init是解决“项目怎么开始”那gen就是解决“业务代码怎么写才统一”。它在已经初始化好的项目里根据用户输入生成一个业务模块。以中后台最常见的“用户管理”模块为例执行t3 gen module user后工具会按照约定去生成一组文件前端页面文件user/index.vue或user.tsx前端 API 请求模块userApi.ts后端路由文件user.route.ts后端 Service 文件user.service.ts数据库表定义或迁移文件如果项目接入了数据库自动在路由注册文件里追加一行路由挂载这套“一个业务模块 页面 接口 路由 服务 数据表”的约定是和三四个团队一起对出来的结果。以前大家写代码风格差异很大有人喜欢把接口直接写在页面里有人习惯拆一个单独 service 文件review 的时候经常为此争论。t3code 直接把这种“默认正确”的写法固化在工具里生成出来的代码就是团队评审过的最佳实践争议自然就少了。实现上gen命令比init更复杂的地方在于文件修改。它不光是“生成新文件”还需要在已有的文件里插入代码比如在路由文件里追加一行。这块操作必须足够谨慎不能直接正则匹配全局替换因为容易误伤注释或字符串也不能简单地把原文件覆盖掉因为用户可能已经在里面加了自定义内容。t3code 的做法是解析文件结构找到约定的挂载点锚点在锚点后面插入新片段并且保证重复执行时不会重复插入。这个“幂等插入”逻辑我写了几十个测试用例算是整个项目里最容易出错的部分。2.4 deploy把部署变成一条命令t3 deploy是这套工具的第三个核心命令也是我后来才补上的。最初版本只负责“生成代码”但用了一段时间后发现项目能跑通不算完真正烦琐的是部署打包、压缩静态资源、连服务器、同步文件、重启服务。如果只靠人肉操作早晚会出问题。t3code 的 deploy 命令本质是一个“部署流水线封装”。它会按顺序做几件事读取.t3rc里的部署配置比如目标服务器地址、路径、SSH 端口构建前端和后端把产物打包成 tar.gz通过 SSH 将压缩包传送到远程服务器在远程执行解压、备份旧版本、重启服务的脚本返回部署结果和日志摘要。这个流程用到的远程操作能力封装在了一个叫remote-run的内部模块里。它内部用的是 Node 的child_process调用ssh和scp命令好处是不用额外引入重量级的 SSH 库坏处是本机必须能通过 SSH 访问目标服务器这个前提条件我会在工具启动时主动检查给出友好提示而不是让用户面对一堆晦涩的报错。3. t3code 实操全流程10 分钟从空目录到线上3.1 安装 t3code 与初始化项目我自己日常使用都是通过把项目 clone 下来后执行npm link把 t3code 直接链接到全局命令。如果只是使用而非开发也可以直接通过 npm 安装npm i -g t3code t3 --version # v0.3.0安装好之后新建一个空目录然后执行初始化命令mkdir my-app cd my-app t3 init终端会出现一组交互问答我第一次执行的时候完整输入内容大概是这样? 项目名称: my-app ? 包名: my-app ? 项目描述: A demo project ? 前端框架: React ? 是否包含 Node 后端: Yes ? 开发端口: 3000 ? 数据库类型: SQLite ? 包管理器: pnpm ? 是否需要 Docker 部署配置: No ? 是否需要 CI 流水线: Yes确认选择后t3code 会在当前目录生成一个完整的项目。整个过程不需要联网下载模板因为所有模板文件都打包在工具内部的.templates目录里。生成完成后命令行会打印出项目结构和下一步提示比如“进入目录执行 pnpm install然后运行 pnpm dev”。这里特别想提一下“开发端口”这个参数。很多人觉得端口随便填一个 3000 就行但 t3code 在生成时会检查端口占用情况。它不是只把这个数字写进配置还会提示你如果本地已经被其他服务占用后续启动时由工具自动尝试邻近端口。这个细节看起来小但在团队落地时非常实用——前几天有两个同事同时初始化项目不约而同选了 3000结果其中一个人启动失败之后很快意识到要改配置另一个则被工具提示直接引导改了端口。3.2 生成一个完整的业务模块项目初始化完成并启动之后假设我现在需要加一个“文章管理”的功能模块。执行t3 gen module articlet3code 会先分析当前项目的技术栈与已有目录结构再执行文件生成和代码插入。生成的目录结构大致如下src/ ├── pages/ │ └── article/ │ ├── List.tsx │ ├── Edit.tsx │ └── ArticleForm.tsx ├── apis/ │ └── article.ts ├── routes/ │ └── article.ts ├── services/ │ └── article.service.ts └── db/ └── migrations/ └── 20240605120000_create_article.js每个文件名和函数名都不是随手写的。article这个模块名会经过命名转换引擎模块名转 PascalCasearticle-Article用于类名和组件名转 camelCasearticle-article用于变量和文件名后缀转 kebab-casearticle-article用于路由前缀复数化处理article-articles用于 API 路径。比如 API 文件里生成的请求路径就是/api/articles。这一套命名规则如果不统一前端 fetch 路径写articles后端路由挂载的却是article联调的时候就只能靠人肉猜。t3code 直接把转换规则固化在工具里谁生成的项目都是同一套规律出错的概率就小了很多。生成结束后的提示信息会告诉你自动做了哪几件事创建了 7 个文件、在路由文件里追加了一条注册记录、在导航菜单配置里增加了一个入口。如果你不需要菜单入口可以加参数--no-menu跳过。3.3 部署与环境配置本地开发没什么问题时就轮到t3 deploy。部署前需要确认.t3rc文件配置是否正确。一个典型配置如下environments: staging: host: 1.2.3.4 port: 22 user: deploy targetDir: /www/my-app buildCommand: pnpm build restartCommand: pm2 restart my-app production: host: 5.6.7.8 port: 22 user: deploy targetDir: /www/my-app buildCommand: pnpm build restartCommand: pm2 restart my-app执行部署t3 deploy --env staging命令依次执行构建、打包、上传、远端部署。成功的话终端会显示每一阶段耗时、远端返回日志的最后 20 行、以及这次部署的产物包哈希值。失败的话会在本地保留一个.t3-deploy临时目录方便排查是构建阶段还是上传阶段出了问题。我实测下来这个流程最合适的是“中等复杂度”的部署场景不是单页静态站那种纯放文件的也不是几十个微服务那种需要完整容器编排的。如果你的项目本来就用了 Kubernetes 这类平台t3code 的部署模块就没什么用武之地直接用平台本身的能力会更合适。4. 维护 t3code 一年我踩过的坑和排查思路4.1 常见问题速查表工具用久了有些问题反复出现。我整理了一份速查表希望对读者有参考价值现象原因解决方法初始化后启动报错“模块不存在”模板中部分文件的 import 路径引用了未生成的模块检查.t3rc中模块能力矩阵配置确保依赖模块被正确声明重复执行gen生成了重复文件幂等插入逻辑没有生效升级到 v0.3.0 以上旧版本请先删除生成文件再执行Windows 下生成的代码路径分割符是\Linux 下无法运行模板中直接使用了路径拼接统一改用path.join处理模板内路径模板中禁止手写路径分隔符ESLint 报错“变量未使用”生成的配置代码包含用不到的默认变量在生成时通过模板变量注入而不是先写死再由用户改部署时 SSH 一直卡住本机缺失 SSH 公钥配置或远程服务器未信任对应密钥先手动执行ssh userhost确认可免密登录再跑 deploy环境变量拼写不一致文档和模板各写各的环境变量统一以.t3rc为唯一来源生成时注入模板4.2 三个值得抄走的设计细节第一所有模板变量都应该集中在单一配置里而不是散落在模板文件中。t3code 早期的版本里每个模板文件顶部都写了类似{{ projectName }}的模板变量结果有一次修改了配置文件里项目名前缀的逻辑却没有同步到所有模板生成出来的项目出现了两种命名风格。现在我把所有可变量集中在一个TemplateContext里模板中只能引用 context 已有的字段。新增变量必须先在 context 中声明模板文件里如果出现了未声明的变量t3code 会在渲染阶段直接报错。第二给所有生成命令加一个--dry-run选项。这个选项不会真正创建或修改文件而是打印一份“将要发生什么”的清单会在哪些位置创建什么文件、会在哪个文件的哪一行插入什么代码。刚启动 dry-run 是给我自己调试用的后来发现团队里其他人也非常依赖它——尤其是在跑gen的时候大家想知道这次操作会不会改动到自己正在写的那部分代码。一个十余行的小功能回报率却非常高。第三尽量不修改用户已经改动过的文件。t3code 的 gen 命令虽然会在路由文件里插入代码但我在实现时做了边界检查如果检测到目标文件距离上次生成后被用户大量修改工具不会强行插入而是把建议的插入片段写入一个.t3-pending.patch文件让用户自己决定要不要应用。这个保守策略救了我很多次因为自动插入代码最大的风险不是插错位置而是覆盖用户手写的内容一旦发生用户对工具的信任度就会瞬间崩塌。4.3 关于依赖和版本管理的一点经验另外一个想单独拿出来说的是依赖管理。t3code 作为脚手架工具自己身上的依赖必须非常克制否则用户安装一次就要拉下几百 MB 的 node_modules体感很差。早期版本我引入了比较重的 CLI 交互库和模板渲染引擎最终体积接近 60 MB。后来我重写了交互部分只在 npm 包发布时保留必要依赖模板文件本身不做打包压缩体积降到了 15 MB 以内。但这里有一个反向的坑t3code 安装时体积小了生成项目时却需要依赖用户环境里的 Node 版本。有些模板代码里使用了较新的语法如果用户环境的 Node 版本太旧初始化后项目可能直接跑不起来。我在 init 命令里加了一个 Node 版本检查低于设定版本时会主动降级生成兼容代码或者干脆提示用户升级 Node。这个检查逻辑本身很简单但特别能体现一个工具对用户环境的敬畏程度。最后再分享一点个人的感受。t3code 现在维护了一年多真正让我觉得有价值的不是它帮我省了多少创建文件的时间而是它逼着我把“默认正确”的工作方式沉淀成了可以复用的代码。最开始配置的那几套约定后来逐渐成为团队新成员快速上手的重要文档因为他们不需要读很长的 README只需要跑几条命令就能看到一份结构清晰、可以直接开写的项目。如果你也想做类似的内部脚手架我的建议是别一上来就把功能铺得很大先让你的 init 命令稳定跑通再逐步加入 gen 和 deploy每一步都要像对待正式项目一样写好测试。工具类和业务代码不同你写坏的每一行都会在无数个项目里复现。