28 · 代码生成与规范化工具链
本篇解决什么问题: 团队规范靠"口头约定 + Code Review"是守不住的,必须固化成工具自动执行。 本篇串起 代码生成 → 静态检查 → 提交门禁 → 版本发布这条自动化流水线。
📌 原则:能自动化的规则,不要靠人记。工具链的目标是让"不合规的代码根本提交不上去"。
开篇速览
| 环节 | 工具 | 拦住什么 |
|---|---|---|
| 代码生成 | Plop / Hygen | 文件结构不一致 |
| 静态检查 | ESLint / Stylelint | 语法错误、坏味道、框架误用 |
| 格式统一 | Prettier / EditorConfig | 风格争论(缩进、引号、换行) |
| 提交门禁 | husky + lint-staged + commitlint | 脏代码入库、提交信息混乱 |
| 版本发布 | changesets / semantic-release | 版本号拍脑袋、CHANGELOG 靠手写 |
一、Plop / Hygen:项目内的代码生成器
解决的问题:新建组件要手动建 4 个文件(index.tsx / Component.tsx / .test.tsx / styles.module.css), 每次都写得不一样 → 用生成器把结构固化。
1.1 Plop 配置
// plopfile.js
module.exports = function (plop) {
// 自定义 helper
plop.setHelper('pascalCase', t => t.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase()))
plop.setGenerator('component', {
description: '创建一个 React 组件(含测试与样式)',
prompts: [
{ type: 'input', name: 'name', message: '组件名(kebab-case)',
validate: v => /^[a-z][a-z0-9-]*$/.test(v) || '请用 kebab-case' },
{ type: 'confirm', name: 'withTest', message: '生成测试文件?', default: true },
],
actions: data => {
const actions = [
{ type: 'add', path: 'src/components/{{pascalCase name}}/index.ts', templateFile: 'plop/component/index.hbs' },
{ type: 'add', path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.tsx', templateFile: 'plop/component/component.hbs' },
]
// ✅ 条件动作:根据问答结果决定生成哪些文件
if (data.withTest) {
actions.push({ type: 'add', path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx', templateFile: 'plop/component/test.hbs' })
}
return actions
},
})
}npm plop component # 交互式生成
npm plop component -- --name user-card # 非交互式(CI 里用)1.2 Hygen(更轻的替代)
npx hygen component new --name Button
# 模板放在 _templates/component/new/| 对比 | Plop | Hygen |
|---|---|---|
| 配置 | plopfile.js | 目录约定 _templates/ |
| 交互 | ✅ inquirer | ✅ |
| 学习成本 | 略高 | 较低(约定优于配置) |
💡 与脚手架的分工:脚手架管"项目诞生",Plop 管"项目内无数次"(见 A26)。
二、ESLint / Prettier / Stylelint:三件套的职责边界
最容易搞混的一点:ESLint 管质量,Prettier 管格式,不要混用。
| 工具 | 职责 | 例子 |
|---|---|---|
| ESLint | 代码质量 + 潜在 bug | 未使用变量、条件里的赋值、Hooks 规则误用 |
| Prettier | 纯格式(不做质量判断) | 缩进、引号、换行、尾逗号 |
| Stylelint | CSS/样式质量 | 无效颜色、重复选择器、属性顺序 |
2.1 让它们不打架
npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier// .eslintrc.cjs
module.exports = {
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'prettier', // ✅ 必须放最后:关掉 ESLint 中与 Prettier 冲突的**格式规则**
],
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'eqeqeq': ['error', 'smart'],
},
}🎯 关键:
eslint-config-prettier关掉冲突规则, 然后格式交给 Prettier,质量交给 ESLint。 顺序错了(把prettier放前面)会导致两条规则互相报错。
2.2 统一编辑器行为:EditorConfig
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true💡 它管的是编辑器层面(换行符、缩进),Prettier 管语言层面格式化,两者互补。
end_of_line = lf能避免 Windows/Mac 混用导致的整个文件 diff。
三、提交门禁:husky + lint-staged + commitlint
3.1 装与初始化
npm i -D husky lint-staged @commitlint/cli @commitlint/config-conventional
npx husky init3.2 三个钩子
# .husky/pre-commit → 提交前**只检查改动的文件**
npx lint-staged
# .husky/commit-msg → 校验提交信息格式
npx --no -- commitlint --edit "$1"// lint-staged 配置(package.json)
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] }提交信息格式(Conventional Commits):
<type>(<scope>): <subject>
type: feat | fix | docs | style | refactor | perf | test | build | ci | chore | revert✅ 合法:feat(frontend): 新增用户卡片组件 ❌ 非法:更新了一些东西 / fix bug
3.3 为什么 lint-staged 而不是全量 lint
| 做法 | 问题 |
|---|---|
pre-commit 跑全量 ESLint | 大仓库等 30 秒,开发会用 --no-verify 绕过 |
| lint-staged(只查暂存区) | 1~2 秒完成,开发愿意用 |
🎯 门禁设计的原则:快到不会被绕过。任何让人想加
--no-verify的钩子都会失效。
3.4 兜底:CI 里跑全量
本地门禁可以绕过,所以 CI 必须再跑一次全量:
# GitHub Actions / GitLab CI 片段
- run: npm ci
- run: npm run lint # 全量 ESLint
- run: npm run type-check # tsc --noEmit
- run: npm test四、版本发布:changesets vs semantic-release
| changesets | semantic-release | |
|---|---|---|
| 流程 | 开发者手动写 .changeset/xxx.md 描述变更 | 从 commit message 自动推断 |
| 版本决定 | 合并 changeset 时自动算 | 全自动 |
| 人工介入 | 需要(写变更描述) | 几乎不需要 |
| 约束 | 宽松(不强制 commit 格式) | 强依赖 Conventional Commits |
| 适合 | Monorepo、需要精细描述 | 单包、团队已规范 commit |
| CHANGELOG | 用你写的描述,可读性好 | 由 commit 生成,质量取决于 commit |
4.1 changesets 用法(推荐给 Monorepo)
npx changeset init
# 开发完一个变更后,写一份"变更说明"
npx changeset
# → 选择影响的包 → 选择版本类型(patch/minor/major)→ 写描述
# 生成 .changeset/brave-lions-dance.md
# 发版时:消费所有 changeset,自动升版本 + 写 CHANGELOG + 打 tag
npx changeset version
git add . && git commit -m "chore: release"
npx changeset publish # 发布到 npm生成的 .changeset 文件示例:
---
'@my/ui': minor
---
新增 UserCard 组件,支持受控与非受控两种模式4.2 semantic-release 要点
完全自动化,但前提是 commit 严格规范(所以必须配 commitlint,否则会自动发错版本)。
⚠️ 风险:
fix: 改了个错别字会触发 patch 发布。团队 commit 不规范时慎用。
4.3 选型
| 场景 | 推荐 |
|---|---|
| Monorepo / 多包 | changesets |
| 单包 + commit 已规范 | semantic-release |
| 需要高质量的 CHANGELOG | changesets(人写的描述更清楚) |
| 想全自动、团队纪律好 | semantic-release |
五、一条完整的落地流水线
① 写代码 Plop 生成结构一致的文件
↓
② 保存时 编辑器按 EditorConfig + Prettier 自动格式化
↓
③ git add 触发 lint-staged:只查改动文件(秒级)
↓
④ git commit commitlint 校验提交信息
↓
⑤ git push CI 跑全量 lint + type-check + test(兜底)
↓
⑥ 合并主干 changeset version + publish(自动发版 + CHANGELOG)自检清单
- [ ] 能说清 ESLint 与 Prettier 的职责边界,以及
eslint-config-prettier为何要放最后 - [ ] 能用 Plop 生成"多文件 + 条件文件"的组件骨架
- [ ] 能配出 husky + lint-staged + commitlint 三件套,并说出 lint-staged 的意义
- [ ] 知道本地门禁可被绕过,必须有 CI 兜底
- [ ] 能按 Monorepo / 单包 选择 changesets 或 semantic-release
常见误区
| ❌ 误区 | ✅ 事实 |
|---|---|
| "ESLint 和 Prettier 功能重复" | ESLint 管质量,Prettier 管格式 |
"pre-commit 跑全量检查更稳" | 太慢 → 开发用 --no-verify 绕过 → 门禁形同虚设 |
| "commit message 随便写" | semantic-release 靠它判断版本;混乱会导致发错版本 |
| "配了 husky 就够了" | 本地可绕过,CI 必须跑全量 |
".editorconfig 和 Prettier 二选一" | 互补:编辑器行为 vs 语言格式化 |
动手练习
- 给项目加 Plop,把"新建组件"做成一条命令(含
index/ 组件 / 测试三个文件)。 - 配好 ESLint + Prettier,故意提交一段格式混乱且含
console.log的代码,观察 lint-staged 如何拦截并自动修复。 - 用 changesets 对一个双包 Monorepo 做一次
version + publish(可发到私有源或--dry-run)。
相关文档
- 项目级脚手架 → A26 · 脚手架原理与实战 · A27 · 主流脚手架对比
- CI 兜底与发布流水线 → A16 · CI/CD 与部署运维
- 测试策略 → A11 · 前端测试策略
- 工程化整体选型 → A02 · 前端工程化