Skip to content

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 配置 ​

js
// 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
    },
  })
}
bash
npm plop component     # 交互式生成
npm plop component -- --name user-card   # 非交互式(CI 里用)

1.2 Hygen(更轻的替代) ​

bash
npx hygen component new --name Button
# 模板放在 _templates/component/new/
对比PlopHygen
配置plopfile.js目录约定 _templates/
交互✅ inquirer✅
学习成本略高较低(约定优于配置)

💡 与脚手架的分工:脚手架管"项目诞生",Plop 管"项目内无数次"(见 A26)。


二、ESLint / Prettier / Stylelint:三件套的职责边界 ​

最容易搞混的一点:ESLint 管质量,Prettier 管格式,不要混用。

工具职责例子
ESLint代码质量 + 潜在 bug未使用变量、条件里的赋值、Hooks 规则误用
Prettier纯格式(不做质量判断)缩进、引号、换行、尾逗号
StylelintCSS/样式质量无效颜色、重复选择器、属性顺序

2.1 让它们不打架 ​

bash
npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier
js
// .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 ​

ini
# .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 装与初始化 ​

bash
npm i -D husky lint-staged @commitlint/cli @commitlint/config-conventional
npx husky init

3.2 三个钩子 ​

bash
# .husky/pre-commit   → 提交前**只检查改动的文件**
npx lint-staged

# .husky/commit-msg   → 校验提交信息格式
npx --no -- commitlint --edit "$1"
js
// lint-staged 配置(package.json)
{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss}": ["stylelint --fix", "prettier --write"],
    "*.{json,md}": ["prettier --write"]
  }
}
js
// 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 必须再跑一次全量:

yaml
# GitHub Actions / GitLab CI 片段
- run: npm ci
- run: npm run lint        # 全量 ESLint
- run: npm run type-check  # tsc --noEmit
- run: npm test

详见 A16 · CI/CD 与部署运维。


四、版本发布:changesets vs semantic-release ​

changesetssemantic-release
流程开发者手动写 .changeset/xxx.md 描述变更从 commit message 自动推断
版本决定合并 changeset 时自动算全自动
人工介入需要(写变更描述)几乎不需要
约束宽松(不强制 commit 格式)强依赖 Conventional Commits
适合Monorepo、需要精细描述单包、团队已规范 commit
CHANGELOG用你写的描述,可读性好由 commit 生成,质量取决于 commit

4.1 changesets 用法(推荐给 Monorepo) ​

bash
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 文件示例:

md
---
'@my/ui': minor
---

新增 UserCard 组件,支持受控与非受控两种模式

4.2 semantic-release 要点 ​

完全自动化,但前提是 commit 严格规范(所以必须配 commitlint,否则会自动发错版本)。

⚠️ 风险:fix: 改了个错别字 会触发 patch 发布。团队 commit 不规范时慎用。

4.3 选型 ​

场景推荐
Monorepo / 多包changesets
单包 + commit 已规范semantic-release
需要高质量的 CHANGELOGchangesets(人写的描述更清楚)
想全自动、团队纪律好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 语言格式化

动手练习 ​

  1. 给项目加 Plop,把"新建组件"做成一条命令(含 index / 组件 / 测试三个文件)。
  2. 配好 ESLint + Prettier,故意提交一段格式混乱且含 console.log 的代码,观察 lint-staged 如何拦截并自动修复。
  3. 用 changesets 对一个双包 Monorepo 做一次 version + publish(可发到私有源或 --dry-run)。

相关文档 ​