去年我在团队里推提交信息规范的时候,发现一个很有意思的现象:几乎没有人反对“规范提交信息”这件事,大家都在问“到底按什么格式写”。光是把 Angular 的提交风格贴在文档里还不够,因为只要一打开终端,大多数人还是会习惯性地敲 git commit -m "fixed something"。这时候 Commitizen 适配器的价值就体现出来了——它把“写提交信息”从一道自由发挥的填空题,变成了一个有标准答案的选择题。Commitizen 本身只是一个调度器,真正决定问题的顺序、选项的枚举、消息的拼装规则的,是它的适配器。这篇文章我打算把适配器这个组件彻底拆开讲一遍,包括为什么要设计成可插拔、它的接口协议、主流适配器怎么选,以及如何从零手写一个走完整个调试流程。
1. 先搞清楚一件事:Commitizen 为什么需要适配器
1.1 提交规范与交互工具不该耦合
如果你去看 Commitizen 的源码,会发现它做的事情非常克制:拦截 git commit,弹出一系列交互式问题,收集答案后拼成一条 commit message,然后交给 Git 去执行。但它不关心这些问题长什么样。
不同团队的提交规范差异很大。有的团队面向开源项目,要求严格遵循 Conventional Commits;有的团队需要在提交信息里带上 Jira 单号;有的团队希望允许类似 feat: xxx 和 chore: xxx 的简单枚举;还有的团队会用中文写 type 描述。如果 Commitizen 把这些规范直接内置,它就变成了一个“只支持某一种规范”的工具。更合理的做法是把规则部分抽象出去,让每个团队选择自己的规则集。这就是适配器的价值:适配器负责定义交互流程和消息模板,Commitizen 负责调度和执行。
1.2 适配器模式在 Commitizen 里的具体体现
适配器模式本身是一个很经典的设计思路:客户端依赖一个抽象接口,而不是依赖具体实现。在 Commitizen 生态里,抽象接口就是“一个 npm 包默认导出一个 prompt 函数”,具体实现则是一堆 cz- 开头的包。
你只需要在配置里告诉 Commitizen 当前项目用哪一个适配器,比如:
json复制{
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
它就会加载这个包,调用里面的交互逻辑。换适配器的成本只是改一行配置,不用改动 Commitizen 本身,也不用改 Git 的钩子逻辑。这一点对一个团队的基础设施来说非常重要:当规范演进时,你不需要把工具链推翻重来。
1.3 使用适配器前后的提交体验对比
没有适配器时,提交信息的质量完全取决于开发者的自觉:
code复制git commit -m "fix bug"
这行提交信息放到后面的 changelog 生成工具里,几乎不能提供任何有效信息。引入 Commitizen 和合适适配器之后,同样一次修复,流程变成了:运行 git cz,在列出的类型里选 fix,填写影响范围,填写主题描述,确认是否有破坏性变更。最终生成:
code复制fix(login): 修复登录接口在 token 过期后返回 401 的问题
这段过程最大的价值,不是帮你省下了敲键盘的时间,而是把模糊的规范变成了具体的操作路径。人在看到一个空输入框时会产生选择困难,但看到一组明确选项时,会顺畅得多。如果你手头恰好管理着一个代码规范文档,把文档里的文字转换成适配器里的交互式问题,往往比贴文档更容易落地。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 适配器的接口协议到底长什么样
2.1 一个适配器的唯一硬性要求
Commitizen 对适配器的要求其实非常小:这个 npm 包需要导出名为 prompt 的函数,函数接收一个 inquirer 实例,返回一个 Promise,最终 resolve 出一个包含提交信息的对象。
我用一个最小示例来说明:
js复制// index.js
module.exports = {
prompt: (inquirer) => {
const questions = [
{
type: 'list',
name: 'type',
message: '提交类型:',
choices: [
{ name: 'feat: 新功能', value: 'feat' },
{ name: 'fix: 修复缺陷', value: 'fix' },
{ name: 'refactor: 重构', value: 'refactor' }
]
},
{
type: 'input',
name: 'subject',
message: '主题描述:'
}
];
return inquirer.prompt(questions).then((answers) => {
return {
commit: `${answers.type}: ${answers.subject}`
};
});
}
};
只要 Commitiz
