这个东西说来也挺有意思——我是在一次批量交付项目的时候,被逼着写下第一行t3code代码的。当时手上有三个后台管理系统要同期开工,前端结构几乎一模一样,无非是登录页、权限路由、若干CRUD页面换个名字,可我依旧要在每个工程里重复做骨架、拉依赖、调配置。折腾到第三遍的时候我非常确定:这不是我一个人在重复,而是团队所有成员都在把时间浪费在同样的体力劳动上。于是我花了一个周末写了这个内部工具,起名叫“t3code”,它要做的就是一件事——把“从零搭一个标准项目/模块”的重复过程,压缩成一条命令。
文章写出来之后,陆陆续续被不少做前端基建、搞代码生成的朋友问过实现思路。这里把完整的架构设计、编码过程和踩坑经验整理出来,希望能给同样想造轮子的你一点参考。
1. 我为什么非要做t3code这个脚手架工具
先交代一下背景。我们团队日常交付的产品是典型的中后台类型,技术栈基本固定在TypeScript + React + Ant Design,后端限定在一套自研的Node框架上。这个栈本身没有太大问题,问题在于每次新建一个业务模块都要手工拷贝之前项目中的公共代码,然后替换命名、调整路由、修改接口前缀。
1.1 重复劳动的隐性成本
你可能觉得拷贝代码能有多麻烦,无非是几秒钟Ctrl+C、Ctrl+V。但真实场景远没有那么简单:
- 公共目录下的工具函数、类型定义、请求封装往往有几十个文件,漏掉一个看着无关紧要,跑起来才发现某个import直接报错。
- 不同项目的约定并不完全一致。有的项目把页面放在
pages目录,有的放在views目录,还有的项目里混了两种写法,复制粘贴过去还得手工对齐。 - 环境配置、构建配置、lint规则在历史项目里有多个分支版本,照抄最新项目备份不算难,但很难判断哪个更新、哪个已经废弃。
- 新建模块时,路由、菜单、权限码、页面文件、接口类型需要保持命名同步,纯粹靠人肉修改,改漏一个字段排查起来就是半天。
这些成本单次看起来都微不足道,但乘以项目数量和团队成员人数之后,浪费相当可观。更麻烦的是,一旦我在基础结构上做了某种改进,比如统一换了请求封装的错误处理方式,就必须把改进同步到所有存量项目里去,这个同步过程几乎不存在效率可言。
1.2 为什么选择自研而不是直接使用现成脚手架
也有人说,社区不是有很多脚手架工具吗?像create-react-app、plop这些,为什么还要自己写?
我的取舍是这样的:
- 通用脚手架解决的是“把技术栈跑起来”的问题,它不会知道你公司内部的接口规范、权限模型、页面命名约定,更不会替你生成符合团队风格的路由配置。
plop这类文件生成器确实灵活,但它只解决“生成文件”这一步,不会把“依赖安装、代码检查、目录初始化、模板下载”整条链路串起来。而t3code的目标是端到端自动化。- 内部工具由团队成员自己维护,修改需求随时提、随时改,不用等开源项目的版本更新。
所以t3code从一开始就不是一个“通用项目生成器”,而是一个针对我们团队业务的“私有脚手架”。它的核心策略是:把所有重复的、有章可循的、可模板化的工作交给机器,把需要判断的部分留在配置里给人工决定。
1.3 命名里藏的三个层次
t3code这个名字,拆开是t、3、code。t代表template,模板;code代表生成代码本身;“3”代表我这个工具聚焦的三层内容——项目模板层、模块生成层、代码规范层。
项目模板层负责初始化整个工程骨架,模块生成层负责在已有项目里快速新增一个可运行的业务模块,代码规范层负责生成出来的文件自动统一格式、统一lint标准。三层各有独立的处理流程,在架构上互不干扰,后面详细展开。
2. t3code的架构设计:模板引擎加任务编排
真正动手写代码之前,我先在纸上把t3code需要承担的职责理了一遍,最终确定了五个核心模块。这个划分在后续开发中几乎没有大改,如果你也要做类似工具,建议先做这个环节再做其他事情。
| 模块 | 职责 | 对外暴露的接口 |
|---|---|---|
| 参数解析器 | 接收命令行参数、配置文件,合并成统一上下文 | parse(options) |
| 交互提示器 | 在缺少必填参数时弹出一系列问题引导用户补全 | ask(questions) |
| 模板渲染器 | 将模板文件结合上下文渲染为最终文本 | render(tpl, context) |
| 文件写入器 | 输出文件,支持冲突检测、权限校验、自动备份 | write(file, content) |
| 任务编排器 | 按流程执行“初始化-渲染-写入-后置命令” | run(flow) |
2.1 把模板当数据看待
t3code构建的核心思想其实一句话就能说清:模板文件就是普通数据,上下文就是模板的输入,渲染就是一次确定性的函数调用。
我以前用过一些代码生成工具,不少会把“模板”做成复杂的JavaScript函数,在模板里面写运算逻辑、写分支判断,结果模板越来越像程序,程序也越来越难维护。t3code反过来,要求模板里尽量只有占位符和简单条件判断,复杂逻辑全部前移到上下文数据里计算好,模板负责展示,逻辑负责计算,两边彻底分开。
举个例子,一个路由文件模板长这样:
// route.ts.tpl import type { RouteObject } from 'react-router-dom'; import { {{moduleName}}Page } from '../pages/{{moduleName}}'; export const {{moduleName}}Routes: RouteObject[] = [ { path: '/{{modulePath}}', element: <{{moduleName}}Page />, meta: { title: '{{pageTitle}}', permission: '{{permissionCode}}' } } ];渲染时,上下文可能是:
{ "moduleName": "Order", "modulePath": "order", "pageTitle": "订单管理", "permissionCode": "order:query" }模板里没有循环、没有复杂判断,所有变量在传入之前已经被计算完毕。这样写的好处很多:模板文件不需要单元测试,上下文构造逻辑可以单独测试;模板编辑器可以放心交给非核心开发人员维护;出了问题排查起来也简单,模板不背锅,肯定是上下文算错了。
2.2 任务编排采用状态机而非线性脚本
最开始我想得很简单,把整个生成流程写成按顺序执行的脚本就行:
const flow = [ (ctx) => parseArgs(ctx), (ctx) => askQuestions(ctx), (ctx) => renderTemplate(ctx), (ctx) => writeFiles(ctx), (ctx) => runPostCommands(ctx) ]; for (const step of flow) { await step(ctx); }但实际设计时我还是改成了一棵树,也就是状态机。因为很多步骤并不是线性的——比如用户选择了“不要交互模式”,就跳过提问环节;生成了文件之后,可能还要根据项目当前状态决定要不要安装依赖;如果检测到是monorepo,安装依赖的路径又要变化。这些分支判断塞在顺序数组里会让代码迅速变得难以阅读,用状态节点描述每个步骤的前置条件和后置动作就清晰很多。
核心调度代码差不多是这样:
type StepNode = { name: string; condition?: (ctx: Context) => boolean; run: (ctx: Context) => Promise<void>; next?: string[]; }; const nodes: Record<string, StepNode> = { init: { name: 'init', run: initContext, next: ['parse'] }, parse: { name: 'parse', run: parseArgs, next: ['ask'] }, ask: { name: 'ask', condition: (ctx) => ctx.interactive && !ctx.passedAllRequired, run: askQuestions, next: ['checkConflict'] }, checkConflict: { name: 'checkConflict', run: checkFileConflict }, render: { name: 'render', run: renderAllTemplates }, write: { name: 'write', run: writeAllFiles }, post: { name: 'post', run: runPostCommands } };每个节点只关心自己执行完毕后跳转到哪个节点,这样后续加减流程,只需要在next数组里做文章,核心渲染逻辑完全不用动。这也是t3code能做到“快速迭代”的重要基础。
3. 关键实现一:命令行参数解析与交互式提问
t3code既支持一次性传参的批处理模式,也支持不带参数的交互模式。这个设计是为了兼顾老手的高效操作和新手的上手体验。
3.1 参数解析器的配置策略
参数解析器依赖commander库,但我在它上面做了一层封装,让参数定义直接变成上下文结构的描述:
import { Command } from 'commander'; const program = new Command(); program .name('t3code') .description('generate project/module from template') .option('-n, --name <name>', 'module name') .option('-t, --type <type>', 'project or module', 'module') .option('-p, --path <path>', 'target directory') .option('--css <css>', 'css solution: less|scss|tailwind', 'less') .option('--no-interactive', 'disable interactive prompts') .option('--dry-run', 'do not write files, only print result');为了减少命令行输入成本,我还支持读取.t3code.config.js。配置文件的优先级比命令行参数低,但比内置默认值高。比如团队内部统一用antd主题色、统一使用less,这些写在配置里即可,命令行参数反而能够覆盖它。
// .t3code.config.js module.exports = { templateRepo: 'git@internal-server:templates/base-project.git', css: 'less', routerPrefix: '/admin', injectRedux: true };参数合并的逻辑遵循一个简单的优先级链:内置默认值 < 配置文件 < 命令行参数 < 交互回答。这样用户永远可以通过命令行覆盖一切,但又不强制每次把所有参数写全。合并代码用了一个很常规的reduce:
const finalContext = [defaults, fileConfig, cliArgs, answers] .reduce((acc, source) => ({ ...acc, ...source }), {});3.2 交互提问:避免让用户背参数
如果用户只输入npx t3code不跟任何参数,工具会自动进入交互模式。这一步我用的是prompts库,但同样做了一层封装——把问题定义也当作普通数据:
const questionDefs = [ { name: 'moduleName', type: 'text', message: '模块名称:', validate: (v) => /^[A-Za-z][A-Za-z0-9]*$/.test(v) || '模块名称必须是合法的标识符' }, { name: 'modulePath', type: 'text', message: '路由路径:', initial: (prev, answers) => kebabCase(answers.moduleName) }, { name: 'permissionCode', type: 'text', message: '权限码:', initial: (prev, answers) => `${answers.modulePath}:query` } ];提问顺序不是随便排的。我的原则是:**后面的问题尽量复用前面的答案作为默认值,减少用户的输入成本。**比如模块名填了Order,路由路径默认就是order,权限码默认拼上order:。
交互模式的体验会直接影响这个工具是否被团队接受。如果每次交互都让用户填十几个问题,大概率用两天就没人碰了。所以t3code做了一个贴心的设计——记忆上一次的交互答案,存放到用户目录下的.t3code_history.json里。下次遇到相同问题时,上次填过的值会作为默认值直接呈现,用户只需回车确认即可。
3.3 参数校验是容易忽略的工程
开始我忽略了一个细节:模板代码是直接写进文件系统的,如果用户填的模块名是一个非法变量名,生成的代码根本跑不起来。好在我在参数解析器里补了统一的校验逻辑,每个字段都声明了对应的validator:
| 字段 | 校验规则 | 错误示例 |
|---|---|---|
moduleName | 必须以字母开头,仅含字母数字 | 2order、order-list |
modulePath | kebab-case,仅含小写字母、数字、连字符 | /order、order_list |
permissionCode | 非空,按:分隔的权限码 | order::query |
css | 必须是less、scss、tailwind之一 | css=stylus |
校验失败的提示文案必须包含“为什么失败”,光说“参数不合法”等于没写。比如路径填了/order,提示就会说“modulePath不要以斜杠开头,路由会自动处理前缀”。这些小细节会让工具看起来像是一个有经验的同事在帮忙把关,而不是一个冷冰冰的脚本。
4. 关键实现二:模板渲染与文件生成
参数解析完,接下来就是t3code最核心的部分——模板渲染和文件写入。这一节我分两部分聊:一是模板引擎的选择与封装,二是文件写入器如何避免覆盖用户已有代码。
4.1 模板引擎选型:为什么我弃用了EJS
最开始我选了EJS作为模板语言,因为它在npm上生态最成熟。真正跑起来之后发现两个麻烦:
- EJS使用的是
<%= value %>这种标签,而项目的Vue/React模板里经常出现{{ value }}这种双大括号,两者语义完全不同,容易混。 - EJS模板里的内联JS自由度太大,一旦有人在模板里写上复杂循环,模板的可读性就直线下降,回过头来维护的人一脸懵。
t3code最后用了Handlebars,双大括号语法与前端框架保持一致,团队成员迁移成本低。而且Handlebars默认抑制了模板内的任意函数调用,只允许预定义的helper,对安全性和可维护性都是加分项。
渲染引擎的入口封装成了单函数:
import Handlebars from 'handlebars'; import { kebabCase, camelCase, upperFirst } from 'lodash-es'; export function renderTemplate(tplContent: string, context: Record<string, unknown>): string { const template = Handlebars.compile(tplContent, { noEscape: true }); return template(provideHelpers(context)); } function provideHelpers(ctx: Record<string, unknown>) { return { ...ctx, _kebab: (s: string) => kebabCase(s), _camel: (s: string) => camelCase(s), _pascal: (s: string) => upperFirst(camelCase(s)) }; }你可能会问noEscape会不会有安全风险。这里明确说一下:t3code生成的是源码文件,不是HTML片段,所以不需要转义<、>这类字符。如果哪一天想支持HTML模板,到时候再单独关掉即可。
4.2 文件名和路径的命名统一问题
模板渲染不仅要处理文件内容,还要处理文件名和目录名。这个环节是新手写脚手架容易忽略的,但对使用体验影响特别大。
t3code的做法是:模板目录里所有文件名都使用占位符。比如要生成一个OrderPage.tsx,模板目录里对应的文件就叫{{moduleName}}Page.tsx.tpl。
存放模板的目录结构:
templates/ module/ src/ pages/ {{moduleName}}Page.tsx.tpl services/ {{moduleName}}Service.ts.tpl models/ {{moduleName}}Store.ts.tpl routes/ {{moduleName}}Routes.ts.tpl渲染完成后,每个模板文件都经过一次“路径解析”:
function resolveTargetPath(tplPath: string, ctx: Record<string, unknown>): string { const fileName = path.basename(tplPath).replace(/\.tpl$/, ''); const dirName = path.dirname(tplPath); const renderedName = renderTemplate(fileName, ctx); const renderedDir = renderTemplate(dirName, ctx); return path.join(renderedDir, renderedName); }这一步的规范性很重要。团队常见的错误是:目录命名用驼峰、文件命名用中划线、路由命名用点号,风格飘忽不定。t3code直接在模板里统一了规则,目录一律kebab-case,组件文件一律PascalCase,工具函数文件一律camelCase。新生成的代码天然符合团队约定,整体代码风格一对比,整洁度立刻提升。
4.3 文件写入器的冲突检测:绝不静默覆盖
生成器最容易引发事故的操作就是覆盖已有文件。业内不少工具的问题恰恰在这里——用户运行命令时没注意,本地刚刚改过的文件被无声无息地覆盖了,等发现时已经迟了。t3code避免踩这个坑的机制分三级:
- dry-run模式:只打印将要生成的文件列表,不实际写入。适合执行前的确认。
- 存在性检测:目标文件已存在且内容相同,直接跳过并提示“unchanged”。
- 冲突提示:目标文件已存在且内容不同,默认中断流程,列出冲突文件列表,让用户选择“覆盖/跳过/备份并覆盖”。
写入器代码大概是这样的:
const conflicts: string[] = []; for (const file of renderedFiles) { if (fs.existsSync(file.path)) { const existing = fs.readFileSync(file.path, 'utf-8'); if (existing === file.content) { console.log(`[skip] ${file.path}`); continue; } conflicts.push(file.path); } } if (conflicts.length > 0 && !ctx.force) { console.error('存在冲突文件,请使用 --force 覆盖或 --skip 跳过:'); conflicts.forEach((c) => console.error(` - ${c}`)); process.exit(1); }这套机制看起来简单,实际上特别拯救用户。上线的第一周就有同事反馈,t3code帮他拦下了一次险些覆盖改了一半的接口文件的误操作。从那之后,我更加坚定了“宁可多问一次,也不要默认覆盖”的原则。
5. 关键实现三:项目初始化和后置任务编排
文件生成完毕,只完成了整个工具的一半工作量。另一半是把一个“看起来是项目的目录”真正变成“能跑起来的项目”。t3code在这里引入了后置任务系统。
5.1 模板克隆与依赖安装
项目级模板和模块级模板的运行逻辑不一样。模块级模板是在已有项目中新增模块,不涉及依赖安装;项目级模板则要新建一个仓库,通常需要克隆模板仓库、安装依赖、初始化Git。
t3code的项目初始化流程分为五个阶段,按顺序执行:
t3code project my-app --template react-admin每个阶段做的事情:
| 阶段 | 动作 | 检查点 |
|---|---|---|
| 1. fetch | 从内部模板仓库拉取基础代码 | 确认目标目录为空 |
| 2. render | 渲染所有模板变量 | 校验变量完整性 |
| 3. write | 写入文件 | 冲突检测 |
| 4. install | 根据包管理器安装依赖 | 网络/镜像确认 |
| 5. verify | 执行lint和类型检查 | 错误信息汇总 |
5.2 后置任务的插件化设计
后置任务不是硬编码在流程里的。t3code采用插件注册机制,每种任务都实现了相同的接口:
interface PostTask { name: string; check(ctx: Context): Promise<boolean>; run(ctx: Context): Promise<void>; }比如“安装依赖”这个任务:
class InstallDepsTask implements PostTask { name = 'install-deps'; async check(ctx: Context) { // 只有项目初始化才需要安装依赖 return ctx.kind === 'project' && !ctx.skipInstall; } async run(ctx: Context) { const pkgManager = detectPkgManager(ctx.targetDir); const commands = { npm: 'npm install', pnpm: 'pnpm install', yarn: 'yarn install' }; await execa(commands[pkgManager], { cwd: ctx.targetDir, stdio: 'inherit' }); } }这个设计使得新增一个后置任务非常轻松。比如后来团队要求生成项目之后自动做一次git init并创建development分支,我只需要新建一个GitInitTask,注册到插件列表,完全不用碰主流程代码。任务编排器逐项执行任务,每项任务都打印耗时,便于发现瓶颈。
5.3 做dry-run时的真实感
t3code的--dry-run功能我做得比较重,不只是打印文件名,而是会把渲染后的文件内容和最终目录结构完整展示出来。对新手来说,这能真正理解这条命令会带来什么变化;对老手来说,也方便在正式执行前核对预期结果。
输出大概是这样的:
[DRY RUN] 将生成以下文件: src/pages/OrderPage.tsx 784 bytes src/services/OrderService.ts 532 bytes src/models/OrderStore.ts 620 bytes src/routes/OrderRoutes.ts 488 bytes [DRY RUN] 将执行任务:install-deps, verify最后总是会弹出一个确认提示“是否继续执行?(y/N)”。这个环节看起来多此一举,但恰好是把工具从“能用”推向“好用”的关键一步。我现在很笃定的一点是:脚手架这类工具,给用户安全感和给用户速度感同等重要。
6. 实测效果与性能优化:一个页面模块的生成过程
工具的最终价值看实测。这里记录一次完整的用法,以及我在试运行阶段做的性能优化。
6.1 一条命令生成订单管理模块
我用t3code在测试项目里新增一个订单管理模块,命令如下:
npx t3code module Order --path order --permission order:query --css less工具执行的过程日志:
t3code v0.4.2 [1/5] 解析参数... done in 18ms [2/5] 校验参数... done in 3ms [3/5] 渲染模板文件... done in 42ms - src/pages/OrderPage.tsx - src/services/OrderService.ts - src/models/OrderStore.ts - src/routes/OrderRoutes.ts - src/__tests__/OrderPage.spec.tsx [4/5] 写入文件... done in 8ms [5/5] 执行 verify... done in 2.3s ✔ 完成!共生成 5 个文件,耗时 2.4s生成出来的OrderPage.tsx长这样:
import React from 'react'; import { Card, Table, Space, Button } from 'antd'; import { OrderService } from '../services/OrderService'; import { OrderStore } from '../models/OrderStore'; import { OrderPageContainer } from './style'; const OrderPage: React.FC = () => { const store = OrderStore.useStore(); React.useEffect(() => { store.fetchList(); }, []); return ( <Card title="订单管理"> <Space style={{ marginBottom: 16 }}> <Button type="primary" onClick={() => store.openCreate()}> 新建订单 </Button> </Space> <Table rowKey="id" dataSource={store.list} columns={store.columns} loading={store.loading} pagination={store.pagination} /> </Card> ); }; export default OrderPage;生成的代码可以直接运行,路由已经注册好,权限码已经写到meta里,接口服务的URL前缀已经从配置自动推导出来。同事第一次看到这个效果时还以为是打包时候的新特性,确定是工具生成的之后才真正对它有了信任。
6.2 渲染耗时分析:瓶颈不在渲染,在启动
我最初期的版本,把文件生成整个流程跑完大概在2.8秒左右。其中渲染模板只占40毫秒,写入文件只占8毫秒,大头全都耗在Node进程启动和依赖加载上。
针对这个情况做了三个优化:
- 去重依赖引入:把
commander、prompts、Handlebars这些大库放到一个入口里做lazy load,用的时候再require,启动时间从400ms降到150ms。 - 减少同步IO:模板读取从
fs.readFileSync改为fs.promises异步批量读取,同时把无关小文件的IO合并。 - 跳过不必要步骤:如果用户执行
--dry-run,则后置的verify任务默认跳过,节省约2秒。
优化后的启动时间压到了1.9秒左右。这个数据在大规模编译场景可能微不足道,但对使用者来说,“快”和“非常快”是两种截然不同的使用意愿,尤其是高频使用的时候。
6.3 第一次覆盖事故的教训
工具上线第二周,我收到一条紧急反馈:同事在t3code module User --force时发现本地一个手写的UserService.ts被覆盖了。虽然他加的是--force参数,但这个参数的本意是“允许覆盖冲突文件”,而不是“遇到任何情况都覆盖”。问题出在我把--force和“跳过冲突检测”直接划了等号。
那次事故之后,我改成了两个参数:
--force:覆盖冲突文件前不再提问,但必须把冲突文件打印到日志里;--backup:覆盖前自动将旧文件改为.bak后缀保存。
同时把“打印冲突列表”提升为强制行为,不管带不带--force,都必须输出冲突文件路径。格式如下:
注意:以下文件将被覆盖: - src/services/UserService.ts(已备份为 UserService.ts.bak)这套机制上线后,类似事故再也没发生过。我在这件事上拿到的经验是:工具默认要保守,用户显式选择冒险时,依然要想办法帮用户留后路。
7. 实际运维心得:模板仓库的版本管理和团队协作
t3code从第一天起就是把模板单独放在一个Git仓库里管理的。这个决定在后期被证明极其正确,这里单独说说。
7.1 模板仓库的独立版本化
t3code的代码仓库和模板仓库是两个独立的仓库:
t3code仓库:存放工具源码、渲染逻辑、配置解析,作为CLI分发包。t3code-templates仓库:存放项目模板和模块模板,每个模板文件都是一次普通代码提交。
这样划分有两个直接好处:代码更新不依赖模板更新,模板更新也不用等工具发版。团队里任何一个成员在真实项目里发现模板结构需要改动,直接提交到模板仓库,其他人下一次同步就能拿到最新模板。模板仓库自身也有分支的概念,比如默认的main分支用于稳定版模板,experimental分支用于试验新结构,工具通过配置文件分支选择到底拉取哪个分支。
7.2 模板的review流程
模板虽是代码,它的“用户”是所有使用工具生成代码的人。因此模板的变更也必须走review流程。我在模板仓库的README里明确规范:
- 每个模板必须带有最小示例文件,说明渲染后长什么样;
- 模板文件名必须包含
{{placeholder}},否则无法参与渲染; - 模板中禁止写死业务属性名,所有资料类字段必须来自上下文。
这个规范刚开始执行的时候还有人觉得小题大做,直到有一次review发现一个模板把某个用户的公司名硬编码在了页面标题里,才让所有人意识到规范的价值。
7.3 冷启动成本:让同事真正愿意用
工具做出来了,最大的难题是让团队真的用起来。t3code采用“增量渗透”的策略,我没有急着推广到所有人,而是先找了两位对自动化感兴趣的前端同事,用一个真实的中等复杂度模块跑通全流程。
跑通之后,我们针对他们的反馈做了两处修改:
- 新增
--template参数,允许为模块选择不同的模板变体; - 增加
--output绝对路径,解决部分同事在子目录下操作时的路径错乱问题。
紧接着我组织了一场30分钟的操作演示,核心只讲三件事:能生成什么、怎么用、覆盖文件时会有什么提示。演示结束后过半的前端组同事开始在日常开发中使用t3code。没有强制推广,但大家自己发现“省下来时间是真的香”。
8. 可继续扩展的方向:从单机脚手架到团队协作平台
t3code目前的形态是本地CLI工具,但它完全可以继续长成更强大的内部平台。我在实际使用中,已经看到几个自然的扩展方向了。
8.1 模板片段的可视化配置面板
现在所有上下文数据都是命令行参数或配置文件里的JSON,团队里不熟悉命令行的产品/后端同事用起来有门槛。可以做一个简单的Web页面,上面用表单代替命令行,点按钮直接生成下载包。后端逻辑复用现在这套渲染管线,只是把process.argv换成HTTP请求参数。
8.2 依赖分析的自动升级
当模板仓库的公共依赖升级时,存量项目也需要同步升级。目前这个操作靠各项目维护者手动进行,无法保证所有人都及时跟上。设想未来t3code增加upgrade子命令,自动扫描项目里的package.json与模板仓库声明的依赖版本做对比,输出差异报告。这会让团队的技术栈演进速度快很多。
8.3 生成代码的可追溯性
现在t3code生成的代码没有留下任何元数据标记,时间久了很难分辨哪些文件是手动改过的,哪些是纯模板生成以后未动过的。我计划在生成文件顶部加一个夹带注释,比如:
// generated by t3code v0.4.2, template rev 3a2f9e1这样在审计、排错、重构时能快速定位来源,也让工具的任何改动效果都可以回溯。
最后分享一点个人的经验
t3code这个项目从动手到稳定运行,累计投入大概一个周末加两个晚上。它不是什么高深的技术,论架构谈不上创新,论性能谈不上极致,但它解决的是我自己真实的痛点,所以它对我来说价值巨大。
如果你也想写类似的工具,我的建议是:**别看太多别人的设计,先把自己最重复的三件事列出来,然后从最小可用的脚手架开始写,跑通第一条命令之后再逐步加功能。**工具越用越好用,越用越想用,正向循环一旦形成,你才会真正愿意投入时间去打磨它。
我现在每次新建项目或者新增模块,只要敲一条t3code,剩下十几秒就是它自己的工作,我端着茶杯等结果。这种感觉,自己造出来的东西自己用,比从货架上拿下来的工具踏实得多。