在我看來初婆,nodejs 的成功原因除了它采用了前端 js 相同的語法链峭,直接吸引了一大波前端開發(fā)者作為初始用戶之外妒貌,它內(nèi)置的包管理器 npm 也居功至偉。npm 能夠很好的管理 nodejs 項(xiàng)目的依賴沈撞,也使得開發(fā)者發(fā)布自己的包變的異常容易。這樣一來雕什,不論你使用別人的包缠俺,還是自己發(fā)布包給別人使用显晶,成本都不大。這和我大學(xué)學(xué)習(xí)的 Java 1.x 相比就輕松愉快的多(現(xiàn)在 Java 已今非昔比晋修,我不敢亂評論)吧碾,開發(fā)者熱情高漲的話,整個生態(tài)就會更加活躍墓卦,進(jìn)步速度也就更加快了倦春。看一看 GitHub 上 JS 項(xiàng)目的占比落剪,再看看 npm 官網(wǎng)包的數(shù)量睁本,就能略知一二。
前陣子公司的一名新人問了我一個問題:如何區(qū)分項(xiàng)目的依賴中忠怖,哪些應(yīng)該放在 dependencies呢堰,而哪些應(yīng)該放在 devDependencies 呢?
其實(shí)這個問題我在早先也有過凡泣,所以非常能夠體會他的心情枉疼。為了防止誤人子弟,我查閱了一些資料鞋拟,發(fā)現(xiàn)其實(shí) nodejs 中總共有 5 種依賴:
dependencies (常用)
devDependencies (常用)
peerDependencies (不太常用)
bundledDependencies (我之前沒用過)
optionalDependencies (我之前沒用過)
所以我趁此機(jī)會骂维,整理了這篇文章,分享給更多仍有此迷茫的人們贺纲。
dependencies
這是 npm 最基本的依賴航闺,通過命令 npm i xxx -S 或者 npm i xxx —save 來安裝一個包,并且添加到 package.json 的 dependencies 里面(這里 i 是 install 的簡寫猴誊,兩者均可)潦刃。
如果直接只寫一個包的名字,則安裝當(dāng)前 npm registry 中這個包的最新版本懈叹;如果要指定版本的乖杠,可以把版本號寫在包名后面,例如 npm i webpack@3.0.0 —save项阴。
npm install 也支持 tag滑黔,tar 包地址等等,不過那些不太常用环揽,可以查看官方文檔略荡。
dependencies 比較簡單,我就不再多做解釋了歉胶。注意一點(diǎn):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ū)別于實(shí)際的依賴感耙。也就是說褂乍,在線上狀態(tài)不需要使用的依賴,就是開發(fā)依賴即硼。
再說意義逃片。為什么 npm 要把它單獨(dú)分拆出來呢?最終目的是為了減少 node_modules 目錄的大小以及 npm install 花費(fèi)的時間只酥。因?yàn)?npm 的依賴是嵌套的褥实,所以可能看上去 package.json 中只有幾個依賴,但實(shí)際上它又?jǐn)U散到 N 個裂允,而 N 個又?jǐn)U散到 N 平方個性锭,一層層擴(kuò)散出去,可謂子子孫孫無窮盡也叫胖。如果能夠盡量減少不使用的依賴,那么就能夠節(jié)省線上機(jī)器的硬盤資源她奥,也可以節(jié)省部署上線的時間瓮增。
在實(shí)際開發(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ù)處理器
這里指的是對源代碼進(jìn)行一定的處理掂骏,生成最終代碼的工具。比較典型的有 CSS 中的 less, stylus, sass, scss 等等厚掷,以及 JS 中的 coffee-script, babel 等等弟灼。它們做的事情雖然各有不同级解,但原理是一致的。
以 babel 為例田绑,常用的有兩種使用方式勤哗。其一是內(nèi)嵌在 webpack 或者 rollup 等構(gòu)件工具中,一般以 loader 或者 plugin 的形式出現(xiàn)掩驱,例如 babel-loader芒划。其二是單獨(dú)使用(小項(xiàng)目較多),例如 babel-cli昙篙。babel 還額外有自己的插件體系腊状,例如 xxx-babel-plugin。類似地苔可,less 也有與之對應(yīng)的 less-loader 和 lessc缴挖。這些都算作開發(fā)依賴。
在 babel 中還有一個注意點(diǎn)焚辅,那就是 babel-runtime 是 dependencies 而不是 devDependencies映屋。具體分析我在之前的 babel 文章中提過屑埋,就不再重復(fù)了瘦穆。
測試工具
嚴(yán)格來說,測試和開發(fā)并不是一個過程捷雕。但它們同屬于“線上狀態(tài)不需要使用的依賴”湾蔓,因此也就歸入開發(fā)依賴了瘫析。常用的如 chai, e2e, karma, coveralls 等等都在此列。
真的是開發(fā)才用的依賴包
最后一類比較雜默责,很難用一個大類囊括起來贬循,總之就是開發(fā)時需要使用的,而實(shí)際上線時要么是已經(jīng)打包成最終代碼了桃序,要么就是不需要使用了杖虾。比如 webpack-dev-server 支持開發(fā)熱加載,線上是不用的媒熊;babel-register 因?yàn)樾阅茉蛞膊荒苡迷诰€上奇适。其他還可能和具體業(yè)務(wù)相關(guān),就看各位開發(fā)者自己識別了芦鳍。
把依賴安裝成開發(fā)依賴嚷往,則可以使用 npm i -D 或者 npm i —save-dev 命令。
如果想達(dá)成剛才說的縮減安裝包的目的柠衅,可以使用命令 npm i —production 忽略開發(fā)依賴间影,只安裝依賴,這通常在線上機(jī)器(或者 QA 環(huán)境)上使用。因此還有一個最根本的識別依賴的方式魂贬,那就是用這條命令安裝巩割,如果項(xiàng)目跑不起來,那就是識別有誤了付燥。
peerDependencies
如果僅作為 npm 包的使用者宣谈,了解前兩項(xiàng)就足夠我們?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 也會作為依賴的一部分被安裝到使用者的項(xiàng)目中矮冬。
那我們還為什么需要這個 peerDependencies 呢?
根據(jù) npm 官網(wǎng)的文檔次哈,這個屬性主要用于插件類 (Plugin) 項(xiàng)目胎署。常規(guī)來說,為了插件生態(tài)的繁榮窑滞,插件項(xiàng)目一般會被設(shè)計(jì)地盡量簡單琼牧,通過數(shù)據(jù)結(jié)構(gòu)和固定的方法接口進(jìn)行耦合,而不會要求插件項(xiàng)目去依賴本體哀卫。例如我們比較熟悉的 express 中間件障陶,只要你返回一個方法 return function someMiddleware(req, res, next),它就成為了 express 中間件聊训,受本體調(diào)用,并通過三個參數(shù)把本體的信息傳遞過來恢氯,在插件內(nèi)部使用带斑。因此 express middleware 是不需要依賴 express 的。類似的情況還包括 Grunt 插件勋拟,Chai 插件和 Winston transports 等勋磕。
但很明顯,這類插件脫離本體是不能單獨(dú)運(yùn)行的敢靡。因此雖然插件不依賴本體挂滓,但想要自己能夠?qū)嶋H運(yùn)行起來,還得要求使用者把本體也納入依賴啸胧。這就是介于“不依賴”和“依賴”之間的中間狀態(tài)赶站,就是 peerDependencies 的主要使用場景幔虏。
例如我們提供一個包,其中的 package.json 如下:
{
"name": "my-greate-express-middleware",
"version": "1.0.0",
"peerDependencies": {
"express": "^3.0.0"
}
}
在 npm 3.x 及以后版本贝椿,如果使用者安裝了我們的插件想括,并且在自己的項(xiàng)目中沒有依賴 express 時,會在最后彈出一句提示烙博,表示有一個包需要您依賴 express 3.x瑟蜈,因此您必須自己額外安裝。另外如果使用者依賴了不同版本的 express渣窜,npm 也會彈出提示铺根,讓開發(fā)者自己決斷是否繼續(xù)使用這個包。
bundledDependencies
這是一種比起 peerDependencies 更加少見的依賴項(xiàng)乔宿,也可以寫作 bundleDependencies (bundle 后面的 d 省略)位迂。和上述的依賴不同,這個屬性并不是一個鍵值對的對象予颤,而是一個數(shù)組囤官,元素為表示包的名字的字符串。例如
{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized", "super-streams"
]
}
當(dāng)我們希望以壓縮包的方式發(fā)布項(xiàng)目時(比如你不想放到 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ā)布的話饲常,這個屬性不會生效蹲堂;而作為使用方的話,大部分項(xiàng)目也都是從 npm registry 中搜索并引用依賴的贝淤,所以使用到的場景也相當(dāng)少柒竞。
optionalDependencies
這也是一種很少見的依賴項(xiàng),從名字可以得知播聪,它描述一種”可選“的依賴朽基。和 dependencies 相比,它的不同點(diǎn)有:
即使這個依賴安裝失敗离陶,也不影響整個安裝過程
程序應(yīng)該自己處理安裝失敗時的情況
關(guān)于第二點(diǎn)稼虎,我想表達(dá)的意思是:
let foo
let fooVersion
try {
foo = require('foo')
fooVersion = require('foo/package.json').version
} catch (e) {
// 安裝依賴失敗時找不到包,需要自己處理
}
// 如果安裝的依賴版本不符合實(shí)際要求招刨,我們也需要自己處理霎俩,當(dāng)做沒安裝到
if (!isSupportVersion(fooVersion)) {
foo = null
}
// 如果安裝成功,執(zhí)行某些操作
if (foo) {
foo.doSomeThing()
}
需要注意的是,如果一個依賴同時出現(xiàn)在 dependencies 和 optionalDependencies 中打却,那么 optionalDependencies 會獲得更高的優(yōu)先級杉适,可能造成預(yù)期之外的效果,因此最好不要出現(xiàn)這種情況学密。
在實(shí)際項(xiàng)目中淘衙,如果某個包已經(jīng)失效,我們通常會尋找他的替代者腻暮,或者壓根換一個實(shí)現(xiàn)方案彤守。使用這種”不確定“的依賴,一方面會增加代碼中的判斷哭靖,增加邏輯的復(fù)雜度具垫;另一方面也會大大降低測試覆蓋率,增加構(gòu)造測試用例的難度试幽。所以我不建議使用這個依賴項(xiàng)筝蚕,如果你原先就不知道有這個,那就繼續(xù)當(dāng)做不知道吧铺坞。
版本號的寫法
如上的 5 種依賴起宽,除了 bundledDependencies,其他四種都是需要寫版本號的济榨。如果作為使用者坯沪,使用 npm i —save 或者 npm i —save-dev 會自動生成依賴的版本號,不過我建議大家還是略微了解下常用的版本號的寫法擒滑。
首先我們得搞清三位版本號的定義腐晾,以 “a.b.c” 舉例,它們的含義是:
a - 主要版本(也叫大版本丐一,major version)
大版本的升級很可能意味著與低版本不兼容的 API 或者用法藻糖,是一次顛覆性的升級(想想 webpack 3 -> 4)。
b - 次要版本(也叫小版本库车,minor version)
小版本的升級應(yīng)當(dāng)兼容同一個大版本內(nèi)的 API 和用法巨柒,因此應(yīng)該對開發(fā)者透明。所以我們通常只說大版本號柠衍,很少會精確到小版本號洋满。
特殊情況是如果大版本號是 0 的話,意味著整個包處于內(nèi)測狀態(tài)拧略,所以每個小版本之間也可能會不兼容。所以在選擇依賴時瘪弓,盡量避開大版本號是 0 的包垫蛆。
c - 補(bǔ)丁 (patch)
一般用于修復(fù) bug 或者很細(xì)微的變更,也需要保持向前兼容。
之后我們看一下常規(guī)的版本號寫法:
“1.2.3” - 無視更新的精確版本號
表示只依賴這個版本袱饭,任何其他版本號都不匹配川无。在一些比較重要的線上項(xiàng)目中,我比較建議使用這種方式鎖定版本虑乖。前陣子的 npm 挖礦以及 ant-design 彩蛋懦趋,其實(shí)都可以通過鎖定版本來規(guī)避問題(彩蛋略難一些,挖礦是肯定可以規(guī)避)疹味。
“^1.2.3” - 兼具更新和安全的折中考慮
這是 npm i xxx —save 之后系統(tǒng)生成的默認(rèn)版本號(^ 加上當(dā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”。因?yàn)樽钭髠?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 就表示項(xiàng)目正式發(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” 是相同的算法。比較而言毯炮,~ 更加謹(jǐn)慎逼肯。當(dāng)首位是 0 并且列出了第二位的時候,兩者是等價的否副,例如 ~0.2.3 和 ^0.2.3汉矿。
在 nodejs 的上古版本(v0.10.26,2014年2月發(fā)布的)备禀,npm i —save 默認(rèn)使用的是 ~洲拇,現(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”。
因?yàn)槲挥诮Y(jié)尾的通配符一般可以省略鹏倘,而常規(guī)也不太可能像正則那樣把匹配符寫在中間薯嗤,所以大多數(shù)情況通配符都可以省略。使用最多的還是匹配所有版本的 * 這個了纤泵。
“1.2.3-beta.2” - 帶預(yù)發(fā)布關(guān)鍵詞的骆姐,如 alpha, beta, rc, pr 等
先說預(yù)發(fā)布的定義,我們需要以包開發(fā)者的角度來考慮這個問題捏题。假設(shè)當(dāng)前線上版本是 “1.2.3”玻褪,如果我作了一些改動需要發(fā)布版本 “1.2.4”,但我不想直接上線(因?yàn)槭褂?“~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” 都是認(rèn)可的跪楞。
“~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)校焦,或(表達(dá)式1 || 表達(dá)式2)等等赊抖,因?yàn)橛玫牟欢啵@里不再展開寨典。詳細(xì)的文檔可以參見 semver氛雪,它同時也是一個 npm 包,可以用來比較兩個版本號的大小耸成,以及是否符合要求等报亩。
其他寫法
除了版本號,依賴包還可以通過如下幾種方式來進(jìn)行依賴(使用的也不算太多井氢,可以粗略了解一下):
Tag
除了版本號之外弦追,通常某個包還可能會有 Tag 來標(biāo)識一些里程碑意義的版本。例如 express@next 表示即將到來的下一個大版本(可提前體驗(yàn))毙沾,而 some-lib@latest 等價于 some-lib骗卜,因?yàn)?latest 是默認(rèn)存在并指向最新版本的。其他的自定義 Tag 都可以由開發(fā)者通過 npm tag 來指定左胞。
因?yàn)?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鸟顺。既然講到了依賴,我就額外擴(kuò)展一下這個 package-lock.json 的結(jié)構(gòu)和作用器虾。
package-lock.json 內(nèi)部記錄的是每一個依賴的實(shí)際安裝信息讯嫂,例如名字,安裝的版本號兆沙,安裝的地址 (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)建項(xiàng)目時)曲楚,就按照它的記錄逐層遞歸安裝依賴,并生成一個 package-lock.json 文件褥符。如果發(fā)現(xiàn)根目錄下兩者皆有(這通常發(fā)生在開發(fā)同事把項(xiàng)目 checkout 到本地之后)龙誊,則 npm 會比較兩者。如果兩者所示含義不同喷楣,則以 package.json 為準(zhǔn)趟大,并更新 package-lock.json鹤树;否則就直接按 package-lock 所示的版本號安裝。
它存在的意義主要有 4 點(diǎn):
在團(tuán)隊(duì)開發(fā)中逊朽,確保每個團(tuán)隊(duì)成員安裝的依賴版本是一致的罕伯。否則因?yàn)橐蕾嚢姹静灰恢聦?dǎo)致的效果差異,一般很難查出來叽讳。
通常 node_modules 目錄都不會提交到代碼庫捣炬,因此要回溯到某一天的狀態(tài)是不可能的。但現(xiàn)在 node_modules 目錄和 package.json 以及 package-lock.json 是一一對應(yīng)的绽榛。所以如果開發(fā)者想回退到之前某一天的目錄狀態(tài),只要把這兩個文件回退到那一天的狀態(tài)婿屹,再 npm i 就行了灭美。
因?yàn)?package-lock.json 已經(jīng)足以描述 node_modules 的大概信息(尤其是深層嵌套依賴),所以通過這個文件就可以查閱某個依賴包是被誰依賴進(jìn)來的昂利,而不用去翻 node_modules 目錄(事實(shí)上現(xiàn)在目錄結(jié)構(gòu)打平而非嵌套届腐,翻也翻不出來了)
在安裝過程中,npm 內(nèi)部會檢查 node_modules 目錄中已有的依賴包蜂奸,并和 package-lock.json 進(jìn)行比較犁苏。如果重復(fù),則跳過安裝扩所,能大大優(yōu)化安裝時間围详。
npm 官網(wǎng)建議:把 package-lock.json 一起提交到代碼庫中,不要 ignore祖屏。但是在執(zhí)行 npm publish 的時候助赞,它會被忽略而不會發(fā)布出去。
yarn
從 nodejs 誕生之初袁勺,npm 就是其內(nèi)置的包管理器雹食,并且以其易于使用,易于發(fā)布的特點(diǎn)極大地助推了 nodejs 在開發(fā)者中的流行和使用期丰。但事物總有其兩面性群叶,易于發(fā)布的確大大推動生態(tài)的繁榮,但同時也降低了發(fā)布的門檻钝荡。包的數(shù)量在突飛猛進(jìn)街立,一個項(xiàng)目的依賴項(xiàng)從幾個上升到幾十個,再加上內(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)點(diǎn)砾省,例如上面談?wù)摰?package-lock.json鸡岗,最早就出自 yarn.lock。所以我們來粗略比較一下兩者的區(qū)別编兄,以及我們應(yīng)當(dāng)如何選擇轩性。
版本鎖定
這個在 package-lock.json 已經(jīng)討論過了,不再贅述狠鸳。 在這個功能點(diǎn)上揣苏,兩者都已具備。
多個包的管理 (monorepositories)
一個包在 npm 中可以被稱為 repositories件舵。通常我們發(fā)布某個功能卸察,其實(shí)就是發(fā)布一個包,由它提供各種 API 來提供功能铅祸。但隨著功能越來越復(fù)雜以及按需加載坑质,把所有東西全部放到一個包中發(fā)布已經(jīng)不夠優(yōu)秀,因此出現(xiàn)了多個包管理的需求临梗。
通常一個類庫會把自己的功能分拆為核心部分和其他部分洪乍,然后每個部分是一個 npm repositories,可以單獨(dú)發(fā)布夜焦。而使用者通常在使用核心之后壳澳,可以自己選擇要使用哪些額外的部分。這種方式比較常見的如 babel 和它的插件茫经,express 和它的中間件等巷波。
作為一個多個包的項(xiàng)目的開發(fā)者/維護(hù)者,安裝依賴和發(fā)布都會是一件很麻煩的事情卸伞。因?yàn)?npm 只認(rèn)根目錄的 package.json抹镊,那么就必須進(jìn)入每個包進(jìn)行 npm install。而發(fā)布時荤傲,也必須逐個修改每個包的版本號垮耳,并到每個目錄中進(jìn)行 npm publish。
為了解決這個問題,社區(qū)一個叫做 lerna 的庫通過增加 lerna.json 來幫助我們管理所有的包终佛。而在 yarn 這邊俊嗽,引入了一個叫做工作區(qū)(workspace)的概念。因此這點(diǎn)上來說铃彰,應(yīng)該是 yarn 勝出了绍豁,不過 npm 配合 lerna 也能夠?qū)崿F(xiàn)這個需求。
安裝速度
npm 被詬病最多的問題之一就是其安裝速度牙捉。有些依賴很多的項(xiàng)目竹揍,安裝 npm 需要耗費(fèi) 5-10 分鐘甚至更久。造成這個問題的本質(zhì)是 npm 采用串行的安裝方式邪铲,一個裝完再裝下一個芬位。針對這一點(diǎn),yarn 改為并行安裝带到,因此本質(zhì)上提升了安裝速度昧碉。
離線可用
yarn 默認(rèn)支持離線安裝,即安裝過一次的包阴孟,會在電腦中保留一份(緩存位置可以通過 yarn config set yarn-offline-mirror 進(jìn)行指定)。之后再次安裝税迷,直接復(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 來完全使用緩存(意思是緩存沒有就安裝失敗)毕泌。
控制臺信息
常年使用 npm 的同學(xué)知道喝检,安裝完依賴后,npm 會列出一顆依賴樹撼泛。這顆樹通常會很長很復(fù)雜挠说,我們不會過多關(guān)注。因此 yarn 精簡了這部分信息愿题,直接輸出安裝結(jié)果损俭。這樣萬一安裝過程中有報(bào)錯日志也不至于被刷掉。
不過 npm 5.x 也把這顆樹給去掉了潘酗。這又是一個互相借鑒提高的例子杆兵。
總結(jié)來說,yarn 的推出主要是針對 npm 早期版本的很多問題仔夺。但 npm 也意識到了來自競爭對手的強(qiáng)大壓力琐脏,因此在 5.x 開始逐個優(yōu)化看齊。從 5.x 開始就已經(jīng)和 yarn 不分伯仲了,因此如何選擇多數(shù)看是否有歷史包袱日裙。如果是新項(xiàng)目的話吹艇,就看程序員個人的喜好了。
后記
本文從一個很小的問題開始阅签,本意是想分享如何鑒別一個應(yīng)用應(yīng)該歸類在 dependencies 還是 devDependencies掐暮。后來層層深入,通過查閱資料發(fā)現(xiàn)了好多依賴相關(guān)的知識政钟,例如其他幾種依賴路克,版本鎖定的機(jī)制以及和 yarn 的比較等等,最終變成一篇長文养交。希望通過本文能讓大家了解到依賴管理的一些大概精算,在之后的搬磚道路上能夠更加順利,也能反過來為整個生態(tài)的繁榮貢獻(xiàn)自己的力量碎连。
參考文章
npm 官網(wǎng)的 dependencies 文檔
npm 官方微博的 peerDependencies 介紹 - 這篇有點(diǎn)老了灰羽,npm 依賴還是嵌套關(guān)系
Why use peerDependencies in npm for plugins - 比較簡略,不過說的在點(diǎn)上
Types of dependencies - 雖然是 yarn 的介紹鱼辙,但概念和 npm 一致廉嚼,且很精煉。
semver - npm 官方用來比較版本號的包
“npm install —save” No Longer Using Tildes - 早期的一篇博客倒戏,npm 對依賴版本號默認(rèn)處理的變更
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 的重要改進(jìn)點(diǎn)