1. 程式人生 > >8102年底如何開發和維護一個npm專案

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 [email protected]

可以通過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管理工具。