2018 年了,你仍是隻會 npm install 嗎?

本文同步發表於做者博客: 2018 年了,你仍是隻會 npm install 嗎?html

nodejs 社區乃至 Web 前端工程化領域發展到今天,做爲 node 自帶的包管理工具的 npm 已經成爲每一個前端開發者必備的工具。可是現實情況是,咱們不少人對這個nodejs基礎設施的使用和了解還停留在: 會用 npm install 這裏(一言不合就刪除整個 node_modules 目錄而後從新 install 這種事你沒作過嗎?)前端

固然 npm 能成爲如今世界上最大規模的包管理系統,很大程度上確實歸功於它足夠用戶友好,你看即便我只會執行 install 也沒必要太擔憂出什麼大岔子. 可是 npm 的功能遠不止於 install 一下那麼簡單,這篇文章幫你扒一扒那些你可能不知道的 npm 原理、特性、技巧,以及(我認爲的)最佳實踐。node

你懶得讀的 npm 文檔,我幫你翻譯而後試驗整理過來了🐶🐶🐶react

1. npm init

咱們都知道 package.json 文件是用來定義一個 package 的描述文件, 也知道npm init 命令用來初始化一個簡單的 package.json 文件,執行該命令後終端會依次詢問 name, version, description 等字段。linux

1.1 npm init 執行默認行爲

 而若是想要偷懶步免去一直按 enter,在命令後追加 --yes 參數便可,其做用與一路下一步相同。webpack

npm init --yesgit

1.2 自定義 npm init 行爲

npm init 命令的原理並不複雜,調用腳本,輸出一個初始化的 package.json 文件就是了。因此相應地,定製 npm init 命令的實現方式也很簡單,在 Home 目錄建立一個 .npm-init.js 便可,該文件的 module.exports 即爲 package.json 配置內容,須要獲取用戶輸入時候,使用 prompt() 方法便可。程序員

例如編寫這樣的 ~/.npm-init.jsgithub

const desc = prompt('description?', 'A new package...')
const bar = prompt('bar?', '')
const count = prompt('count?', '42')

module.exports = {
  key: 'value',
  foo: {
    bar: bar,
    count: count
  },
  name: prompt('name?', process.cwd().split('/').pop()),
  version: prompt('version?', '0.1.0'),
  description: desc,
  main: 'index.js',
}
複製代碼

此時在 ~/hello 目錄下執行 npm init 將會獲得這樣的 package.json:web

{
  "key": "value",
  "foo": {
    "bar": "",
    "count": "42"
  },
  "name": "hello",
  "version": "0.1.0",
  "description": "A new package...",
  "main": "index.js"
}
複製代碼

除了生成 package.json, 由於 .npm-init.js 是一個常規的模塊,意味着咱們能夠執行隨便什麼 node 腳本能夠執行的任務。例如經過 fs 建立 README, .eslintrc 等項目必需文件,實現項目腳手架的做用。

2. 依賴包安裝

依賴管理是 npm 的核心功能,原理就是執行 npm install 從 package.json 中的 dependencies, devDependencies 將依賴包安裝到當前目錄的 ./node_modules 文件夾中。

2.1 package定義

咱們都知道要手動安裝一個包時,執行 npm install <package> 命令便可。這裏的第三個參數 package 一般就是咱們所要安裝的包名,默認配置下 npm 會從默認的源 (Registry) 中查找該包名對應的包地址,並下載安裝。但在 npm 的世界裏,除了簡單的指定包名, package 還能夠是一個指向有效包名的 http url/git url/文件夾路徑。

閱讀 npm的文檔, 咱們會發現package 準確的定義,只要符合如下 a) 到 g) 其中之一條件,就是一個 package:

# 說明 例子
a) 一個包含了程序和描述該程序的 package.json 文件 的 文件夾 ./local-module/
b) 一個包含了 (a) 的 gzip 壓縮文件 ./module.tar.gz
c) 一個能夠下載獲得 (b) 資源的 url (一般是 http(s) url) https://registry.npmjs.org/webpack/-/webpack-4.1.0.tgz
d) 一個格式爲 <name>@<version> 的字符串,可指向 npm 源(一般是官方源 npmjs.org)上已發佈的可訪問 url,且該 url 知足條件 (c) webpack@4.1.0
e) 一個格式爲 <name>@<tag> 的字符串,在 npm 源上該<tag>指向某 <version> 獲得 <name>@<version>,後者知足條件 (d) webpack@latest
f) 一個格式爲 <name> 的字符串,默認添加 latest 標籤所獲得的 <name>@latest 知足條件 (e) webpack
g) 一個 git url, 該 url 所指向的代碼庫知足條件 (a) git@github.com:webpack/webpack.git

2.2 安裝本地包/遠程git倉庫包

上面表格的定義意味着,咱們在共享依賴包時,並非非要將包發表到 npm 源上才能夠提供給使用者來安裝。這對於私有的不方便 publish 到遠程源(即便是私有源),或者須要對某官方源進行改造,但依然須要把包共享出去的場景來講很是實用。

場景1: 本地模塊引用

nodejs 應用開發中不可避免有模塊間調用,例如在實踐中常常會把須要被頻繁引用的配置模塊放到應用根目錄;因而在建立了不少層級的目錄、文件後,極可能會遇到這樣的代碼:

const config = require('../../../../config.js');
複製代碼

除了看上去很醜之外,這樣的路徑引用也不利於代碼的重構。而且身爲程序員的自我修養告訴咱們,這樣重複的代碼多了也就意味着是時候把這個模塊分離出來供應用內其餘模塊共享了。例如這個例子裏的 config.js 很是適合封裝爲 package 放到 node_modules 目錄下,共享給同應用內其餘模塊。

無需手動拷貝文件或者建立軟連接到 node_modules 目錄,npm 有更優雅的解決方案。

方案:

  1. 建立 config 包:
    新增 config 文件夾; 重命名 config.js 爲 config/index.js 文件; 建立 package.json 定義 config 包

    {
        "name": "config",
        "main": "index.js",
        "version": "0.1.0"
    }
    複製代碼
  2. 在應用層 package.json 文件中新增依賴項,而後執行 npm install; 或直接執行第 3 步

    {
        "dependencies": {
            "config": "file:./config"
        }
    }
    複製代碼
  3. (等價於第 2 步)直接在應用目錄執行 npm install file:./config

    此時,查看 node_modules 目錄咱們會發現多出來一個名爲 config,指向上層 config/ 文件夾的軟連接。這是由於 npm 識別 file: 協議的url,得知這個包須要直接從文件系統中獲取,會自動建立軟連接到 node_modules 中,完成「安裝」過程。

    相比手動軟鏈,咱們既不須要關心 windows 和 linux 命令差別,又能夠顯式地將依賴信息固化到 dependencies 字段中,開發團隊其餘成員能夠執行 npm install 後直接使用。

場景2: 私有 git 共享 package

有些時候,咱們一個團隊內會有一些代碼/公用庫須要在團隊內不一樣項目間共享,但可能因爲包含了敏感內容,或者代碼太爛拿不出手等緣由,不方便發佈到源。

這種狀況下,咱們能夠簡單地將被依賴的包託管在私有的 git 倉庫中,而後將該 git url 保存到 dependencies 中. npm 會直接調用系統的 git 命令從 git 倉庫拉取包的內容到 node_modules 中。

npm 支持的 git url 格式:

<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]
複製代碼

git 路徑後可使用 # 指定特定的 git branch/commit/tag, 也能夠 #semver: 指定特定的 semver range.

例如:

git+ssh://git@github.com:npm/npm.git#v1.0.27
git+ssh://git@github.com:npm/npm#semver:^5.0
git+https://isaacs@github.com/npm/npm.git
git://github.com/npm/npm.git#v1.0.27
複製代碼

場景3: 開源 package 問題修復

使用某個 npm 包時發現它有某個嚴重bug,但也許最初做者已再也不維護代碼了,也許咱們工做緊急,沒有足夠的時間提 issue 給做者再慢慢等做者發佈新的修復版本到 npm 源。

此時咱們能夠手動進入 node_modules 目錄下修改相應的包內容,也許修改了一行代碼就修復了問題。可是這種作法很是不明智!

首先 node_modules 自己不該該放進版本控制系統,對 node_modules 文件夾中內容的修改不會被記錄進 git 提交記錄;其次,就算咱們非要反模式,把 node_modules 放進版本控制中,你的修改內容也很容易在下次 team 中某位成員執行 npm installnpm update 時被覆蓋,而這樣的一次提交極可能包含了幾十幾百個包的更新,你本身所作的修改很容易就被淹沒在龐大的 diff 文件列表中了。

方案:

最好的辦法應當是 fork 原做者的 git 庫,在本身所屬的 repo 下修復問題後,將 dependencies 中相應的依賴項更改成本身修復後版本的 git url 便可解決問題。(Fork 代碼庫後,也便於向原做者提交 PR 修復問題。上游代碼庫修復問題後,再次更新咱們的依賴配置也不遲。)

3. npm install 如何工做 —— node_modules 目錄結構

npm install 執行完畢後,咱們能夠在 node_modules 中看到全部依賴的包。雖然使用者無需關注這個目錄裏的文件夾結構細節,只管在業務代碼中引用依賴包便可,但瞭解 node_modules 的內容能夠幫咱們更好理解 npm 如何工做,瞭解從 npm 2 到 npm 5 有哪些變化和改進。

爲簡單起見,咱們假設應用目錄爲 app, 用兩個流行的包 webpack, nconf 做爲依賴包作示例說明。而且爲了正常安裝,使用了「上古」 npm 2 時期的版本 webpack@1.15.0, nconf@0.8.5.

3.1 npm 2

npm 2 在安裝依賴包時,採用簡單的遞歸安裝方法。執行 npm install 後,npm 2 依次遞歸安裝 webpacknconf 兩個包到 node_modules 中。執行完畢後,咱們會看到 ./node_modules 這層目錄只含有這兩個子目錄。

node_modules/
├── nconf/
└── webpack/
複製代碼

進入更深一層 nconf 或 webpack 目錄,將看到這兩個包各自的 node_modules 中,已經由 npm 遞歸地安裝好自身的依賴包。包括 ./node_modules/webpack/node_modules/webpack-core , ./node_modules/conf/node_modules/async 等等。而每個包都有本身的依賴包,每一個包本身的依賴都安裝在了本身的 node_modules 中。依賴關係層層遞進,構成了一整個依賴樹,這個依賴樹與文件系統中的文件結構樹恰好層層對應。

最方便的查看依賴樹的方式是直接在 app 目錄下執行 npm ls 命令。

app@0.1.0
├─┬ nconf@0.8.5
│ ├── async@1.5.2
│ ├── ini@1.3.5
│ ├── secure-keys@1.0.0
│ └── yargs@3.32.0
└─┬ webpack@1.15.0
  ├── acorn@3.3.0
  ├── async@1.5.2
  ├── clone@1.0.3
  ├── ...
  ├── optimist@0.6.1
  ├── supports-color@3.2.3
  ├── tapable@0.1.10
  ├── uglify-js@2.7.5
  ├── watchpack@0.2.9
  └─┬ webpack-core@0.6.9
    ├── source-list-map@0.1.8
    └── source-map@0.4.4
複製代碼

這樣的目錄結構優勢在於層級結構明顯,便於進行傻瓜式的管理:

  1. 例如新裝一個依賴包,能夠當即在第一層 node_modules 中看到子目錄
  2. 在已知所需包名和版本號時,甚至能夠從別的文件夾手動拷貝須要的包到 node_modules 文件夾中,再手動修改 package.json 中的依賴配置
  3. 要刪除這個包,也能夠簡單地手動刪除這個包的子目錄,並刪除 package.json 文件中相應的一行便可

實際上,不少人在 npm 2 時代也的確都這麼實踐過,的確也均可以安裝和刪除成功,並不會致使什麼差錯。

但這樣的文件結構也有很明顯的問題:

  1. 對複雜的工程, node_modules 內目錄結構可能會太深,致使深層的文件路徑過長而觸發 windows 文件系統中,文件路徑不能超過 260 個字符長的錯誤
  2. 部分被多個包所依賴的包,極可能在應用 node_modules 目錄中的不少地方被重複安裝。隨着工程規模愈來愈大,依賴樹愈來愈複雜,這樣的包狀況會愈來愈多,形成大量的冗餘。

——在咱們的示例中就有這個問題,webpacknconf 都依賴 async 這個包,因此在文件系統中,webpack 和 nconf 的 node_modules 子目錄中都安裝了相同的 async 包,而且是相同的版本。

+-------------------------------------------+
|                   app/                    |
+----------+------------------------+-------+
           |                        |
           |                        |
+----------v------+       +---------v-------+
|                 |       |                 |
|  webpack@1.15.0 |       |  nconf@0.8.5    |
|                 |       |                 |
+--------+--------+       +--------+--------+
         |                         |
   +-----v-----+             +-----v-----+
   |async@1.5.2|             |async@1.5.2|
   +-----------+             +-----------+
複製代碼

3.2 npm 3 - 扁平結構

主要爲了解決以上問題,npm 3 的 node_modules 目錄改爲了更加扁平狀的層級結構。文件系統中 webpack, nconf, async 的層級關係變成了平級關係,處於同一級目錄中。

+-------------------------------------------+
         |                   app/                    |
         +-+---------------------------------------+-+
           |                                       |
           |                                       |
+----------v------+    +-------------+   +---------v-------+
|                 |    |             |   |                 |
|  webpack@1.15.0 |    | async@1.5.2 |   |  nconf@0.8.5    |
|                 |    |             |   |                 |
+-----------------+    +-------------+   +-----------------+
複製代碼

雖然這樣一來 webpack/node_modules 和 nconf/node_modules 中都再也不有 async 文件夾,但得益於 node 的模塊加載機制,他們均可以在上一級 node_modules 目錄中找到 async 庫。因此 webpack 和 nconf 的庫代碼中 require('async') 語句的執行都不會有任何問題。

這只是最簡單的例子,實際的工程項目中,依賴樹不可避免地會有不少層級,不少依賴包,其中會有不少同名但版本不一樣的包存在於不一樣的依賴層級,對這些複雜的狀況, npm 3 都會在安裝時遍歷整個依賴樹,計算出最合理的文件夾安裝方式,使得全部被重複依賴的包均可以去重安裝。

npm 文檔提供了更直觀的例子解釋這種狀況:

假如 package{dep} 寫法表明包和包的依賴,那麼 A{B,C}, B{C}, C{D} 的依賴結構在安裝以後的 node_modules 是這樣的結構:

A
+-- B
+-- C
+-- D
複製代碼

這裏之因此 D 也安裝到了與 B C 同一級目錄,是由於 npm 會默認會在無衝突的前提下,儘量將包安裝到較高的層級。

若是是 A{B,C}, B{C,D@1}, C{D@2} 的依賴關係,獲得的安裝後結構是:

A
+-- B
+-- C
   `-- D@2
+-- D@1
複製代碼

這裏是由於,對於 npm 來講同名但不一樣版本的包是兩個獨立的包,而同層不能有兩個同名子目錄,因此其中的 D@2 放到了 C 的子目錄而另外一個 D@1 被放到了再上一層目錄。

很明顯在 npm 3 以後 npm 的依賴樹結構再也不與文件夾層級一一對應了。想要查看 app 的直接依賴項,要經過 npm ls 命令指定 --depth 參數來查看:

npm ls --depth 1
複製代碼

PS: 與本地依賴包不一樣,若是咱們經過 npm install --global 全局安裝包到全局目錄時,獲得的目錄依然是「傳統的」目錄結構。而若是使用 npm 3 想要獲得「傳統」形式的本地 node_modules 目錄,使用 npm install --global-style 命令便可。

3.3 npm 5 - package-lock 文件

npm 5 發佈於 2017 年也是目前最新的 npm 版本,這一版本依然沿用 npm 3 以後扁平化的依賴包安裝方式,此外最大的變化是增長了 package-lock.json 文件。

package-lock.json 的做用是鎖定依賴安裝結構,若是查看這個 json 的結構,會發現與 node_modules 目錄的文件層級結構是一一對應的。

以依賴關係爲: app{webpack} 的 'app' 項目爲例, 其 package-lock 文件包含了這樣的片斷。

{
    "name":  "app",
    "version":  "0.1.0",
    "lockfileVersion":  1,
    "requires":  true,
    "dependencies": {
        // ... 其餘依賴包
        "webpack": {
            "version": "1.8.11",
            "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.8.11.tgz",
            "integrity": "sha1-Yu0hnstBy/qcKuanu6laSYtgkcI=",
            "requires": {
                "async": "0.9.2",
                "clone": "0.1.19",
                "enhanced-resolve": "0.8.6",
                "esprima": "1.2.5",
                "interpret": "0.5.2",
                "memory-fs": "0.2.0",
                "mkdirp": "0.5.1",
                "node-libs-browser": "0.4.3",
                "optimist": "0.6.1",
                "supports-color": "1.3.1",
                "tapable": "0.1.10",
                "uglify-js": "2.4.24",
                "watchpack": "0.2.9",
                "webpack-core": "0.6.9"
            }
        },
        "webpack-core": {
            "version": "0.6.9",
            "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
            "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
            "requires": {
                "source-list-map": "0.1.8",
                "source-map": "0.4.4"
            },
            "dependencies": {
                "source-map": {
                    "version": "0.4.4",
                    "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
                    "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
                    "requires": {
                        "amdefine": "1.0.1"
                    }
                }
            }
        },
        //... 其餘依賴包
    }
}
複製代碼

看懂 package-lock 文件並不難,其結構是一樣類型的幾個字段嵌套起來的,主要是 version, resolved, integrity, requires, dependencies 這幾個字段而已。

  • version, resolved, integrity 用來記錄包的準確版本號、內容hash、安裝源的,決定了要安裝的包的準確「身份」信息
  • 假設蓋住其餘字段,只關注文件中的 dependencies: {} 咱們會發現,整個文件的 JSON 配置裏的 dependencies 層次結構與文件系統中 node_modules 的文件夾層次結構是徹底對照的
  • 只關注 requires: {} 字段又會發現,除最外層的 requires 屬性爲 true 之外, 其餘層的 requires 屬性都對應着這個包的 package.json 裏記錄的本身的依賴項

由於這個文件記錄了 node_modules 裏全部包的結構、層級和版本號甚至安裝源,它也就事實上提供了 「保存」 node_modules 狀態的能力。只要有這樣一個 lock 文件,無論在那一臺機器上執行 npm install 都會獲得徹底相同的 node_modules 結果。

這就是 package-lock 文件致力於優化的場景:在從前僅僅用 package.json 記錄依賴,因爲 semver range 的機制;一個月前由 A 生成的 package.json 文件,B 在一個月後根據它執行 npm install 所獲得的 node_modules 結果極可能許多包都存在不一樣的差別,雖然 semver 機制的限制使得同一份 package.json 不會獲得大版本不一樣的依賴包,但同一份代碼在不一樣環境安裝出不一樣的依賴包,依然是可能致使意外的潛在因素。

相同做用的文件在 npm 5 以前就有,稱爲 npm shrinkwrap 文件,兩者做用徹底相同,不一樣的是後者須要手動生成,而 npm 5 默認會在執行 npm install 後就生成 package-lock 文件,而且建議你提交到 git/svn 代碼庫中。

package-lock.json 文件在最初 npm 5.0 默認引入時也引發了至關大的爭議。在 npm 5.0 中,若是已有 package-lock 文件存在,若手動在 package.json 文件新增一條依賴,再執行 npm install, 新增的依賴並不會被安裝到 node_modules 中, package-lock.json 也不會作相應的更新。這樣的表現與使用者的天然指望表現不符。在 npm 5.1 的首個 Release 版本中這個問題得以修復。這個事情告訴咱們,要升級,不要使用 5.0。

——但依然有反對的聲音認爲 package-lock 太複雜,對此 npm 也提供了禁用配置:

npm config set package-lock false
複製代碼

4. 依賴包版本管理

依賴包安裝完並不意味着就萬事大吉了,版本的維護和更新也很重要。這一章介紹依賴包升級管理相關知識,太長不看版本請直接跳到 [4.3 最佳實踐].

4.1 semver

npm 依賴管理的一個重要特性是採用了語義化版本 (semver) 規範,做爲依賴版本管理方案。

semver 約定一個包的版本號必須包含3個數字,格式必須爲 MAJOR.MINOR.PATCH, 意爲 主版本號.小版本號.修訂版本號.

  • MAJOR 對應大的版本號迭代,作了不兼容舊版的修改時要更新 MAJOR 版本號
  • MINOR 對應小版本迭代,發生兼容舊版API的修改或功能更新時,更新MINOR版本號
  • PATCH 對應修訂版本號,通常針對修復 BUG 的版本號

對於包做者(發佈者),npm 要求在 publish 以前,必須更新版本號。npm 提供了 npm version 工具,執行 npm version major|minor|patch 能夠簡單地將版本號中相應的數字加1.

若是包是一個 git 倉庫,npm version 還會自動建立一條註釋爲更新後版本號的 git commit 和名爲該版本號的 tag

對於包的引用者來講,咱們須要在 dependencies 中使用 semver 約定的 semver range 指定所需依賴包的版本號或版本範圍。npm 提供了網站 https://semver.npmjs.com 可方便地計算所輸入的表達式的匹配範圍。經常使用的規則示例以下表:

range 含義
^2.2.1 指定的 MAJOR 版本號下, 全部更新的版本 匹配 2.2.3, 2.3.0; 不匹配 1.0.3, 3.0.1
~2.2.1 指定 MAJOR.MINOR 版本號下,全部更新的版本 匹配 2.2.3, 2.2.9 ; 不匹配 2.3.0, 2.4.5
>=2.1 版本號大於或等於 2.1.0 匹配 2.1.2, 3.1
<=2.2 版本號小於或等於 2.2 匹配 1.0.0, 2.2.1, 2.2.11
1.0.0 - 2.0.0 版本號從 1.0.0 (含) 到 2.0.0 (含) 匹配 1.0.0, 1.3.4, 2.0.0

任意兩條規則,用空格鏈接起來,表示「與」邏輯,即兩條規則的交集:

>=2.3.1 <=2.8.0 能夠解讀爲: >=2.3.1<=2.8.0

  • 能夠匹配 2.3.1, 2.4.5, 2.8.0
  • 但不匹配 1.0.0, 2.3.0, 2.8.1, 3.0.0

任意兩條規則,經過 || 鏈接起來,表示「或」邏輯,即兩條規則的並集:

^2 >=2.3.1 || ^3 >3.2

  • 能夠匹配 2.3.1, 2,8.1, 3.3.1
  • 但不匹配 1.0.0, 2.2.0, 3.1.0, 4.0.0

PS: 除了這幾種,還有以下更直觀的表示版本號範圍的寫法:

  • *x 匹配全部主版本
  • 11.x 匹配 主版本號爲 1 的全部版本
  • 1.21.2.x 匹配 版本號爲 1.2 開頭的全部版本

PPS: 在常規僅包含數字的版本號以外,semver 還容許在 MAJOR.MINOR.PATCH 後追加 - 後跟點號分隔的標籤,做爲預發佈版本標籤 - Prerelese Tags,一般被視爲不穩定、不建議生產使用的版本。例如:

  • 1.0.0-alpha
  • 1.0.0-beta.1
  • 1.0.0-rc.3

上表中咱們最多見的是 ^1.8.11 這種格式的 range, 由於咱們在使用 npm install <package name> 安裝包時,npm 默認安裝當前最新版本,例如 1.8.11, 而後在所安裝的版本號前加^號, 將 ^1.8.11 寫入 package.json 依賴配置,意味着能夠匹配 1.8.11 以上,2.0.0 如下的全部版本。

4.2 依賴版本升級

問題來了,在安裝完一個依賴包以後有新版本發佈了,如何使用 npm 進行版本升級呢?——答案是簡單的 npm installnpm update,但在不一樣的 npm 版本,不一樣的 package.json, package-lock.json 文件,安裝/升級的表現也不一樣。

咱們不妨還以 webpack 舉例,作以下的前提假設:

  • 咱們的工程項目 app 依賴 webpack
  • 項目最初初始化時,安裝了當時最新的包 webpack@1.8.0,而且 package.json 中的依賴配置爲: "webpack": "^1.8.0"
  • 當前(2018年3月) webpack 最新版本爲 4.2.0, webpack 1.x 最新子版本爲 1.15.0

若是咱們使用的是 npm 3, 而且項目不含 package-lock.json, 那麼根據 node_modules 是否爲空,執行 install/update 的結果以下 (node 6.13.1, npm 3.10.10 環境下試驗):

# package.json (BEFORE) node_modules (BEFORE) command (npm 3) package.json (AFTER) node_modules (AFTER)
a) webpack: ^1.8.0 webpack@1.8.0 install webpack: ^1.8.0 webpack@1.8.0
b) webpack: ^1.8.0 install webpack: ^1.8.0 webpack@1.15.0
c) webpack: ^1.8.0 webpack@1.8.0 update webpack: ^1.8.0 webpack@1.15.0
d) webpack: ^1.8.0 update webpack: ^1.8.0 webpack@1.15.0

根據這個表咱們能夠對 npm 3 得出如下結論:

  • 若是本地 node_modules 已安裝,再次執行 install 不會更新包版本, 執行 update 纔會更新; 而若是本地 node_modules 爲空時,執行 install/update 都會直接安裝更新包;
  • npm update 老是會把包更新到符合 package.json 中指定的 semver 的最新版本號——本例中符合 ^1.8.0 的最新版本爲 1.15.0
  • 一旦給定 package.json, 不管後面執行 npm install 仍是 update, package.json 中的 webpack 版本一直頑固地保持 一開始的 ^1.8.0 巋然不動

這裏不合理的地方在於,若是最開始團隊中第一我的安裝了 webpack@1.8.0, 而新加入項目的成員, checkout 工程代碼後執行 npm install 會安裝獲得不太同樣的 1.15.0 版本。雖然 semver 約定了小版本號應當保持向下兼容(相同大版本號下的小版本號)兼容,但萬一有不熟悉不遵循此約定的包發佈者,發佈了不兼容的包,此時就可能出現因依賴環境不一樣致使的 bug。

下面由 npm 5 帶着 package-lock.json 閃亮登場,執行 install/update 的效果是這樣的 (node 9.8.0, npm 5.7.1 環境下試驗):

下表爲表述簡單,省略了包名 webpack, install 簡寫 i, update 簡寫爲 up

# package.json (BEFORE) node_modules (BEFORE) package-lock (BEFORE) command package.json (AFTER) node_modules (AFTER)
a) ^1.8.0 @1.8.0 @1.8.0 i ^1.8.0 @1.8.0
b) ^1.8.0 @1.8.0 i ^1.8.0 @1.8.0
c) ^1.8.0 @1.8.0 @1.8.0 up ^1.15.0 @1.15.0
d) ^1.8.0 @1.8.0 up ^1.8.0 @1.15.0
e) ^1.15.0 @1.8.0 (舊) @1.15.0 i ^1.15.0 @1.15.0
f) ^1.15.0 @1.8.0 (舊) @1.15.0 up ^1.15.0 @1.15.0

與 npm 3 相比,在安裝和更新依賴版本上主要的區別爲:

  • 不管什麼時候執行 install, npm 都會優先按照 package-lock 中指定的版原本安裝 webpack; 避免了 npm 3 表中情形 b) 的情況;
  • 不管什麼時候完成安裝/更新, package-lock 文件總會跟着 node_modules 更新 —— (所以能夠視 package-lock 文件爲 node_modules 的 JSON 表述)
  • 已安裝 node_modules 後若執行 npm update,package.json 中的版本號也會隨之更改成 ^1.15.0

因而可知 npm 5.1 使得 package.json 和 package-lock.json 中所保存的版本號更加統一,解決了 npm 以前的各類問題。只要遵循好的實踐習慣,團隊成員能夠很方便地維護一套應用代碼和 node_modules 依賴都一致的環境。

皆大歡喜。

4.3 最佳實踐

總結起來,在 2018 年 (node 9.8.0, npm 5.7.1) 時代,我認爲的依賴版本管理應當是:

  • 使用 npm: >=5.1 版本, 保持 package-lock.json 文件默認開啓配置

  • 初始化:第一做者初始化項目時使用 npm install <package> 安裝依賴包, 默認保存 ^X.Y.Z 依賴 range 到 package.json中; 提交 package.json, package-lock.json, 不要提交 node_modules 目錄

  • 初始化:項目成員首次 checkout/clone 項目代碼後,執行一次 npm install 安裝依賴包

  • 不要手動修改 package-lock.json

  • 升級依賴包:

    • 升級小版本: 本地執行 npm update 升級到新的小版本
    • 升級大版本: 本地執行 npm install <package-name>@<version> 升級到新的大版本
    • 也可手動修改 package.json 中版本號爲要升級的版本(大於現有版本號)並指定所需的 semver, 而後執行 npm install
    • 本地驗證升級後新版本無問題後,提交新的 package.json, package-lock.json 文件
  • 降級依賴包:

    • 正確: npm install <package-name>@<old-version> 驗證無問題後,提交 package.json 和 package-lock.json 文件
    • 錯誤: 手動修改 package.json 中的版本號爲更低版本的 semver, 這樣修改並不會生效,由於再次執行 npm install 依然會安裝 package-lock.json 中的鎖定版本
  • 刪除依賴包:

    • Plan A: npm uninstall <package> 並提交 package.jsonpackage-lock.json
    • Plan B: 把要卸載的包從 package.json 中 dependencies 字段刪除, 而後執行 npm install 並提交 package.jsonpackage-lock.json
  • 任什麼時候候有人提交了 package.json, package-lock.json 更新後,團隊其餘成員應在 svn update/git pull 拉取更新後執行 npm install 腳本安裝更新後的依賴包

恭喜你終於能夠跟 rm -rf node_modules && npm install 這波操做說拜拜了(其實並不會)

5. npm scripts

5.1 基本使用

npm scripts 是 npm 另外一個很重要的特性。經過在 package.json 中 scripts 字段定義一個腳本,例如:

{
    "scripts": {
        "echo": "echo HELLO WORLD"
    }
}
複製代碼

咱們就能夠經過 npm run echo 命令來執行這段腳本,像在 shell 中執行該命令 echo HELLO WORLD 同樣,看到終端輸出 HELLO WORLD.

—— npm scripts 的基本使用就是這麼簡單,它提供了一個簡單的接口用來調用工程相關的腳本。關於更詳細的相關信息,能夠參考阮一峯老師的文章 npm script 使用指南 (2016年10月).

簡要總結阮老師文章內容:

  1. npm run 命令執行時,會把 ./node_modules/.bin/ 目錄添加到執行環境的 PATH 變量中,所以若是某個命令行包未全局安裝,而只安裝在了當前項目的 node_modules 中,經過 npm run 同樣能夠調用該命令。
  2. 執行 npm 腳本時要傳入參數,須要在命令後加 -- 標明, 如 npm run test -- --grep="pattern" 能夠將 --grep="pattern" 參數傳給 test 命令
  3. npm 提供了 pre 和 post 兩種鉤子機制,能夠定義某個腳本先後的執行腳本
  4. 運行時變量:在 npm run 的腳本執行環境內,能夠經過環境變量的方式獲取許多運行時相關信息,如下均可以經過 process.env 對象訪問得到:
    • npm_lifecycle_event - 正在運行的腳本名稱
    • npm_package_<key> - 獲取當前包 package.json 中某個字段的配置值:如 npm_package_name 獲取包名
    • npm_package_<key>_<sub-key> - package.json 中嵌套字段屬性:如 npm_pacakge_dependencies_webpack 能夠獲取到 package.json 中的 dependencies.webpack 字段的值,即 webpack 的版本號

5.2 node_modules/.bin 目錄

上面所說的 node_modules/.bin 目錄,保存了依賴目錄中所安裝的可供調用的命令行包。

何謂命令行包?例如 webpack 就屬於一個命令行包。若是咱們在安裝 webpack 時添加 --global 參數,就能夠在終端直接輸入 webpack 進行調用。但若是不加 --global 參數,咱們會在 node_modules/.bin 目錄裏看到名爲 webpack 的文件,若是在終端直接輸入 ./node_modules/.bin/webpack 命令,同樣能夠執行。

這是由於 webpackpackage.json 文件中定義了 bin 字段爲:

{
    "bin": {
        "webpack": "./bin/webpack.js"
    }
}
複製代碼

bin 字段的配置格式爲: <command>: <file>, 即 命令名: 可執行文件. npm 執行 install 時,會分析每一個依賴包的 package.json 中的 bin 字段,並將其包含的條目安裝到 ./node_modules/.bin 目錄中,文件名爲 <command>。而若是是全局模式安裝,則會在 npm 全局安裝路徑的 bin 目錄下建立指向 <file> 名爲 <command> 的軟鏈。所以,./node_modules/.bin/webpack 文件在經過命令行調用時,實際上就是在執行 node ./node_modules/.bin/webpack.js 命令。

正如上一節所說,npm run 命令在執行時會把 ./node_modules/.bin 加入到 PATH 中,使咱們可直接調用全部提供了命令行調用接口的依賴包。因此這裏就引出了一個最佳實踐:

將項目依賴的命令行工具安裝到項目依賴文件夾中,而後經過 npm scripts 調用;而非全局安裝

舉例而言 webpack 做爲前端工程標配的構建工具,雖然咱們都習慣了全局安裝並直接使用命令行調用,但不一樣的項目依賴的 webpack 版本可能不一樣,相應的 webpack.config.js 配置文件也可能只兼容了特定版本的 webpack. 若是咱們僅全局安裝了最新的 webpack 4.x 並使用 webpack 命令調用,在一個依賴 webpack 3.x 的工程中就會沒法成功執行構建。

但若是這類工具老是本地安裝,咱們要調用一個命令,要手動添加 ./node_modules/.bin 這個長長的前綴,未免也太麻煩了,咱們 nodejs 開發者都很懶的。因而 npm 從5.2 開始自帶了一個新的工具 npx.

5.3 npx

npx 的使用很簡單,就是執行 npx <command> 便可,這裏的 <command> 默認就是 ./node_modules 目錄中安裝的可執行腳本名。例如上面本地安裝好的 webpack 包,咱們能夠直接使用 npx webpack 執行便可。

除了這種最簡單的場景, npm cli 團隊開發者 Kat Marchán 還在這篇文章中介紹了其餘幾種 npx 的神奇用法: Introducing npx: an npm package runner, 國內有位開發者 robin.law 將原文翻譯爲中文 npx是什麼,爲何須要npx?.

有興趣的能夠戳連接瞭解,懶得點連接的,看總結:

場景a) 一鍵執行遠程 npm 源的二進制包

除了在 package 中執行 ./node_modules/.bin 中已安裝的命令, 還能夠直接指定未安裝的二進制包名執行。例如咱們在一個沒有 package.json 也沒有 node_modules 的目錄下,執行:

npx cowsay hello
複製代碼

npx 將會從 npm 源下載 cowsay 這個包(但並不安裝)並執行:

_______ 
< hello >
 ------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
複製代碼

這種用途很是適合 1. 在本地簡單測試或調試 npm 源上這些二進制包的功能;2. 調用 create-react-app 或 yeoman 這類每每每一個項目只須要使用一次的腳手架工具

PS: 此處有彩蛋,執行這條命令試試:

npx workin-hard
複製代碼

場景b) 一鍵執行 GitHub Gist

還記得前面提到的 [2.1 package定義] 麼,npm install <package> 能夠是包含了有效 package.json 的 git url.

恰好 GitHub Gist 也是 git 倉庫 的一種,集合 npx 就能夠方便地將簡單的腳本共享給其餘人,擁有該連接的人無需將腳本安裝到本地工做目錄便可執行。將 package.json 和 需執行的二進制腳本上傳至 gist, 在運行 npx <gist url> 就能夠方便地執行該 gist 定義的命令。

原文做者 Kat Marchán 提供了這個示例 gist, 執行:

npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
複製代碼

可獲得一個來自 GitHubGist 的 hello world 問候。

場景c) 使用不一樣版本 node 執行命令

將 npx 與 Aria Stewart 建立的 node 包 (https://www.npmjs.com/package/node) 結合,能夠實如今一行命令中使用指定版本的 node 執行命令。

例如前後執行:

npx node@4 -e "console.log(process.version)"
npx node@6 -e "console.log(process.version)"
複製代碼

將分別輸出 v4.8.7v6.13.0.

往常這種工做是由 nvm 這類 node 版本管理工具來作的,但 npx node@4 這種方式免去 nvm 手動切換配置的步驟,更加簡潔簡單。

6. npm 配置

6.1 npm config

npm cli 提供了 npm config 命令進行 npm 相關配置,經過 npm config ls -l 可查看 npm 的全部配置,包括默認配置。npm 文檔頁爲每一個配置項提供了詳細的說明 https://docs.npmjs.com/misc/config .

修改配置的命令爲 npm config set <key> <value>, 咱們使用相關的常見重要配置:

  • proxy, https-proxy: 指定 npm 使用的代理
  • registry 指定 npm 下載安裝包時的源,默認爲 https://registry.npmjs.org/ 能夠指定爲私有 Registry 源
  • package-lock 指定是否默認生成 package-lock 文件,建議保持默認 true
  • save true/false 指定是否在 npm install 後保存包爲 dependencies, npm 5 起默認爲 true

刪除指定的配置項命令爲 npm config delete <key>.

6.2 npmrc 文件

除了使用 CLI 的 npm config 命令顯示更改 npm 配置,還能夠經過 npmrc 文件直接修改配置。

這樣的 npmrc 文件優先級由高到低包括:

  • 工程內配置文件: /path/to/my/project/.npmrc
  • 用戶級配置文件: ~/.npmrc
  • 全局配置文件: $PREFIX/etc/npmrc (即npm config get globalconfig 輸出的路徑)
  • npm內置配置文件: /path/to/npm/npmrc

經過這個機制,咱們能夠方便地在工程跟目錄建立一個 .npmrc 文件來共享須要在團隊間共享的 npm 運行相關配置。好比若是咱們在公司內網環境下需經過代理纔可訪問 registry.npmjs.org 源,或需訪問內網的 registry, 就能夠在工做項目下新增 .npmrc 文件並提交代碼庫。

proxy = http://proxy.example.com/
https-proxy = http://proxy.example.com/
registry = http://registry.example.com/
複製代碼

由於項目級 .npmrc 文件的做用域只在本項目下,因此在非本目錄下,這些配置並不生效。對於使用筆記本工做的開發者,能夠很好地隔離公司的工做項目、在家學習研究項目兩種不一樣的環境。

將這個功能與 ~/.npm-init.js 配置相結合,能夠將特定配置的 .npmrc 跟 .gitignore, README 之類文件一塊兒作到 npm init 腳手架中,進一步減小手動配置。

6.3 node 版本約束

雖然一個項目的團隊都共享了相同的代碼,但每一個人的開發機器可能安裝了不一樣的 node 版本,此外服務器端的也可能與本地開發機不一致。

這又是一個可能帶來不一致性的因素 —— 但也不是很難解決,聲明式約束+腳本限制便可。

聲明:經過 package.jsonengines 屬性聲明應用運行所需的版本運行時要求。例如咱們的項目中使用了 async, await 特性,查閱兼容性表格得知最低支持版本爲 7.6.0,所以指定 engines 配置爲:

{
    "engines": { "node": ">=7.6.0"}
}
複製代碼

強約束(可選):在 npm 中以上字段內容僅做爲建議字段使用,若要在私有項目中添增強約束,須要本身寫腳本鉤子,讀取並解析 engines 字段的 semver range 並與運行時環境作對比校驗並適當提醒。

7. 小結 npm 最佳實踐

  • 使用 npm-init 初始化新項目
  • 統一項目配置: 需團隊共享的 npm config 配置項,固化到 .npmrc 文件中
  • 統一運行環境,統一 package.json,統一 package-lock 文件
  • 合理使用多樣化的源安裝依賴包: npm install <git url>|<local file>
  • 使用 npm: >=5.2 版本
  • 使用 npm scripts 與 npx (npm: >=5.2) 腳本管理應用相關腳本

8. 更多資料

參考

文檔

延伸閱讀

相關文章
相關標籤/搜索