用代码固化工程经验:t3code的前端工程化设计与实践 做技术这些年我发现自己反复在做同一件事从零搭项目、配置代码规范、部署流水线每个新项目都要在环境搭建上耗掉两三天真正写业务逻辑的时间反而被压缩。后来我实在受不了这种低效循环就把自己最顺手的一套工具链固化成了一个名叫t3code的项目。这个名字不是缩写也不是跟风某个框架它的核心意思是三件事——技术选型Tech、工具链Tool、技巧沉淀Trick全部用代码表达出来。简单说t3code 是一套面向个人开发者和小型团队的前端工程化基础工具它把你的编码习惯、质量标准和自动化流程打包成可直接复用的配置与脚本。这篇文章我想把 t3code 的完整设计思路、关键配置、实操过程以及我踩过的一些坑讲清楚。它适合正在被“项目初始化”烦扰的开发者也适合想把自己的工作流标准化、但又不知道怎么落地的朋友还有那些对工程化体系只停留在“听说过名字”层面的前端新人。里面不会有云里雾里的概念只要你有 Node.js 基础照着操作就能跑起来。1. 项目整体设计与思路拆解1.1 解决什么问题告别重复劳动把工程经验代码化我先说痛点。很多开发者包括以前的我每接到一个新项目流程是固定的脚手架建项目然后手动装 ESLint、Prettier、husky、commitlint再到 CI 平台里配一遍 lint、test、build 流水线顺便还得去写 README 里的 badge 图标。这套流程至少重复二十遍以后你对它的厌恶程度会直线上升。t3code 的思路跟普通脚手架不一样。脚手架解决的是“把项目初始化出来”的问题t3code 解决的是“把项目初始化成我习惯的样子”的问题。它围绕三个维度组织内容Tech技术选型锁定一套经过验证的依赖版本组合避免每次都要重新调研技术栈也避免团队成员各装各的版本导致环境不一致。Tool工具链把代码质量工具、格式化工具、提交检查工具、自动化部署脚本全部配置好做到克隆即用不需要任何手动设置。Trick技巧沉淀把日常开发中反复用到的小技巧比如 git 别名、调试快捷键、常见 bug 的处理模板以 markdown 文档和 shell 脚本的形式固化成资产随取随用。很多团队会抱怨“工程化太重了、太复杂了”本质上是因为他们一开始就把三大件ESLint、Prettier、Husky各自单独配置依赖冲突、规则打架、版本不兼容的问题一堆。t3code 的做法是把配置当成一个整体来设计版本锁死、规则互通、脚本串联这样复杂度就是可控的。1.2 为什么选择“模块化配置”而不是“重量级框架”做这个项目的时候我有过两个方向的纠结一个是学一些知名脚手架的做法做成一个全局命令一条命令生成整个项目另一个是走配置包化路线提供一份基线配置然后按需选择性引入。最后我选了后者原因很现实全局命令意味着升级困难如果你在二十个项目里都用了npx create-t3code生成某一天你更新了规则要同步到所有旧项目会很麻烦。模块化配置的形态是t3code 仓库本身提供配置文件模板和安装脚本你通过 git clone 或者下载归档把它引入项目再执行一条初始化命令。这样每一个项目都有独立的配置副本互不干扰。后续规则升级了你可以选择性地合并 diff而不是被全局升级强制改动。这里有一个重要设计原则小团队和个人项目不需要中心化服务。很多商业化工程化平台需要搭建自己的服务端要维护权限系统、插件市场这对三五个人的团队来说是巨大的负担。t3code 把所有东西降级成“一个模板仓库加一串命令”能跑通核心场景也保留了你自由修改的空间。1.3 t3code 目录结构一眼就能看懂每个文件是干嘛的我把整个项目仓库按功能拆分成了几个清晰区块t3code/ ├── templates/ # 各类项目模板React、Vue、Node 等 │ ├── react-ts/ │ │ ├── .eslintrc.cjs │ │ ├── .prettierrc.cjs │ │ ├── commitlint.config.cjs │ │ └── tsconfig.json ├── scripts/ # 自动化脚本初始化、构建、发布 │ ├── init.js # 一键初始化入口 │ ├── test.js # 测试辅助脚本 │ └── release.sh # 版本发布脚本 ├── docs/ # 经验沉淀文档 │ ├── git-tricks.md # git 使用技巧 │ ├── debug-guide.md # 调试指南 │ └── faq.md # 常见问题汇总 └── package.json # 工具链依赖声明templates目录是核心因为它定义了你项目的地基。scripts是对外提供自动化能力的关键init.js会读取你在命令行传入的参数决定复制哪套模板、装哪些依赖、生成什么样的配置。docs目录看着不重要实际上它是 t3code 与一般脚手架最大的区别——你积累的那些“写在博客里吃灰”的经验终于有了一个随项目走的存放位置。2. 核心细节解析与实操要点2.1 代码质量三件套ESLint、Prettier、TypeScript 的配合先说最容易被搞乱的一部分ESLint、Prettier、TypeScript 同时启用时规则冲突和性能问题极为常见。很多人的 ESLint 配置里放了一大堆规则不管是不是自己需要的Prettier 和 ESLint 同时管格式两条线互相打架再加上 TypeScript 的 type-aware linting 没配好lint 一次要跑十几秒。t3code 的处理方式如下ESLint 只负责代码质量比如未使用变量、隐式类型转换、条件判断写法等格式类的规则全部交给 Prettier。Prettier 完全接管格式化并且通过eslint-config-prettier把 ESLint 里可能跟 Prettier 冲突的规则关掉边界干净。TypeScript 的 type-aware linting 只对项目源码开启不对 node_modules 和构建产物执行保证速度。这是一份我在模板里实际使用的.eslintrc.cjs核心部分// .eslintrc.cjs module.exports { root: true, extends: [ eslint:recommended, plugin:typescript-eslint/recommended, plugin:typescript-eslint/recommended-requiring-type-checking, plugin:react-hooks/recommended, prettier, // 必须放最后关闭所有格式类规则 ], parser: typescript-eslint/parser, parserOptions: { project: ./tsconfig.eslint.json, tsconfigRootDir: __dirname, }, plugins: [typescript-eslint, react-refresh], rules: { typescript-eslint/no-explicit-any: error, typescript-eslint/no-unused-vars: [error, { argsIgnorePattern: ^_ }], no-console: [warn, { allow: [warn, error] }], }, ignorePatterns: [dist, node_modules, .eslintrc.cjs], };这里要特别讲一下tsconfig.eslint.json的作用。在大型项目里直接让 ESLint 引用项目的tsconfig.json会带来一个问题它可能包含了include之外的声明文件或者全局类型文件type-aware linting 一旦加载错误配置就会直接报错。t3code 单独生成一个仅供 ESLint 使用的 tsconfig只 include 需要检查的源码目录能从根上把这类报错屏蔽掉。2.2 提交规范化Husky、lint-staged、Commitlint 的串联逻辑提交信息这块很多人觉得“差不多就行”但项目越往后迭代提交记录的可读性越重要。你要回溯某个功能是在哪个版本引入的如果提交信息全是 “fix bug”“update code”你只能对着 git log 发呆。t3code 的提交规范串联了三个工具各自负责一段Husky在 git 执行 commit 之前的钩子。安装后在.husky/目录下生成pre-commit和commit-msg两个脚本。lint-staged只对暂存区的文件执行 lint 和 format而不是整个项目都跑一遍。一个大型项目 commit 的时候全量 lint 可能要一分钟lint-staged 让它变成三秒。Commitlint校验提交信息是否符合 Conventional Commits 规范不符合直接拒绝提交。.husky/pre-commit的脚本内容#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx lint-stagedlint-staged配置在 package.json 里{ lint-staged: { *.{ts,tsx}: [eslint --fix, prettier --write], *.{css,scss}: [prettier --write] } }而.husky/commit-msg则负责压最后一道线#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx --no-install commitlint --edit $1如果你用过别的规范体系会觉得这套组合不稀奇。但里面容易踩坑的是Husky 在 npm 7 上的安装方式。以前用husky install手动设置钩子路径现在 Husky 9 的初始化方式略有不同。t3code 模板里直接放好了.husky/目录文件不依赖安装时的自动生成这样任何人克隆仓库后执行npm install钩子文件天然就在不需要额外跑npx husky init。2.3 版本发布与代码检查脚本一键触发闭环处理一个成熟的工程化体系不能只到“提交”为止发布这一步也要标准化。t3code 的scripts/release.sh会做这些事运行测试确保全部通过。通过standard-version分析 commit 记录自动推导版本号并更新 CHANGELOG。构建项目产出目标产物。生成 git tag并推送远端与 tag。脚本关键片段#!/usr/bin/env bash set -e # 防止在未暂存状态下发布 if [[ -n $(git status --porcelain) ]]; then echo 工作区不干净请先提交或暂存所有改动 exit 1 fi npm run test npm run build npx standard-version git push --follow-tags origin mainset -e的作用是让脚本在任何一个命令失败时立即退出避免“测失败了还继续发版”的糟糕情况。判断git status --porcelain这一步很多人会漏但它极其重要——如果你在发布前误改了某个文件且没提交版本号已经生成、tag 已经推进最后推上去的代码可能不是你想发布的版本。3. 实操过程与核心环节实现3.1 快速初始化一个项目从零到跑通的完整路径我把 t3code 的初始化命令设计成两条分支全新项目初始化和既有项目接入因为两种场景的行为完全不同。全新项目初始化git clone https://github.com/yourname/t3code.git my-project cd my-project npm install npm run init -- --template react-ts --name my-appinit.js脚本的动作包括读取--template参数从templates/下复制对应目录里的所有文件到当前目录。自动改写package.json中的name字段。如果检测到--name包含中文或特殊字符给出警告并建议修改。执行npm install安装依赖。检查当前是否已经初始化过 git没有则执地git init。注意init.js有一个保护逻辑——它会检查当前目录是不是空目录如果非空且没有--force参数直接拒绝执行。这样防止把配置覆盖到已有代码上丢掉你的改动。真的要把 t3code 接入既有项目我提供另外一条命令。npm run init -- --template base --merge--merge模式下脚本不会覆盖已有的配置文件而是把模板里的配置拆解成“新增”和“建议修改”两类。比如你项目里已经有.eslintrc.cjs它会先备份成.eslintrc.cjs.bak-YYYYMMDD然后写入新配置但你项目里的src目录不被动。3.2 关键命令和依赖版本说明t3code 的模板 package.json 里我把常用依赖版本做了锁死避免“最新版”在将来把项目带崩{ devDependencies: { typescript-eslint/eslint-plugin: ^7.12.0, typescript-eslint/parser: ^7.12.0, eslint: ^8.57.0, eslint-config-prettier: ^9.1.0, husky: ^9.0.11, lint-staged: ^15.2.2, prettier: ^3.2.5, standard-version: ^9.5.0, typescript: ^5.4.5 } }锁版本的逻辑是稳定优先于追新。你可以看到 eslint 我特意用了 8.x 而不是 9.x因为 eslint 9 的配置文件格式发生了破坏性变化很多生态插件还没有完全跟上。在实际业务开发中你不可能每周花半天处理工具链升级的连锁反应锁住一个大版本把注意力放在业务代码上这种取舍是划算的。3.3 配置合并策略与项目级覆盖规则t3code 不完全限制你的个性化需求。它引入了一套“基线与覆盖”的规则templates 下的配置是基线项目根目录如果存在t3code.config.js里面的内容可以覆盖基线。t3code.config.js的形态是// t3code.config.js module.exports { ignoreLintPaths: [src/generated/**], additionalEslintRules: { no-restricted-imports: [error, { patterns: [mui/system] }], }, customAliases: { : ./src, }, };init 脚本在生成最终配置的时候会先读取基线再读取 project-level 的覆盖最后合并且写入。这个机制的好处是你不必复制整份.eslintrc.cjs去改一行规则而是在逻辑上做增量覆盖。相应的坏处是如果你不熟悉合并规则有可能发生“我以为我覆盖了实际没覆盖”的问题。所以我在docs/faq.md里专门列过一条常见问题配置覆盖不生效先检查t3code.config.js是否被正确加载这通常是因为文件放在了错误的目录层级或者是 CommonJS 模块语法写成了 ESM。4. 常见问题与排查技巧实录4.1 lint 速度慢到无法接受怎么处理type-aware linting 非常消耗性能尤其是在组件库、中后台系统这种文件数几百起步的项目里。t3code 模板中做了三层优化第一层lint-staged 只扫描暂存文件这个最立竿见影。第二层ESLint 的no-lint目录直接写进.eslintignore比如dist、coverage、*.d.ts。第三层如果你的项目实在太大tsconfig.eslint.json 里不 include 全部文件改成{ extends: ./tsconfig.json, include: [src/**/*.ts, src/**/*.tsx], exclude: [src/mocks/**, src/stories/**] }这里注意exclude的优先级要高于includemock 文件和 storybook 示例不参与 type-aware linting 能明显提速。但也要提醒一点如果你把这些文件从 lint 范围里排除它们的类型错误就不会被 lint 发现属于“用轻微风险换取速度”的权衡。4.2 Husky 钩子不生效或执行环境 PATH 问题Husky 最经典的问题是在没有图形界面的服务器上或者通过 GUI 工具提交时钩子找不到node。因为图形工具继承的 PATH 环境变量不包含 nvm 路径。t3code 模板里的.husky/_/husky.sh在原版基础上加了 PATH 补充逻辑export PATH$PATH:$HOME/.nvm/versions/node/$(ls -t $HOME/.nvm/versions/node | head -1)/bin这样即使通过 SourceTree、VS Code 的图形提交面板也能正常触发 lint。还有一种更隐蔽的情况你重装了 Node 版本但.husky目录下的脚本还是旧版本的环境路径。我的排查心得是先手动执行npx husky run pre-commit看能不能正常跑完如果能那问题大概率出在 git 或 GUI 工具的 PATH 继承上。4.3 版本发布时误操作回滚指南standard-version 把版本发布变成一个自动化流程但它操作的是 git tag 和 package.json出错了需要及时回滚。我把自己的回滚套路写进了docs/faq.md假如发布后发现版本号推错了而且已经 push 到远端git tag -d v1.0.1 git push origin :refs/tags/v1.0.1 git reset --hard HEAD~1这会把最后一个发布相关 commit 撤销并删除远端 tag。但注意git reset --hard会丢弃工作区未提交的改动执行前务必确认没有重要文件。这里我想强调好的回滚流程不是让你避免错误而是让你在错误发生后十秒钟内恢复而不是花两小时查日志。4.4 实战避坑速查表问题现象可能原因解决路径commit 时钩子没触发依赖安装时 Husky 脚本没设置检查git config core.hooksPath是否等于.huskylint 规则互相冲突ESLint 和 Prettier 没有正确串联确认 extends 末尾加了prettierinit 脚本报错终止当前目录非空使用--force或手动清空目录注意备份commitlint 报错了但不显示具体原因提交信息格式问题手动执行npx commitlint --from HEAD~1 --to HEAD查看新成员克隆项目后 install 报 package-lock 冲突使用了不同的 npm 版本删除 node_modules 和 package-lock.json重新 install这个表里我想额外讲一下第一行git config core.hooksPath。Husky 9 的安装逻辑是把.husky设置为 git 的 hooks 目录如果你执行过某种全局 git 配置比如设置了core.hooksPath指向另一个目录Husky 的钩子就会被绕过。这个排查点不太起眼但它几乎能解释所有“我明明装好了为什么没生效”的疑难杂症。5. 经验沉淀与后续演进方向5.1 把文档当代码维护经验沉淀的三种形态t3code 的docs/目录不是摆设。我的习惯是在开发过程中一旦遇到非直觉性的问题就把它记成一条文档。这类知识通常不会出现在框架官方文档中却是团队里最有价值的东西。目前文档库分三类决策记录ADR为什么用 vite 而不是 webpack、为什么 eslint 锁 8.x。每条记录包含背景、决策、后果。这样三个月后回看你能还原当时的思考路径。操作手册常用命令、环境配置、发布流程。新人来了不需要反复问“仪式感”问题。故障笔记每次解决问题的全过程包括前兆、排查链路和最终修复方案。这是宝贵的调试教学材料。我见过太多团队“做项目很忙沉淀靠运气”。t3code 试图用“文档即代码”的形态扭转这个局面文档版本跟随项目代码一并提交不会散落在个人笔记里。5.2 从单项目到多项目复用变量化配置与模板扩展t3code 目前有针对 React TypeScript 的完整模板也有基础 Node 模板后续我计划按业务场景拆出 Express API 模板和站内后台模板。扩展模板唯一要遵循的是“不要重复造变量名”——所有模板都从.env.example读取环境变量不会在代码里硬编码任何项目名或部署地址。如果在两个不同项目里改配置改到想骂人那就是直接的信号变量化做晚了。早期我就吃过这个亏模板里写死了一个项目的接口域名另一个项目接入后忘了改测试环境被搞得很乱。所以现在init.js里专门加了占位符替换逻辑它会扫描所有配置文件把__PROJECT_NAME__、__API_BASE__这样的占位符替换成实际值。5.3 后续扩展的几个方向与我个人的使用体验t3code 现阶段只是一个基础框架我计划给它加更多自动化能力针对 monorepo 场景的版本联动、自动生成 API 文档的命令、还有把 lint 规则做成本地插件包而不是每次都复制配置的发布流程。不过这些扩展都会遵守同一条原则不开新服务端不建新平台永远保持一个仓库加一条命令的复杂度。我个人实际使用下来的感觉是这套东西最大的价值不在配置本身而在于它逼迫我把“习惯”变成了“代码”。以前我嘴上说要规范但每次新项目都会顺手省掉几个步骤有了 t3code跑起来是默认值想偷懒得主动绕开心理负担完全不一样。如果你也想把手头重复的工程活沉淀下来建议现在就从你的下一个新项目开始先把提交规范和一键发布这两件成本最低、收益最大的事情固定住剩下的再慢慢往里加。