开发流程
package.json
{
...
"scripts": {
...
"cz": "git cz",
"lint": "eslint --fix src/",
"lint:style": "stylelint --fix 'src/**/*.less'",
"test": "cross-env BABEL_ENV=test jest --colors --config .jest.js",
"pre-commit": "lint-staged",
"x": "npm version major -m 'Upgrade version to %s '",
"y": "npm version minor -m 'Upgrade version to %s '",
"z": "npm version patch -m 'Upgrade version to %s '",
"c3-v": "c3-version upgrade",
"simple": "c3-version changelog",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"preversion": "npm run changelog && git add CHANGELOG.md",
"postversion": "git push && git push --tags",
"es": "c3-tools run es",
"lib": "c3-tools run commonjs",
"dist:umd": "c3-tools run dist:umd",
"dist:cjs": "c3-tools run dist:cjs",
"dist:es": "c3-tools run dist:es",
"compile": "npm run es && npm run lib",
"dist": "npm run dist:umd && npm run dist:cjs && npm run dist:es",
"prepublish": "npm run compile && npm run dist",
"postpublish": "rm -rf es && rm -rf lib && rm -rf dist"
...
},
"devDependencies": {
"c3-version": "^0.1.0",
"c3-tools": "^1.5.0",
"commitizen": "^2.10.1",
"conventional-changelog": "^2.0.3",
"conventional-changelog-cli": "^2.0.5",
"husky": "^1.1.2",
"lint-staged": "^7.2.2",
"babel-preset-ytian": "^3.0.0",
"eslint-config-ytian": "^2.1.1",
"stylelint-config-ytian": "^1.0.0"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"lint-staged": {
"src/**/*.js": [
"eslint --fix",
"git add"
],
"src/**/*.less": [
"stylelint --fix",
"git add"
]
},
"babel": {
"presets": [
"ytian"
]
},
"eslintConfig": {
"extends": [
"eslint-config-ytian"
]
},
"eslintIgnore": [
"build"
],
"stylelint": {
"extends": [
"stylelint-config-ytian"
]
},
...
}
代码规范
使用eslint,对JavaScript的书写规范做一定的限制,可以在一些通过配置的基础上增加一些团队自己的限制项。
使用stylelint对样式文件做一些规范化的工作,也可以根据团队的需要做一些定制化。
使用.editorconfig来配置编辑器的规范,保证缩进和换行等的一致性。
SemVer
SemVer的中文名称是语义化版本控制规范。npm默认使用SemVer来进行模块的版本控制。一个发布到npm的包要严格遵守SemVer的版本规范,不然会发布失败。
版本格式
主版本号.次版本号.修订号,可以用x.y.z的写法来简单表示。
- 修订号:只有在做了向下兼容的修正时才可以递增,可以理解为bug fix版本
- 次版本号:只有在新增了可以向下兼容的新功能的时候,才可以递增,可以理解为feature版本。
- 主版本号:只有在新增了无法向下兼容的API的时候,才可以递增。
先行版本
当要进行大版本迭代的时候,或者增加一些核心的功能,但又不能保证新版本百分之百正常,这个时候就可以发布先行版本。SemVer规范中使用alpha、beta和rc来修饰先行版本。
- alpha:内部版本
- beta:公测版本
- rc:Release candiate,正式版本的候选版本
先行版本的版本号可以使用:1.0.0-alpha、1.0.0-beta.1、1.0.0- rc.1、1.0.0-0.3.7等。
版本号的优先级
进行版本号比较时,x、y、z依次比较
先行版本号的规则是rc > beta > alpha
1.0.0 < 2.0.0 < 2.1.0 < 2.1.1
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0- rc.1 < 1.0.0
更多内容可以看SemVer
husky & lint-staged
在项目中需要单测和对代码规范的校验,如果每次修改,都对项目的所有代码进行校验,会有性能和时间上的浪费;还有如果老项目没有接入单测和代码规范,那么如果对所有的代码都进行校验的话,会导致错误太多无法提交代码。现在项目中已经使用的方案是husky & lint-staged。
husky在安装的时候,会执行这个包的npm install这个script,对项目的Git钩子进行重写,我们就可以在git的钩子函数中做一些代码方面的校验工作。lint-staged这个库只会新加入暂存区的文件进行相关的操作,这样就可以优化触发操作的文件范围。
package.json
{
...
"scripts": {
"lint": "eslint --fix src/",
"lint:style": "stylelint --fix 'src/**/*.less'",
"test": "cross-env BABEL_ENV=test jest --colors --config .jest.js",
"pre-commit": "lint-staged"
},
"lint-staged": {
"ignore": [
"build/*",
"node_modules"
],
"linters": {
"src/*.js": [
"eslint --fix",
"git add"
],
"src/**/*.less": [
"stylelint --fix",
"git add"
],
"src/components/**/*.js": [
"jest --findRelatedTests --config .jest.js",
"git add"
],
"src/utils/*.js": [
"jest --findRelatedTests --config .jest.js",
"git add"
]
}
}
...
}
git commit & changelog
规范化commit信息,有助于将修改的问题进行分类,快速定位修复的问题,并提取出有用的提交信息来生成最终的changelog文件。
社区中比较好的方案是commitizen和conventional-changelog。
commitizen
commitizen
用来规范commit message,比较主流是的是使用AngularJS的规范来编写commit message。
全局安装commitizen
sudo npm install -g commitizen
然后在项目里执行下面的语句,让commitizen支持AngularJS的message规范。
commitizen init cz-conventional-changelog --save-dev --save-exact
执行以后,会在项目的devDependencies加入cz-conventional-changelog这个依赖,并在package.json中加入如下的配置项
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
完成上面的步骤以后,以后所有的git commit 命令都用git cz来替换。
AngularJS的提交风格如下
<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>
由Header、Body和Footer三个部分组成,其中Header是必须的,Body和Footer都可以省略。
type表示commit的类型,有如下七种类型:
- feat:新功能(feature)
- fix:修补bug
- docs:文档(documentation)
- style: 格式(不影响代码运行的变动)
- refactor:重构(即不是新增功能,也不是修改bug的代码变动)
- test:增加测试
- chore:构建过程或辅助工具的变动
scope表示这次commit的影响范围
subject是commit的简单描述,不能超过50个字符
body是对这次commit的具体描述,可以是多行的
footer只用于两种情况
- 不兼容变动,如果是上个版本不兼容的改动,用BREAKING CHANGE作为开头
- 关闭 Issue,例如 Closes #234
生成changelog
如果所有的提交记录都符合AngularJS的规范,那么可以使用命令来自动生成changelog文件。
必须安装conventional-changelog-cli的依赖
yarn add conventional-changelog-cli --dev
或者
npm install --save-dev conventional-changelog-cli
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
}
}
生成的文档只会收集type为feat、fix还有Breaking changes这三种类型的提交记录。
如果强制使用的话,可以validate-commit-msg来对commit message进行校验,如果格式不符合,就阻止提交。
如果不想使用规范化的提交,也可以直接收集所有的提交信息来生成changelog。
编译和打包
在项目开发的时候都是通过npm去安装第三方包到本地的node_modules里面,而且为了加快项目的构建速度,会忽略对node_modules里面模块的处理,所以这就需要我们在开发npm包的时候提前做好编译打包的工作。
一般来说,用于node环境的包,只要提供符合CMD规范的包即可,但是用于web的包,就需要提供更多的选项。
- lib:符合commonjs规范的文件,一般放在lib这个文件夹里面,入口是mian
- es:符合ES module对方的文件,一般放在es这个文件夹里面,入口是module
- dist:经过压缩的文件,一般是可以通过script标签直接引用的文件
babel VS TypeScript
Babel是JavaScript的一个编译器,用来将ES6的代码转换成ES5的代码,关于babel更多的介绍可以参考之前的文章babel从入门到放弃。
TypeScript是JavaScript的一个超集,支持JavaScript的多有语法和语义,对于一些新的语法也会有及时的跟进,并且在此之上提供了更多额外的特性,比如静态类型和风丰富的语法。TS的代码也可以通过编译转换成正常的JavaScript代码,所有现在也有一种思路是用JavaScript的语法去进行开发,但是用TS的编译器对代码进行转换。
webpack VS rollup
webpack是现在主流的打包工具,有着活跃和庞大的社区支持。rollup号称是下一代打包方案,很多实验性的功能都是它最先实现的,比如scope hoisting 和tree shaking。webpack由于自己实现了一套类似于node的module方案,所以在打包文件的大小上以及文件的可读性上都存在一定的问题,而且相比于webpack复杂的配置文件,rollup的配置相来说更简单。所以库文件的打包比较好的方案是rollup + babel。
持续迭代
在一般的迭代过程中,步骤可能是
- 修改完本地代码以后,提交这次的修改,运行git add . && git commit && git push
- 修改package.json中的version字段,实现版本号的自增
- 运行git add . && git commit && git push
- 给这个版本打一个tag,git tag
&& git push --tags - 发布到npm,运行npm publish
npm version
npm version用来自动更新npm包的version,对SemVer的版本规范有很好的支持。
npm version [
| major | minor | patch | premajor | preminor | prepatch | prerelease [--preid= ] | from-git] 例:初始版本为1.0.0
npm version prepatch
//预备补丁版本号 v1.0.1-0
npm version prerelease
//预发布版本号 v1.0.1-1
npm version patch
//补丁版本号 v1.0.2
npm version preminor
//预备次版本号 v1.1.0-0
npm version minor
//次版本号 v1.1.0
npm version premajor
//预备主版本号 v2.0.0-0
npm version major
//主版本号 v2.0.0
常用的是major
,minor
和patch
,分别对应规范中的x,y,z。
当仓库已经被git初始化了,那么运行npm version
修改完版本号以后,还会运行git add 、git commit和git tag的命令,其中commit的信息默认是自改完的版本号。如果想自定义commit的信息,可以提供 -m 或者 —message 的选项,如果有"%s"的符号,会被替换为版本号。
npm version patch -m "Upgrade to %s for reasons"
npm version还支持pre和post的钩子,可以利用这两个钩子函数做一些自动化的配置。
在preversion这个钩子中,生成changelog文件,并将新生成的文件推入到缓存区中。
在postversion这个钩子中,进行仓库和tag的推送。
简化操作,可以做如下配置
{
"scripts": {
"simple": "c3-version changelog", //生成简单的changelog文件
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"preversion": "npm run changelog && git add CHANGELOG.md",
"postversion": "git push && git push --tags",
"x": "npm version major -m 'Upgrade version to %s '",
"y": "npm version minor -m 'Upgrade version to %s '",
"z": "npm version patch -m 'Upgrade version to %s '"
},
}
npm version对预发版本的递增不太友好,我写了一个简单的包,用来替代npm version,实现版本的递增,依赖了node-semver这个库。
yarn add c3-version --dev
会在项目的package.json的scripts中自动注入一下配置
{
"scripts": {
"simple": "c3-version changelog",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"preversion": "npm run changelog && git add CHANGELOG.md",
"postversion": "git push && git push --tags",
"x": "npm version major -m 'Upgrade version to %s '",
"y": "npm version minor -m 'Upgrade version to %s '",
"z": "npm version patch -m 'Upgrade version to %s '",
"c3-v": "c3-version upgrade"
},
}
npm publish
和npm version一样,在执行npm publish这个命令的时候,npm会依次执行scripts中的prepublish、publish、postpublish的命令,如果有定义的话。
npm5的版本中,prepublish用来代替prepublishOnly这个钩子,只在publish之前进行调用,建议npm升级到5及以上的版本,保证钩子的一致性。
在包发布之前和之后,我们可以利用prepublish和postpublish这个两个钩子做一些相关的工作。
在开发中,我们都是使用ES6的语法来进行开发的,所以在发布的时候会涉及到代码的编译。一般的开源项目,比如redux、antd,都会提供最少三种的文件格式
1、经过压缩的dist文件,一般放在dist文件夹中,可以用script进行直接引用
2、符合commonjs规范的文件,一般放在lib文件夹中
3、符合ES6模块规范的文件,一般放在es文件夹中
4、符合umd通过规范的文件,在浏览器和node中都可以使用
所以具体的流程为:
- prepublish,对包进行打包编译
- publish,只发布编译后的文件
- postpublish,删除编译生成的文件
"scripts": {
"es": "c3-tools run es",
"lib": "c3-tools run commonjs",
"dist:umd": "c3-tools run dist:umd",
"dist:cjs": "c3-tools run dist:cjs",
"dist:es": "c3-tools run dist:es",
"compile": "npm run es && npm run lib",
"dist": "npm run dist:umd && npm run dist:cjs && npm run dist:es",
"prepublish": "npm run compile && npm run dist",
"postpublish": "rm -rf es && rm -rf lib && rm -rf dist",
}
npm tag
使用npm publish发布包的时候,会有一个--tag的选项,如果不提供的话,会默认设置为latest,并且在使用npm install某个包的时候,默认也会安装latest这个tag的包。
但是在进行包的迭代的时候,可能会需要发布不同的版本来做新功能的测试,这时候就需要结合SemVer和--tag来进行相应的处理。
npm publish --tag
使用上面的命令,可以发布对应的dist-tag。
如果我们想进行下一个大版本的迭代,并用next的dist-tag来表示
npm publist --tag next
如果用户想安装这个tag下的包,可以使用下面的命令
npm install package@next
可以通过dist-tag来查看某个包的dist-tag
npm dist-tag ls redux
latest: 4.0.0 next: 4.0.0-rc.1
当预发版本稳定以后,可以使用npm dist-tag add beta latest
把预发版本设置为稳定版本。
Others
files & .npmignore & .gitignore
有三个方法可以控制npm发布的包中包含哪些文件
package.json#files
files字段是一个数组,用来表示可以包含哪些文件,格式和.gitignore的写法一样
.npmignore
这个文件用来表示哪些文件将被忽略,格式和.gitignore的写法一样
.gitignore
也可以用来表示要忽略哪些文件
这三个的优先级是files > .npmignore > .gitignore
files包含的文件,就算出现在.npmignore和.gitignore,也不会被忽略。如果既没有files字段,也没有.npmignore文件,那么npm会读取.gitignore文件,忽略里面的文件。
main & module & sideEffect
package.json#mai
n 和 package.json#module
这两个字段是用来指定npm包的入口文件,但是两者有一定的不同。
npm在一开始的时候,是node的包管理平台,所有的包都是基于CommonJS 规范规范的,main这个字段是npm自带的,一般表示符合CommonJS规范的文件入口。
rollup实现了基于ES模块静态分析,对代码进行Tree Shaking,它通过识别package.json中的module字段,将它当成是符合ES模块规范的文件入口。webpack之后也进行跟进,也能识别module字段,并且在webpack的默认配置中,module的优先级要高于main,因此符合ES模块规范的代码能进行Tree Shaking,减少项目最终打包出来的代码。
因为一般的项目在配置babel的时候,为了提高构建速度,都会忽略node_modules里面的文件,所以module入口的文件最好是符合ESmodule规范的ES5的代码,webpack最终会把ESmodule转换为它自己的commonjs规范的代码。
package.json#sideEffect
这个字段是webpack4中新增的一个特性,用来表示npm包的代码是否具有副作用。ES6的代码在经过babel编译为ES5的代码后,就算是符合ES6的模块规范,也会出现UglifyJs无法Tree Shaking的问题。webpack4通过sideEffect这个字段,使UglifyJs强行进行Tree Shaking。
sideEffect可以设置为Boolean或者数组
- 当为false时,表明这个包是没有副作用的,可以进行按需引用
- 如果为数组时,数组的每一项表示的是有副作用的文件
在组件库开发的时候,如果有样式文件,需要把样式文件的路径放到sideEffect的数组中,因为UglifyJs只能识别js文件,如果不设置的话,最后打包的时候会把样式文件忽略掉。
{ "sideEffects": [ "components/**/*.less" ] }
npm link
在开发包的时候,会遇到调试问题,希望能够一边开发一边调试,不用频繁的去发布版本。
使用npm link
可以达到这个效果,它会在在全局的node_modules目录中生成一个符号链接,指向模块的本地目录。
假设你要开发一个包,叫tool,需要在本地的项目work-center中去使用
在命令行中进入tool的目录,运行npm link
这个命令,就会生成一个符号链接。
进入项目work-center的目录,运行npm link tool
,就可以使用这个包了。
tool的所有改动都会映射到安装的项目中,但是带来的问题就是一处改动多处影响。
在调试结束后,运行npm unlink tool
来删除符号链接。
oh-my-zsh
在Mac上使用oh-my-zsh可以提高命令行的开发效率,具体的配置可以参考这篇文章mac下oh-my-zsh的配置
安装了oh-my-zsh以后,可以简化git的命令行操作,提高键盘的寿命,常用命令如下
zsh-git快捷键
gst - git status
gl - git pull
gp - git push
ga - git add
gcmsg - git commit -m
gco - git checkout
gcm - git checkout master
monorepo & lerna
monorepo 是单代码仓库,与之对应的是multirepo,多代码仓库。
monorepo是把所有的module都放在一个代码仓库中,进行统一管理;multirepo是把module拆分开来,单独去管理。multirepo的问题是issue、changelog和版本号不好管理;monorepo的问题是单个仓库代码量比较大。现在一些主流的开发项目,比如babel、react、vue、vue-cli,都是使用monorepo的方式来管理代码仓库的;rollup和antd是使用multirepo。
lerna是babel官方开源的monorepo管理工具。