1. 从“t3code”这个名字说起它到底是什么第一次看到“t3code”这个词很多人会一头雾水。它不像“Vue”“React”那样一眼能看出是前端框架也不像“Docker”“K8s”那样自带运维标签。我最初接触它是在一个内部工具链的讨论里有人甩出一句“这块逻辑用 t3code 串一下就行”当时我脑子里第一反应是这又是什么新造的词后来花时间扒了一圈资料、翻了几份项目文档才把它的轮廓拼出来。简单说t3code 是一套围绕“轻量级代码生成与模板化输出”构建的实践方案核心目标是把重复性的代码编写、配置生成、结构搭建这类工作用一套可维护的模板体系自动化掉。它不是一个具体的 npm 包或者 GitHub 仓库名更像是一种约定俗成的工程模式——用三层结构Template、Transform、Target来组织代码生成逻辑所以叫“t3”。你可能会问代码生成这事不是早就有了吗Yeoman、Plop、各种 CLI 脚手架满天飞。没错但 t3code 的差异点在于它不绑定特定语言或框架不要求你装一堆全局依赖甚至不需要你写复杂的 AST 操作。它的核心思路是用最薄的抽象层把“模板 数据 输出规则”这三件事拆清楚然后让开发者用最少的代码量完成从“描述需求”到“产出可用文件”的闭环。适合谁来了解这个内容三类人最受益一是中后台项目的维护者每天跟大量 CRUD 页面、API 接口、数据模型打交道手工写重复代码写到吐二是工具链建设者需要给团队提供统一的代码规范落地手段但又不想引入过重的框架三是独立开发者或小团队项目迭代快希望用最低成本把“写代码”这件事部分自动化把精力留给真正的业务逻辑。我自己的体会是t3code 这套东西上手门槛低但想用好、用稳需要理解它背后的几个关键设计取舍。接下来我会从整体设计思路、核心细节、实操过程、常见问题四个维度把我在实际项目里踩过的坑和总结的经验完整分享出来。2. 内容整体设计与思路拆解2.1 为什么是“三层结构”而不是“一个大模板”很多代码生成工具的设计思路是“一个大模板搞定所有”比如你写一个component.hbs里面塞满{{#if}}、{{#each}}所有变体都靠条件分支覆盖。这种方案在初期很爽模板文件少改起来也直观。但项目一旦超过三个月模板就会变成一坨谁都不敢动的“屎山”——加一个新字段要改五个地方删一个旧逻辑要担心影响其他分支。t3code 选择把职责拆成三层本质上是在解决可维护性和可组合性的问题。我画个简单的类比这就像做菜。Template 是菜谱告诉你“红烧肉需要五花肉、酱油、糖、料酒”Transform 是备菜和烹饪步骤负责把原始食材处理成半成品Target 是装盘规则决定这道菜端到哪个桌子、用哪个盘子。具体来说Template 层只负责“长什么样”。它接收一个扁平的上下文对象输出字符串。这一层不关心数据从哪来、要到哪去纯粹是文本渲染。好处是模板可以独立测试给一组数据就能验证输出不需要启动整个生成流程。Transform 层负责“数据怎么来、怎么变”。它从各种数据源数据库 schema、API 定义、配置文件、甚至手写的 JSON读取原始信息然后做归一化、补全、计算派生字段。这一层是纯函数式的输入原始数据输出模板需要的上下文。因为不涉及 IO 和渲染单元测试写起来非常轻。Target 层负责“输出到哪、叫什么名字、要不要覆盖”。它定义文件路径规则、命名策略、冲突处理逻辑。比如同一个模板可能输出到src/components/或src/pages/取决于 Target 的配置。这种拆法的优势在项目规模变大后特别明显。我经历过一个项目前端有 40 多个业务模块每个模块都有列表页、详情页、表单页、API 文件、类型定义五类文件。如果用传统单模板方案模板文件会膨胀到几千行改一个公共样式要全局搜索替换。换成 t3code 三层结构后Template 只有五个基础模板Transform 按模块类型分了三组Target 用配置文件驱动。新增一个模块只需要在配置里加一行生成逻辑完全复用。注意三层结构不是银弹。如果你的项目只有两三种文件类型、生成频率极低硬套三层反而增加理解成本。我的经验是当重复文件类型超过五种、或者团队超过三人需要协作维护生成规则时三层结构的收益才开始显现。2.2 模板引擎选型为什么我最终放弃了 Handlebarst3code 本身不限定模板引擎但社区里最常见的搭配是 Handlebars、EJS、Nunjucks 这三家。我最初用的是 Handlebars因为它的{{#if}}、{{#each}}写起来很顺手文档也全。但用了两个月后我换成了 EJS原因有三个。第一Handlebars 的“逻辑无政府主义”问题。Handlebars 的设计哲学是“模板里不应该有逻辑”所以它把条件判断和循环都做成了 helper。但实际写代码生成模板时你经常需要做一些简单的字符串处理比如把user_name转成UserName或userName。Handlebars 原生不支持你得注册自定义 helper或者提前在 Transform 层算好。而 EJS 直接就是 JavaScript% name.replace(/_/g, ) %一行搞定不需要额外抽象。第二调试体验。Handlebars 模板报错时错误信息经常指向编译后的函数堆栈里全是ret和buffer定位到具体哪一行很痛苦。EJS 报错直接告诉你模板文件第几行、哪个变量未定义排查效率高一个数量级。第三性能差异在生成场景下可以忽略。有人会说 Handlebars 预编译后性能更好但代码生成通常是一次性任务生成几百个文件也就几百毫秒的差距对开发体验毫无影响。为了这点性能牺牲灵活性不划算。当然如果你团队里有人对 Handlebars 特别熟或者项目里已经有大量 Handlebars 模板继续用也没问题。工具是为人服务的统一比最优更重要。2.3 数据源接入策略从“手写 JSON”到“Schema 驱动”t3code 的 Transform 层需要数据输入。最原始的方式是手写一个 JSON 文件描述每个模块的字段、类型、路径。这种方式在模块数量少的时候没问题但一旦超过二十个模块手写 JSON 的维护成本就上来了——改一个字段名要全局搜索替换还容易漏。我的做法是分阶段演进。项目初期手写 JSON 快速验证流程模块数量超过十个后把数据源切换到数据库 Schema 或 API 定义文件。比如后端用 Prisma 或 TypeORM可以直接读取 schema 文件解析出模型名、字段名、字段类型、关联关系。前端如果用了 GraphQL可以从 schema introspection 结果里提取类型信息。这样做的好处是单一数据源。数据库 schema 变了代码生成的数据源自动跟着变不需要手动同步。我见过一个团队后端改了字段名但忘了通知前端结果生成的 API 调用代码里还是旧字段联调时才发现。Schema 驱动后这种问题在生成阶段就会暴露——字段不存在Transform 层直接报错根本到不了运行时。实操心得Schema 驱动虽然好但不要一开始就上。先用硬编码 JSON 把生成流程跑通确认模板和 Target 规则稳定后再替换数据源。否则你会在“数据源解析”和“模板调试”两个战场同时作战效率极低。3. 核心细节解析与实操要点3.1 Template 层如何写出“可组合、可覆盖”的模板Template 层的核心挑战不是“怎么写模板”而是“怎么让模板可组合”。举个例子你有一个基础组件模板输出一个 React 函数组件的基本结构。但不同业务模块需要在这个基础上加不同的东西有的要加useEffect有的要加forwardRef有的要加memo。如果把这些变体都塞进一个模板用{{#if hasEffect}}之类的条件包裹模板会迅速膨胀。我的做法是把模板拆成“骨架 插槽”。骨架模板只包含最基础的结构插槽位置用特殊标记占位比如/* __SLOT_EFFECT__ */。然后每个变体是一个独立的“片段模板”Transform 层根据配置决定把哪些片段注入到哪些插槽。这样做的好处是新增一个变体只需要加一个片段模板不需要改骨架。片段之间互不影响测试也独立。我实测下来一个中等复杂度的组件模板拆成骨架加五个片段后总行数比单模板少了 40%而且可读性大幅提升。另一个要点是模板的命名和目录结构。我习惯按“输出类型”分目录而不是按“业务模块”分。比如templates/ component/ skeleton.ejs fragments/ with-effect.ejs with-memo.ejs with-forward-ref.ejs api/ skeleton.ejs fragments/ with-auth.ejs with-pagination.ejs type/ skeleton.ejs这样当你要生成一个新类型的文件时只需要新建一个目录不会跟现有模板混在一起。Target 层配置路径时也清晰templates/component/skeleton.ejs一眼就知道是干什么的。3.2 Transform 层数据归一化的五个关键步骤Transform 层是 t3code 里最“重”的一层也是最容易写乱的一层。我总结了一个五步归一化流程基本能覆盖大部分场景。第一步原始数据读取。从数据源拿到原始对象。如果是数据库 schema可能是{ tableName: users, columns: [...] }如果是手写 JSON可能是{ name: user, fields: [...] }。这一步不做任何处理原样读取。第二步字段名标准化。不同数据源的命名风格不一样数据库可能是snake_case前端可能是camelCaseAPI 可能是PascalCase。Transform 层需要统一成一种风格我通常统一成camelCase因为它在 JavaScript 生态里最通用。转换逻辑很简单但要注意边界情况连续下划线、数字开头、保留字冲突。第三步类型映射。数据库的varchar(255)要映射成 TypeScript 的stringint映射成numbertimestamp映射成Date。这一步需要维护一张映射表而且不同目标语言TypeScript、Python、Go的映射规则不同。我的做法是把映射表做成配置按目标语言加载不同的映射文件。第四步派生字段计算。有些字段不是原始数据里有的需要计算出来。比如pluralName复数形式、kebabName短横线形式、constantName全大写形式。这些派生字段在模板里经常用到提前算好可以避免模板里写复杂逻辑。第五步上下文组装。把标准化后的字段、派生字段、以及一些全局配置比如项目名、作者、生成时间组装成一个扁平的上下文对象传给 Template 层。这个对象的结构要稳定因为模板里会直接引用它的属性。注意Transform 层一定要保持纯函数。不要在这里做文件读写、网络请求、或者依赖全局状态。我见过有人在 Transform 里直接读文件系统结果单元测试没法跑每次测试都要准备一堆 fixture 文件。纯函数的好处是给同样的输入永远得到同样的输出测试和调试都简单。3.3 Target 层路径规则与冲突处理的实战配置Target 层看起来简单不就是拼路径、写文件吗但实际项目里这一层出的问题最多。我列几个常见的坑和对应的解法。路径拼接的跨平台问题。Windows 用反斜杠Unix 用正斜杠。如果你用字符串拼接dir / filename在 Windows 上可能生成src\components/Button.tsx这种混合路径。虽然 Node.js 的path.join能处理但如果你在模板里直接写路径就容易出问题。我的做法是所有路径都用正斜杠最后用path.normalize统一处理。文件名冲突。同一个目录下生成两个同名文件后生成的会覆盖先生成的。这在批量生成时很危险因为你可能意识不到冲突发生了。我的做法是在 Target 层加一个冲突检测生成前先扫描目标目录如果文件已存在且内容不同抛出错误并列出冲突文件让开发者决定是覆盖还是跳过。如果内容相同直接跳过避免无意义的写入。覆盖策略的粒度。有些文件希望每次生成都覆盖比如纯自动生成的 API 文件有些文件希望只生成一次、后续手动修改比如业务组件。我的做法是在 Target 配置里给每个输出类型加一个overwrite标志支持always、never、if-empty三种策略。if-empty表示文件不存在才生成存在就跳过适合那些“生成后需要手动改”的文件。目录创建。生成文件前要确保目标目录存在。Node.js 的fs.mkdir默认不递归需要加{ recursive: true }。这个细节看起来小但漏了就会报ENOENT错误。我习惯在 Target 层封装一个ensureDir函数所有写文件前先调用。3.4 配置驱动如何让非开发者也能参与生成规则调整t3code 的一个隐藏价值是把生成规则从代码里抽出来变成配置文件。这样团队里不写代码的人比如产品经理、测试也能参与调整。比如产品经理说“这个模块的列表页需要加一个导出按钮”你不需要改模板代码只需要在配置里加一行features: [export]Transform 层读取到这个标志后自动注入对应的片段模板。我的配置结构通常长这样{ modules: [ { name: user, type: crud, features: [export, import, audit], fields: [ { name: id, type: uuid, primary: true }, { name: username, type: string, required: true }, { name: email, type: string, required: true } ] } ] }这个配置文件的维护者可以是任何人只要理解字段含义就行。Transform 层负责把features翻译成具体的模板片段Target 层负责根据type决定输出哪些文件。整个流程对配置者来说是透明的他们不需要知道底层用了什么模板引擎、什么文件结构。实操心得配置文件一定要加 JSON Schema 校验。我吃过亏有人把features写成了字符串而不是数组结果生成时静默失败排查了半天。加上 Schema 校验后配置错误在加载阶段就报出来省了很多时间。4. 实操过程与核心环节实现4.1 环境准备与项目初始化假设你现在要从零搭一套 t3code 流程我按实际操作的顺序走一遍。首先建目录结构mkdir t3code-demo cd t3code-demo mkdir -p templates/component/fragments templates/api/fragments templates/type mkdir -p src/transform src/target src/config mkdir -p output然后初始化package.json安装核心依赖。我用的组合是ejs做模板渲染fs-extra做文件操作比原生fs多了递归创建、复制等便利方法ajv做配置校验。npm init -y npm install ejs fs-extra ajvpackage.json里加一个脚本{ scripts: { generate: node src/index.js } }目录结构说明一下templates放模板文件src/transform放数据转换逻辑src/target放输出规则src/config放配置文件和 Schemaoutput是生成结果的目标目录实际项目里会直接生成到src下这里为了演示单独放。4.2 编写第一个模板从骨架到片段先写一个最简单的组件骨架模板templates/component/skeleton.ejsimport React from react; interface % componentName %Props { % fields.forEach(function(field) { -% % field.name %: % field.tsType %; % }); -% } export const % componentName %: React.FC% componentName %Props (props) { return ( div className% kebabName % {/* __SLOT_CONTENT__ */} /div ); };这个模板接收componentName、fields、kebabName三个上下文变量。fields是一个数组每个元素有name和tsType。__SLOT_CONTENT__是插槽标记后续片段会替换它。然后写一个片段模板templates/component/fragments/with-effect.ejsuseEffect(() { // TODO: implement effect logic }, []);这个片段很简单就是一段useEffect代码。Transform 层会根据配置决定是否注入这个片段。4.3 Transform 层实现从配置到上下文的完整链路src/transform/index.js的核心逻辑const typeMap { uuid: string, string: string, int: number, boolean: boolean, timestamp: Date, }; function toCamelCase(str) { return str.replace(/_([a-z])/g, (_, letter) letter.toUpperCase()); } function toKebabCase(str) { return str.replace(/_/g, -).toLowerCase(); } function toPascalCase(str) { const camel toCamelCase(str); return camel.charAt(0).toUpperCase() camel.slice(1); } function transformModule(moduleConfig) { const fields moduleConfig.fields.map((field) ({ name: toCamelCase(field.name), originalName: field.name, tsType: typeMap[field.type] || any, required: field.required || false, primary: field.primary || false, })); return { componentName: toPascalCase(moduleConfig.name), kebabName: toKebabCase(moduleConfig.name), camelName: toCamelCase(moduleConfig.name), fields, features: moduleConfig.features || [], }; } module.exports { transformModule };这段代码做了几件事字段名转驼峰、类型映射、派生名称计算。注意toCamelCase的实现它用正则匹配下划线加小写字母替换成大写字母。这个逻辑对user_name有效但对user__name连续下划线会出问题实际项目里需要加边界处理。4.4 Target 层实现路径规则与写入逻辑src/target/index.jsconst path require(path); const fs require(fs-extra); function resolvePath(outputDir, moduleName, fileType) { const base path.join(outputDir, moduleName); const fileNames { component: ${moduleName}.tsx, api: ${moduleName}.api.ts, type: ${moduleName}.types.ts, }; return path.join(base, fileNames[fileType]); } async function writeFile(filePath, content, overwrite always) { const exists await fs.pathExists(filePath); if (exists) { if (overwrite never) { console.log(skip: ${filePath} (already exists)); return; } if (overwrite if-empty) { const existing await fs.readFile(filePath, utf-8); if (existing.trim().length 0) { console.log(skip: ${filePath} (not empty)); return; } } } await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, content, utf-8); console.log(write: ${filePath}); } module.exports { resolvePath, writeFile };resolvePath根据模块名和文件类型拼路径。writeFile处理覆盖策略ensureDir确保目录存在。注意fs.pathExists是fs-extra提供的方法比原生fs.existsSync更安全因为它返回 Promise不会阻塞事件循环。4.5 主流程串联从配置加载到文件生成src/index.js把所有环节串起来const fs require(fs-extra); const path require(path); const ejs require(ejs); const Ajv require(ajv); const { transformModule } require(./transform); const { resolvePath, writeFile } require(./target); const configSchema { type: object, properties: { modules: { type: array, items: { type: object, properties: { name: { type: string }, type: { type: string }, features: { type: array, items: { type: string } }, fields: { type: array, items: { type: object, properties: { name: { type: string }, type: { type: string }, required: { type: boolean }, primary: { type: boolean }, }, required: [name, type], }, }, }, required: [name, type, fields], }, }, }, required: [modules], }; async function generate() { const configPath path.join(__dirname, config, modules.json); const config await fs.readJson(configPath); const ajv new Ajv(); const valid ajv.validate(configSchema, config); if (!valid) { console.error(config validation failed:, ajv.errors); process.exit(1); } const outputDir path.join(__dirname, .., output); const skeletonPath path.join(__dirname, .., templates, component, skeleton.ejs); const skeleton await fs.readFile(skeletonPath, utf-8); for (const moduleConfig of config.modules) { const context transformModule(moduleConfig); let content ejs.render(skeleton, context); if (context.features.includes(effect)) { const effectFragment await fs.readFile( path.join(__dirname, .., templates, component, fragments, with-effect.ejs), utf-8 ); content content.replace({/* __SLOT_CONTENT__ */}, effectFragment); } const filePath resolvePath(outputDir, context.kebabName, component); await writeFile(filePath, content, always); } } generate().catch((err) { console.error(err); process.exit(1); });这个主流程做了五件事读配置、校验配置、遍历模块、渲染模板、写文件。注意片段注入的逻辑先渲染骨架然后把插槽标记替换成片段内容。这里用的是字符串替换简单直接。如果插槽有多个可以用replaceAll或者正则全局替换。4.6 运行验证与输出检查准备一个配置文件src/config/modules.json{ modules: [ { name: user_profile, type: crud, features: [effect], fields: [ { name: id, type: uuid, primary: true }, { name: user_name, type: string, required: true }, { name: email_address, type: string, required: true }, { name: created_at, type: timestamp } ] } ] }运行npm run generate输出目录下会生成user-profile/user-profile.tsx。打开文件检查import React from react; interface UserProfileProps { id: string; userName: string; emailAddress: string; createdAt: Date; } export const UserProfile: React.FCUserProfileProps (props) { return ( div classNameuser-profile useEffect(() { // TODO: implement effect logic }, []); /div ); };字段名从user_name转成了userName类型从uuid转成了stringtimestamp转成了Date。effect片段成功注入到插槽位置。整个流程跑通了。注意这个示例里useEffect被注入到了 JSX 内部实际 React 代码里useEffect应该在组件函数体顶部、return之前。这说明片段注入的位置需要根据语义调整不能随便找个插槽就塞。我的做法是给插槽加语义标签比如__SLOT_HOOKS__、__SLOT_JSX__Transform 层根据片段类型选择正确的插槽。5. 常见问题与排查技巧实录5.1 模板渲染报错变量未定义或类型不匹配这是最常见的问题。EJS 渲染时如果引用了不存在的变量会直接抛ReferenceError。排查步骤看错误信息里的变量名确认 Transform 层是否输出了这个字段。检查字段名大小写是否一致。userName和username是两个不同的变量。如果变量是数组确认是否为空数组。空数组在forEach里不会报错但如果你直接访问fields[0].name就会得到undefined。我的习惯是在 Transform 层输出上下文后先console.log(JSON.stringify(context, null, 2))看一眼确认所有模板需要的字段都存在。这个习惯帮我省了很多来回调试的时间。5.2 生成文件路径错误目录不存在或文件名冲突路径错误的典型表现是ENOENT: no such file or directory。原因通常是目标目录没创建。解法是在writeFile里先调ensureDir。另一个原因是路径拼接用了错误的斜杠Windows 上尤其常见。统一用path.join可以避免。文件名冲突的表现是后生成的文件覆盖了先生成的但没有任何提示。解法是在writeFile里加冲突检测发现同名文件时打印警告。如果冲突是预期的比如同一个模块的不同文件类型确保文件名规则有区分度比如user.tsx和user.api.ts。5.3 配置校验失败JSON Schema 报错解读Ajv 的报错信息有时候比较晦涩。比如data.modules[0].features should be array意思是第一个模块的features字段应该是数组但你写成了字符串。常见的配置错误包括错误信息原因解法should be array字段类型写错检查配置确保数组字段用[]should have required property name缺少必填字段补上name字段should NOT have additional properties多了不认识的字段删除多余字段或更新 Schemashould match pattern字段值不符合正则检查字段值格式比如模块名不能有空格我的经验是Schema 校验的报错信息虽然不直观但结合配置文件一起看基本能定位到问题。如果实在看不懂把报错信息里的路径比如data.modules[0].features复制出来对照配置文件逐层找。5.4 生成结果不符合预期模板逻辑与数据不匹配有时候生成流程没报错但输出内容不对。比如字段顺序乱了、条件判断没生效、片段没注入。排查思路字段顺序EJS 的forEach按数组顺序遍历如果 Transform 层对字段做了排序输出顺序会跟着变。检查 Transform 里有没有意外的sort调用。条件判断EJS 的% if (condition) { %如果条件永远为假对应的内容就不会输出。检查条件表达式里的变量是否在上下文里存在。片段注入如果插槽标记被替换了但片段内容为空可能是片段模板文件读取失败。检查文件路径是否正确、文件是否为空。我踩过最坑的一次是片段模板文件末尾多了个换行导致注入后代码格式乱了。后来在读取片段后加.trim()解决。5.5 性能问题生成大量文件时的优化技巧当模块数量超过一百个、每个模块生成五个文件时生成过程可能变慢。优化点模板预编译EJS 支持把模板编译成函数缓存起来避免每次渲染都重新解析。用ejs.compile(templateString)得到函数然后反复调用。并行写入文件写入是 IO 操作可以并行。用Promise.all批量写入比串行快很多。但注意不要一次性打开太多文件句柄可以分批比如每批 20 个。增量生成只生成有变化的文件。在 Target 层记录上次生成的文件哈希如果内容没变就跳过写入。这个优化在大型项目里效果明显能把生成时间从几十秒降到几秒。实操心得不要过早优化。我见过有人一上来就搞并行写入、增量生成结果代码复杂度飙升调试困难。先用最简单的串行方案跑通确认功能正确后再根据实际性能数据决定是否优化。大多数项目在模块数量少于五十个时串行生成完全够用。6. 进阶扩展t3code 还能怎么玩6.1 与 CI/CD 集成提交时自动生成把生成脚本挂到 Git hooks 或 CI 流程里每次提交代码前自动运行。这样配置文件的变更会立即反映到生成结果里避免手动忘记运行。我的做法是在package.json里加一个precommit脚本用husky触发。但要注意生成结果如果被提交到仓库可能会产生大量 diff。我的建议是生成结果不提交只提交配置和模板CI 构建时再生成。这样仓库干净也避免了生成结果和配置不一致的问题。6.2 多语言输出同一份配置生成不同语言的代码t3code 的三层结构天然支持多语言输出。Template 层按语言分目录Transform 层根据目标语言加载不同的类型映射Target 层根据语言决定文件扩展名。比如同一份用户模块配置可以同时生成 TypeScript 的前端代码和 Python 的后端代码。这个能力在前后端同构项目里特别有用字段定义只写一次两边代码自动同步。6.3 模板市场团队内共享模板片段当团队规模变大后不同项目组会重复造模板。我的做法是建一个内部模板仓库把通用模板比如 CRUD 页面、API 客户端、类型定义发布成 npm 包各项目通过依赖引入。Transform 层支持从多个来源加载模板优先用项目本地的本地没有就用共享的。这样既保证了灵活性又避免了重复劳动。6.4 与低代码平台的区别与结合有人会问t3code 和低代码平台有什么区别低代码平台是“可视化拖拽 运行时解释”t3code 是“配置 编译时生成”。前者适合业务人员快速搭页面后者适合开发者保持代码可控。两者可以结合低代码平台负责收集需求和配置t3code 负责把配置翻译成高质量代码。这样既享受了低代码的便捷又避免了运行时解释的性能损耗和调试困难。我个人在实际操作中的体会是t3code 这套东西的价值不在于“省了多少行代码”而在于把重复性决策从人脑转移到配置和模板里。人脑适合做创造性决策比如业务逻辑设计、架构选型不适合做重复性决策比如“这个字段叫什么名字”“这个文件放哪个目录”。把后者自动化掉团队的整体效率会有肉眼可见的提升。踩过几次坑之后我现在的习惯是任何重复超过三次的代码编写任务都先想想能不能用 t3code 的方式解决。这个思维方式的转变比具体工具的使用更重要。