阅读 660

8102年底如何开发和维护一个npm项目

开发流程

初始化

首先在npm官网进行注册登录

运行npm init,可以通过命令行进行一些初始化的设置,如果想快速进行设置,可以运行npm init -y,会在项目的根目录生成一个package.json的文件,具体包含哪些配置可以参考官方文档,下面介绍一些常用的配置。

  • name:npm包的名称

  • version:包的版本号

  • description:对包的功能进行描述

  • main:包的入口文件,默认是index.js

  • repository:代码的托管信息,一般是github地址

  • keywords:关键字信息,便于包的搜索

  • author:作者

  • license:开源协议,一般是MIT和ISC

  • bugs:提bug的页面,默认是github的issue页面

  • homepage:项目的主页

最好增加README.md,用来对项目进行简单的说明,比如如何安装使用,以及一些api的介绍和例子。

在代码开发完成以后,在命令行进行npm登陆npm login

最后使用npm publish对包进行发布。

代码规范

使用eslint,对JavaScript的书写规范做一定的限制,可以在一些通过配置的基础上增加一些团队自己的限制项。

使用stylelint对样式文件做一些规范化的工作,也可以根据团队的需要做一些定制化。

使用.editorconfig来配置编辑器的规范,保证缩进和换行等的一致性。

SemVer

SemVer的中文名称是语义化版本控制规范。npm默认使用SemVer来进行模块的版本控制。一个发布到npm的包要严格遵守SemVer的版本规范,不然会发布失败。

版本格式

主版本号.次版本号.修订号,可以用x.y.z的写法来简单表示。

  • 修订号(patch):只有在做了向下兼容的修正时才可以递增,可以理解为bug fix版本

  • 次版本号(minor):只有在新增了可以向下兼容的新功能的时候,才可以递增,可以理解为feature版本。

  • 主版本号(major):只有在新增了无法向下兼容的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只用于两种情况

    1. 不兼容变动,如果是上个版本不兼容的改动,用BREAKING CHANGE作为开头

    2. 关闭 Issue,例如 Closes #234

生成changelog

如果所有的提交记录都符合AngularJS的规范,那么可以使用命令来自动生成changelog文件。

必须安装conventional-changelog-cli的依赖

npm install --save-dev conventional-changelog-cli

{
  "scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -w -r 0"
  }
}复制代码

生成的文档只会收集type为feat、fix还有Breaking changes这三种类型的提交记录。

如果强制使用的话,可以validate-commit-msg来对commit message进行校验,如果格式不符合,就阻止提交。

如果不想使用规范化的提交,也可以使用下面的方法,收集所有的提交信息来生changelog。

gitCommitMsg.js

const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');
const formatOptions = ['log', '--pretty=format:%ad  %cn  committed  %s  %h', '--date=format:%Y-%m-%d'];

const writeStream = fs.createWriteStream(path.join(process.cwd(), 'CHANGELOG.md'));

const child = execFile('git', formatOptions, {
    cwd: process.cwd(),
    maxBuffer: Infinity,
});

child.stdout
  .pipe(writeStream);复制代码

编译和打包

在项目开发的时候都是通过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。

持续迭代

在一般的迭代过程中,步骤可能是

  1. 修改完本地代码以后,提交这次的修改,运行git add . && git commit && git push

  2. 修改package.json中的version字段,实现版本号的自增

  3. 运行git add . && git commit && git push

  4. 给这个版本打一个tag,git tag <package.version> && git push --tags

  5. 发布到npm,运行npm publish

npm version

npm version用来自动更新npm包的version,对SemVer的版本规范有很好的支持。

npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | 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

常用的是majorminorpatch,分别对应规范中的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": "node gitCommitMsg.js", //生成简单的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 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": "tools run es",
    "lib": "tools run commonjs",
    "dist:umd": "tools run dist:umd",
    "dist:cjs": "tools run dist:cjs",
    "dist:es": "tools run dist:es",
    "dist:min": "tools run dist:min",
    "compile": "npm run es && npm run lib",
    "dist": "npm run dist:umd && npm run dist:cjs && npm run dist:es && npm run dist:min",
    "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 <tagname>

使用上面的命令,可以发布对应的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把预发版本设置为稳定版本。

最终的package#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",
    "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 '",
    "es": "tools run es",
    "lib": "tools run commonjs",
    "dist:umd": "tools run dist:umd",
    "dist:cjs": "tools run dist:cjs",
    "dist:es": "tools run dist:es",
    "dist:min": "tools run dist:min",
    "compile": "npm run es && npm run lib",
    "dist": "npm run dist:umd && npm run dist:cjs && npm run dist:es && npm run dist:min",
    "prepublish": "npm run compile && npm run dist"
    "postpublish": "rm -rf es && rm -rf lib && rm -rf dist",
}复制代码

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#main 和 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 register

npm全局安装后,它的register是 registry.npmjs.org ,如果你使用淘宝的镜像重写了register,那么可能会在登陆和发布的时候出错。

npm config list @cfe:registry = "mirrors.npm.private.caocaokeji.cn/repository/…

registry = "registry.npm.taobao.org/"

可以使用下面的命令进行登陆和发布

npm login --registry registry.npmjs.org

npm publish --registry registry.npmjs.org

或者在开发npm包的时候,将registry换成npm的官方地址,开发完以后再换回淘宝的镜像

npm config set set registry www.npmjs.com/

npm config set registry registry.npm.taobao.org


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管理工具。



关注下面的标签,发现更多相似文章
评论