前端项目开发规范

3,710 阅读6分钟
原文链接: github.com

示例仓库

在讲 lerna workflow 前我们先粗话来谈下当今主流的项目代码管理方式

杂谈项目管理方式

multiRepos

multiRepos 它是一种管理 organisation 代码的方式,在这种方式下,独立功能会拆分成独立的 repo

这是最常见的项目管理方式

优点:

  • 功能拆分颗粒度较细,职责界线清晰,功能性模块复用度较高

缺点:

  • 由于历史原因或者拆分问题,一个 repo 内的依赖可能来源于多个 organisation
    • issue 提哪是个问题
    • 项目管理,工作协同比较糟糕
  • 维护成本较高
    • 任何的基层 repo 版本变更,将会引发一系列上层封装版本变动
  • changelog 梳理异常折腾
    • 基本靠口口相传

monoRepo

Monorepo 它是一种管理 organisation 代码的方式,在这种方式下会摒弃原先一个独立功能一个 repo 的方式,取而代之的是把所有的 modules 都放在一个 repo 内来管理,而 lerna 是基于此种理念在工具端集合 git 和 npm 的实现。

优点:

  • 功能依旧可以拆分的细粒度
  • one repo multiple packages 让项目管理比较方便,issue 管理,代码变更都能比较好清晰的体现给协同开发者
  • child package 版本变更会自动化同步至相关的 package

缺点:

  • monoRepo 体积都比较大
  • 配套工具 lerna 有一定的使用成本,开发者比较容易用错,另外它有一些约定俗成,不能妥协的规范,以及限制
    • 对 packages 内的依赖版本管理只能 ^
    • 不支持多个 registry 的推送
    • 等等
  • 配套的 changelog 方案只适配于 github 详见我的另外一篇文章 - introduce lerna,如果是社区项目非常推荐走这一套方案
  • 版本的生成还是存在一定的缺陷,开发者并不知情 break 等信息

总结

项目开发中使用 multiRepos 和 monoRepo 都可以,问题在于项目合不合适。

个人角度上:

合适的项目需要有以下特征

  • 存在多个 package
  • package 与 package 之间相互依赖

符合以上条件我个人比较建议采用 monoRepo,以及与之带来的 lerna workflow。

当前使用 lerna确实还会有些小问题,这也是我们需要解决的点。

lerna workflow

先再次简单的介绍下 lerna

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

lerna 模式

在初始化一个项目之前我们必须要清楚,lerna 对管理 monoRepo 有两种模式

  • Fixed/Locked mode (default)
  • Independent mode

Fixed/Locked 模式: 官方默认推荐模式,当前 babel 的项目管理模式,在该模式下所有的 packages 都会遵循一个版本号,该版本号维护在 lerna.json 的 version 字段中,当需要版本发布时 lerna publish 时,如果一个模块和上一次 release 相比有过变更的话,会自动发布一个新版本。

这种模式的问题在于:当有一个 major 变更的时候,所有 packages 都会都会有一个新的 major 版本。

维护团队认为:版本是一种非常 cheap 的东西,所以不必纠结。

Independent 模式: 在该模式下所有 packages 新版本的生成将会由开发者决定,lerna.json 的 version 字段也会随之失效。这种模式的弊端非常明显,开发者必须要非常清晰该发什么版本,事实上在多人协作项目上很难做到这一点。

简单命令速记

init

$ lerna init

初始化一个 lerna 项目

add

$ lerna add <package>[@version] [--dev]

默认给当前所有的 packages 添加一个依赖

这边需要推荐一个比较有用的命令

$ lerna add module-1 --scope=module-2 # Install module-1 to module-2
$ lerna add babel-core # Install babel-core in all modules

这种方式是可以快速建立 packages 的依赖关系,而不用人为手动建立

bootstrap

$ lerna bootstrap

这个命令会安装好所有 packages 的依赖,以及建立好 packages 相互依赖的软连接

正式流程为:

  1. 安装所有 package 的外部依赖.
  2. 对存在相互依赖的 package 创建软连接.
  3. 在所有已经 bootstrapped 的 package 中执行 npm run prepublish.
  4. 在所有已经 bootstrapped 的 package 中执行 npm run prepare.

publish

$ lerna publish

发布一个版本。

正式流程为:

  1. 执行 lerna updated 来确定哪些包需要被发布.
  2. 如有必要会升级 lerna.json 的 version 字段。
  3. 对所有需要 update 的 package 进行版本的更新,并写入他们的 package.json.
  4. 队友有需要 update 的 package 进行依赖申明 specified with a caret (^).
  5. 创建一个 git commit 和 tag
  6. 把包发布至 npm

较为有用的附加参数

--npm-tag

$ lerna publish --npm-tag=beta

使用传入的 tag 把包发布至 npm 对应的 dist-tag

--conventional-commits

$ lerna publish --conventional-commits

遵从 Conventional Commits Specification 进行版本生成和 changlog 生成。

--skip-git

$ lerna publish --skip-npm

跳过 git 打标

--skip-npm

$ lerna publish --skip-npm

跳过 npm 发布

--cd-version

$ lerna publish --cd-version (major | minor | patch | premajor | preminor | prepatch | prerelease)
# uses the next semantic version(s) value and this skips `Select a new version for...` prompt

指定发包的时的语义版本

clean

$ lerna clean

移除所有 package 下的 node_modules 目录.

import

$ lerna import <path-to-external-repository>

从现有仓库导入一个 package,这种方式下会保留原有的 commit 的信息

run

$ lerna run <script> -- [..args] # runs npm run my-script in all packages that have it
$ lerna run test
$ lerna run build

# watch all packages and transpile on change, streaming prefixed output
$ lerna run --parallel watch

执行 package 下 npm script

exec

$ lerna exec -- <command> [..args] # runs the command in all packages
$ lerna exec -- rm -rf ./node_modules

在任何 package 下执行任意的命令

getting started

step 1:

$ npm install --global lerna

step 2:

$ mkdir lerna-example
$ cd lerna-example

step 3:

$ lerna init

运行完后在 terminal 中执行 tree 后我们可以看到此时的目录结构为

➜  lerna-example git:(master) ✗ tree
.
├── lerna.json
├── package.json
└── packages

step 4:

$  packages git:(master) ✗ mkdir module-a && cd module-a && touch index.js && tnpm init
$  packages git:(master) ✗ mkdir module-b && cd module-b && touch index.js && tnpm init
$  packages git:(master) ✗ mkdir module-base && cd module-base && touch index.js && tnpm init

运行完后在 terminal 中执行 tree 后我们可以看到此时的目录结构为

➜  lerna-example git:(master) ✗ tree
.
├── lerna.json
├── package.json
└── packages
    ├── module-a
    │   ├── index.js
    │   └── package.json
    ├── module-b
    │   ├── index.js
    │   └── package.json
    └── module-base
        ├── index.js
        └── package.json

step 5:

如果已知 module-base 被 module-a 和 module-b 共同依赖,同时 module-a 又 依赖 module-b

➜  lerna-example git:(master) ✗ lerna add @alipay/module-base
➜  lerna-example git:(master) ✗ lerna add @alipay/module-b --scope=@alipay/module-a

项目中使用的问题

在协同开发时,假设如果开发人员在 module-base 上发布了一个并不兼容的提交,此时做为 pm 的同学很难在没有提前沟通的情况下获知此次变更,所以在选择版本发布时也很容易出现,因为 lerna 默认对依赖的描述是 ^,所以这在信息不对称的情况下很容易造成线上故障。

如何破局呢?

  • github 用户使用 introduce lerna 文中提及的 lerna-changelog 来依据 changelog 来管理,这个方案的缺点是,版本号生成时并不是完全自动化的,还是需要人工介入。
  • 非 github 用户或使用 commitizen 用户,可以借由 --conventional-commits,来自动化生成版本以及 changelog

关于 commitzen 相关的可以看我另外一篇文章 用工具思路来规范化 git commit message

第二种方案也是目前我们项目中应用最多的。

应用 commitizen 方案后, package.json 变更为

{
  "private": true,
  "scripts": {
    "ct": "git-cz",
    "changelog": "./tasks/changelog.js",
    "publish": "./tasks/publish.js"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-lerna-changelog"
    }
  },
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -e $GIT_PARAMS",
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.js": [
      "prettier --trailing-comma es5 --single-quote --write",
      "git add"
    ]
  },
  "devDependencies": {
    "@alipay/config-conventional-volans": "^0.1.0",
    "@commitlint/cli": "^6.1.3",
    "commitizen": "^2.9.6",
    "cz-lerna-changelog": "^1.2.1",
    "husky": "v1.0.0-rc.4",
    "lerna": "^2.10.2",
    "lint-staged": "^7.0.4",
    "prettier": "^1.11.1"
  },
  "dependencies": {
    "fs-extra": "^6.0.0",
    "inquirer": "^5.2.0",
    "shelljs": "^0.8.1"
  }
}

commitizen 应用后仓库结构说明

packages 目录下存放的是所有的子仓库
tasks 目录下存放一些全局的任务脚本,当前有用的是 publish.jschangelog.js

  • changelog.js,当有发布任务时,请事先执行 npm run changelog,此举意为生成本次版本发布的 changelog,执行脚本时会提醒,本次发布是正式版还是 beta,会予以生成不同版本信息供予发布
  • publish.js,当 changelog 生成并调整相关内容完毕后,执行 npm run publish,会对如上所有的子 packages 进行版本发布,执行脚本时会提醒,本次发布是正式版还是 beta,会予以不同 npm dist-tag 进行发布

日常开发流程

在常规开发中,我们的操作方式会变更为如下:

第一步:使用 commitizen 替代 git commit

即当我们需要 commit 时,请使用如下命令

$ npm run ct

如果你在全局安装过 commitizen 那么,直接在项目目录下执行

$ git ct

执行时,会有引导式的方式让你书写 commit 的 message 信息

如果你是 sourceTree 用户,其实也不用担心,你完全可以可视化操作完后,再在命令行里面执行 npm run ct 命令,这一部分确实破坏了整体的体验,当前并没有找到更好的方式来解决。

关于为什么需要 commitizen,可以参考 这篇文章

当前我们遵循的是 angular 的 commit 规范。

具体格式为:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

type: 本次 commit 的类型,诸如 bugfix docs style 等
scope: 本次 commit 波及的范围
subject: 简明扼要的阐述下本次 commit 的主旨,在原文中特意强调了几点 1. 使用祈使句,是不是很熟悉又陌生的一个词,来传送门在此 祈使句 2. 首字母不要大写 3. 结尾无需添加标点
body: 同样使用祈使句,在主体内容中我们需要把本次 commit 详细的描述一下,比如此次变更的动机,如需换行,则使用 |
footer: 描述下与之关联的 issue 或 break change,详见案例

第二步:格式化代码

这一步,并不需要人为干预,因为 precommit 中的 lint-staged 会自动化格式,以保证代码风格尽量一致

第三步:commit message 校验

这一步,同样也不需要人为介入,因为 commitmsg 中的 commitlint 会自动校验 msg 的规范

第四步:当有发布需求时,先生成 changelog

使用

$ npm run changelog

在这一步中我们借助了 commitizen 标准化的 commit-msg 以及 lernapublish--conventional-commits 来自动化生成了版本号以及 changelog,但过程中我们忽略了 git tag 以及 npm publish ( --skip-git --skip-npm),原因是我们需要一个时机去修改自动化生成的 changelog。

第五步:再发布

由于第四步中,我们并没有实质意义上做版本发布,而是借以 lerna 的 publish 功能,生成了 changelog,所以后续的 publish 操作被实现在了自定义脚本中,即 publish.js 中。

$ npm run publish

第六步:打 tag

给当前分支打好对应的 git tag 信息,推送到开发分支