来源于互联网
接手过一个没人管规范的项目,master 上直接提交,分支名叫 fix1、fix2、test-new,.env 带着数据库密码躺在仓库里,package.json 里三个日期库并存。改一个小需求要先花两天搞清楚现在跑的到底是哪套代码。
规范这东西平时看着都是废话,真正的价值在于「新人进来第一周能不能自己把环境跑起来」和「半年后你还能不能看懂当时为什么这么写」。这篇是一份从 Git 到 API 设计的完整清单,逐条讲了为什么这么定,并且把其中已经过时的条目单独标出来说明现在的做法。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- Git 分支策略和交互式变基的完整工作流
- Commit message 该怎么写,以及现在主流的 Conventional Commits
- 环境变量的隔离与启动前校验
- 依赖治理的四个动作,从审计到升级
- 测试文件放哪儿、怎么命名、什么样的代码才可测
- 按功能而不是按角色组织目录
- 代码风格标准化的落地手段,从 ESLint 到 Git 钩子
- 日志在生产环境该怎么打
- RESTful API 的资源设计、状态码约定和安全实践
这份清单里有相当一部分条目写于 2018 年,有些已经不适用了。我会在对应位置注明,原始建议保留,方便你对照。
# 一、Git
# 1.1 一些Git规则
这里有一套规则要牢记。
在功能分支中执行开发工作
所有的工作都是在专用的分支而不是在主分支上隔离完成的。它允许你提交多个 pull request 而不会导致混乱,可以持续迭代提交,而不会使得那些很可能还不稳定而且还未完成的代码污染 master 分支。
从 develop 独立出分支
这样可以保持 master 分支中的代码稳定性,不会导致构建问题,并且几乎可以直接用于发布。
永远也不要把分支直接推送到 develop 或者 master,请使用合并请求(Pull Request)
通过这种方式,它可以通知整个团队某个功能的开发已经完成。这样开发伙伴就可以更容易对代码进行 code review,同时还可以互相讨论所提交的需求功能。
在推送所开发的功能并且发起合并请求前,请更新本地的 develop 分支并且完成交互式变基操作(interactive rebase)
rebase 操作会把被请求合并的分支(master 或 develop)的历史作为基底,并将你本地进行的提交应用于所有历史提交的最顶端,而不会去创建额外的合并提交(假设没有冲突的话),从而可以保持一个漂亮而干净的历史提交记录。
这条我一开始是反对的,因为 rebase 会改写提交历史,感觉不安全。用了一段时间之后改主意了。关键在于「只 rebase 还没被别人依赖的本地分支」,这个前提下它没有任何风险,换来的是 git log --oneline 出来是一条干净的直线,出问题二分定位快得多。真正危险的是 rebase 已经被别人 pull 下去的公共分支,那确实不能干。
请确保在变基并发起合并请求之前解决完潜在的冲突
合并分支后删除本地和远程功能分支
如果不删除需求分支,大量僵尸分支的存在会导致分支列表的混乱。而且该操作还能确保有且仅有一次合并到 master 或 develop。只有当这个功能还在开发中时,对应的功能分支才存在。
在进行合并请求之前,请确保功能分支可以成功构建,并已经通过了所有的测试(包括代码规则检查)
因为你即将把代码提交到这个稳定的分支。如果功能分支测试未通过,那目标分支的构建有很大的概率也会失败。此外,确保在进行合并请求之前应用代码规则检查,因为它有助于代码的可读性,并减少格式化的代码与实际业务代码更改混在一起导致的混乱问题。
这一条现在基本不用靠人自觉了,GitHub Actions 或者 GitLab CI 里配一个必须通过的 check,加上分支保护规则,构建不绿就点不了 merge 按钮。
使用一个合适的 .gitignore 文件
好的模板已经囊括了不应该和代码一起推送至远程仓库的系统文件列表。另外它还排除了大多数编辑器的设置文件夹和文件,以及最常见的依赖目录。
保护你的 develop 和 master 分支
这样可以保护生产分支免受意外情况和不可回退的变更。
具体到平台上,至少要开这几项:禁止直接 push、要求 PR 至少一人 approve、要求 CI 通过、禁止 force push。前两条防人为失误,后两条防灾难。
# 1.2 Git 工作流
基于以上原因,我们把功能分支工作流、交互式变基的使用方法,结合一些 Gitflow 中的基础(比如命名和使用一个 develop 分支)一起使用。主要步骤如下。
针对一个新项目,在项目目录初始化。如果是已有项目的后续功能开发,这一步请忽略。
cd <项目目录>
git init
检出(Checkout)一个新的功能或故障修复(feature/bug-fix)分支。
git checkout -b <分支名称>
新增代码变更。git commit -a 会独立启动一个编辑器用来编辑说明信息,这样的好处是可以专注于写这些注释说明。
git add
git commit -a
切换至功能分支,通过交互式变基从 develop 分支中获取最新的代码提交,以更新你的功能分支。可以使用 --autosquash 将所有提交压缩到单个提交,没有人会愿意看到 develop 分支中的单个功能开发就占据这么多提交历史。
git checkout <branchname>
git rebase -i --autosquash develop
如果没有冲突请跳过此步骤。如果有冲突,就需要解决它们并且继续变基操作。
git add <file1> <file2> ...
git rebase --continue
推送功能分支。变基操作会改变提交历史,所以必须用 -f 强制推送到远程功能分支。
git push -f
这里要单独提醒一句。如果其他人与你在该分支上进行协同开发,请使用破坏性没那么强的 --force-with-lease 参数。它会先检查远程分支的状态跟你上次 fetch 到的是否一致,不一致就拒绝推送,也就是说别人在你之后推过东西,你不会把他的提交直接抹掉。裸 -f 是不做这个检查的,覆盖了就找不回来。我的建议是把 --force-with-lease 设成别名,从此不再手打 -f。
提交一个合并请求(Pull Request),由负责代码审查的同事接受、合并和关闭。
完成开发之后记得删除本地分支。
git branch -d <分支>
远程已经删掉的分支,本地还留着一堆引用,用这行一次清干净。
git fetch -p && for branch in `git branch -vv | grep ': gone]' | awk '{print $1}'`; do git branch -D $branch; done
# 1.3 如何写好 Commit Message
坚持遵循关于提交的标准指南,会让与他人合作使用 Git 时更容易。这里有一些经验法则。
用新的空行将标题和主体两者隔开
Git 会把提交消息的第一行识别为摘要。如果你用 git shortlog 而不是 git log,会看到一个很长的提交消息列表,只包含提交的 id 以及摘要,不会包含主体部分。
将标题行限制为 50 个字符,并将主体中一行超过 72 个字符的部分折行显示
提交应尽可能简洁明了,而不是写一堆冗余的描述。这两个数字不是随便定的,50 是为了在 GitHub 列表页和 git log --oneline 里不被截断,72 是留出 git log 缩进后仍能在 80 列终端里完整显示。
标题首字母大写
不要用句号结束标题
使用主体部分去解释「是什么」和「为什么」,而不是「怎么做」
怎么做的部分 diff 里已经写了,代码本身就是最准确的答案。真正会丢的信息是当时的判断,为什么选这个方案、放弃了哪个、有什么约束。半年后回来看,你需要的是这个。
顺着上面聊,这套「标题首字母大写、不加句号」的规则来自英文 git 社区的传统。现在国内团队更常用的是 Conventional Commits 那一套,feat: / fix: / docs: / refactor: / chore: 打头,后面跟一句中文描述。它的好处是机器可读,可以直接由工具生成 CHANGELOG 和推断语义化版本号。两套规范不冲突,Conventional Commits 只是给标题多加了一个类型前缀。
配合 commitlint 加 husky 在提交时校验格式,不合规直接拦下来,比在 code review 里提醒人改要省事得多。
# 二、文档
文档这块的条目看着最像废话,但它是新人上手成本的唯一决定因素。
- 可以使用这个模板作为 README 的一个参考
- 对于具有多个存储库的项目,请在各自的 README 文件中提供它们的链接
- 随项目的进展,持续地更新 README
- 给代码添加详细的注释,这样就可以清楚每个主要部分的含义
- 不要把注释作为坏代码的借口,保持代码干净整洁
- 也不要把那些清晰的代码作为不写注释的借口
- 当代码更新,也请确保注释的同步更新
README 里我认为必须有的只有三块,怎么把它跑起来、目录结构里每个顶层文件夹装什么、部署走什么流程。其他的都可以后补,这三块缺了新人就得来问你。
注释这块补一条自己的判断。注释应该解释「为什么」,不是「在干什么」。// 循环遍历用户列表 这种注释是纯噪音,代码自己就写着 users.forEach。真正值钱的注释长这样,「这里必须用 setTimeout 延迟一帧,否则 Safari 上拿到的 offsetHeight 是 0」。这类信息不写下来,下一个人会把它当无用代码删掉,然后 bug 复现。
# 三、环境
如果需要,请分别定义 development、test 和 production 三个环境
不同的环境可能需要不同的数据、token、API、端口等。你可能需要一个隔离的 development 环境,它调用 mock 的 API,mock 会返回可预测的数据,使自动和手动测试变得更加容易。或者你可能只想在 production 环境中才启用 Google Analytics。
依据不同的环境变量加载部署的相关配置,不要将这些配置作为常量写进代码库
你会有令牌、密码和其他有价值的信息。这些配置应正确地从应用程序内部分离开来,这样代码库就可以随时独立发布,不会包含这些敏感配置信息。
具体做法是用 .env 文件来存储环境变量,并将其添加到 .gitignore 中排除掉不被提交。另外再提交一个 .env.example 作为开发人员的参考配置。对于生产环境,应该依旧以标准化的方式设置环境变量。
.env.example 这个文件很多人会省掉,别省。它的作用是「有哪些变量需要配」这份清单本身进版本库,而值不进。新人 clone 下来 cp .env.example .env 填值就能跑,不用来问你少了什么。
这里有个坑要注意,密钥一旦提交过就等于泄露了,改回来也没用,Git 历史里还在。真发生了只有一条路,去服务商那边把这个 key 撤销重签。我这个踩过,当时以为 git rm --cached 加一次提交就完事了,后来在别人 fork 的仓库里还能翻出来。
建议在应用程序启动之前校验一下环境变量
它可能会把其他人从几小时的故障排查中解救出来。少一个环境变量,程序不会当场崩,它会一路跑到某个深处的 fetch(undefined + '/api') 才报错,而那个报错跟根因看起来毫无关系。启动时校验的意义就是把这类问题从「运行时随机爆炸」变成「启动时明确告诉你少了 PORT」。