t3code:告别样板代码的TypeScript命令行代码生成器

发布时间:2026/10/26 8:38:09
t3code:告别样板代码的TypeScript命令行代码生成器 凌晨一点我盯着屏幕上第 N 份从旧项目里复制过来的表单页CtrlH 替换完字段名正想长舒一口气却发现漏改了一个地方编译直接报错。那一刻我意识到作为一个靠“CtrlC / CtrlV”行走江湖的程序员我真正缺的不是手速而是一个能把我从样板代码地狱里捞上来的工具。于是 t3code 就这么诞生了。它是我花了一周业余时间写出来的一款命令行代码生成器你告诉它“我要一个用户模块”它会先问你几个问题模块名叫什么、要不要路由、要不要状态管理、用哪种接口风格然后按团队规范把整套目录、页面、API 层、类型定义一口气生成出来落地就是能编译、能跑的代码。名字里的 t3是我自己都没想太多取的代号——这已经是第三版重写了第一版用 Python 写过脚本第二版用 Node 写了一半第三版换成 TypeScript 全量重构。如果你也经常被重复代码折磨、维护过一堆没人愿意看的内部脚手架这篇文章应该能帮到你——我会把 t3code 从设计动机到核心实现再到我踩进去爬出来的几个深坑完整拆开讲一遍。1. 为什么会有 t3code被样板代码逼出来的轮子1.1 我盘点过自己最耗时的三类重复劳动动手写工具前我先把日常开发里最浪费时间的事情列了个清单。结果挺惊人的每周光是“重复劳动”就要占掉将近三分之一的工作时间而且这些活儿完全不需要思考新业务模块的初始化建目录、建路由文件、写页面壳子、挂菜单配置一套流程下来少说半小时多的时候能折腾两小时因为不同项目的目录规范还不一样。接口对接层后端接口一多前端就得手动写 service 方法、定义请求参数类型、写 loading 状态、处理错误返回。每个接口大概要重复 50 到 80 行几乎没区别的代码。状态管理样板每加一个数据维度就要手动写 action、reducer、types、selector四件套缺一不可复制粘贴的时候最怕贴错文件。最开始我靠代码片段工具来救急但片段只能解决“单文件里插入一段代码”解决不了“多个文件按规则联动生成”的问题。我希望达到的效果是运行一条命令整个模块的文件树自动长出来并且命名、路径、导入导出全部符合项目约定。1.2 为什么没直接选现成的脚手架工具其实“代码生成”这个方向早就有很多成熟工具了Yeoman、plop、hygen 都用过。我逐个试了一遍各有各的好但也各有各的别扭Yeoman功能是真强大生态也老但 generator 需要写一套比较重的 Node 代码团队想维护一个内部 generator学习成本不低很多同事看一眼文档就跑了。plop轻量把模板和配置放在一起跑起来很顺但它的定位偏“单文件微生成”做多文件联动、目录重构的时候还是得写不少定制逻辑。hygen处理“在指定位置插入模板”很优雅但对“根据用户回答决定整棵目录结构”这种场景不够直观。这些工具都验证了一件事代码生成器本质上就是“配置 模板 上下文变量”三件事。我真正需要的不是一个通用平台而是一个能贴合自己团队规范、心智负担足够低的小工具。与其在大而全的工具里做二次开发不如花点时间写一把趁手的专用小刀。1.3 给 t3code 定边界做什么不做什么工具重构到第三版我最大的心得是想清楚“不做什么”比想清楚“做什么”更重要。t3code 明确划了几条边界做“文件级别的生成”根据模板目录结构按规则渲染并写出一批文件不做 AST 级别的精准代码注入。做“约定优于配置”默认按团队目录规范生成异常情况通过配置覆盖但永远不会承诺可视化拖拽。做“生成前的交互”必要的变量通过问答收集不搞暗黑魔法所有生成内容必须能看懂、能改。不做低代码平台t3code 只负责把重复劳动自动化业务逻辑永远由开发者自己写。这条边界让我在架构设计时省了很多心。后面所有的实现细节都围绕这四个字展开窄而深。2. t3code 的整体架构先定数据流再写一行代码2.1 一次生成动作的完整数据流第三版我学乖了没一上来就写代码先把整个处理流程在草稿纸上画清楚。一次完整的 t3code 运行链路是这样的用户在终端输入命令例如t3code gen user。工具加载配置文件项目根目录下的.t3coderc.ts合并内置默认配置。解析命令行参数把“positional 参数”和“选项参数”变成初始变量集合。根据配置文件中定义的 prompts逐个询问用户缺失的变量如模块名、目标语言、是否包含 CRUD。所有变量收集完毕后形成一个不可变的 renderContext 对象。遍历模板清单逐个读取模板文件用 renderContext 渲染出文本内容。根据目标输出路径做冲突检测决定 overwrite、skip 或 rename。写入文件后如果配置了 afterScript依次执行格式化 prettier、类型检查 tsc --noEmit或 lint。这条链路看起来平平无奇但它解决了两个关键问题第一变量收集和渲染彻底分离模板里永远不用写“判断用户是否回答了某个问题”的逻辑第二每一步都有明确的数据结构输入和输出排查问题的时候可以顺着链路一路打日志。我把每一层的数据都定义成了 TypeScript 类型比如RenderContext、TemplateItem、PromptDef写起来非常顺手。2.2 配置文件用 TypeScript 写配置而不是手搓 JSON第一版用的是 JSON 配置结果遇到函数、正则表达式、甚至多行描述时非常痛苦。后来改成了.t3coderc.ts因为配置本身就需要灵活性——比如根据平台判断默认目录。配置文件核心结构长这样import { defineConfig } from t3code; export default defineConfig({ prompts: [ { name: moduleName, type: input, message: 模块名称小驼峰如 userProfile, validate: (v: string) /^[a-z][A-Za-z0-9]*$/.test(v), }, { name: needService, type: confirm, message: 是否同时生成 API 服务层, initial: true, }, ], templates: [ { source: templates/react-module/**/*, target: src/features/{{camelCase moduleName}}/**/*, when: (ctx) ctx.projectType react, }, { source: templates/vanilla-module/**/*, target: src/modules/{{camelCase moduleName}}/**/*, when: (ctx) ctx.projectType vanilla, }, ], after: [prettier --write {target}, tsc --noEmit], });这个配置有三个细节值得展开。第一个是templates数组的顺序越靠前的模板优先级越高同名文件冲突时默认以先匹配到的为准。第二个是source和target都支持 glob 模式最终的文件名和路径由变量渲染出来这样模块名一变整条路径链路的命名都跟着变不会出现文件夹叫userProfile、文件名却是UserProfile.tsx这种不一致。第三个是when函数基于上一问的答案做条件分支。实际运行中我问完前两问第三问就已经定好了交互体验非常顺。2.3 模板系统选型为什么我用 EJS 而不是手写字符串拼接模板引擎是代码生成器的灵魂。我评估过 Handlebars、EJS、Nunjucks 三个选项最后选的是 EJS。没有别的原因纯粹因为它“足够笨”里面可以直接写 JavaScript 表达式团队里任何写过几年代码的人都能秒上手。有人可能会问生成器渲染的是源代码不是 HTML为什么不用更简单的字符串模板答案是“字符串拼接”根本撑不住复杂场景。一个真实模块至少包含路由文件、页面组件、API 层、类型定义四个文件每个文件里都有大量条件判断和循环。比如 API 层要根据用户勾选的 CRUD 方法动态生成函数列表页面要按字段配置循环输出表单控件。这种复杂度下模板引擎的if/for/管道函数就是必需品。用 EJS 写模板会有一个很直接的收益你能在模板里使用任意自定义 helper 函数。我在 t3code 里内置了pascalCase、camelCase、kebabCase、pluralize这几个命名助手模板里写import {{pascalCase moduleName}} from ./{{kebabCase moduleName}}这种代码时可读性非常高。3. 核心模块的实战拆解从读取配置到文件落盘3.1 配置解析与交互问答把“人话”变成 renderContext交互提问用的是一个在社区里很成熟的包prompts。它支持 input、confirm、select、multiselect 等常用题型正好覆盖我的需求。这里有个经验所有 Prompts 的name必须和配置里templates用到的变量名严格对应我会在启动阶段做一次静态校验直接把不一致的变量名当错误抛出来而不是让用户生成完才发现“怎么输出的是空字符串”。问答结束后我会把命令行参数、prompts 回答、内置默认值合并成一个最终的 renderContext 对象。这个对象的合并顺序非常重要默认值 配置文件 命令行参数 交互式回答。命令行参数是优先级最高的“提前回答”当用户用--nameuser --yes跳过交互时t3code 直接进入非交互模式这在 CI/CD 流水线里特别好用。3.2 模板渲染每个文件都是一次独立的“编译”拿到 renderContext 之后就可以开始逐文件渲染了。我用 glob 把模板目录下的所有文件读出来对每个文件做这样几件事读文件内容为字符串。用 EJS 的render函数渲染内容传入 renderContext 和 helpers。对文件的“相对路径”也做一次模板插值因为路径里可能含有模块名。把渲染后的文本写入对应的目标路径。这个流程看起来简单但有个很关键的优化渲染文件时我会在文件尾部追加一段固定格式的“生成标记注释”比如// Generated by t3code, do not edit manually.下面的内容有双重作用。对于人来说它让后来人一眼知道这个文件是生成出来的改坏了别慌重跑命令就行对于工具本身来说如果你开启了“追加模式”t3code 会优先寻找这段标记并替换标记之间的内容。这样生成器就不只能“一次生成一整个文件”还能“精准更新文件中的某个区块”。比如修改器名后我只想更新 API 层文件里被标记包围的那段代码其他手写的业务逻辑原样保留这个能力在实际使用中非常实用。3.3 文件冲突处理宁可停下来问也不直接覆盖写文件最怕误覆盖。我一开始做过一次蠢事在旧项目里跑新配置直接把同事改过的路由文件覆盖掉了。从那以后t3code 的写盘逻辑我重写得非常保守如果目标文件不存在直接创建。如果目标文件已存在且不在“生成标记”模式提示用户选择overwrite、skip或review。review模式会列出旧文件和新文件的内容差异让用户自己判断。只要没有显式传--force默认行为永远是“跳过已存在文件”。这个设计在自动化和安全性之间取了个平衡。CI 环境里用--yes --force一把梭日常开发里则用默认的安全模式防止手滑把改动覆盖了。3.4 命令行参数设计尽量少记尽量可猜CLI 参数我用的是commander但参数设计更有讲究。我的原则是位置参数只放一个“动作对象”所有可选行为都用带--的选项表达。常用命令长这样t3code gen user t3code gen user --with-service --with-model t3code gen order --typeform --fieldsname,phone,email t3code list t3code validate t3code --version--dry-run是我个人最常用的参数它会把“将要生成哪些文件、每个文件的内容预览”打印出来但不真正写盘。在对模板做改动时我通常会先跑一遍 dry-run 确认目录结构再实际生成。这个习惯帮我避免了很多“模板写错、一整批文件全部生成歪掉”的惨剧。4. 我在 t3code 里踩过的三个典型坑4.1 模板语法和源码里的%短句冲突第一个坑是我自己给自己挖的。因为用了 EJS模板里% %是控制流符号。但问题是前端代码里经常会用到对象字面量、求模运算、甚至 JSX 里的{}表达式如果模板里恰好有一行const progress ${completed}%;EJS 会把%当成自己的闭合标签吞掉轻则渲染结果错乱重则整个模板直接报错。我第一次遇到时对着屏幕愣了半天后来查了 EJS 文档才想起有转义写法想输出字面量%得写%%想输出%得写%%。模板里出现百分比场景时我一开始都改成%%来转义。这个解法虽然能跑但在代码里留了一堆奇奇怪怪的符号后来我干脆给 t3code 加了一个“严格模板校验”渲染前先检查模板里有没有不配对的 EJS 标签直接把问题拦截在生成阶段。4.2 Windows 路径分隔符把配置坑惨了第二个坑典型到让人哭笑不得。t3code 的模板目标路径支持变量渲染我最初用的是path.join来拼接目标路径。在 Windows 上它会产出反斜杠\而我们的代码库统一用正斜杠/写 import 路径。结果在 Windows 上生成出来的文件里面的相对路径导入全带着反斜杠一到 Linux CI 上就报模块找不到。修复方案其实很简单所有写入配置文件、模板路径、生成文件内 import 的路径统统使用正斜杠只在真正调用文件系统 API 的时候才用系统分隔符。我写了一个toPosixPath()helper专门处理这层转换。这个坑让我养成了个习惯涉及路径的公共库工具类一定要在 Windows 和 Linux 都跑一遍测试用例。4.3 打包工具把 EJS 整个打进了 CLI 产物第三个坑发生在用 esbuild 打包 t3code 时。t3code 本身是 TypeScript 写的 CLI发布时应该打包成单个可执行文件。但 EJS 内部有个特性它会用require()动态加载模板文件。esbuild 在做依赖分析时没办法完全静态识别于是它做了个“保守的打包策略”把 EJS 及其依赖的一大堆代码全部打进产物里。结果 CLI 文件从几百 KB 直接膨胀到好几 MB启动速度肉眼可见地变慢。排查完原因其实不复杂EJS 在运行时有可能加载自定义的include文件所以打包器必须保留完整的动态加载能力。我的解决办法是用 esbuild 的external选项把ejs和 template 相关的包排除出打包范围让它们在运行时从 node_modules 里加载。代价是分发时不能只发一个纯单文件但换来的是启动速度和产物体量的大幅优化。对 CLI 工具来说我认为这是值得的取舍。5. 让 t3code 真正可用的收尾工作5.1 生成后的验证机制能跑只是起点能编译才是底线模板写多了你会发现一个问题模板本身也要“保鲜”。团队依赖升级、目录规范调整、包名变更都会让旧模板生成出来的代码瞬间过时。我在 t3code 里加入了validate命令它会扫描所有模板样例在临时目录里生成一份“测试产物”然后依次执行tsc --noEmit和eslint。任何模板导致的类型错误、lint 错误都会在提交模板变动前暴露出来。这个机制相当于给模板上了“单元测试”。一开始觉得繁琐运行时间也比预期长但坚持用下来之后团队模板的质量稳定了很多再也没出现过“模板更新把线上生成搞崩”的事故。5.2 插件化思路从个人工具到团队共享t3code 刚开始只有 React 模块模板属于自产自用。后来同事听说了也开始用但他们的项目结构和规范跟我这边不一样硬套模板比手写还慢。于是我做了一个很轻量的“模板包”机制模板根目录下放一个template.config.ts里面声明这个模板包适用于什么项目、需要哪些 prompts、生成到哪个目录。t3code 支持从远程 Git 仓库拉取模板包并在本地做版本缓存。日常维护的流程变成修改模板 → 提交到模板仓库 → 团队成员执行t3code update拉取最新模板 → 重新生成。这样每个团队都能维护自己的“规范副本”而 t3code 只负责做好渲染和落地。这种“工具薄、规范厚”的分工我觉得是这类项目最终该走的方向。5.3 后续可以扩展的方向写到现在t3code 已经稳定使用了一个季度产生的文件数量估算在三千以上。但我知道它离“完美”还很远有三个方向是我明确想继续做的引入大模型辅助理解需求比如用户输入“做一个订单列表页带搜索和分页”t3code 尝试把这句话转成 prompts 的预填写答案减少手工输入。支持“增量生成”不只是整文件替换而是对 AST 级别的精准操作做更深入支持把标记生成模式升级成更完整的可逆操作。编辑器插件把命令行能力封装成 VS Code 右键菜单里的“用 t3code 生成模块”降低团队成员的使用门槛。最后再分享一个我在多次重构里沉淀下来的小技巧写这类代码生成工具模板就是产品模板的可读性比工具的代码质量更重要。我在模板里写注释的密度比在业务代码里高得多因为模板是团队所有人都会看的。工具可以把边界收窄但模板值得花最多的心思。现在每次执行t3code gen的时候我盯着终端里刷出来的文件结构列表还是会想起那个半夜复制粘贴的瞬间。从一个治标不治本的脚本到一套带验证、带模板管理、能安全落地的生成体系中间跨过的其实不是多少行代码而是想清楚了自己到底在为什么问题买单。如果你也有这种反复出现的重复劳动别急着继续复制粘贴——花点时间把你最痛的场景做成一个小工具哪怕它只解决一件事也一定会值回票价。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询