每次克隆下別人的代碼后,執(zhí)行的第一步就是 npm install 安裝依賴包,安裝成功后所有的包都會放在項目的 node_modules 文件夾下,也會自動生成 package-lock.json 文件。有沒有好奇過 node_modules 下的文件都是啥? package-lock.json 文件的作用是啥?
本文主要解決以下幾個問題:
- package.json中的dependencies和devDependencies的區(qū)別是啥,peerDependencies、bundledDependencies、optionalDependencies又是啥?
- 為什么有的命令寫在package.json中的script中就可以執(zhí)行,但是通過命令行直接執(zhí)行就不行?
- 為什么需要package-lock.json文件?
- 一個包在項目中有可能需要不同的版本,最后安裝到根目錄node_modules中的具體是哪個版本?
帶著這幾個問題,我們先從package.json文件說起。
package.json
https://docs.npmjs.com/cli/v6/configuring-npm/package-json#people-fields-author-contributors官方文檔中列出了好多屬性,感興趣的可以一個個看一遍。下面只列出其中幾個比較常用且重要的屬性。name & version
如果想要發(fā)布一個npm包,name和version屬性是必須的。他們兩個組合會形成一個唯一的標識來表名當前包。以后每更新一次包,version就需要進行相應的更改。如果你不打算發(fā)布包,只想在本地使用,這兩個字段不是必須的。- 長度不能超過214個字符(對于有scoped的包,該限制包括scoped字段)(什么是Scoped packages?)
- 有作用域的包名字可以以.或者_開頭,沒有作用域限制的不可
版本號需要符合semver(語義化版本號)規(guī)則,具體版本格式為:主版本號.次版本號.修訂號, 如1.1.0。- 主版本號(major):做了不兼容的 API 修改
當有一些先行版本需要發(fā)布時,可以在主版本號.次版本號.修訂號之后加上一個中劃線和標識符如alpha(內部版本)、beta(公測版本)、rc(候選版本)等來表明。- 最新的alpha版本:3.0.0-alpha.13
可以通過npm install semver來檢查一個包的命名是否符合semver規(guī)則。有關semver具體的說明可以看這里https://docs.npmjs.com/cli/v6/using-npm/semverdependencies & devDependencies
dependencies和devDependencies大家應該都不陌生,通過npm install xx --save安裝的包會寫入dependencies中,通過npm install xx --save-dev安裝的包會寫入devDependencies。dependencies中的包是生產環(huán)境的依賴,屬于線上代碼的一部分,比如vue、axios、veui等。devDependencies中的包是開發(fā)環(huán)境的依賴,只是在本地開發(fā)的時候需要依賴這里的包,比如 vue-loader、eslint等。我們平時用的npm install命令既會安裝dependencies中的包,也會安裝devDependencies中的包。如果只想安裝dependencies中包,可以使用npm install --production或者將NODE_ENV環(huán)境變量設置為production,通常在生成環(huán)境我們會這么用。需要注意的是,一個模塊會不會被打包取決于我們在項目中是否引入了該模塊,跟該模塊放在dependencies中還是devDependencies并沒有關系。peerDependencies & bundledDependencies & optionalDependencies
這三個屬性在平時我們的項目開發(fā)中都用不到。不同于dependencies & devDependencies面向的是包的使用者,peerDependencies & optionalDependencies & bundledDependencies這三個屬性是面向包的發(fā)布者。我們在一些node_modules包的package.json中可以看到peerDependencies,它用來表明如果你想要使用此插件,此插件要求宿主環(huán)境所安裝的包。比如項目中用到的veui1.0.0-alpha.24版本中:"peerDependencies": {
"vue": "^2.5.16"
}
這表明如果你想要使用veui的1.0.0-alpha.24版本,所要求的vue版本需要滿足>=2.5.16且<3.0.0。在npm3.x以上版本中,如果安裝結束后宿主環(huán)境沒有滿足peerDependencies中的要求,會在控制臺打印出警告信息。當我們想在本地保留一個npm完整的包或者想生成一個壓縮文件來獲取npm包的時候,會用到bundledDependencies。本地使用npm pack打包時會將bundledDependencies中依賴的包一同打包,當npm install時相應的包會同時被安裝。需要注意的是,bundledDependencies中的包不應該包含具體的版本信息,具體的版本信息需要在dependencies中指定。{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized",
"super-streams"
]
}
當我們執(zhí)行npm pack后會生成awesome-web-framework-1.0.0.tgz文件。該文件中包含renderized和super-streams這兩個依賴,當執(zhí)行npm install awesome-web-framework-1.0.0.tgz下載包時,這兩個依賴會被安裝。當我們使用npm publish來發(fā)布包的話,這個屬性不會起作用。從名字上就可以看出,這是可選依賴。如果有包寫在optionalDependencies中,即使npm找不到或者安裝失敗了也不會影響安裝過程。需要注意的是,optionalDependencies中的配置會覆蓋dependencies中的配置,所以不要將同一個包同時放在這兩個里面。如果使用了optionalDependencies,一定記得要在項目中做好異常處理,獲取不到的情況下應該怎么辦。scripts
定義在scripts中的命令,我們通過npm run <command>就可以執(zhí)行。npm run <command>是npm run-script <command>的簡寫。如果不加command,則會列出當前目錄下可執(zhí)行的所有腳本。test、start、restart、stop這幾個命令執(zhí)行時可以不加run,直接npm test、npm start、npm restart、npm stop調用即可。env是一個內置的命令,可以通過npm run env可以獲取到腳本運行時的所有環(huán)境變量。自定義的env命令會覆蓋內置的env命令。之前開發(fā)中遇到一種情況,比如我們想本地通過http-server啟動一個服務器,如果事先沒有全局安裝過http-server包,只是安裝在對應項目的node_modules中。在命令行中輸入http-server會報command not found,但是如果我們在scripts中增加如下一條命令就可以執(zhí)行成功。scripts: {
"server": "http-server",
"eslint": "eslint --ext .js"
}
為什么同樣的命令寫在scripts中就可以成功,但是在命令行中執(zhí)行就不行呢?這是因為npm run命令會將node_modules/.bin/加入到shell的環(huán)境變量PATH中,這樣即使局部安裝的包也可以直接執(zhí)行而不用加node_modules/.bin/前綴。當執(zhí)行結束后,再將其刪除。首先要明確什么是環(huán)境變量。環(huán)境變量就是系統在執(zhí)行一個程序,但是沒有明確表明該程序所在的完整路徑時,需要去哪里尋找該程序。對于局部安裝的包,拿eslint來說,npm會在本地項目./node_modules/.bin目錄下創(chuàng)建一個指向./node_moudles/eslint/bin/eslint.js名為eslint的軟鏈接,即執(zhí)行./node_modules/.bin/eslint實際上是執(zhí)行./node_moudles/eslint/bin/eslint.js。而當我們執(zhí)行npm run eslint的時候,node_modules/.bin/會被加入到環(huán)境變量PATH中,實際上執(zhí)行的是./node_modules/.bin/eslint,這樣就串起來了。首先看一下系統的環(huán)境變量。直接執(zhí)行env即可。然后在當前項目目錄下通過npm run env查看腳本運行時的環(huán)境變量。通過對比可以發(fā)現,運行時的PATH多了兩個環(huán)境變量。即npm指令的路徑和項目/node_modules/.bin的路徑。以上就是package.json中常用 & 重要的幾個屬性,接下來我們來看一看package-lock.json。
package-lock.json
對于npm,package.json文件可以看成它的輸入,node_modules可以做為它的輸出。在理想情況下,npm應該是一個純函數,無論何時執(zhí)行相同的package.json文件都應該產生完全相同的node_modules樹。在一些情況下,這確實可以做到。但是在大多情況下,都實現不了。主要有以下幾個原因:- 使用者的npm版本有可能不同,不同的npm版本有著不同的安裝算法
- 自上次安裝之后,有些符合semver-range的包已經有新的版本發(fā)布。這樣再有別人安裝的時候,會安裝符合要求的最新版本。比如引入vue包:vue:^2.6.1。A小伙伴下載的時候是2.6.1,過一陣有另一個小伙伴B入職在安裝包的時候,vue已經升級到2.6.2,這樣npm就會下載2.6.2的包安裝在他的本地
- 針對第二點,一個解決辦法是固定自己引入的包的版本,但是通常我們不會這么做。即使這樣做了,也只能保證自己引入的包版本固定,也無法保證包的依賴的升級。比如vue其中的一個依賴lodash,lodash:^4.17.4,A下載的是4.17.4, B下載的時候有可能已經升級到了4.17.21
為了解決上述問題,npm5.x開始增加了package-lock.json文件。每當npm install執(zhí)行的時候,npm都會產生或者更新package-lock.json文件。package-lock.json文件的作用就是鎖定當前的依賴安裝結構,與node_modules中下所有包的樹狀結構一一對應。有了這個package-lock.json文件,就能保證團隊每個人安裝的包版本都是相同的,不會出現有些包升級造成我這好使別人那不好使的兼容性問題。下面是less的package-lock.json文件結構:"less": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/less/-/less-3.13.1.tgz",
"integrity": "sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw==",
"dev": true,
"requires": {
"copy-anything": "^2.0.1",
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"native-request": "^1.0.5",
"source-map": "~0.6.0",
"tslib": "^1.10.0"
},
dependencies: {
"copy-anything": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz",
"integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==",
"dev": true,
"requires": {
"is-what": "^3.12.0"
}
}
}
}
- integrity:一個hash值,用來校驗包的完整性
- dev:布爾值,如果為true,表明此包如果不是頂層模塊的一個開發(fā)依賴(寫在devDependencies中),就是一個傳遞依賴(如上面less中的copy-anything)。
- requires: 對應子依賴的依賴,與依賴包的package.json中dependencies的依賴項相同
- dependencies:結構與外層結構相同,存在于包自己的node_modules中的依賴(不是所有的包都有,當子依賴的依賴版本與根目錄的node_modules中的依賴沖突時,才會有)
通過分析上面的package-lock.json文件,也許會有一個問題。為什么有的包可以被安裝在根目錄的node_modules中,有的包卻只能安裝在自己包下面的node_modules中?這就涉及到npm的安裝機制。npm從3.x開始,采用了扁平化的方式來安裝node_modules。在安裝時,npm會遍歷整個依賴樹,不管是項目的直接依賴還是子依賴的依賴,都會優(yōu)先安裝在根目錄的node_modules中。遇到相同名稱的包,如果發(fā)現根目錄的node_modules中存在但是不符合semver-range,會在子依賴的node_modules中安裝符合條件的包。- 獲取package.json文件和分類完畢的元數據信息并把元數據信息插入到克隆樹中
- 遍歷克隆樹,檢測是否有丟失的依賴。如果有,把他們添加到克隆樹中,依賴會盡可能的添加到最高層
- 比較原始樹和克隆樹,列出將原始樹轉換為克隆樹所要采取的具體步驟
- 執(zhí)行,包括install, update, remove and move
以npm官網的例子舉例,假設package{dep}結構代表包和包的依賴,現有如下結構:A{B,C}, B{C}, C{D},按照上述算法執(zhí)行完畢后,生成的node_modules結構如下:對于B,C被安裝在頂層很好理解,因為是A的直接依賴。但是B又依賴C,安裝C的時候發(fā)現頂層已經有C了,所以不會在B自己的node_modules中再次安裝。C又依賴D,安裝D的時候發(fā)現根目錄并沒有D,所以會把D提升到頂層。換成A{B,C}, B{C,D@1}, C{D@2}這樣的依賴關系后,產生的結構如下:A
+-- B
+-- C
+-- D@2
+-- D@1
B又依賴了D@1,安裝時發(fā)現根目錄的node_modules沒有,所以會把D@1安裝在頂層。C依賴了D@2,安裝D@2時,因為npm不允許同層存在兩個名字相同的包,這樣就與跟目錄node_modules的D@1沖突,所以會把D@2安裝在C自己的node_modules中。模塊的安裝順序決定了當有相同的依賴時,哪個版本的包會被安裝在頂層。首先項目中主動引入的包肯定會被安裝在頂層,然后會按照包名稱排序(a-z)進行依次安裝,跟包在package.json中寫入的順序無關。因此,如果上述將B{C,D@1}換成E{C,D@1},那么D@2將會被安裝在頂層。有一種情況,當我們項目中所引用的包版本較低,比如A{B@1,C},而C所需要的是C{B@2}版本,現在的結構應該如下:有一天我們將項目中的B升級到B@2,理想情況下的結構應該如下:但是現在package-lock.json文件的結構卻是這樣的:B@2不僅存在于根目錄的node_modules下,C下也同樣存在。這時需要我們手動執(zhí)行npm dedupe進行去重操作,執(zhí)行完成后會發(fā)現C下面的B@2會消失。大家可以在自己的項目中試一試,優(yōu)化一下package-lock.json文件的結構。以下是在我的項目中執(zhí)行npm dedupe的結果:removed 41 packages, moved 15 packages and audited 1994 packages in 18.538s
在npm5.x之前,可以手動通過npm shrinkwrap生成npm-shrinkwrap.json文件,與package-lock.json文件的作用相同。當項目中同時存在npm-shrinkwrap.json和package-lock.json,將以npm-shrinkwrap.json為主。本文只是一些理論基礎,之后會介紹一些npm源碼相關的知識。
點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺“回復“ 入群 ”即可加入我們的技術交流群,收獲更多的技術文章~