nodejs 中的依賴管理

小蘑菇哥哥發表於2019-01-29

在我看來,nodejs 的成功原因除了它採用了前端 js 相同的語法,直接吸引了一大波前端開發者作為初始使用者之外,它內建的包管理器 npm 也居功至偉。npm 能夠很好的管理 nodejs 專案的依賴,也使得開發者釋出自己的包變的異常容易。這樣一來,不論你使用別人的包,還是自己釋出包給別人使用,成本都不大。這和我大學學習的 Java 1.x 相比就輕鬆愉快的多(現在 Java 已今非昔比,我不敢亂評論),開發者熱情高漲的話,整個生態就會更加活躍,進步速度也就更加快了。看一看 GitHub 上 JS 專案的佔比,再看看 npm 官網包的數量,就能略知一二。

前陣子公司的一名新人問了我一個問題:如何區分專案的依賴中,哪些應該放在 dependencies,而哪些應該放在 devDependencies 呢?

其實這個問題我在早先也有過,所以非常能夠體會他的心情。為了防止誤人子弟,我查閱了一些資料,發現其實 nodejs 中總共有 5 種依賴:

  • dependencies (常用)
  • devDependencies (常用)
  • peerDependencies (不太常用)
  • bundledDependencies (我之前沒用過)
  • optionalDependencies (我之前沒用過)

所以我趁此機會,整理了這篇文章,分享給更多仍有此迷茫的人們。

dependencies

這是 npm 最基本的依賴,通過命令 npm i xxx -S 或者 npm i xxx --save 來安裝一個包,並且新增到 package.json 的 dependencies 裡面(這裡 iinstall 的簡寫,兩者均可)。

如果直接只寫一個包的名字,則安裝當前 npm registry 中這個包的最新版本;如果要指定版本的,可以把版本號寫在包名後面,例如 npm i webpack@3.0.0 --save

npm install 也支援 tag,tar 包地址等等,不過那些不太常用,可以檢視官方文件

dependencies 比較簡單,我就不再多做解釋了。注意一點:npm 5.x 開始可以省略 --save,即如果執行 npm install xxx,npm 一樣會把包的依賴新增到 package.json 中去。要關閉這個功能,可以使用 npm config set save false

devDependencies

很多 nodejs 新手都分不清 dependencies 和 devDependencies,導致依賴隨便分配,或者把依賴統統都寫在 dependencies。這也是我編寫本文的初衷。

先說定義。顧名思義,devDependencies 就是開發中使用的依賴,它區別於實際的依賴。也就是說,線上上狀態不需要使用的依賴,就是開發依賴

再說意義。為什麼 npm 要把它單獨分拆出來呢?最終目的是為了減少 node_modules 目錄的大小以及 npm install 花費的時間。因為 npm 的依賴是巢狀的,所以可能看上去 package.json 中只有幾個依賴,但實際上它又擴散到 N 個,而 N 個又擴散到 N 平方個,一層層擴散出去,可謂子子孫孫無窮盡也。如果能夠儘量減少不使用的依賴,那麼就能夠節省線上機器的硬碟資源,也可以節省部署上線的時間。

在實際開發中,大概有這麼幾類可以歸為開發依賴:

  1. 構建工具

    現在比較熱門的是 webpack 和 rollup,以往還有 grunt, gulp 等等。這些構建工具會生成生產環境的程式碼,之後線上上使用時就直接使用這些壓縮過的程式碼。所以這類構建工具是屬於開發依賴的。

    像 webpack 還分為程式碼方式使用(webpack)和命令列方式使用 (webpack-cli),這些都是開發依賴。另外它們可能還會提供一些內建的常用外掛,如 xxx-webpack-plugin,這些也都算開發依賴。

  2. 前處理器

    這裡指的是對原始碼進行一定的處理,生成最終程式碼的工具。比較典型的有 CSS 中的 less, stylus, sass, scss 等等,以及 JS 中的 coffee-script, babel 等等。它們做的事情雖然各有不同,但原理是一致的。

    以 babel 為例,常用的有兩種使用方式。其一是內嵌在 webpack 或者 rollup 等構件工具中,一般以 loader 或者 plugin 的形式出現,例如 babel-loader。其二是單獨使用(小專案較多),例如 babel-cli。babel 還額外有自己的外掛體系,例如 xxx-babel-plugin。類似地,less 也有與之對應的 less-loaderlessc。這些都算作開發依賴。

    在 babel 中還有一個注意點,那就是 babel-runtime 是 dependencies 而不是 devDependencies。具體分析我在之前的 babel 文章中提過,就不再重複了。

  3. 測試工具

    嚴格來說,測試和開發並不是一個過程。但它們同屬於“線上狀態不需要使用的依賴”,因此也就歸入開發依賴了。常用的如 chai, e2e, karma, coveralls 等等都在此列。

  4. 真的是開發才用的依賴包

    最後一類比較雜,很難用一個大類囊括起來,總之就是開發時需要使用的,而實際上線時要麼是已經打包成最終程式碼了,要麼就是不需要使用了。比如 webpack-dev-server 支援開發熱載入,線上是不用的;babel-register 因為效能原因也不能用線上上。其他還可能和具體業務相關,就看各位開發者自己識別了。

把依賴安裝成開發依賴,則可以使用 npm i -D 或者 npm i --save-dev 命令。

如果想達成剛才說的縮減安裝包的目的,可以使用命令 npm i --production 忽略開發依賴,只安裝依賴,這通常線上上機器(或者 QA 環境)上使用。因此還有一個最根本的識別依賴的方式,那就是用這條命令安裝,如果專案跑不起來,那就是識別有誤了。

peerDependencies

如果僅作為 npm 包的使用者,瞭解前兩項就足夠我們日常的使用了。接下來的三種依賴都是作為包的釋出者帶會使用到的欄位,所以我們轉換角色,以釋出者的身份來討論接下來的問題。

如果我們開發一個常規的包,例如命名為 my-lib。其中需要使用 request 這個包來傳送請求,因此程式碼裡一定會有 const request = require('request')。如上面的討論,這種情況下 request 是作為 dependencies 出現在 package.json 裡面的。那麼在使用者通過命令 npm i my-lib 安裝我們的時候,這個 request 也會作為依賴的一部分被安裝到使用者的專案中。

那我們還為什麼需要這個 peerDependencies 呢?

根據 npm 官網的文件,這個屬性主要用於外掛類 (Plugin) 專案。常規來說,為了外掛生態的繁榮,外掛專案一般會被設計地儘量簡單,通過資料結構和固定的方法介面進行耦合,而不會要求外掛專案去依賴本體。例如我們比較熟悉的 express 中介軟體,只要你返回一個方法 return function someMiddleware(req, res, next),它就成為了 express 中介軟體,受本體呼叫,並通過三個引數把本體的資訊傳遞過來,在外掛內部使用。因此 express middleware 是不需要依賴 express 的。類似的情況還包括 Grunt 外掛,Chai 外掛和 Winston transports 等。

但很明顯,這類外掛脫離本體是不能單獨執行的。因此雖然外掛不依賴本體,但想要自己能夠實際執行起來,還得要求使用者把本體也納入依賴。這就是介於“不依賴”和“依賴”之間的中間狀態,就是 peerDependencies 的主要使用場景。

例如我們提供一個包,其中的 package.json 如下:

{
  "name": "my-greate-express-middleware",
  "version": "1.0.0",
  "peerDependencies": {
    "express": "^3.0.0"
  }
}
複製程式碼

在 npm 3.x 及以後版本,如果使用者安裝了我們的外掛,並且在自己的專案中沒有依賴 express 時,會在最後彈出一句提示,表示有一個包需要您依賴 express 3.x,因此您必須自己額外安裝。另外如果使用者依賴了不同版本的 express,npm 也會彈出提示,讓開發者自己決斷是否繼續使用這個包。

bundledDependencies

這是一種比起 peerDependencies 更加少見的依賴項,也可以寫作 bundleDependencies (bundle 後面的 d 省略)。和上述的依賴不同,這個屬性並不是一個鍵值對的物件,而是一個陣列,元素為表示包的名字的字串。例如

{
  "name": "awesome-web-framework",
  "version": "1.0.0",
  "bundledDependencies": [
    "renderized", "super-streams"
  ]
}
複製程式碼

當我們希望以壓縮包的方式釋出專案時(比如你不想放到 npm registry 裡面去),我們會使用 npm pack 來生成(如上述例子,就會生成 awesome-web-framework-1.0.0.tgz)。編寫了 bundledDependencies 之後,npm 會把這裡面的兩個包 (renderized, super-streams) 也一起加入到壓縮包中。這樣之後其他使用者執行 npm install awesome-web-framework-1.0.0.tgz 時也會安裝這兩個依賴了。

如果我們使用常規的 npm publish 的方式來發布的話,這個屬性不會生效;而作為使用方的話,大部分專案也都是從 npm registry 中搜尋並引用依賴的,所以使用到的場景也相當少。

optionalDependencies

這也是一種很少見的依賴項,從名字可以得知,它描述一種”可選“的依賴。和 dependencies 相比,它的不同點有:

  1. 即使這個依賴安裝失敗,也不影響整個安裝過程

  2. 程式應該自己處理安裝失敗時的情況

關於第二點,我想表達的意思是:

let foo
let fooVersion
try {
  foo = require('foo')
  fooVersion = require('foo/package.json').version
} catch (e) {
  // 安裝依賴失敗時找不到包,需要自己處理
}

// 如果安裝的依賴版本不符合實際要求,我們也需要自己處理,當做沒安裝到
if (!isSupportVersion(fooVersion)) {
  foo = null
}

// 如果安裝成功,執行某些操作
if (foo) {
  foo.doSomeThing()
}
複製程式碼

需要注意的是,如果一個依賴同時出現在 dependencies 和 optionalDependencies 中,那麼 optionalDependencies 會獲得更高的優先順序,可能造成預期之外的效果,因此最好不要出現這種情況。

在實際專案中,如果某個包已經失效,我們通常會尋找他的替代者,或者壓根換一個實現方案。使用這種”不確定“的依賴,一方面會增加程式碼中的判斷,增加邏輯的複雜度;另一方面也會大大降低測試覆蓋率,增加構造測試用例的難度。所以我不建議使用這個依賴項,如果你原先就不知道有這個,那就繼續當做不知道吧。

版本號的寫法

如上的 5 種依賴,除了 bundledDependencies,其他四種都是需要寫版本號的。如果作為使用者,使用 npm i --save 或者 npm i --save-dev 會自動生成依賴的版本號,不過我建議大家還是略微瞭解下常用的版本號的寫法。

首先我們得搞清三位版本號的定義,以 "a.b.c" 舉例,它們的含義是:

  1. a - 主要版本(也叫大版本,major version)

    大版本的升級很可能意味著與低版本不相容的 API 或者用法,是一次顛覆性的升級(想想 webpack 3 -> 4)。

  2. b - 次要版本(也叫小版本,minor version)

    小版本的升級應當相容同一個大版本內的 API 和用法,因此應該對開發者透明。所以我們通常只說大版本號,很少會精確到小版本號。

    特殊情況是如果大版本號是 0 的話,意味著整個包處於內測狀態,所以每個小版本之間也可能會不相容。所以在選擇依賴時,儘量避開大版本號是 0 的包。

  3. c - 補丁 (patch)

    一般用於修復 bug 或者很細微的變更,也需要保持向前相容。

之後我們看一下常規的版本號寫法:

  1. "1.2.3" - 無視更新的精確版本號

    表示只依賴這個版本,任何其他版本號都不匹配。在一些比較重要的線上專案中,我比較建議使用這種方式鎖定版本。前陣子的 npm 挖礦以及 ant-design 彩蛋,其實都可以通過鎖定版本來規避問題(彩蛋略難一些,挖礦是肯定可以規避)。

  2. "^1.2.3" - 兼具更新和安全的折中考慮

    這是 npm i xxx --save 之後系統生成的預設版本號(^ 加上當前最新版本號),官方的定義是“能夠相容除了最左側的非 0 版本號之外的其他變化”(Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple)。這句話很拗口,舉幾個例子大家就明白了:

    1. "^1.2.3" 等價於 ">= 1.2.3 < 2.0.0"。即只要最左側的 "1" 不變,其他都可以改變。所以 "1.2.4", "1.3.0" 都可以相容。

    2. "^0.2.3" 等價於 ">= 0.2.3 < 0.3.0"。因為最左側的是 "0",所以這個不算,順延到第二位 "2"。那麼只要這個 "2" 不變,其他的都相容,比如 "0.2.4" 和 "0.2.99"。

    3. "^0.0.3" 等價於 ">= 0.0.3 < 0.0.4"。這裡最左側的非 0 只有 "3",且沒有其他版本號了,所以這個也等價於精確的 "0.0.3"。

    從這幾個例子可以看出,^ 是一個更新和安全相容的寫法。一般大版本號升級到 1 就表示專案正式釋出了,而 0 開頭就表示還在測試版,這也是 ^ 區別對待兩者的原因。

  3. "~1.2.3" - 比 ^ 更加安全的小版本更新

    關於 ~ 的定義分為兩部分:如果列出了小版本號(第二位),則只相容 patch(第三位)的修改;如果沒有列出小版本號,則相容第二和第三位的修改。我們分兩種情況理解一下這個定義:

    1. "~1.2.3" 列出了小版本號(2),因此只相容第三位的修改,等價於 ">= 1.2.3 < 1.3.0"。

    2. "~1.2" 也列出了小版本號,因此和上面一樣相容第三位的修改,等價於 ">= 1.2.0 < 1.3.0"。

    3. "~1" 沒有列出小版本號,可以相容第二第三位的修改,因此等價於 ">= 1.0.0 < 2.0.0"

    ^ 不同的是,~ 並不對 0 或者 1 區別對待,所以 "~0" 等價於 ">= 0.0.0 < 1.0.0",和 "~1" 是相同的演算法。比較而言,~ 更加謹慎。當首位是 0 並且列出了第二位的時候,兩者是等價的,例如 ~0.2.3^0.2.3

    在 nodejs 的上古版本(v0.10.26,2014年2月釋出的),npm i --save 預設使用的是 ~,現在已經改成 ^ 了。這個改動也是為了讓使用者能最大限度的更新依賴包。

  4. "1.x" 或者 "1.*" - 使用萬用字元

    這個比起上面那兩個符號就好理解的多。x(大小寫皆可)和 * 的含義相同,都表示可以匹配任何內容。具體來說:

    1. "*" 或者 "" (空字串) 表示可以匹配任何版本。

    2. "1.x", "1.*" 和 "1" 都表示要求大版本是 1,因此等價於 ">=1.0.0 < 2.0.0"。

    3. "1.2.x", "1.2.*" 和 "1.2" 都表示鎖定前兩位,因此等價於 ">= 1.2.0 < 1.3.0"。

    因為位於結尾的萬用字元一般可以省略,而常規也不太可能像正則那樣把匹配符寫在中間,所以大多數情況萬用字元都可以省略。使用最多的還是匹配所有版本的 * 這個了。

  5. "1.2.3-beta.2" - 帶預釋出關鍵詞的,如 alpha, beta, rc, pr 等

    先說預釋出的定義,我們需要以包開發者的角度來考慮這個問題。假設當前線上版本是 "1.2.3",如果我作了一些改動需要釋出版本 "1.2.4",但我不想直接上線(因為使用 "~1.2.3" 或者 `^1.2.3" 的使用者都會直接靜默更新),這就需要使用預釋出功能。因此我可能會發布 "1.2.4-alpha.1" 或者 "1.2.4-beta.1" 等等。

    理解了它誕生的初衷,之後的使用就很自然了。

    1. ">1.2.4-alpha.1",表示我接受 "1.2.4" 版本所有大於1的 alpha 預釋出版本。因此如 "1.2.4-alpha.7" 是符合要求的,但 "1.2.4-beta.1" 和 "1.2.5-alpha.2" 都不符合。此外如果是正式版本(不帶預釋出關鍵詞),只要版本號符合要求即可,不檢查預釋出版本號,例如 "1.2.5", "1.3.0" 都是認可的。

    2. "~1.2.4-alpha.1" 表示 ">=1.2.4-alpha.1 < 1.3.0"。這樣 "1.2.5", "1.2.4-alpha.2" 都符合條件,而 "1.2.5-alpha.1", "1.3.0" 不符合。

    3. "^1.2.4-alpha.1" 表示 ">=1.2.4-alpha.1 < 2.0.0"。這樣 "1.2.5", "1.2.4-alpha.2", "1.3.0" 都符合條件,而 "1.2.5-alpha.1", "2.0.0" 不符合。

版本號還有更多的寫法,例如範圍(a - b),大於小於號(>=a <b),或(表示式1 || 表示式2)等等,因為用的不多,這裡不再展開。詳細的文件可以參見 semver,它同時也是一個 npm 包,可以用來比較兩個版本號的大小,以及是否符合要求等。

其他寫法

除了版本號,依賴包還可以通過如下幾種方式來進行依賴(使用的也不算太多,可以粗略瞭解一下):

Tag

除了版本號之外,通常某個包還可能會有 Tag 來標識一些里程碑意義的版本。例如 express@next 表示即將到來的下一個大版本(可提前體驗),而 some-lib@latest 等價於 some-lib,因為 latest 是預設存在並指向最新版本的。其他的自定義 Tag 都可以由開發者通過 npm tag 來指定。

因為 npm i package@versionnpm i package@tag 的語法是相同的,因此 Tag 和版本號必須不能重複。所以一般建議 Tag 不要以數字或者字母 v 開頭。

URL

可以指定 URL 指明依賴包的源地址,通常是一個 tar 包,例如 "https://some.site.com/lib.tar.gz"。這個 tar 包通常是通過 npm pack 來發布的。

順帶提一句:本質上,npm 的所有包都是以 tar 包釋出的。使用 npm publish 常規釋出的包也是被 npm 冠上版本號等字尾,由 npm registry 託管供大家下載的。

Git URL

可以指定一個 Git 地址(不單純指 GitHub,任何 git 協議的均可),npm 自動從該地址下載並安裝。這裡就需要指明協議,使用者名稱,密碼,路徑,分支名和版本號等,比較複雜。詳情可以檢視官方文件,舉例如下:

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

作為最大的 Git 程式碼庫,如果使用的是 GitHub 存放程式碼,還可以直接使用 user/repo 的簡寫方式,例如:

{
  "dependencies": {
    "express": "expressjs/express",
    "mocha": "mochajs/mocha#4727d357ea",
    "module": "user/repo#feature\/branch"
  }
}
複製程式碼

本地路徑

npm 支援使用本地路徑來指向一個依賴包,這時候需要在路徑之前新增 file:,例如:

{
  "dependencies": {
    "bar1": "file:../foo/bar1",
    "bar2": "file:~/foo/bar2",
    "bar3": "file:/foo/bar3"
  }
}
複製程式碼

package-lock.json

從 npm 5.x 開始,在執行 npm i 之後,會在根目錄額外生成一個 package-lock.json。既然講到了依賴,我就額外擴充套件一下這個 package-lock.json 的結構和作用。

package-lock.json 內部記錄的是每一個依賴的實際安裝資訊,例如名字,安裝的版本號,安裝的地址 (npm registry 上的 tar 包地址)等等。額外的,它會把依賴的依賴也記錄起來,因此整個檔案是一個樹形結構,儲存依賴巢狀關係(類似以前版本的 node_modules 目錄)。一個簡單的例子如下:

{
  "name": "my-lib",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "array-union": {
      "version": "1.0.2",
      "resolved": "http://registry.npm.taobao.org/array-union/download/array-union-1.0.2.tgz",
      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
      "dev": true,
      "requires": {
        "array-uniq": "^1.0.1"
      }
    }
  }
}
複製程式碼

在執行 npm i 的時候,如果發現根目錄下只有 package.json 存在(這通常發生在剛建立專案時),就按照它的記錄逐層遞迴安裝依賴,並生成一個 package-lock.json 檔案。如果發現根目錄下兩者皆有(這通常發生在開發同事把專案 checkout 到本地之後),則 npm 會比較兩者。如果兩者所示含義不同,則以 package.json 為準,並更新 package-lock.json;否則就直接按 package-lock 所示的版本號安裝。

它存在的意義主要有 4 點:

  1. 在團隊開發中,確保每個團隊成員安裝的依賴版本是一致的。否則因為依賴版本不一致導致的效果差異,一般很難查出來。

  2. 通常 node_modules 目錄都不會提交到程式碼庫,因此要回溯到某一天的狀態是不可能的。但現在 node_modules 目錄和 package.json 以及 package-lock.json 是一一對應的。所以如果開發者想回退到之前某一天的目錄狀態,只要把這兩個檔案回退到那一天的狀態,再 npm i 就行了。

  3. 因為 package-lock.json 已經足以描述 node_modules 的大概資訊(尤其是深層巢狀依賴),所以通過這個檔案就可以查閱某個依賴包是被誰依賴進來的,而不用去翻 node_modules 目錄(事實上現在目錄結構打平而非巢狀,翻也翻不出來了)

  4. 在安裝過程中,npm 內部會檢查 node_modules 目錄中已有的依賴包,並和 package-lock.json 進行比較。如果重複,則跳過安裝,能大大優化安裝時間。

npm 官網建議:把 package-lock.json 一起提交到程式碼庫中,不要 ignore。但是在執行 npm publish 的時候,它會被忽略而不會發布出去。

yarn

從 nodejs 誕生之初,npm 就是其內建的包管理器,並且以其易於使用,易於釋出的特點極大地助推了 nodejs 在開發者中的流行和使用。但事物總有其兩面性,易於釋出的確大大推動生態的繁榮,但同時也降低了釋出的門檻。包的數量在突飛猛進,一個專案的依賴項從幾個上升到幾十個,再加上內部的巢狀迴圈依賴,給使用者帶來了極大的麻煩,node_modules 目錄越來越大,npm install 的時間也越來越長。

在這種情況下,Facebook 率先站出來,釋出了由他們開發的另一個包管理器 yarn(1.0版本於2017年9月)。一旦有了挑戰者出現,勢必會引發雙方對於功能,穩定性,易用性等各方面的競爭,對於開發者來說也是極其有利的。從結果來說,npm 也吸收了不少從 yarn 借鑑來的優點,例如上面談論的 package-lock.json,最早就出自 yarn.lock。所以我們來粗略比較一下兩者的區別,以及我們應當如何選擇。

yarn

  1. 版本鎖定

    這個在 package-lock.json 已經討論過了,不再贅述。 在這個功能點上,兩者都已具備。

  2. 多個包的管理 (monorepositories)

    一個包在 npm 中可以被稱為 repositories。通常我們釋出某個功能,其實就是釋出一個包,由它提供各種 API 來提供功能。但隨著功能越來越複雜以及按需載入,把所有東西全部放到一個包中釋出已經不夠優秀,因此出現了多個包管理的需求。

    通常一個類庫會把自己的功能分拆為核心部分和其他部分,然後每個部分是一個 npm repositories,可以單獨釋出。而使用者通常在使用核心之後,可以自己選擇要使用哪些額外的部分。這種方式比較常見的如 babel 和它的外掛,express 和它的中介軟體等。

    作為一個多個包的專案的開發者/維護者,安裝依賴和釋出都會是一件很麻煩的事情。因為 npm 只認根目錄的 package.json,那麼就必須進入每個包進行 npm install。而釋出時,也必須逐個修改每個包的版本號,併到每個目錄中進行 npm publish

    為了解決這個問題,社群一個叫做 lerna 的庫通過增加 lerna.json 來幫助我們管理所有的包。而在 yarn 這邊,引入了一個叫做工作區(workspace)的概念。因此這點上來說,應該是 yarn 勝出了,不過 npm 配合 lerna 也能夠實現這個需求。

  3. 安裝速度

    npm 被詬病最多的問題之一就是其安裝速度。有些依賴很多的專案,安裝 npm 需要耗費 5-10 分鐘甚至更久。造成這個問題的本質是 npm 採用序列的安裝方式,一個裝完再裝下一個。針對這一點,yarn 改為並行安裝,因此本質上提升了安裝速度。

  4. 離線可用

    yarn 預設支援離線安裝,即安裝過一次的包,會在電腦中保留一份(快取位置可以通過 yarn config set yarn-offline-mirror 進行指定)。之後再次安裝,直接複製過來就可以。

    npm 早先是全部通過網路請求的(為了保持其時效性),但後期也借鑑了 yarn 建立了快取。從 npm 5.x 開始我們可以使用 npm install xxx --prefer-offline優先使用快取(意思是快取沒有再傳送網路請求),或者 npm install xxx --offline完全使用快取(意思是快取沒有就安裝失敗)。

  5. 控制檯資訊

    常年使用 npm 的同學知道,安裝完依賴後,npm 會列出一顆依賴樹。這顆樹通常會很長很複雜,我們不會過多關注。因此 yarn 精簡了這部分資訊,直接輸出安裝結果。這樣萬一安裝過程中有報錯日誌也不至於被刷掉。

    不過 npm 5.x 也把這顆樹給去掉了。這又是一個互相借鑑提高的例子。

總結來說,yarn 的推出主要是針對 npm 早期版本的很多問題。但 npm 也意識到了來自競爭對手的強大壓力,因此在 5.x 開始逐個優化看齊。從 5.x 開始就已經和 yarn 不分伯仲了,因此如何選擇多數看是否有歷史包袱。如果是新專案的話,就看程式設計師個人的喜好了。

後記

本文從一個很小的問題開始,本意是想分享如何鑑別一個應用應該歸類在 dependencies 還是 devDependencies。後來層層深入,通過查閱資料發現了好多依賴相關的知識,例如其他幾種依賴,版本鎖定的機制以及和 yarn 的比較等等,最終變成一篇長文。希望通過本文能讓大家瞭解到依賴管理的一些大概,在之後的搬磚道路上能夠更加順利,也能反過來為整個生態的繁榮貢獻自己的力量。

參考文章

相關文章