ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零自研T3技术栈脚手架:t3code的工程化实践与踩坑记录

从零自研T3技术栈脚手架:t3code的工程化实践与踩坑记录 1. 项目缘起放着现成脚手架不用我为什么自己写了 t3code大概半年前我开始动手写 t3code 这个项目——一个基于 T3 技术栈TypeScript、Tailwind CSS、tRPC 加 Next.js的项目脚手架生成工具。起因特别简单团队里新项目初始化太慢每次手动补齐的内容都一模一样重复劳动多了人就容易产生干脆写个工具把这事自动化的冲动。t3code 这个名字没花什么心思T3 栈加 code 生成器拆开念就是 t3-code顺手就在 npm 上搜了一下没有重名直接发布。1.1 从 T3 技术栈说起先给不熟悉的读者简单交代一下背景。T3 技术栈是这几年在 React 全栈开发里很流行的一套组合TypeScript 提供类型安全Tailwind CSS 负责样式tRPC 让你在不写 REST 接口文档的情况下实现前后端类型共享应用框架用的是 Next.js。这个组合最大的好处是端到端类型安全——你改一个后端返回字段的类型前端编辑器里立刻就能报错不用等联调。我第一次用 create-t3-app 拉起项目的时候体验确实很好几分钟就能得到一个带完整 tRPC 链路和 Tailwind 样板的工程。但用得多了问题就浮出来了。create-t3-app 是大众的脚手架它的默认配置面向最通用的场景而团队工程实践一旦有自己的约定这套默认配置就不够用了。我们团队的要求包括每个新项目必须带 docs 目录、必须用 pnpm 而不是 npm、ESLint 必须开 import 排序规则、依赖要锁定精确版本号、必须包含 .env.example 模板、CI 脚本要用统一的 Node 版本。这些约定说多不多说少不少每次初始化完 create-t3-app 都要手动改一遍改完还要手动建目录、拷公共工具函数。一次两次能忍十次二十次就非常烦躁了。我还见过更糟的情况有同事初始化完项目以后忘了补 .env.example直接把带真实数据库连接串的配置提交到了仓库虽然最后及时改了回来但这种风险不应该靠人的记忆力去兜底。所以我的核心诉求非常清楚要把团队自己的工程约定固化成一个可复现的脚本让新建一个符合团队规范的 T3 项目从半小时缩减到两分钟。t3code 就是在这个背景下诞生的。1.2 现成脚手架的三个痛点在决定自研之前我把市面上能找的脚手架都过了一遍包括 create-t3-app、create-next-app、各种社区模板仓库。它们的痛点归纳起来有三个。第一模板不可定制或定制成本高。create-t3-app 虽然提供了不少配置选项但它不开放模板机制你想加自己的 CI 脚本、自己的工具函数目录只能生成之后手动改。社区模板仓库倒是可以 fork但 fork 之后每次上游更新都要手动合并追版本追得心累。第二团队约定无法沉淀。脚手架工具本质上是工程经验的载体但现成工具承载的是作者的工程经验不是你的。团队里的目录规范、代码风格、提交规范、环境变量管理方式这些只有自己人最清楚指望一个社区工具替你管理根本不现实。第三生成产物黑盒。很多脚手架生成完之后用户对项目里每一份文件的出处一无所知。出了问题只能整个删掉重建没办法针对性地修某一处模板逻辑。对于需要长期维护的团队工程基线来说这不是小事。1.3 我想要的工程化基线因此我给 t3code 定下的目标很明确它不是一个通用的代码生成器而是团队工程化基线的一个载体。具体要做到四件事——交互收集参数、拷贝模板文件、渲染动态内容、执行收尾动作。后面所有设计决策都是围绕这四件事展开的。范围定小了复杂度自然就下来了核心逻辑加起来不到一千行剩下全是模板代码。这篇文章会把设计思路、核心实现和踩坑记录完整写出来。如果你在团队里做前端基建或者想给自己的团队落地一套内部脚手架又或者只是好奇一个 CLI 工具是怎么从零做出来的应该都能从中找到一些可以复用的经验。内容不需要多高深Node.js 基础加一点模板引擎知识就能看懂。2. 整体设计思路脚手架工具要解决的四个问题2.1 先想清楚t3code 不是什么比它是什么更重要动手之前我做的最重要的一件事是给自己划界线。t3code 不是低代码平台不是代码生成器更不是要替代 create-t3-app 的通用方案。它就是项目初始化加速器把创建项目过程中的重复劳动压缩成一条命令。这个定位听起来简单但它直接影响后面每一个设计选择范围不扩大复杂度就可控模板只在团队内部用就不需要做复杂的远程拉取用户都是有经验的开发者就不需要做图形化界面。明确了定位之后核心功能就清晰了。t3code 做的事包括四件第一交互收集参数包括项目名、包名、是否启用 CI、是否初始化 Git、选择哪个业务模板第二拷贝模板文件把内置模板目录完整复制到目标目录第三渲染动态内容把项目名、版本号、npm registry 地址等变量替换进模板文件第四执行收尾动作自动安装依赖、初始化 Git、打印启动命令。这四件事每一件拆开都不复杂但组合起来加上各种边缘情况的处理就是完整的工具了。我见过不少脚手架工具交互做得很花哨结果复制文件时不处理隐藏文件、不处理 .gitignore生成出来的项目根本不是用户预期的样子。所以 t3code 从第一天起就刻意保持小体积小到核心逻辑出了问题能一眼定位。2.2 技术选型为什么是 Node.js Commander而不是 Go、Rust 或 Shell选型这件事我纠结过两天。做一个脚手架工具摆面前有几条路。第一条是用 Go 或 Rust 写编译型二进制优点是没有运行时依赖、启动速度快可以做成一条命令直接放在任意机器上跑但缺点是模板分发麻烦模板如果打包进二进制那么每改一次模板就要重新编译团队里非 Go 开发者想贡献模板要先装工具链这门槛对前端团队来说太高了。第二条路是写 Shell 脚本。简单场景下 Shell 完全够用比如 mkdir、cp、sed 一把梭。但只要涉及到交互式问答、跨平台路径处理、JSON 变量替换、错误重试这些操作Shell 很快就会变成一团乱麻。尤其是 macOS 的 zsh 和 Linux 的 bash 行为还有差异Windows 更是直接劝退。第三条路是用 Node.js 写一个 npm 包类型的 CLI这也是我最终的选择。原因很实际团队里前端工程师人人都有 Node 环境npx t3code 一条命令就能跑起来模板直接打包在 npm 包里发布新模板就是发一个新版本包。模板文件对前端工程师来说是纯文本改起来没有任何额外学习成本。Node.js 生态里 Commander、Inquirer、execa、Handlebars 这些库都是久经考验的完全不用重新造轮子。最终 t3code 的依赖清单如下commander命令行参数解析。inquirer交互式问答。handlebars模板渲染。execa子进程执行安装依赖、git 命令。fs-extra递归拷贝和文件操作。每个库都放在自己最擅长的位置上。commander 负责参数inquirer 负责问答handlebars 负责渲染execa 负责外部命令fs-extra 负责文件系统操作。依赖虽然不少但没有一个是可有可无的。2.3 模板目录结构约定优于配置配置优于代码t3code 的模板不是简单的一堆文件它有明确的两层结构。第一层是项目级模板比如 next-trpc、next-plain、library每个模板对应一类项目形态第二层是模板内部的可选区块比如某个模板里可以选择是否生成 GitHub Actions 工作流、是否生成 Dockerfile。这么设计是为了让按需生成成为可能同时不让这种灵活性泛滥成灾。模板目录的结构大致是这样的templates/ ├── next-trpc/ │ ├── base/ │ │ ├── .env.example │ │ ├── .eslintrc.cjs │ │ ├── package.json.j2 │ │ ├── tsconfig.json │ │ ├── next.config.mjs │ │ ├── src/ │ │ └── README.md.j2 │ ├── optional/ │ │ ├── ci-github/ │ │ ├── docker/ │ │ └── monorepo/ │ └── manifest.json ├── next-plain/ └── library/base 目录下的文件是必选内容optional 目录下都是可选的增量文件。manifest.json 声明了这个模板支持哪些可选区块、哪些文件需要渲染、默认推荐选项是什么。模板里以 .j2 结尾的文件表示需要经过 Handlebars 渲染其他文件一律原样拷贝。这个约定的好处是模板作者一眼就能看出哪些文件会被动态处理哪些不会。模板即代码是我在这个项目里最坚持的原则。业务方想加一段自定义配置不需要改 t3code 的源码只需要在 templates 目录下新增一个 optional 区块然后更新 manifest.json 的描述文案。后来我们团队干脆把模板仓库单独拆了出去通过 git submodule 的方式在发版本时同步进主仓库模板的维护和 CLI 代码的维护彻底解耦。这一步让我体会到模板结构设计得清晰很多后续的流程问题都会自动消失。3. 核心实现拆解从用户输入到项目落地的完整链路3.1 入口命令与参数解析t3code 的命令入口是 package.json 的 bin 字段指向的 JS 文件。我用 Commander 定义了三个子命令init 是交互式创建项目list 列出所有可用模板doctor 检查当前环境是否满足生成条件。三个命令各有定位init 是日常主力list 让用户知道有哪些选择doctor 则是排障专用的环境有问题时先跑一下它。下面贴 init 命令的解析逻辑这是整个工具的主入口#!/usr/bin/env node const { Command } require(commander); const program new Command(); program .name(t3code) .description(T3 技术栈项目脚手架生成器) .version(1.4.2); program .command(init) .description(初始化一个 T3 项目) .argument([projectName], 项目目录名称例如 my-app) .option(-t, --template name, 指定模板例如 next-trpc) .option(--no-git, 跳过 git init) .option(--no-install, 跳过依赖安装) .option(-r, --registry url, 指定 npm registry 地址) .action((projectName, options) { runInit(projectName, options).catch((err) { console.error([t3code] 初始化失败:, err.message); process.exit(1); }); }); program.parse(process.argv);Commander 的 argument 和 option 分离得很清晰。项目名是位置参数可以写在命令后面模板、registry 等是选项参数用短横线语法传。这里我考虑过要不要加一个 --yes 参数跳过所有交互直接使用默认值后来决定不支持。原因是我见过太多脚手架默认值藏在文档里用户完全不知道自己在用什么。t3code 面向的是有一定经验的开发者把关键选项问清楚比快速跳过重要得多。3.2 交互式问答把决策放在用户眼前把校验做在输入之前如果用户没有在命令行里指定项目名或模板init 流程就会进入 Inquirer 的问答环节。我把问答分成两层第一层是项目基本信息第二层是根据 manifest.json 动态生成的可选区块问题。第一层的问题非常直接但校验必须严格。比如项目名的校验const basicQuestions [ { type: input, name: projectName, message: 项目目录名称:, validate: (input) { if (!input.trim()) return 项目名不能为空; if (!/^[a-z0-9-]$/.test(input)) return 只能包含小写字母、数字和中划线; return true; }, }, { type: input, name: packageName, message: npm 包名默认与项目名一致:, default: (answers) answers.projectName, validate: (input) { if (!/^[a-z0-9-]$/.test(input)) return 包名只能包含小写字母、数字和中划线; return true; }, }, ];这个校验规则是我踩坑踩出来的。第一版只做了非空校验结果有同事输入了中文项目名后面生成的 next.config.mjs 直接被 Node.js 解析报错还得手动改一堆文件名。从那之后所有用户输入都必须过规则校验。宁可在 prompt 阶段多问一遍也不要在生成之后返工。正则限定得严一点没有坏处因为项目名和包名都会进入后续的模板变量一旦出现非法字符问题往往不止一处。第二层问题来自模板的 manifest.json。比如模板声明了 ci-github 这个可选区块init 流程就会自动生成一个确认类型的问题是否生成 GitHub Actions 工作流。这层逻辑虽然只用了几行代码但它的意义在于模板能力扩展不再需要修改代码只要改 manifest.json交互层就是通用的。3.3 模板渲染为什么选 Handlebars而不是字符串拼接模板渲染是整个工具的技术核心。最初我想过最简单的方式在模板里写PROJECT_NAME之类的占位符然后用字符串 replace 替换成实际值。这个方案实现最快但有一个致命问题——如果配置内容需要根据用户选项条件性地出现字符串拼接就完全无力了。举个例子package.json 里如果用户选择了 Docker 区块scripts 里就要多一个 docker:build 命令如果选择了 CI 区块devDependencies 里就要多几个包。用字符串拼接去组织这些条件逻辑代码会迅速腐烂。所以我改用 Handlebars。它有三个好处语法简单模板作者不需要学一门新语言原生支持 #if 条件判断和 #each 循环覆盖了我 99% 的需求有完整的转义机制不会出现模板变量破坏 JSON 格式的问题。下面是一个真实模板片段来自 next-trpc 模板的 package.json.j2{ name: {{packageName}}, version: 0.1.0, scripts: { dev: next dev, build: next build, start: next start, lint: next lint, {{#if withDocker}} docker:build: docker build -t {{projectName}}:latest ., {{/if}} typecheck: tsc --noEmit }, devDependencies: { typescript: ^5.4.0, tailwindcss: ^3.4.0, eslint: ^8.57.0, eslint-config-next: ^14.1.0, {{#if withCI}} changesets/cli: ^2.27.0, {{/if}} eslint-plugin-tailwindcss: ^0.5.0 } }渲染的时候把前面收集到的所有答案整理成一个大的 context 对象传给 Handlebars 编译之后的函数const Handlebars require(handlebars); const context { projectName: my-app, packageName: my-app, withDocker: true, withCI: false, author: your-name, registry: https://registry.npmjs.org, }; const source await fs.readFile(templateFile, utf-8); const render Handlebars.compile(source); const output render(context);这里有一个我从实际使用中总结出来的关键经验模板文件凡是 .json 结尾的渲染完成之后必须通过 JSON.parse 校验才能落盘。因为 Handlebars 的 #if 块如果缩进或者逗号位置处理不当很容易在 JSON 文件里多出一个逗号或者少一个闭合括号。我在 renderFile 函数里加了一个钩子如果源文件扩展名是 .json渲染结果必须 JSON.parse 成功否则直接报错并且把渲染结果连同原始模板一起打印出来。这个钩子帮我拦下了很多模板编写不规范的问题。3.4 文件落盘与目录创建最容易翻车的环节渲染完成之后就该写文件了。这个环节看起来最没有技术含量实际上最容易翻车。我用 fs-extra 的 copy 方法先把模板目录完整复制到目标目录然后逐文件处理渲染。注意顺序很重要先复制再渲染可以保证非模板文件比如图片、字体、二进制文件也能被原样带上如果先渲染再复制二进制文件可能会在读写过程中损坏。核心代码如下const fse require(fs-extra); await fse.copy(templateBaseDir, targetDir, { filter: (src) !src.includes(node_modules), }); // 遍历目标目录渲染所有 .j2 结尾的文件 const files await findAllJ2Files(targetDir); for (const file of files) { const rendered await renderTemplateFile(file, context); const outputPath file.replace(/\.j2$/, ); await fse.outputFile(outputPath, rendered); await fse.remove(file); }有几个细节必须强调。第一遍历文件时要用 fs.readdir 的 withFileTypes 参数判断目录类型不能用简单的字符串包含判断否则遇到名字里带点的目录比如 .next、.github会误判成文件。第二隐藏文件在 copy 阶段是正常处理的但如果你选了某些第三方复制库要确认它的过滤逻辑不会把隐藏文件丢掉。第三目标目录如果已经存在且非空init 命令应该直接拒绝执行必须加一个 --force 选项才能覆盖。这个保护非常重要我因为早期忘了写这个检查曾经把同事一个正在开发的目录直接覆盖了。项目内容没丢但那次经历绝对不想再来一次。3.5 依赖安装与 Git 初始化外部命令的静默陷阱文件生成完毕最后一步是安装依赖和初始化 Git。这里我用了 execa 而不是 Node.js 自带的 child_process.exec原因是 execa 对 Windows 的支持更好还支持超时时间和 stdio 模式设置。以下是依赖安装和 Git 初始化的代码const execa require(execa); async function installDependencies(targetDir, { registry }) { const args [install]; if (registry) { args.push(--registry, registry); } const subprocess execa(npm, args, { cwd: targetDir, stdio: inherit, timeout: 120000, }); try { await subprocess; } catch (err) { throw new Error(依赖安装失败: ${err.message}); } } async function initGit(targetDir) { if (!(await fse.exists(path.join(targetDir, .git)))) { await execa(git, [init, -b, main], { cwd: targetDir }); await execa(git, [add, .], { cwd: targetDir }); await execa(git, [commit, -m, chore: init project via t3code], { cwd: targetDir, }).catch(() { // 如果用户全局 git 配置不全缺 name/emailcommit 会失败 // 这里不做强制只留下提示 console.warn([t3code] 自动 commit 失败请检查 git 用户配置); }); } }git init 之后要不要自动 commit我犹豫过。自动 commit 的好处是用户拿到的是一个干净的工作区可以直接开新分支写代码坏处是如果用户的全局 git 配置不全commit 失败会中断整个流程。后来我做了容错处理commit 失败只打印警告不阻塞流程。同时用户也可以用 --no-git 完全跳过 Git 相关操作。依赖安装这里我特意保留了 stdio: inherit让 npm 的安装日志直接打到终端上。有些脚手架喜欢把安装过程藏起来只显示一个 spinner但实际经验是安装卡住的时候用户最需要原始进度信息。宁可输出丑一点也要让用户知道它到底卡在哪一步。npm install 超过两分钟超时之后错误信息会包含具体命令的完整输出这比安装失败四个字有用得多。4. 实测过程从一条命令到完整可用的 T3 项目4.1 完整跑一遍 t3code init我拿一台配置干净的新电脑做了一次完整实测确保从空目录到项目跑起来没有断点。执行命令npx t3code init my-app -t next-trpc由于指定了模板交互问答会自动跳过模板选择剩下的问题只有四个npm 包名、是否生成 Dockerfile、是否生成 CI 工作流、是否自动执行依赖安装和 git init。这四个问题的默认值我都做了认真设计包名默认等于项目名Dockerfile 默认不生成CI 默认生成安装和 git init 默认执行。默认值的选取原则是多数场景下不需要改而不是保守选项避免出错。选择完成之后大概过了一分多钟大部分时间是 npm install 在跑。等命令结束我用 tree 命令看了一眼生成的项目结构my-app/ ├── .env.example ├── .eslintrc.cjs ├── .github/workflows/ci.yml ├── .gitignore ├── README.md ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── tailwind.config.ts ├── tsconfig.json └── src/ ├── app/ │ ├── api/trpc/[trpc]/route.ts │ ├── layout.tsx │ ├── page.tsx │ └── globals.css ├── server/api/root.ts ├── server/api/routers/post.ts ├── trpc/react.tsx └── trpc/server.ts然后执行 npm run dev本机 3000 端口直接起了一个带有 tRPC 完整链路的 Next.js 项目。从 React 组件到后端路由全类型安全新项目的第一个 commit 就已经是一个可以开发的起点。整个流程走完我的感受是工具的价值不在于它生成了多少文件而在于它把想清楚再动手这件事变成了默认行为。新项目一创建目录规范、命名规范、环境变量管理、CI 检查全部就位。4.2 验证生成内容的核心链路类型和 CI 都要真的能跑光能跑起来还不算数我特意做了两件验证工作。第一件是验证端到端类型安全是否真的成立。我在 src/trpc/react.tsx 里调用 useQuery 获取数据然后故意把服务端 router 返回的字段类型改掉编辑器里立刻出现了类型错误。这说明 tRPC 的端到端类型推断在生成的样板工程里是通的。这个验证很重要因为 t3code 的核心卖点之一就是类型安全如果模板里某个配置文件版本不匹配导致类型推断断裂整个项目的开发体验会大打折扣。第二件是验证 CI 脚本能真正跑通。我把生成出来的 .github/workflows/ci.yml 放进一个 GitHub 仓库里触发了一次流水线确认 lint、typecheck、build 三个步骤都能通过并且用的是模板里锁定的 Node 版本。这两项验证帮我发现了一个暗处的问题模板里 .env.example 的 DATABASE_URL 用的是本地 localhost 默认值但 CI 环境里根本没有这个数据库所以 CI 脚本里所有依赖数据库的步骤我都提前加上了注释用户需要按自己的实际情况调整。这个问题不算是 bug但它体现了模板作者该有的自觉——模板里必须留下足够的注释明确告诉使用者哪些地方必须改。4.3 参数化细节版本号为什么要统一管理生成出来的 package.json 里依赖版本号是精确锁定的。这个决策当时有同事反对觉得应该用 latest 或者 ^ 前缀让 npm 自动解析到最新版。我坚持用精确版本号原因很简单脚手架生成的项目是团队的长期基线如果每次生成都拉到最新版某天某个依赖升级引入了 breaking change所有新项目同时中招问题定位成本会非常高。精确锁定版本让升级这件事发生在可控的时间点比自动最新稳定得多。为此我在模板引擎里做了一个扩展context 里注入一个 versions 对象所有依赖版本都从一份统一的 versions.json 读取。每次升级基础依赖只需要改 versions.json 然后发布一个新版 t3code不用在一堆模板文件里翻找版本号。这是单一数据源原则在脚手架里的实际落地它保证了团队所有新项目用的基础依赖版本完全一致不会出现张三的新项目用 React 18李四的新项目还在用 React 17 这种混乱情况。{ next: 14.1.0, react: 18.2.0, react-dom: 18.2.0, trpc/server: 10.45.0, trpc/client: 10.45.0, trpc/react-query: 10.45.0, trpc/next: 10.45.0, typescript: 5.4.0, tailwindcss: 3.4.1 }模板里引用版本号时写成这样{ dependencies: { next: {{versions.next}}, react: {{versions.react}}, trpc/server: {{versions.trpc-server}} } }versions.json 里的 key 和模板里的引用并不是靠约定来保证一致的我加了一个配套的单元测试模板文件里出现的所有 versions.xxx 引用必须在 versions.json 里有对应定义否则测试直接失败。这个测试是我踩了一次大坑之后才补上的。有一次我删掉了某个不再需要的依赖版本定义但忘了模板里还在引用发布出去的版本生成的项目依赖直接失效排查了很久才定位到是版本错配。从那以后凡是模板和数据源之间的引用关系一律用自动化测试兜底不再靠人肉记忆。5. 常见问题与排查技巧实录5.1 模板渲染后 JSON 格式被破坏这是 t3code 用户反馈最多的一类问题。Handlebars 的 #if 块在 JSON 文件里非常脆弱只要缩进或者逗号位置不对渲染结果就是非法 JSON。举一个真实例子。某个用户自定义模板里写了这样的片段{ scripts: { dev: next dev, {{#if withE2E}} e2e: playwright test, {{/if}} build: next build } }如果 withE2E 为 false渲染结果会保留一个多余的空行JSON.parse 不一定失败但可读性很差。真正致命的是另一种写法——把逗号放在 #if 块前面{ scripts: { dev: next dev, {{#if withE2E}} e2e: playwright test {{/if}} } }当 withE2E 为 false 时dev: next dev, 后面直接跟着一个 }这就是非法 JSON。解决这个问题最稳妥的方式是要求模板作者遵守一条约定任何可能被 #if 移除的条目它的前导逗号必须写在 #if 块内部而不是写在块外面。我把这条约定写进了文档同时保留了 JSON.parse 校验钩子。双保险下来这类问题基本绝迹了。5.2 Windows 兼容性三个高频雷区我平时的主力开发机是 macOS但团队里 Windows 同事不少。t3code 早期版本在 Windows 上的问题集中出现在三处。第一是路径分隔符。生成出来的某些配置需要写路径比如 Dockerfile 里的 COPY 命令。早期代码直接用了 path.join 拼接路径在 Windows 上会生成反斜杠Dockerfile 解析直接失败。后来所有写进模板的路径统一使用正斜杠只有真正操作文件系统的路径才用 path.sep。第二是换行符。模板文件在 Windows 上被 Git 检出后变成 CRLF渲染出来的文件也是 CRLF。Linux 容器或者 shell 脚本对 CRLF 非常敏感会报一些莫名其妙的错误。我在工具里加了一个 lineEnding 配置项默认按模板文件本身的行尾处理但允许用户统一转为 lf。第三是外部命令的调用方式。在 Windows 上通过 Node.js 调用 npm.cmd 这类文件时execa 是安全的但如果直接用 child_process.exec 并且开启了 shell 选项很容易被路径里的空格或特殊字符坑到。统一走 execa 之后这类问题基本不再出现。5.3 依赖安装超时和内网源问题生成项目之后的第一道坎往往就是 npm install。网络环境不稳定的时候安装一个中等规模的项目动辄几十秒超过默认超时时间就会失败。t3code 把超时做成了可配置项同时在 init 命令里提供了一个 -r 参数直接指定 npm registry。这个参数很实用比如在受限网络环境下用户可以传一个镜像地址不用去改全局 .npmrc。还有一个容易被忽略的细节如果用户已经配置了 .npmrc 里的 registryexeca 启动 npm 时会自动读到这个配置。这个行为有好有坏。好的方面是用户不需要额外配置坏的方面是如果用户配了一个错误的镜像地址安装失败后第一时间不会怀疑 .npmrc而会认为是 t3code 的问题。我在安装失败的错误信息里加了一行提示提醒用户检查 .npmrc 中的 registry 配置。这条提示帮我挡掉了不少重复的 issue也让用户排查问题的路径短了很多。5.4 模板分发与版本错配的教训t3code 的模板存储在 npm 包内模板和 CLI 代码共享版本号。对于小项目来说这个方案够用但模板数量上来之后就会出现代码没变、模板更新也要发版本的情况。目前我的处理是遵循语义化版本规范模板改动如果只是内容层面的变化发 minor 版本模板数据结构变化比如 manifest.json 格式调整发 major 版本。同时我做了一个虽然简单但非常有用的机制doctor 命令会检查当前 CLI 版本与最新版本之间的差异如果差异过大就提示用户升级。这个检查不是为了骚扰用户而是因为模板和 CLI 强耦合版本不对齐会生成错误的内容。这个设计是真实事故换来的。有一次用户用旧版 CLI 搭配新模板生成出来的 package.json 里引用了一个不存在的脚本排查了很久才发现是版本错配。现在 doctor 命令会在用户跑 init 之前先做版本检查不一致时给出明确提示。6. 后续扩展的方向工具的生命力在于被真实使用最后聊一聊我接下来想做的事。t3code 目前的形态已经能解决团队的日常问题但它距离我理想中的工程基线工具还有一段路。我自己打算按下面几个方向慢慢推进也写出来给大家做个参考。6.1 插件机制当前可选区块是写在 manifest.json 里的静态声明数据和逻辑都不够灵活。如果支持插件让第三方通过一个钩子函数注入自定义渲染逻辑t3code 就能变成一个更通用的工程能力平台。比如有人做了一套企业级日志方案写一个插件任何人在生成项目时都能一键接入。这个方向投入不小目前优先级不算最高但长期来看是让工具突破单团队自用边界的关键。6.2 模板远程化现在模板打包在 CLI 包里每次想加模板都要发一个版本。如果模板能放在 Git 仓库里CLI 通过 URL 直接拉取指定 tag 的模板那么团队里的非前端同学也能通过维护仓库来更新模板完全不碰 CLI 代码。这一步能把模板即代码的理念贯彻得更彻底也是我比较看好的方向。6.3 生成后自动校验目前 t3code 生成完项目后只做了依赖安装没有对生成产物做深度校验。我打算加一个 post-init 钩子在目标目录里自动跑一遍 typecheck 和 lint如果失败直接指出哪些模板文件有问题。这个能力的价值在于模板作者改完模板后能立刻知道模板本身引入了编译错误而不是等用户创建项目之后才发现。6.4 更多项目模板t3code 的核心价值是 T3 技术栈的工程化基线但同样的机制完全可以用于生成 NestJS 后端项目、React Native 项目甚至纯 npm 库的基线。底层逻辑都是一样的交互收集参数、模板渲染、收尾动作变的只是模板内容。这个方向不复杂主要看团队实际需求什么时候出现。根据我个人的体会脚手架工具最怕的不是功能少而是功能没人用。t3code 从立项到现在最大的收获不是代码量而是逼着我把团队里很多默认大家都知道的工程约定写成了文档化的、可验证的模板。这个过程中很多原本模糊的规范变得清晰了很多原本靠口头传授的经验变成了代码。如果你也在维护团队的工程基建我真心建议试一次把自己的脚手架工具写出来哪怕只服务三个人它带来的规范沉淀也比任何现成工具都值。
返回列表