每次克隆下別人的代碼後,執行的第一步就是npm install
安裝依賴包,安裝成功後全部的包都會放在項目的node_modules
文件夾下,也會自動生成package-lock.json
文件。有沒有好奇過node_modules
下的文件都是啥?package-lock.json
文件的做用是啥?前端
本文主要解決如下幾個問題:vue
package.json
中的dependencies
和devDependencies
的區別是啥,peerDependencies
、bundledDependencies
、optionalDependencies
又是啥?package.json
中的script
中就能夠執行,可是經過命令行直接執行就不行?package-lock.json
文件?node_modules
中的具體是哪一個版本?帶着這幾個問題,咱們先從package.json
文件提及。node
最靠譜的官方文檔請點這裏ios
官方文檔中列出了好多屬性,感興趣的能夠一個個看一遍。下面只列出其中幾個比較經常使用且重要的屬性。web
若是想要發佈一個npm
包,name
和version
屬性是必須的。他們兩個組合會造成一個惟一的標識來表名當前包。之後每更新一次包,version
就須要進行相應的更改。若是你不打算髮布包,只想在本地使用,這兩個字段不是必須的。算法
name
字段命名的規則以下:shell
version
字段npm
版本號須要符合semver
(語義化版本號)規則,具體版本格式爲:主版本號.次版本號.修訂號
, 如1.1.0。json
當有一些先行版本須要發佈時,能夠在主版本號.次版本號.修訂號
以後加上一個中劃線和標識符如alpha(內部版本)、beta(公測版本)、rc(候選版本)等來代表。axios
以vue的版本爲例:
能夠經過npm install semver
來檢查一個包的命名是否符合semver
規則。有關semver具體的說明能夠看這裏
dependencies
和devDependencies
你們應該都不陌生,經過npm install xx --save
安裝的包會寫入dependencies
中,經過npm install xx --save-dev
安裝的包會寫入devDependencies
。
dependencies
中的包是生產環境的依賴,屬於線上代碼的一部分,好比vue
、axios
、veui
等。devDependencies
中的包是開發環境的依賴,只是在本地開發的時候須要依賴這裏的包,好比 vue-loader
、eslint
等。
咱們平時用的npm install
命令既會安裝dependencies
中的包,也會安裝devDependencies
中的包。若是隻想安裝dependencies
中包,可使用npm install --production
或者將NODE_ENV
環境變量設置爲production
,一般在生成環境咱們會這麼用。
須要注意的是,一個模塊會不會被打包取決於咱們在項目中是否引入了該模塊,跟該模塊放在dependencies
中仍是devDependencies
並無關係。
這三個屬性在平時咱們的項目開發中都用不到。不一樣於dependencies
& devDependencies
面向的是包的使用者,peerDependencies
& optionalDependencies
& bundledDependencies
這三個屬性是面向包的發佈者。
peerDependencies
咱們在一些node_modules
包的package.json
中能夠看到peerDependencies
,它用來代表若是你想要使用此插件,此插件要求宿主環境所安裝的包。好比項目中用到的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
以上版本中,若是安裝結束後宿主環境沒有知足peerDependencies
中的要求,會在控制檯打印出警告信息。
bundledDependencies
當咱們想在本地保留一個npm
完整的包或者想生成一個壓縮文件來獲取npm
包的時候,會用到bundledDependencies
。本地使用npm pack
打包時會將bundledDependencies
中依賴的包一同打包,當npm install
時相應的包會同時被安裝。須要注意的是,bundledDependencies
中的包不該該包含具體的版本信息,具體的版本信息須要在dependencies
中指定。
例如一個package.json
文件以下:
{ "name": "awesome-web-framework", "version": "1.0.0", "bundledDependencies": [ "renderized", "super-streams" ] }
當咱們執行npm pack
後會生成awesome-web-framework-1.0.0.tgz
文件。該文件中包含renderized
和super-streams
這兩個依賴,當執行npm install awesome-web-framework-1.0.0.tgz
下載包時,這兩個依賴會被安裝。
當咱們使用npm publish
來發布包的話,這個屬性不會起做用。
optionalDependencies
從名字上就能夠看出,這是可選依賴。若是有包寫在optionalDependencies
中,即便npm
找不到或者安裝失敗了也不會影響安裝過程。須要注意的是,optionalDependencies
中的配置會覆蓋dependencies
中的配置,因此不要將同一個包同時放在這兩個裏面。
若是使用了optionalDependencies
,必定記得要在項目中作好異常處理,獲取不到的狀況下應該怎麼辦。
定義在scripts
中的命令,咱們經過npm run <command>
就能夠執行。npm run <command>
是npm run-script <command>
的簡寫。若是不加command
,則會列出當前目錄下可執行的全部腳本。
test
、start
、restart
、stop
這幾個命令執行時能夠不加run
,直接npm test
、npm start
、npm restart
、npm stop
調用便可。
env
是一個內置的命令,能夠經過npm run env
能夠獲取到腳本運行時的全部環境變量。自定義的env
命令會覆蓋內置的env
命令。
以前開發中遇到一種狀況,好比咱們想本地經過http-server
啓動一個服務器,若是事先沒有全局安裝過http-server
包,只是安裝在對應項目的node_modules
中。在命令行中輸入http-server
會報command not found
,可是若是咱們在scripts
中增長以下一條命令就能夠執行成功。
scripts: { "server": "http-server", "eslint": "eslint --ext .js" }
爲何一樣的命令寫在scripts
中就能夠成功,可是在命令行中執行就不行呢?這是由於npm run
命令會將node_modules/.bin/
加入到shell
的環境變量PATH
中,這樣即便局部安裝的包也能夠直接執行而不用加node_modules/.bin/
前綴。當執行結束後,再將其刪除。
是否是仍是沒明白,下面咱們來具體分析一下。
首先要明確什麼是環境變量。環境變量就是系統在執行一個程序,可是沒有明確代表該程序所在的完整路徑時,須要去哪裏尋找該程序。
對於局部安裝的包,拿eslint
來講,npm
會在本地項目./node_modules/.bin
目錄下建立一個指向./node_moudles/eslint/bin/eslint.js
名爲eslint
的軟連接,即執行./node_modules/.bin/eslint
其實是執行./node_moudles/eslint/bin/eslint.js
。而當咱們執行npm run eslint
的時候,node_modules/.bin/
會被加入到環境變量PATH
中,實際上執行的是./node_modules/.bin/eslint
,這樣就串起來了。
理論說完以後,咱們來實際驗證一下。
首先看一下系統的環境變量。直接執行env
便可。
而後在當前項目目錄下經過npm run env
查看腳本運行時的環境變量。
經過對比能夠發現,運行時的PATH
多了兩個環境變量。即npm
指令的路徑和項目/node_modules/.bin
的路徑。
以上就是package.json
中經常使用 & 重要的幾個屬性,接下來咱們來看一看package-lock.json
。
對於npm
,package.json
文件能夠當作它的輸入,node_modules
能夠作爲它的輸出。在理想狀況下,npm
應該是一個純函數,不管什麼時候執行相同的package.json
文件都應該產生徹底相同的node_modules
樹。在一些狀況下,這確實能夠作到。可是在大多狀況下,都實現不了。主要有如下幾個緣由:
npm
版本有可能不一樣,不一樣的npm
版本有着不一樣的安裝算法semver-range
的包已經有新的版本發佈。這樣再有別人安裝的時候,會安裝符合要求的最新版本。好比引入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
執行的時候,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" } } } }
hash
值,用來校驗包的完整性true
,代表此包若是不是頂層模塊的一個開發依賴(寫在devDependencie
s中),就是一個傳遞依賴(如上面less
中的copy-anything
)。package.json
中dependencies
的依賴項相同node_modules
中的依賴(不是全部的包都有,當子依賴的依賴版本與根目錄的node_modules
中的依賴衝突時,纔會有)經過分析上面的package-lock.json
文件,也許會有一個問題。爲何有的包能夠被安裝在根目錄的node_modules
中,有的包卻只能安裝在本身包下面的node_modules
中?這就涉及到npm
的安裝機制。
npm從3.x
開始,採用了扁平化的方式來安裝node_modules
。在安裝時,npm會遍歷整個依賴樹,不論是項目的直接依賴仍是子依賴的依賴,都會優先安裝在根目錄的node_modules
中。遇到相同名稱的包,若是發現根目錄的node_modules
中存在可是不符合semver-range
,會在子依賴的node_modules
中安裝符合條件的包。
具體的安裝算法以下:
node_modules
樹node_modules
樹package.json
文件和分類完畢的元數據信息並把元數據信息插入到克隆樹中install
, update
, remove
and move
以npm
官網的例子舉例,假設package{dep}
結構表明包和包的依賴,現有以下結構:A{B,C}
, B{C}
, C{D}
,按照上述算法執行完畢後,生成的node_modules
結構以下:
A +-- B +-- C +-- D
對於B
,C
被安裝在頂層很好理解,由於是A
的直接依賴。可是B
又依賴C
,安裝C
的時候發現頂層已經有C
了,因此不會在B
本身的node_modules
中再次安裝。C
又依賴D
,安裝D
的時候發現根目錄並無D
,因此會把D
提高到頂層。
換成A{B,C}
, B{C,D@1}
, C{D@2}
這樣的依賴關係後,產生的結構以下:
A +-- B +-- C `-- D@2 +-- D@1
B
又依賴了D@1
,安裝時發現根目錄的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}
版本,如今的結構應該以下:
A +-- B@1 +-- C `-- B@2
有一天咱們將項目中的B
升級到B@2
,理想狀況下的結構應該以下:
A +-- B@2 +-- C
可是如今package-lock.json
文件的結構倒是這樣的:
A +-- B@2 +-- C `-- B@2
B@2
不只存在於根目錄的node_modules
下,C
下也一樣存在。這時須要咱們手動執行npm dedupe
進行去重操做,執行完成後會發現C
下面的B@2
會消失。你們能夠在本身的項目中試一試,優化一下package-lock.json
文件的結構。
如下是在個人項目中執行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
源碼相關的知識。