在我看來,nodejs 的成功原因除了它采用了前端 js 相同的語法,直接吸引了一大波前端開發(fā)者作為初始用戶之外,它內(nèi)置的包管理器 npm 也居功至偉。npm 能夠很好的管理 nodejs 項目的依賴,也使得開發(fā)者發(fā)布自己的包變的異常容易。這樣一來,不論你使用別人的包,還是自己發(fā)布包給別人使用,成本都不大。這和我大學(xué)學(xué)習(xí)的 Java 1.x 相比就輕松愉快的多(現(xiàn)在 Java 已今非昔比,我不敢亂評論),開發(fā)者熱情高漲的話,整個生態(tài)就會更加活躍,進步速度也就更加快了??匆豢?GitHub 上 JS 項目的占比,再看看 npm 官網(wǎng)包的數(shù)量,就能略知一二。
前陣子公司的一名新人問了我一個問題:如何區(qū)分項目的依賴中,哪些應(yīng)該放在 dependencies,而哪些應(yīng)該放在 devDependencies 呢?
其實這個問題我在早先也有過,所以非常能夠體會他的心情。為了防止誤人子弟,我查閱了一些資料,發(fā)現(xiàn)其實 nodejs 中總共有 5 種依賴:
dependencies (常用)
devDependencies (常用)
peerDependencies (不太常用)
bundledDependencies (我之前沒用過)
optionalDependencies (我之前沒用過)
所以我趁此機會,整理了這篇文章,分享給更多仍有此迷茫的人們。
dependencies
這是 npm 最基本的依賴,通過命令 npm i xxx -S 或者 npm i xxx —save 來安裝一個包,并且添加到 package.json 的 dependencies 里面(這里 i 是 install 的簡寫,兩者均可)。
如果直接只寫一個包的名字,則安裝當前 npm registry 中這個包的最新版本;如果要指定版本的,可以把版本號寫在包名后面,例如 npm i?webpack@3.0.0?—save。
npm install 也支持 tag,tar 包地址等等,不過那些不太常用,可以查看官方文檔。
dependencies 比較簡單,我就不再多做解釋了。注意一點:npm 5.x 開始可以省略 —save,即如果執(zhí)行 npm install xxx,npm 一樣會把包的依賴添加到 package.json 中去。要關(guān)閉這個功能,可以使用 npm config set save false。
devDependencies
很多 nodejs 新手都分不清 dependencies 和 devDependencies,導(dǎo)致依賴隨便分配,或者把依賴統(tǒng)統(tǒng)都寫在 dependencies。這也是我編寫本文的初衷。
先說定義。顧名思義,devDependencies 就是開發(fā)中使用的依賴,它區(qū)別于實際的依賴。也就是說,在線上狀態(tài)不需要使用的依賴,就是開發(fā)依賴。
再說意義。為什么 npm 要把它單獨分拆出來呢?最終目的是為了減少 node_modules 目錄的大小以及 npm install 花費的時間。因為 npm 的依賴是嵌套的,所以可能看上去 package.json 中只有幾個依賴,但實際上它又擴散到 N 個,而 N 個又擴散到 N 平方個,一層層擴散出去,可謂子子孫孫無窮盡也。如果能夠盡量減少不使用的依賴,那么就能夠節(jié)省線上機器的硬盤資源,也可以節(jié)省部署上線的時間。
在實際開發(fā)中,大概有這么幾類可以歸為開發(fā)依賴:
構(gòu)建工具
現(xiàn)在比較熱門的是 webpack 和 rollup,以往還有 grunt, gulp 等等。這些構(gòu)建工具會生成生產(chǎn)環(huán)境的代碼,之后在線上使用時就直接使用這些壓縮過的代碼。所以這類構(gòu)建工具是屬于開發(fā)依賴的。
像 webpack 還分為代碼方式使用(webpack)和命令行方式使用 (webpack-cli),這些都是開發(fā)依賴。另外它們可能還會提供一些內(nèi)置的常用插件,如 xxx-webpack-plugin,這些也都算開發(fā)依賴。
預(yù)處理器
這里指的是對源代碼進行一定的處理,生成最終代碼的工具。比較典型的有 CSS 中的 less, stylus, sass, scss 等等,以及 JS 中的 coffee-script, babel 等等。它們做的事情雖然各有不同,但原理是一致的。
以 babel 為例,常用的有兩種使用方式。其一是內(nèi)嵌在 webpack 或者 rollup 等構(gòu)件工具中,一般以 loader 或者 plugin 的形式出現(xiàn),例如 babel-loader。其二是單獨使用(小項目較多),例如 babel-cli。babel 還額外有自己的插件體系,例如 xxx-babel-plugin。類似地,less 也有與之對應(yīng)的 less-loader 和 lessc。這些都算作開發(fā)依賴。
在 babel 中還有一個注意點,那就是 babel-runtime 是 dependencies 而不是 devDependencies。具體分析我在之前的 babel 文章中提過,就不再重復(fù)了。
測試工具
嚴格來說,測試和開發(fā)并不是一個過程。但它們同屬于“線上狀態(tài)不需要使用的依賴”,因此也就歸入開發(fā)依賴了。常用的如 chai, e2e, karma, coveralls 等等都在此列。
真的是開發(fā)才用的依賴包
最后一類比較雜,很難用一個大類囊括起來,總之就是開發(fā)時需要使用的,而實際上線時要么是已經(jīng)打包成最終代碼了,要么就是不需要使用了。比如 webpack-dev-server 支持開發(fā)熱加載,線上是不用的;babel-register 因為性能原因也不能用在線上。其他還可能和具體業(yè)務(wù)相關(guān),就看各位開發(fā)者自己識別了。
把依賴安裝成開發(fā)依賴,則可以使用 npm i -D 或者 npm i —save-dev 命令。
如果想達成剛才說的縮減安裝包的目的,可以使用命令 npm i —production 忽略開發(fā)依賴,只安裝依賴,這通常在線上機器(或者 QA 環(huán)境)上使用。因此還有一個最根本的識別依賴的方式,那就是用這條命令安裝,如果項目跑不起來,那就是識別有誤了。
peerDependencies
如果僅作為 npm 包的使用者,了解前兩項就足夠我們?nèi)粘5氖褂昧恕=酉聛淼娜N依賴都是作為包的發(fā)布者帶會使用到的字段,所以我們轉(zhuǎn)換角色,以發(fā)布者的身份來討論接下來的問題。
如果我們開發(fā)一個常規(guī)的包,例如命名為 my-lib。其中需要使用 request 這個包來發(fā)送請求,因此代碼里一定會有 const request = require(‘request’)。如上面的討論,這種情況下 request 是作為 dependencies 出現(xiàn)在 package.json 里面的。那么在使用者通過命令 npm i my-lib 安裝我們的時候,這個 request 也會作為依賴的一部分被安裝到使用者的項目中。
那我們還為什么需要這個 peerDependencies 呢?
根據(jù) npm 官網(wǎng)的文檔,這個屬性主要用于插件類 (Plugin) 項目。常規(guī)來說,為了插件生態(tài)的繁榮,插件項目一般會被設(shè)計地盡量簡單,通過數(shù)據(jù)結(jié)構(gòu)和固定的方法接口進行耦合,而不會要求插件項目去依賴本體。例如我們比較熟悉的 express 中間件,只要你返回一個方法 return function someMiddleware(req, res, next),它就成為了 express 中間件,受本體調(diào)用,并通過三個參數(shù)把本體的信息傳遞過來,在插件內(nèi)部使用。因此 express middleware 是不需要依賴 express 的。類似的情況還包括 Grunt 插件,Chai 插件和 Winston transports 等。
但很明顯,這類插件脫離本體是不能單獨運行的。因此雖然插件不依賴本體,但想要自己能夠?qū)嶋H運行起來,還得要求使用者把本體也納入依賴。這就是介于“不依賴”和“依賴”之間的中間狀態(tài),就是 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 也會彈出提示,讓開發(fā)者自己決斷是否繼續(xù)使用這個包。
bundledDependencies
這是一種比起 peerDependencies 更加少見的依賴項,也可以寫作 bundleDependencies (bundle 后面的 d 省略)。和上述的依賴不同,這個屬性并不是一個鍵值對的對象,而是一個數(shù)組,元素為表示包的名字的字符串。例如
{
????"name": "awesome-web-framework",
????"version": "1.0.0",
????"bundledDependencies": [
????????"renderized",
?????????"super-streams"
????]
}
當我們希望以壓縮包的方式發(fā)布項目時(比如你不想放到 npm registry 里面去),我們會使用 npm pack 來生成(如上述例子,就會生成 awesome-web-framework-1.0.0.tgz)。編寫了 bundledDependencies 之后,npm 會把這里面的兩個包 (renderized, super-streams) 也一起加入到壓縮包中。這樣之后其他使用者執(zhí)行 npm install awesome-web-framework-1.0.0.tgz 時也會安裝這兩個依賴了。
如果我們使用常規(guī)的 npm publish 的方式來發(fā)布的話,這個屬性不會生效;而作為使用方的話,大部分項目也都是從 npm registry 中搜索并引用依賴的,所以使用到的場景也相當少。
optionalDependencies
這也是一種很少見的依賴項,從名字可以得知,它描述一種”可選“的依賴。和 dependencies 相比,它的不同點有:
即使這個依賴安裝失敗,也不影響整個安裝過程
程序應(yīng)該自己處理安裝失敗時的情況
關(guān)于第二點,我想表達的意思是:
let foo
let fooVersion
try {
????foo = require('foo')
????fooVersion = require('foo/package.json').version
} catch (e) {
// 安裝依賴失敗時找不到包,需要自己處理}// 如果安裝的依賴版本不符合實際要求,我們也需要自己處理,當做沒安裝到
if (!isSupportVersion(fooVersion)) {
????foo = null
}// 如果安裝成功,執(zhí)行某些操作
if (foo) {
????foo.doSomeThing()
}
需要注意的是,如果一個依賴同時出現(xiàn)在 dependencies 和 optionalDependencies 中,那么 optionalDependencies 會獲得更高的優(yōu)先級,可能造成預(yù)期之外的效果,因此最好不要出現(xiàn)這種情況。
在實際項目中,如果某個包已經(jīng)失效,我們通常會尋找他的替代者,或者壓根換一個實現(xiàn)方案。使用這種”不確定“的依賴,一方面會增加代碼中的判斷,增加邏輯的復(fù)雜度;另一方面也會大大降低測試覆蓋率,增加構(gòu)造測試用例的難度。所以我不建議使用這個依賴項,如果你原先就不知道有這個,那就繼續(xù)當做不知道吧。
版本號的寫法
如上的 5 種依賴,除了 bundledDependencies,其他四種都是需要寫版本號的。如果作為使用者,使用 npm i —save 或者 npm i —save-dev 會自動生成依賴的版本號,不過我建議大家還是略微了解下常用的版本號的寫法。
首先我們得搞清三位版本號的定義,以 “a.b.c” 舉例,它們的含義是:
a - 主要版本(也叫大版本,major version)
大版本的升級很可能意味著與低版本不兼容的 API 或者用法,是一次顛覆性的升級(想想 webpack 3 -> 4)。
b - 次要版本(也叫小版本,minor version)
小版本的升級應(yīng)當兼容同一個大版本內(nèi)的 API 和用法,因此應(yīng)該對開發(fā)者透明。所以我們通常只說大版本號,很少會精確到小版本號。
特殊情況是如果大版本號是 0 的話,意味著整個包處于內(nèi)測狀態(tài),所以每個小版本之間也可能會不兼容。所以在選擇依賴時,盡量避開大版本號是 0 的包。
c - 補丁 (patch)
一般用于修復(fù) bug 或者很細微的變更,也需要保持向前兼容。
之后我們看一下常規(guī)的版本號寫法:
“1.2.3” - 無視更新的精確版本號
表示只依賴這個版本,任何其他版本號都不匹配。在一些比較重要的線上項目中,我比較建議使用這種方式鎖定版本。前陣子的 npm 挖礦以及 ant-design 彩蛋,其實都可以通過鎖定版本來規(guī)避問題(彩蛋略難一些,挖礦是肯定可以規(guī)避)。
“^1.2.3” - 兼具更新和安全的折中考慮
這是 npm i xxx —save 之后系統(tǒng)生成的默認版本號(^ 加上當前最新版本號),官方的定義是“能夠兼容除了最左側(cè)的非 0 版本號之外的其他變化”(Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple)。這句話很拗口,舉幾個例子大家就明白了:
“^1.2.3” 等價于 “>= 1.2.3 < 2.0.0”。即只要最左側(cè)的 “1” 不變,其他都可以改變。所以 “1.2.4”, “1.3.0” 都可以兼容。
“^0.2.3” 等價于 “>= 0.2.3 < 0.3.0”。因為最左側(cè)的是 “0”,所以這個不算,順延到第二位 “2”。那么只要這個 “2” 不變,其他的都兼容,比如 “0.2.4” 和 “0.2.99”。
“^0.0.3” 等價于 “>= 0.0.3 < 0.0.4”。這里最左側(cè)的非 0 只有 “3”,且沒有其他版本號了,所以這個也等價于精確的 “0.0.3”。
從這幾個例子可以看出,^ 是一個更新和安全兼容的寫法。一般大版本號升級到 1 就表示項目正式發(fā)布了,而 0 開頭就表示還在測試版,這也是 ^ 區(qū)別對待兩者的原因。
“~1.2.3” - 比 ^ 更加安全的小版本更新
關(guān)于 ~ 的定義分為兩部分:如果列出了小版本號(第二位),則只兼容 patch(第三位)的修改;如果沒有列出小版本號,則兼容第二和第三位的修改。我們分兩種情況理解一下這個定義:
“~1.2.3” 列出了小版本號(2),因此只兼容第三位的修改,等價于 “>= 1.2.3 < 1.3.0”。
“~1.2” 也列出了小版本號,因此和上面一樣兼容第三位的修改,等價于 “>= 1.2.0 < 1.3.0”。
“~1” 沒有列出小版本號,可以兼容第二第三位的修改,因此等價于 “>= 1.0.0 < 2.0.0”
和 ^ 不同的是,~ 并不對 0 或者 1 區(qū)別對待,所以 “~0” 等價于 “>= 0.0.0 < 1.0.0”,和 “~1” 是相同的算法。比較而言,~ 更加謹慎。當首位是 0 并且列出了第二位的時候,兩者是等價的,例如 ~0.2.3 和 ^0.2.3。
在 nodejs 的上古版本(v0.10.26,2014年2月發(fā)布的),npm i —save 默認使用的是 ~,現(xiàn)在已經(jīng)改成 ^ 了。這個改動也是為了讓使用者能最大限度的更新依賴包。
“1.x” 或者 “1.“ - 使用通配符
這個比起上面那兩個符號就好理解的多。x(大小寫皆可)和
的含義相同,都表示可以匹配任何內(nèi)容。具體來說:
“*” 或者 “” (空字符串) 表示可以匹配任何版本。
“1.x”, “1.*” 和 “1” 都表示要求大版本是 1,因此等價于 “>=1.0.0 < 2.0.0”。
“1.2.x”, “1.2.*” 和 “1.2” 都表示鎖定前兩位,因此等價于 “>= 1.2.0 < 1.3.0”。
因為位于結(jié)尾的通配符一般可以省略,而常規(guī)也不太可能像正則那樣把匹配符寫在中間,所以大多數(shù)情況通配符都可以省略。使用最多的還是匹配所有版本的 * 這個了。
“1.2.3-beta.2” - 帶預(yù)發(fā)布關(guān)鍵詞的,如 alpha, beta, rc, pr 等
先說預(yù)發(fā)布的定義,我們需要以包開發(fā)者的角度來考慮這個問題。假設(shè)當前線上版本是 “1.2.3”,如果我作了一些改動需要發(fā)布版本 “1.2.4”,但我不想直接上線(因為使用 “~1.2.3” 或者 `^1.2.3” 的用戶都會直接靜默更新),這就需要使用預(yù)發(fā)布功能。因此我可能會發(fā)布 “1.2.4-alpha.1” 或者 “1.2.4-beta.1” 等等。
理解了它誕生的初衷,之后的使用就很自然了。
“>1.2.4-alpha.1”,表示我接受 “1.2.4” 版本所有大于1的 alpha 預(yù)發(fā)布版本。因此如 “1.2.4-alpha.7” 是符合要求的,但 “1.2.4-beta.1” 和 “1.2.5-alpha.2” 都不符合。此外如果是正式版本(不帶預(yù)發(fā)布關(guān)鍵詞),只要版本號符合要求即可,不檢查預(yù)發(fā)布版本號,例如 “1.2.5”, “1.3.0” 都是認可的。
“~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” 不符合。
“^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 都可以由開發(fā)者通過 npm tag 來指定。
因為 npm i package@version?和 npm i package@tag?的語法是相同的,因此 Tag 和版本號必須不能重復(fù)。所以一般建議 Tag 不要以數(shù)字或者字母 v 開頭。
URL
可以指定 URL 指明依賴包的源地址,通常是一個 tar 包,例如 “https://some.site.com/lib.tar.gz"。這個?tar 包通常是通過 npm pack 來發(fā)布的。
順帶提一句:本質(zhì)上,npm 的所有包都是以 tar 包發(fā)布的。使用 npm publish 常規(guī)發(fā)布的包也是被 npm 冠上版本號等后綴,由 npm registry 托管供大家下載的。
Git URL
可以指定一個 Git 地址(不單純指 GitHub,任何 git 協(xié)議的均可),npm 自動從該地址下載并安裝。這里就需要指明協(xié)議,用戶名,密碼,路徑,分支名和版本號等,比較復(fù)雜。詳情可以查看官方文檔,舉例如下:
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 開始,在執(zhí)行 npm i 之后,會在根目錄額外生成一個 package-lock.json。既然講到了依賴,我就額外擴展一下這個 package-lock.json 的結(jié)構(gòu)和作用。
package-lock.json 內(nèi)部記錄的是每一個依賴的實際安裝信息,例如名字,安裝的版本號,安裝的地址 (npm registry 上的 tar 包地址)等等。額外的,它會把依賴的依賴也記錄起來,因此整個文件是一個樹形結(jié)構(gòu),保存依賴嵌套關(guān)系(類似以前版本的 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"
????????????}
????????}
? ????}
}
在執(zhí)行 npm i 的時候,如果發(fā)現(xiàn)根目錄下只有 package.json 存在(這通常發(fā)生在剛創(chuàng)建項目時),就按照它的記錄逐層遞歸安裝依賴,并生成一個 package-lock.json 文件。如果發(fā)現(xiàn)根目錄下兩者皆有(這通常發(fā)生在開發(fā)同事把項目 checkout 到本地之后),則 npm 會比較兩者。如果兩者所示含義不同,則以 package.json 為準,并更新 package-lock.json;否則就直接按 package-lock 所示的版本號安裝。
它存在的意義主要有 4 點:
在團隊開發(fā)中,確保每個團隊成員安裝的依賴版本是一致的。否則因為依賴版本不一致導(dǎo)致的效果差異,一般很難查出來。
通常 node_modules 目錄都不會提交到代碼庫,因此要回溯到某一天的狀態(tài)是不可能的。但現(xiàn)在 node_modules 目錄和 package.json 以及 package-lock.json 是一一對應(yīng)的。所以如果開發(fā)者想回退到之前某一天的目錄狀態(tài),只要把這兩個文件回退到那一天的狀態(tài),再 npm i 就行了。
因為 package-lock.json 已經(jīng)足以描述 node_modules 的大概信息(尤其是深層嵌套依賴),所以通過這個文件就可以查閱某個依賴包是被誰依賴進來的,而不用去翻 node_modules 目錄(事實上現(xiàn)在目錄結(jié)構(gòu)打平而非嵌套,翻也翻不出來了)
在安裝過程中,npm 內(nèi)部會檢查 node_modules 目錄中已有的依賴包,并和 package-lock.json 進行比較。如果重復(fù),則跳過安裝,能大大優(yōu)化安裝時間。
npm 官網(wǎng)建議:把 package-lock.json 一起提交到代碼庫中,不要 ignore。但是在執(zhí)行 npm publish 的時候,它會被忽略而不會發(fā)布出去。
yarn
從 nodejs 誕生之初,npm 就是其內(nèi)置的包管理器,并且以其易于使用,易于發(fā)布的特點極大地助推了 nodejs 在開發(fā)者中的流行和使用。但事物總有其兩面性,易于發(fā)布的確大大推動生態(tài)的繁榮,但同時也降低了發(fā)布的門檻。包的數(shù)量在突飛猛進,一個項目的依賴項從幾個上升到幾十個,再加上內(nèi)部的嵌套循環(huán)依賴,給使用者帶來了極大的麻煩,node_modules 目錄越來越大,npm install 的時間也越來越長。
在這種情況下,F(xiàn)acebook 率先站出來,發(fā)布了由他們開發(fā)的另一個包管理器 yarn(1.0版本于2017年9月)。一旦有了挑戰(zhàn)者出現(xiàn),勢必會引發(fā)雙方對于功能,穩(wěn)定性,易用性等各方面的競爭,對于開發(fā)者來說也是極其有利的。從結(jié)果來說,npm 也吸收了不少從 yarn 借鑒來的優(yōu)點,例如上面談?wù)摰?package-lock.json,最早就出自 yarn.lock。所以我們來粗略比較一下兩者的區(qū)別,以及我們應(yīng)當如何選擇。
版本鎖定
這個在 package-lock.json 已經(jīng)討論過了,不再贅述。 在這個功能點上,兩者都已具備。
多個包的管理 (monorepositories)
一個包在 npm 中可以被稱為 repositories。通常我們發(fā)布某個功能,其實就是發(fā)布一個包,由它提供各種 API 來提供功能。但隨著功能越來越復(fù)雜以及按需加載,把所有東西全部放到一個包中發(fā)布已經(jīng)不夠優(yōu)秀,因此出現(xiàn)了多個包管理的需求。
通常一個類庫會把自己的功能分拆為核心部分和其他部分,然后每個部分是一個 npm repositories,可以單獨發(fā)布。而使用者通常在使用核心之后,可以自己選擇要使用哪些額外的部分。這種方式比較常見的如 babel 和它的插件,express 和它的中間件等。
作為一個多個包的項目的開發(fā)者/維護者,安裝依賴和發(fā)布都會是一件很麻煩的事情。因為 npm 只認根目錄的 package.json,那么就必須進入每個包進行 npm install。而發(fā)布時,也必須逐個修改每個包的版本號,并到每個目錄中進行 npm publish。
為了解決這個問題,社區(qū)一個叫做 lerna 的庫通過增加 lerna.json 來幫助我們管理所有的包。而在 yarn 這邊,引入了一個叫做工作區(qū)(workspace)的概念。因此這點上來說,應(yīng)該是 yarn 勝出了,不過 npm 配合 lerna 也能夠?qū)崿F(xiàn)這個需求。
安裝速度
npm 被詬病最多的問題之一就是其安裝速度。有些依賴很多的項目,安裝 npm 需要耗費 5-10 分鐘甚至更久。造成這個問題的本質(zhì)是 npm 采用串行的安裝方式,一個裝完再裝下一個。針對這一點,yarn 改為并行安裝,因此本質(zhì)上提升了安裝速度。
離線可用
yarn 默認支持離線安裝,即安裝過一次的包,會在電腦中保留一份(緩存位置可以通過 yarn config set yarn-offline-mirror 進行指定)。之后再次安裝,直接復(fù)制過來就可以。
npm 早先是全部通過網(wǎng)絡(luò)請求的(為了保持其時效性),但后期也借鑒了 yarn 創(chuàng)建了緩存。從 npm 5.x 開始我們可以使用 npm install xxx —prefer-offline 來優(yōu)先使用緩存(意思是緩存沒有再發(fā)送網(wǎng)絡(luò)請求),或者 npm install xxx —offline 來完全使用緩存(意思是緩存沒有就安裝失?。?/p>
控制臺信息
常年使用 npm 的同學(xué)知道,安裝完依賴后,npm 會列出一顆依賴樹。這顆樹通常會很長很復(fù)雜,我們不會過多關(guān)注。因此 yarn 精簡了這部分信息,直接輸出安裝結(jié)果。這樣萬一安裝過程中有報錯日志也不至于被刷掉。
不過 npm 5.x 也把這顆樹給去掉了。這又是一個互相借鑒提高的例子。
總結(jié)來說,yarn 的推出主要是針對 npm 早期版本的很多問題。但 npm 也意識到了來自競爭對手的強大壓力,因此在 5.x 開始逐個優(yōu)化看齊。從 5.x 開始就已經(jīng)和 yarn 不分伯仲了,因此如何選擇多數(shù)看是否有歷史包袱。如果是新項目的話,就看程序員個人的喜好了。
后記
本文從一個很小的問題開始,本意是想分享如何鑒別一個應(yīng)用應(yīng)該歸類在 dependencies 還是 devDependencies。后來層層深入,通過查閱資料發(fā)現(xiàn)了好多依賴相關(guān)的知識,例如其他幾種依賴,版本鎖定的機制以及和 yarn 的比較等等,最終變成一篇長文。希望通過本文能讓大家了解到依賴管理的一些大概,在之后的搬磚道路上能夠更加順利,也能反過來為整個生態(tài)的繁榮貢獻自己的力量。
參考文章
npm 官網(wǎng)的 dependencies 文檔
npm 官方微博的 peerDependencies 介紹 - 這篇有點老了,npm 依賴還是嵌套關(guān)系
Why use peerDependencies in npm for plugins - 比較簡略,不過說的在點上
Types of dependencies - 雖然是 yarn 的介紹,但概念和 npm 一致,且很精煉。
semver - npm 官方用來比較版本號的包
“npm install —save” No Longer Using Tildes - 早期的一篇博客,npm 對依賴版本號默認處理的變更
npm 官網(wǎng)的 package-lock.json 文檔
Workspaces in Yarn - yarn 官網(wǎng)介紹的 workspace 功能
Here’s what you need to know about npm 5 - 介紹 npm 5.x 的重要改進點
作者:@小蘑菇小哥
原文:https://zhuanlan.zhihu.com/p/56002037