1. t3code 到底是个什么东西:先从一个尴尬场景说起
大概半年前,我接了一个内部管理系统的重构活。团队从零搭项目,技术栈选了 Next.js + TypeScript,按理说这套组合足够现代,但在真正开工的第一天,我们就被一个老生常谈的问题卡住了——环境的初始化效率。
每个人 clone 代码之后,都要手动装依赖、配 ESLint、配 Prettier、写基础的 CRUD 接口、建目录结构。有人习惯把组件放在components/ui,有人直接全堆在components下。前端三个人,写出来的目录风格能凑齐三套。更别提那些重复到让人怀疑人生的增删改查代码,每个页面长得几乎一模一样,除了字段名不同,其他全是复制粘贴。
那段时间我就在想,这种初始化和重复劳动明明可以用工具解决,为什么我们一直在手动做?后来我试着去翻社区里的脚手架方案,要么太重,上来就是整个微前端体系,要么太轻,只管个初始化,真正的业务模板还得自己写。也就是那个时候,我注意到 t3code 这个项目。
t3code 本质上是一个代码生成与项目初始化工具——你可以把它理解成"脚手架的脚手架"。它不是帮你初始化某个固定技术栈的模板,而是让你自己定义模板规则,然后基于这些规则批量生成项目骨架和业务代码。模板支持嵌套目录、变量替换、条件判断,甚至可以在生成时自动执行命令。我第一次完整跑通它的时候,感受有点像第一次用代码片段扩展:之前以为是花架子,实际用起来才知道省了多少事。
这篇文章,我打算从头到尾把 t3code 讲清楚。它会解决什么问题、核心原理是什么、我实际是怎么配置怎么用的、中间踩了哪些坑、以及把它放进团队工作流之后带来了什么改变。如果你也在为项目初始化和重复代码头疼,这篇应该能帮到你。
2. 为什么要自己造脚手架:t3code 解决的问题边界
先说清楚一点,t3code 不是要替代 Create Next App、Vite CLI 这类工具。它们解决的是"从一个官方模板创建项目"的问题,而 t3code 解决的是"从你自己的规范创建任意项目"的问题。
2.1 官方脚手架 vs 自定义生成器的差距
用官方脚手架初始化一个项目,速度确实快,但你拿到的只是一个"能跑"的空壳。真正的项目规范——比如目录结构、错误处理封装、请求层封装、组件命名规则、路由约定——这些通通没人帮你弄。你今天在新项目里重新搭一遍,明天在另一个新项目里又搭一遍,搭完之后发现上次的封装思路比这次好,又回去改,改完之后还得同步给团队其他成员。
这就是官方脚手架的天花板:它可以给你一个积木盒子,但积木怎么分类、怎么摆、怎么保证每个人摆出来的形状一样,它管不了。
2.2 t3code 的定位:模板驱动的代码生成器
我第一次看 t3code 的 README 时,被它的定位吸引了:模板驱动、可配置、可嵌套。它不绑定任何框架,模板里你写什么,它就生成什么。换句话说,React 项目能用,Vue 项目能用,纯 Node 脚本项目也能用,甚至你只是想要一套统一的配置文件集合,它也能管。
举一个最直观的例子。我们团队现在的项目初始化流程是这样的:
- 维护一套统一的
web-frontend-template模板仓库,里面写好目录结构、基础配置、公共组件、请求封装; - 新项目启动时,用 t3code 基于这套模板生成初始代码;
- 生成完成后,自动执行
npm install和依赖初始化。
整个过程不到两分钟,所有新项目的代码风格、目录规范、基础设施全部一致。
这还不是最省事的部分。t3code 支持变量替换,意味着同一个模板可以根据参数生成不同风格的代码。比如我输入--name=user,它会生成一个 User 模块的完整 CRUD 代码,包括 Controller、Service、Repository、路由注册、DTO 校验;输入--name=order,同样的流程再来一遍,但所有类名、路径、表名都换成了 Order。这种"模板一次编写,业务无限生成"的方式,才是它真正的价值所在。
2.3 适合用它的人和场景
这几类场景,是 t3code 最适合发挥的:
- 团队有多个前端/后端项目,希望代码风格和目录结构统一;
- 业务中存在大量重复的 CRUD 模块、列表页、表单页,手动编写浪费时间;
- 项目启动频繁,需要快速从零搭建带规范的基础骨架;
- 希望把团队规范用代码模板沉淀下来,而不是写在文档里供人自觉遵守。
如果你的情况符合其中任何一条,t3code 都值得试一下。反之,如果你只是偶尔写一个单页脚本,或者团队只有一个人怎么写都行,那它带来的收益会小很多。工具这东西,从来不是越重越好,关键是切中你的真实场景。
3. 核心原理拆解:t3code 的模板引擎与生成流程
用了一个多月之后,我开始好奇 t3code 内部到底是怎么工作的。把源码翻了一遍,发现它的核心逻辑其实不复杂,但设计得很巧妙。搞清楚它的工作原理,对后续写自定义模板会有很大帮助。
3.1 一次完整的生成过程
t3code 执行一次模板生成,大致走这样几个步骤:
- 读取入口配置文件(默认
t3code.config.ts),拿到模板目录路径、输出路径、变量定义; - 扫描模板目录下所有文件,识别文件名和文件内容的模板语法;
- 解析用户输入或配置文件传入的变量,替换模板中的占位符;
- 处理条件渲染和循环渲染的逻辑块;
- 根据规则生成目标目录和文件;
- 执行配置文件中声明的生命周期钩子(比如安装依赖、启动 git、格式化代码)。
第 3 步和第 4 步是最核心的。t3code 的模板引擎用的是非常经典的变量插值语法——{{ variableName }}——同时支持{{#if condition}}...{{/if}}、{{#each items}}...{{/each}}这类逻辑块。如果你写过 Handlebars 或者 Mustache,上手 t3code 几乎没有成本。
3.2 变量从哪来?三种注入方式
在 t3code 里,模板中使用的变量有三种来源,我分别试过,实际用起来各有适用场景。
第一种是命令行参数注入。在运行t3code generate的时候通过--key=value的形式传递。适合临时指定项目名、模块名这些随手要用的值。
第二种是配置文件预设。在t3code.config.ts里预先声明好变量和默认值。适合团队统一维护的路径、组织名、作者信息、版本号这些固定的值。
第三种是交互式问答。t3code 支持在配置文件里定义提问列表,运行时像聊天一样逐个问你问题,你的回答会作为变量注入模板。适合参数多的时候,避免记不住命令行参数导致输错。
我这里放一段简单的配置示例,展示三种方式的配合:
// t3code.config.ts import { defineConfig } from 't3code'; export default defineConfig({ templates: [ { name: 'module', source: './templates/module', target: './src/modules/{{moduleName}}', variables: { moduleName: '', // 由命令行 --moduleName 或交互问答填充 author: 'myname', // 预设值 createdAt: new Date().toISOString(), }, prompts: [ { type: 'input', name: 'moduleName', message: '请输入模块名称:', required: true, }, ], hooks: { afterGenerate: ['npm run format'], }, }, ], });这个配置的意思很直白:module模板从./templates/module目录读取,生成到./src/modules/模块名下。moduleName由交互问答提供,author是写死的预设值,生成完成之后自动执行一次格式整理。
3.3 模板文件的形态:命名即规则
这是 t3code 我觉得最巧妙的设计——它把模板变量直接用在文件名上。
举个例子。假设我想做一个通用的 React 组件模板,传统思路是写一个ComponentTemplate.tsx,然后生成时改成UserCard.tsx。t3code 的做法是直接把模板文件命名为{{componentName}}.tsx,生成时文件名自动替换成实际变量值。同样,目录名也可以带变量,比如./src/modules/{{moduleName}}/,生成的目录结构直接就是最终想要的样子。
一个常规的前端模块模板,在我这边长这样:
templates/module/ ├── {{moduleName}}Api.ts ├── {{moduleName}}List.tsx ├── {{moduleName}}Form.tsx ├── {{moduleName}}Types.ts └── hooks/ └── use{{moduleName}}.ts执行t3code generate module --moduleName=User之后,生成的是:
src/modules/User/ ├── UserApi.ts ├── UserList.tsx ├── UserForm.tsx ├── UserTypes.ts └── hooks/ └── useUser.ts这套机制看起来简单,但实际用下来的感受是:正确的东西不需要复杂的解释。它把"命名即规则"贯彻到生成器的设计里,让模板结构一目了然,新同事看一遍模板就能参与到维护中。
4. 本地实战:用 t3code 生成一个带完整 CRUD 的用户模块
光说原理容易飘,我直接拿一个实际例子演示整个流程。以下是我在一台干净环境上的完整操作记录,从安装到跑通大约花了六分钟,其中还包括我手滑输错一次命令的时间。
4.1 环境准备与安装
t3code 是一个 npm 包,全局安装或者用npx直接跑都行。我推荐项目里用npx,版本跟着 package.json 走,避免全局包升级影响已有模板。
# 全局安装(个人机器,图省事) npm install -g t3code # 或者 npx 方式(推荐团队协作场景) npx t3code init初始化命令会在当前目录生成t3code.config.ts和templates/目录,相当于把工具自身的骨架先搭好。第一次跑的时候我一度以为它卡住了——因为它没有打印任何"初始化完成"的提示,后来才注意到它只是安静地创建完了文件。这点小反馈缺失后续版本可以优化,但不影响使用。
安装完之后检查一下版本,确保核心文件和配置都正常:
t3code --version4.2 编写用户模块模板
这次的目标是生成一个"用户管理"模块,包含前后端联调需要的四件套:类型定义、API 方法、列表页、表单页。模板全部放在templates/module/目录下。
首先是类型定义模板。所有跟后端字段相关的类型都先通过变量占位,后续生成时再替换成具体实际类型。真实项目里的字段远比这个多,这里只保留最核心的展示建模方法:
// templates/module/{{moduleName}}Types.ts export interface {{moduleName}}Item { id: number; {{#each fields}} {{fieldName}}: {{fieldType}}; {{/each}} createdAt: string; updatedAt: string; } export interface {{moduleName}}Query { page: number; pageSize: number; keyword?: string; } export interface {{moduleName}}Payload { {{#each fields}} {{fieldName}}: {{fieldType}}; {{/each}} }这里用到了{{#each fields}}的循环语法。fields是一个数组,每个元素有fieldName和fieldType两个属性。这意味着我可以传入任意多个字段,模板自己循环生成,不用为三字段和五字段分别维护模板。
接着是 API 层模板。正常情况下这里应该写需要调用的接口地址;这个模板故意做成从配置文件里读取后端路径的方式,便于后续在真实环境里切换。做一个示意版:
// templates/module/{{moduleName}}Api.ts import request from '@utils/request'; import { {{moduleName}}Item, {{moduleName}}Query, {{moduleName}}Payload } from './{{moduleName}}Types'; const BASE_URL = '/api/{{moduleName.toLowerCase()}}'; export const fetch{{moduleName}}List = (params: {{moduleName}}Query) => request.get<{{moduleName}}Item[]>(BASE_URL, { params }); export const fetch{{moduleName}}Detail = (id: number) => request.get<{{moduleName}}Item>(`${BASE_URL}/${id}`); export const create{{moduleName}} = (payload: {{moduleName}}Payload) => request.post<{{moduleName}}Item>(BASE_URL, payload); export const update{{moduleName}} = (id: number, payload: Partial<{{moduleName}}Payload>) => request.put<{{moduleName}}Item>(`${BASE_URL}/${id}`, payload); export const delete{{moduleName}} = (id: number) => request.delete(`${BASE_URL}/${id}`);注意到一个细节:{{moduleName.toLowerCase()}}。t3code 支持变量的链式调用,内置了toLowerCase、toUpperCase、camelCase、kebabCase、pascalCase这些常用转换方法。这个功能很实用,因为同一个变量在类名、文件名、URL 路径里的命名风格往往不一样。比如模块名叫userProfile,类名要用UserProfile,路径要用user-profile,没有链式转换的话,就得分别传三个变量,麻烦还容易出错。
列表页和表单页模板,就是标准 React 组件的写法,不再完整贴代码。核心思路是一样的:把组件名、API 函数名、类型名都用模板变量引用,生成时自动拼对。
写法大致是这个风格:
// templates/module/{{moduleName}}List.tsx import React, { useEffect, useState } from 'react'; import { Table, Button, Space, Input } from 'antd'; import { {{moduleName}}Item, {{moduleName}}Query } from './{{moduleName}}Types'; import { fetch{{moduleName}}List, delete{{moduleName}} } from './{{moduleName}}Api'; interface Props { onEdit: (record: {{moduleName}}Item) => void; } export default function {{moduleName}}List({ onEdit }: Props) { const [list, setList] = useState<{{moduleName}}Item[]>([]); const [loading, setLoading] = useState(false); const [query, setQuery] = useState<{{moduleName}}Query>({ page: 1, pageSize: 10 }); const loadList = async () => { setLoading(true); try { const res = await fetch{{moduleName}}List(query); setList(res.data || []); } finally { setLoading(false); } }; useEffect(() => { loadList(); }, [query]); return ( <Table rowKey="id" loading={loading} dataSource={list} columns={[ { title: 'ID', dataIndex: 'id' }, // 更多字段列...实际项目里通过循环生成 ]} /> ); }4.3 执行生成命令
模板就位之后,可以跑了。我想生成一个User模块,字段包含姓名、邮箱、手机号、状态,四个字段各不相同:
t3code generate module \ --moduleName=User \ --fields=name:string,email:string,phone:string,status:number这条命令包含的信息量比较复杂,实际执行时也可以传 JSON 文件来避免终端转义问题:
t3code generate module --config ./user-module.json生成完成后,目录结构完全符合预期。src/modules/User下出现五个文件,打开检查各处的引用关系——UserList.tsx里 import 的fetchUserList、UserApi.ts里生成的/api/user路径、UserTypes.ts里的UserItem接口——全部字符串替换正确,没有留下一个{{占位符。
4.4 为什么字段定义走配置化而不是写死在模板里
我在最初设计模板时踩过一次坑:把所有字段直接写死在模板文件里。表面看没什么问题,User 模块需要 name、email、phone,那就写进去。但等到第二个模块 Order 要用 orderNo、amount、status 时,模板就废了,只能重写一份再复制一份。字段一多,模板膨胀到没法看。
后来才改成字段配置化的方案。模板保持相对稳定,关注"结构",字段通过变量注入,关注"内容"。结构是项目的骨架,是不太变的部分;字段是业务的差异,是每个模块都不一样的地方。把两者分流,模板才能复用起来。
5. 进阶玩法:条件渲染、目录级复用与团队规范落地
跑到这一步,基本的生成已经问题不大,但如果你的需求仅仅是"能生成文件",那还不值得投入这么多精力折腾。真正让 t3code 体现出价值的是下面这些进阶用法,我在自己的项目里逐一试过,每一个都实打实节省了时间。
5.1 用条件渲染应对不同业务类型
不是所有模块都需要表单功能。有些模块是纯数据展示的,比如"操作日志",只有列表页和详情页,不需要新增和编辑;有些模块则完整包含前后台所有操作。差异存在的情况下,怎么用一套模板覆盖两类需求?
我的做法是在模板里加条件渲染。比如目录结构可以这么定义:
templates/module/ ├── {{moduleName}}Types.ts # 所有模块都需要 ├── {{moduleName}}Api.ts # 所有模块都需要 ├── {{moduleName}}List.tsx # 所有模块都需要 └── {{#if hasForm}} └── {{moduleName}}Form.tsx # 仅当 hasForm=true 时生成 └── {{moduleName}}Detail.tsx # 仅当 hasForm=true 时生成 {{/if}}生成时根据传入参数决定是否包含表单相关文件:
# 完整模块 t3code generate module --moduleName=User --hasForm=true # 只读模块 t3code generate module --moduleName=OperationLog --hasForm=false这个功能让一套模板的覆盖范围直接翻倍。我原来至少准备两套模板,现在一套就搞定,维护成本自然降下来了。
5.2 目录级模板复用:把公共结构沉淀为模板块
项目里的模块,经常不是孤立的单文件,而是一组配套文件组成的目录。t3code 的模板可以嵌套引用,我把这个特性理解为"模板里的模板"。
举个例子。我们每个模块都必须包含一个 README 文档,说明这个模块的用途、依赖、注意事项。如果每个模板都单独写一份,改一次所有模板都要跟着改。t3code 的做法是抽一个公共的_partials/README.md,然后在各个模板里引用它:
templates/module/ ├── _partials/ │ └── README.md ├── {{moduleName}}.ts └── {{moduleName}}.test.ts在模板文件里通过变量引用公共块:
{{#include _partials/README.md}} ## {{moduleName}} 模块说明 更新时间:{{createdAt}}这样一来,公共内容只维护一份。这个思想跟代码抽取公共组件一模一样,只不过发生在模板层面。
5.3 sticky 屎坑:团队规范的"代码即文档"
前面说的都是技术操作,最后这块算是组织管理层面的心得。我见过太多团队的规范文档写得漂漂亮亮,实际执行时每个人理解都不一样——"路径不是用驼峰吗?我看你写的是小写加横杠啊""这个模块你怎么没有写测试?文档里不是说要写吗""你怎么又用了别名导入?"——规范文档挂在 Wiki 里,文章地址已经变更,内容既没人看也没人改。
t3code 提供的是一种"代码即规范"的落地方式:规范不是靠人遵守,而是靠模板强制生成。所有目录结构、命名风格、文件组织方式,都是从模板里带出来的。从一开始就不存在"个人风格发挥"的空间,因为生成出来的代码在结构上是一致的。
这一点想清楚了,t3code 就不再只是"提升效率的工具",更是"团队共识的载体"。模板仓库需要几个人共同维护,讨论清楚之后合入的每一条变更,都会在后续每一次生成中自动覆盖到所有团队成员。这才是它最大的价值。
6. 实测对比:t3code vs 手写 vs 其他脚手架方案
工具这东西,不对比就没有说服力。我专门用三种方式重建了同一个用户管理模块,记录耗时和体验差异,也顺便验证一下 t3code 真的不是在自我感动。
6.1 同样的模块,三种方式
任务目标:生成一个包含类型定义、API 层、列表页、表单页的完整用户管理模块,前后端接口风格与现有项目一致。
| 方式 | 耗时 | 代码一致性 | 后续维护难度 |
|---|---|---|---|
| 纯手写 | 约40分钟 | 完全取决于个人 | 高,每个模块风格可能不同 |
| 复制已有模块再改 | 约15分钟 | 容易残留旧代码 | 中,复制时容易漏改引用 |
| t3code 生成 | 约1分钟 | 完全一致 | 低,改模板即改所有模块 |
6.2 复制粘贴的问题:漏改引用
我特意把复制粘贴的方式也放进对比,是因为它看起来"挺快",实际暗藏大坑。最常见的翻车场景是:把 UserList 复制成 OrderList,文件改名了,但组件内部的变量引用没改干净,ESLint 报了一堆未定义变量。逐行排查找错漏的时候,时间已经花出去了。而且这种错误一班发生在赶进度的时候,越急越错。
t3code 的模板替换是全局的,生成时一次性把文件路径、文件内容、命名引用全部替换完成,不存在"漏改一处"的空间。这一点在多人协作场景格外重要——人都会犯错,机器不会。
6.3 和其他脚手架工具的取舍
我用过一些备选方案,各有定位,简单交代一下选择和取舍的思路。
- Plop.js:很成熟的代码生成器,专注单体项目的局部代码生成,模板语法和 t3code 相似。如果你只要生成单文件组件,两者差别不大。
- Nx:重量级 monorepo 管理工具,生成代码只是它附带功能,核心是全链路构建和依赖分析。
- **Hygen:铁了心维护一套严格的生成器生态,配置规范性强,但学习成本更高。
- t3code:轻量、配置直观、跟已有项目结构侵入性弱,适合先在小范围内验证效果再逐步扩大使用边界的团队。
最终我选 t3code 的原因是它引入成本最低,不需要改造现有项目结构,可以按目录渐进式渗透使用,团队接受度也高。
7. 踩坑记录与应对方案:几个能让你少掉头发的问题
工具好用归好用,但毕竟是新生事物,版本迭代快,文档还有不少空白。下面这几个坑,都是我实打实踩过的,写出来帮你提前避雷。
7.1 问题一:交互式问答在 CI 环境会卡死
t3code 的交互式提问很好用,但在 CI 流水线里跑的时候,没人能回答它的问题,任务会一直挂在那直到超时。我一开始没意识到这点,第一次在服务器上跑t3code generate的时候,流水线直接卡了 20 分钟,我还以为是网络问题。
解决办法:CI 环境里不用交互问答,全部通过命令行参数或者配置文件传入。我在配置里做了判断,检测到环境变量CI=true时,跳过 prompts 定义,改用默认值:
import { defineConfig } from 't3code'; import process from 'node:process'; const isCI = process.env.CI === 'true'; export default defineConfig({ templates: [ { name: 'module', source: './templates/module', target: './src/modules/{{moduleName}}', variables: { // 非 CI 环境走 prompts,CI 环境用默认值 moduleName: isCI ? process.env.MODULE_NAME || 'Demo' : '', }, prompts: isCI ? [] : [...], }, ], });7.2 问题二:Windows 环境下的路径兼容
团队里不是所有人都用 Mac,Windows 电脑的路径处理逻辑不一样。t3code 在生成模板时,如果模板里有硬编码的/分隔符,在 Windows 上生成的路径会变成奇怪的混合格式,疑似会发生识别异常。
解决方式:模板里的路径统一用path.join或者 t3code 内置的路径规范化方法处理,不要在模板字符串里手写分隔符。我最后的做法是在配置里加了一句强制规范:
export default defineConfig({ pathStyle: 'auto', // 根据操作系统自动选择分隔符 });设置完之后,Windows 和 Mac 生成的目录结构行为就一致了。
7.3 问题三:生成后 lint 报错,模板语法误伤字符串内容
某些模板里会出现原生的 Mustache 语法,比如 Vue 模板里的{{ variable }}。t3code 的解析器会尝试把它当成自己的变量插值语法去解释,结果就是生成的文件直接变成空字符串——值被替换成了 undefined。
这个问题比较隐蔽,因为它属于模板内容的语法冲突。我最后用了转义方案——把 Vue 的表达式写成\{{ variable }},生成后再把转义符去掉。这样 t3code 不会去解析它,最终输出里又是正常的 Vue 插值语法。虽然麻烦一点,但这种"空间换时间"的处理方式在混合模板场景里比较直接。
7.4 问题四:全局安装的 t3code 和项目内版本冲突
之前提到团队场景建议用npx,实测感受是很有必要。如果全局装了一个 t3code 老版本,项目里又引了新版,两边的模板语法可能有细微差异,生成的代码会有莫名其妙的差别。我在一个老项目上就遇到过:全局版不认camelCase链式调用,直接把UserProfile输出成了UserProfile原样,看起来没区别,实际上触发了命名规范告警。
统一用npx加上 package.json 里的版本锁定,这个问题就没再出现过。
8. 把 t3code 放进日常工作流之后:效率提升是实打实的
最后聊点感性层面的体会。
用 t3code 不是一蹴而就的事。刚开始的那一周,我花了不少时间把沉寂多年的代码结构从旧项目里翻出来,重新整理成模板。这个过程挺磨人,因为你要在一堆历史包袱里判断:哪些是真正的规范,哪些只是当时的临时方案。但我做完整套模板之后,获益是全方位的。
一个最直接的例子:上一季度我们新开了三个内部系统,每个系统的初始骨架就是跑一遍 t3code,所有路线图上的基础设施配置、目录规范、公共组件一次到位。过去开新项目,光初始化就要半天,还得几个人分工去搭建个人偏好不一致的环境。现在一个人加一台电脑,开项目从此就是两分钟的事。
另一个体会是——模板本身变成了一种文档,而且是"活的文档"。以前讨论代码规范,大家开会扯半天,最后出一篇文档,放 Wiki 里积灰。现在讨论结果是直接改模板仓库,改完所有人下次生成的时候自动用新规范。规范不是靠提醒来维持的,而是靠生成器强制的。相比之前问"你为什么没按规范写",现在是"规范就是生成器默认行为"。
当然,我也有一些未完成的规划。比如我打算把模板仓库和 CI 流程结合起来,在每次提交模板变更时自动生成一批测试模块,用来验证模板的正确性,避免改坏了某个公共块导致所有模块生成失败。还考虑接入更多内置变量,比如从 Git 配置里自动读取用户名和邮箱,省掉手填的时间。这些想法都有待落地,但方向已经明确了。
写到最后想说一句:与其纠结"这工具能不能用",不如花半小时在临时目录里跑一遍它的 demo,再拿你自己的项目结构做个最小模板试试。工具好不好用,亲手试过之后自然有答案。我的项目,从此再也回不去手动抄模板、复制文件改名的年代了。