現如今,前端開發的同學已經離不開 npm
這個包管理工具,其優秀的包版本管理機制承載了整個繁榮發展的社群,理解其內部機制非常有利於加深我們對模組開發的理解、各項前端工程化的設定以加快我們排查問題(相信不少同學收到過各種依賴問題的困擾)的速度。
本文從三個角度:package.json
、版本管理、依賴安裝結合具體範例對 npm
的包管理機制進行了詳細分析。
在 Node.js
中,模組是一個庫或框架,也是一個 Node.js
專案。Node.js
專案遵循模組化的架構,當我們建立了一個 Node.js
專案,意味著建立了一個模組,這個模組必須有一個描述檔案,即 package.json
。它是我們最常見的組態檔,但是它裡面的設定你真的有詳細瞭解過嗎?設定一個合理的 package.json
檔案直接決定著我們專案的品質,所以首先帶大家分析下 package.json
的各項詳細設定。
package.json
中有非常多的屬性,其中必須填寫的只有兩個:name
和 version
,這兩個屬性組成一個 npm
模組的唯一標識。
name
即模組名稱,其命名時需要遵循官方的一些規範和建議:
包名會成為模組url
、命令列中的一個引數或者一個資料夾名稱,任何非url
安全的字元在包名中都不能使用,可以使用 validate-npm-package-name
包來檢測包名是否合法。
語意化包名,可以幫助開發者更快的找到需要的包,並且避免意外獲取錯誤的包。
若包名稱中存在一些符號,將符號去除後不得與現有包名重複
例如:由於react-native
已經存在,react.native
、reactnative
都不可以再建立。
例如:使用者名稱 conard
,那麼作用域為 @conard
,釋出的包可以是@conard/react
。
name
是一個包的唯一標識,不得和其他包名重複,我們可以執行 npm view packageName
檢視包是否被佔用,並可以檢視它的一些基本資訊:
若包名稱從未被使用過,則會丟擲 404
錯誤:
另外,你還可以去 https://www.npmjs.com/
查詢更多更詳細的包資訊。
{ "description": "An enterprise-class UI design language and React components implementation", "keywords": [ "ant", "component", "components", "design", "framework", "frontend", "react", "react-component", "ui" ] }
description
用於新增模組的的描述資訊,方便別人瞭解你的模組。
keywords
用於給你的模組新增關鍵字。
當然,他們的還有一個非常重要的作用,就是利於模組檢索。當你使用 npm search
檢索模組時,會到description
和 keywords
中進行匹配。寫好 description
和 keywords
有利於你的模組獲得更多更精準的曝光:
描述開發人員的欄位有兩個:author
和 contributors
, author
指包的主要作者,一個 author
對應一個人。 contributors
指貢獻者資訊,一個 contributors
對應多個貢獻者,值為陣列,對人的描述可以是一個字串,也可以是下面的結構:
{ "name" : "ConardLi", "email" : "[email protected]", "url" : "https://github.com/ConardLi" }
{ "homepage": "http://ant.design/", "bugs": { "url": "https://github.com/ant-design/ant-design/issues" }, "repository": { "type": "git", "url": "https://github.com/ant-design/ant-design" }, }
homepage
用於指定該模組的主頁。
repository
用於指定模組的程式碼倉庫。
bugs
指定一個地址或者一個郵箱,對你的模組存在疑問的人可以到這裡提出問題。
我們的專案可能依賴一個或多個外部依賴包,根據依賴包的不同用途,我們將他們設定在下面幾個屬性下:dependencies、devDependencies、peerDependencies、bundledDependencies、optionalDependencies
。
在介紹幾種依賴設定之前,首先我們來看一下依賴的設定規則,你看到的依賴包設定可能是下面這樣的:
"dependencies": { "antd": "ant-design/ant-design#4.0.0-alpha.8", "axios": "^1.2.0", "test-js": "file:../test", "test2-js": "http://cdn.com/test2-js.tar.gz", "core-js": "^1.1.5", }
依賴設定遵循下面幾種設定規則:
依賴包名稱:VERSION
VERSION
是一個遵循SemVer
規範的版本號設定,npm install
時將到npm伺服器下載符合指定版本範圍的包。依賴包名稱:DWONLOAD_URL
DWONLOAD_URL
是一個可下載的tarball
壓縮包地址,模組安裝時會將這個.tar
下載並安裝到本地。依賴包名稱:LOCAL_PATH
LOCAL_PATH
是一個原生的依賴包路徑,例如 file:../pacakges/pkgName
。適用於你在本地測試一個npm
包,不應該將這種方法應用於線上。依賴包名稱:GITHUB_URL
GITHUB_URL
即 github
的 username/modulename
的寫法,例如:ant-design/ant-design
,你還可以在後面指定 tag
和 commit id
。依賴包名稱:GIT_URL
GIT_URL
即我們平時clone程式碼庫的 git url
,其遵循以下形式:<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]
其中 protocal
可以是以下幾種形式:
git://github.com/user/project.git#commit-ish
git+ssh://user@hostname:project.git#commit-ish
git+ssh://user@hostname/project.git#commit-ish
git+http://user@hostname/project/blah.git#commit-ish
git+https://user@hostname/project/blah.git#commit-ish
dependencies
指定了專案執行所依賴的模組,開發環境和生產環境的依賴模組都可以設定到這裡,例如
"dependencies": { "lodash": "^4.17.13", "moment": "^2.24.0", }
有一些包有可能你只是在開發環境中用到,例如你用於檢測程式碼規範的 eslint
,用於進行測試的 jest
,使用者使用你的包時即使不安裝這些依賴也可以正常執行,反而安裝他們會耗費更多的時間和資源,所以你可以把這些依賴新增到 devDependencies
中,這些依賴照樣會在你本地進行 npm install
時被安裝和管理,但是不會被安裝到生產環境:
"devDependencies": { "jest": "^24.3.1", "eslint": "^6.1.0", }
peerDependencies
用於指定你正在開發的模組所依賴的版本以及使用者安裝的依賴包版本的相容性。
上面的說法可能有點太抽象,我們直接拿 ant-design
來舉個例子,ant-design
的 package.json
中有如下設定:
"peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" }
當你正在開發一個系統,使用了 ant-design
,所以也肯定需要依賴 React
。同時, ant-design
也是需要依賴 React
的,它要保持穩定執行所需要的 React
版本是16.0.0
,而你開發時依賴的 React
版本是 15.x
:
這時,ant-design
要使用 React
,並將其引入:
import * as React from 'react'; import * as ReactDOM from 'react-dom';
這時取到的是宿主環境也就是你的環境中的 React
版本,這就可能造成一些問題。在 npm2
的時候,指定上面的 peerDependencies
將意味著強制宿主環境安裝 react@>=16.0.0和react-dom@>=16.0.0
的版本。
npm3
以後不會再要求 peerDependencies
所指定的依賴包被強制安裝,相反 npm3
會在安裝結束後檢查本次安裝是否正確,如果不正確會給使用者列印警告提示。
"dependencies": { "react": "15.6.0", "antd": "^3.22.0" }
例如,我在專案中依賴了 antd
的最新版本,然後依賴了 react
的 15.6.0
版本,在進行依賴安裝時將給出以下警告:
某些場景下,依賴包可能不是強依賴的,這個依賴包的功能可有可無,當這個依賴包無法被獲取到時,你希望 npm install
繼續執行,而不會導致失敗,你可以將這個依賴放到 optionalDependencies
中,注意 optionalDependencies
中的設定將會覆蓋掉 dependencies
所以只需在一個地方進行設定。
當然,參照 optionalDependencies
中安裝的依賴時,一定要做好例外處理,否則在模組獲取不到時會導致報錯。
和以上幾個不同,bundledDependencies
的值是一個陣列,陣列裡可以指定一些模組,這些模組將在這個包釋出時被一起打包。
"bundledDependencies": ["package1" , "package2"]
{ "license": "MIT" }
license
欄位用於指定軟體的開源協定,開源協定裡面詳盡表述了其他人獲得你程式碼後擁有的權利,可以對你的的程式碼進行何種操作,何種操作又是被禁止的。同一款協定有很多變種,協定太寬鬆會導致作者喪失對作品的很多權利,太嚴格又不便於使用者使用及作品的傳播,所以開源作者要考慮自己對作品想保留哪些權利,放開哪些限制。
軟體協定可分為開源和商業兩類,對於商業協定,或者叫法律宣告、許可協定,每個軟體會有自己的一套行文,由軟體作者或專門律師撰寫,對於大多數人來說不必自己花時間和精力去寫繁長的許可協定,選擇一份廣為流傳的開源協定就是個不錯的選擇。
以下就是幾種主流的開源協定:
MIT
:只要使用者在專案副本中包含了版權宣告和許可宣告,他們就可以拿你的程式碼做任何想做的事情,你也無需承擔任何責任。Apache
:類似於 MIT
,同時還包含了貢獻者向使用者提供專利授權相關的條款。GPL
:修改專案程式碼的使用者再次分發原始碼或二進位制程式碼時,必須公佈他的相關修改。如果你對開源協定有更詳細的要求,可以到 choosealicense.com/ 獲取更詳細的開源協定說明。
{ "main": "lib/index.js", }
main
屬性可以指定程式的主入口檔案,例如,上面 antd
指定的模組入口 lib/index.js
,當我們在程式碼用引入 antd
時:import { notification } from 'antd';
實際上引入的就是 lib/index.js
中暴露出去的模組。
當你的模組是一個命令列工具時,你需要為命令列工具指定一個入口,即指定你的命令名稱和本地可指定檔案的對應關係。如果是全域性安裝,npm 將會使用符號連結把可執行檔案連結到 /usr/local/bin
,如果是本地安裝,會連結到 ./node_modules/.bin/
。
{ "bin": { "conard": "./bin/index.js" } }
例如上面的設定:當你的包安裝到全域性時:npm
會在 /usr/local/bin
下建立一個以 conard
為名字的軟連結,指向全域性安裝下來的 conard
包下面的 "./bin/index.js"
。這時你在命令列執行 conard
則會呼叫連結到的這個js檔案。
這裡不再過多展開,更多內容在我後續的命令列工具文章中會進行詳細講解。
{ "files": [ "dist", "lib", "es" ] }
files
屬性用於描述你 npm publish
後推播到 npm
伺服器的檔案列表,如果指定資料夾,則資料夾內的所有內容都會包含進來。我們可以看到下載後的包是下面的目錄結構:
另外,你還可以通過設定一個
.npmignore
檔案來排除一些檔案, 防止大量的垃圾檔案推播到npm
, 規則上和你用的.gitignore
是一樣的。.gitignore
檔案也可以充當.npmignore
檔案。
man
命令是 Linux
下的幫助指令,通過 man
指令可以檢視 Linux
中的指令幫助、組態檔幫助和程式設計幫助等資訊。
如果你的 node.js
模組是一個全域性的命令列工具,在 package.json
通過 man
屬性可以指定 man
命令查詢的檔案地址。
man
檔案必須以數位結尾,或者如果被壓縮了,以 .gz
結尾。數位表示檔案將被安裝到 man
的哪個部分。如果 man
檔名稱不是以模組名稱開頭的,安裝的時候會給加上模組名稱字首。
例如下面這段設定:
{ "man" : [ "/Users/isaacs/dev/npm/cli/man/man1/npm-access.1", "/Users/isaacs/dev/npm/cli/man/man1/npm-audit.1" ] }
在命令列輸入 man npm-audit
:
一個 node.js
模組是基於 CommonJS
模組化規範實現的,嚴格按照 CommonJS
規範,模組目錄下除了必須包含包描述檔案 package.json
以外,還需要包含以下目錄:
bin
:存放可執行二進位制檔案的目錄lib
:存放js程式碼的目錄doc
:存放檔案的目錄test
:存放單元測試用例程式碼的目錄在模組目錄中你可能沒有嚴格按照以上結構組織或命名,你可以通過在 package.json
指定 directories
屬性來指定你的目錄結構和上述的規範結構的對應情況。除此之外 directories
屬性暫時沒有其他應用。
{ "directories": { "lib": "src/lib/", "bin": "src/bin/", "man": "src/man/", "doc": "src/doc/", "example": "src/example/" } }
不過官方檔案表示,雖然目前這個屬性沒有什麼重要作用,未來可能會整出一些花樣出來,例如:doc 中存放的 markdown 檔案、example 中存放的範例檔案,可能會友好的展示出來。
{ "scripts": { "test": "jest --config .jest.js --no-cache", "dist": "antd-tools run dist", "compile": "antd-tools run compile", "build": "npm run compile && npm run dist" } }
scripts
用於設定一些指令碼命令的縮寫,各個指令碼可以互相組合使用,這些指令碼可以覆蓋整個專案的生命週期,設定後可使用 npm run command
進行呼叫。如果是 npm
關鍵字,則可以直接呼叫。例如,上面的設定製定了以下幾個命令:npm run test
、npm run dist
、npm run compile
、npm run build
。
config
欄位用於設定指令碼中使用的環境變數,例如下面的設定,可以在指令碼中使用process.env.npm_package_config_port
進行獲取。
{ "config" : { "port" : "8080" } }
如果你的 node.js
模組主要用於安裝到全域性的命令列工具,那麼該值設定為 true
,當使用者將該模組安裝到本地時,將得到一個警告。這個設定並不會阻止使用者安裝,而是會提示使用者防止錯誤使用而引發一些問題。
如果將 private
屬性設定為 true
,npm將拒絕釋出它,這是為了防止一個私有模組被無意間釋出出去。
"publishConfig": { "registry": "https://registry.npmjs.org/" },
釋出模組時更詳細的設定,例如你可以設定只發布某個 tag
、設定釋出到的私有 npm
源。更詳細的設定可以參考 npm-config
假如你開發了一個模組,只能跑在 darwin
系統下,你需要保證 windows
使用者不會安裝到你的模組,從而避免發生不必要的錯誤。
使用 os
屬性可以幫助你完成以上的需求,你可以指定你的模組只能被安裝在某些系統下,或者指定一個不能安裝的系統黑名單:
"os" : [ "darwin", "linux" ] "os" : [ "!win32" ]
例如,我把一個測試模組指定一個系統黑名單:"os" : [ "!darwin" ]
,當我在此係統下安裝它時會爆出如下錯誤:
在node環境下可以使用 process.platform 來判斷作業系統。
和上面的 os
類似,我們可以用 cpu
屬性更精準的限制使用者安裝環境:
"cpu" : [ "x64", "ia32" ] "cpu" : [ "!arm", "!mips" ]
在node環境下可以使用 process.arch 來判斷 cpu 架構。
Nodejs
成功離不開 npm
優秀的依賴管理系統。在介紹整個依賴系統之前,必須要了解 npm
如何管理依賴包的版本,本章將介紹 npm包
的版本釋出規範、如何管理各種依賴包的版本以及一些關於包版本的最佳實踐。
你可以執行 npm view package version
檢視某個 package
的最新版本。
執行 npm view conard versions
檢視某個 package
在npm伺服器上所有釋出過的版本。
執行 npm ls
可檢視當前倉庫依賴樹上所有包的版本資訊。
npm包
中的模組版本都需要遵循 SemVer
規範——由 Github
起草的一個具有指導意義的,統一的版本號表示規則。實際上就是 Semantic Version
(語意化版本)的縮寫。
SemVer規範官網: https://semver.org/
SemVer
規範的標準版本號採用 X.Y.Z
的格式,其中 X、Y 和 Z 為非負的整數,且禁止在數位前方補零。X 是主版本號、Y 是次版本號、而 Z 為修訂號。每個元素必須以數值來遞增。
major
):當你做了不相容的API 修改minor
):當你做了向下相容的功能性新增patch
):當你做了向下相容的問題修正。例如:1.9.1 -> 1.10.0 -> 1.11.0
當某個版本改動比較大、並非穩定而且可能無法滿足預期的相容性需求時,你可能要先發佈一個先行版本。
先行版本號可以加到「主版本號.次版本號.修訂號」的後面,先加上一個連線號再加上一連串以句點分隔的識別符號和版本編譯資訊。
alpha
):beta
):rc
: 即 Release candiate
下面我們來看看 React
的歷史版本:
可見是嚴格按照 SemVer
規範來發版的:
主版本號.次版本號.修訂號
格式命名16.8.0 -> 16.8.1 -> 16.8.2
alpha
、beta
、rc
等先行版本在修改 npm
包某些功能後通常需要釋出一個新的版本,我們通常的做法是直接去修改 package.json
到指定版本。如果操作失誤,很容易造成版本號混亂,我們可以藉助符合 Semver
規範的命令來完成這一操作:
npm version patch
: 升級修訂版本號npm version minor
: 升級次版本號npm version major
: 升級主版本號在開發中肯定少不了對一些版本號的操作,如果這些版本號符合 SemVer
規範 ,我們可以藉助用於操作版本的npm包semver
來幫助我們進行比較版本大小、提取版本資訊等操作。
Npm 也使用了該工具來處理版本相關的工作。
npm install semver
semver.gt('1.2.3', '9.8.7') // false semver.lt('1.2.3', '9.8.7') // true
semver.valid('1.2.3') // '1.2.3' semver.valid('a.b.c') // null
semver.valid(semver.coerce('v2')) // '2.0.0' semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'
semver.clean(' =v1.2.3 ') // '1.2.3' semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true semver.minVersion('>=1.0.0') // '1.0.0'
以上都是semver最常見的用法,更多詳細內容可以檢視 semver檔案:https://github.com/npm/node-semver
我們經常看到,在 package.json
中各種依賴的不同寫法:
"dependencies": { "signale": "1.4.0", "figlet": "*", "react": "16.x", "table": "~5.4.6", "yargs": "^14.0.0" }
前面三個很容易理解:
"signale": "1.4.0"
: 固定版本號"figlet": "*"
: 任意版本(>=0.0.0
)"react": "16.x"
: 匹配主要版本(>=16.0.0 <17.0.0
)"react": "16.3.x"
: 匹配主要版本和次要版本(>=16.3.0 <16.4.0
)再來看看後面兩個,版本號中參照了 ~
和 ^
符號:
~
: 當安裝依賴時獲取到有新版本時,安裝到 x.y.z
中 z
的最新的版本。即保持主版本號、次版本號不變的情況下,保持修訂號的最新版本。^
: 當安裝依賴時獲取到有新版本時,安裝到 x.y.z
中 y
和 z
都為最新版本。 即保持主版本號不變的情況下,保持次版本號、修訂版本號為最新版本。在 package.json
檔案中最常見的應該是 "yargs": "^14.0.0"
這種格式的 依賴, 因為我們在使用 npm install package
安裝包時,npm
預設安裝當前最新版本,然後在所安裝的版本號前加 ^
號。
注意,當主版本號為 0
的情況,會被認為是一個不穩定版本,情況與上面不同:
0
: ^0.0.z
、~0.0.z
都被當作固定版本,安裝依賴時均不會發生變化。0
: ^0.y.z
表現和 ~0.y.z
相同,只保持修訂號為最新版本。1.0.0 的版本號用於界定公共 API。當你的軟體釋出到了正式環境,或者有穩定的API時,就可以釋出1.0.0版本了。所以,當你決定對外部發佈一個正式版本的npm包時,把它的版本標為1.0.0。
實際開發中,經常會因為各種依賴不一致而產生奇怪的問題,或者在某些場景下,我們不希望依賴被更新,建議在開發中使用 package-lock.json
。
鎖定依賴版本意味著在我們不手動執行更新的情況下,每次安裝依賴都會安裝固定版本。保證整個團隊使用版本號一致的依賴。
每次安裝固定版本,無需計算依賴版本範圍,大部分場景下能大大加速依賴安裝時間。
使用 package-lock.json 要確保npm的版本在5.6以上,因為在5.0 - 5.6中間,對 package-lock.json的處理邏輯進行過幾次更新,5.6版本後處理邏輯逐漸穩定。
關於 package-lock.json
詳細的結構,我們會在後面的章節進行解析。
我們的目的是保證團隊中使用的依賴一致或者穩定,而不是永遠不去更新這些依賴。實際開發場景下,我們雖然不需要每次都去安裝新的版本,仍然需要定時去升級依賴版本,來讓我們享受依賴包升級帶來的問題修復、效能提升、新特性更新。
使用 npm outdated
可以幫助我們列出有哪些還沒有升級到最新版本的依賴:
執行 npm update
會升級所有的紅色依賴。
1.0.0
。主版本號.次版本號.修訂號
格式命名alpha、beta、rc
等先行版本npm
包,此時建議把版本字首改為~
,如果鎖定的話每次子依賴更新都要對主工程的依賴進行升級,非常繁瑣,如果對子依賴完全信任,直接開啟^
每次升級到最新版本。docker
線上,本地還在進行子依賴開發和升級,在docker
版本釋出前要鎖定所有依賴版本,確保本地子依賴釋出後線上不會出問題。npm
的版本在5.6
以上,確保預設開啟 package-lock.json
檔案。npm inatall
後,將 package-lock.json
提交到遠端倉庫。不要直接提交 node_modules
到遠端倉庫。npm update
升級依賴,並提交 lock
檔案確保其他成員同步更新依賴,不要手動更改 lock
檔案。package.json
檔案的依賴版本,執行 npm install
npm install package@version
(改動package.json
不會對依賴進行降級)lock
檔案npm install
大概會經過上面的幾個流程,這一章就來講一講各個流程的實現細節、發展以及為何要這樣實現。
我們都知道,執行 npm install
後,依賴包被安裝到了 node_modules
,下面我們來具體瞭解下,npm
將依賴包安裝到 node_modules
的具體機制是什麼。
在 npm
的早期版本, npm
處理依賴的方式簡單粗暴,以遞迴的形式,嚴格按照 package.json
結構以及子依賴包的 package.json
結構將依賴安裝到他們各自的 node_modules
中。直到有子依賴包不在依賴其他模組。
舉個例子,我們的模組 my-app
現在依賴了兩個模組:buffer
、ignore
:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", } }
ignore
是一個純 JS
模組,不依賴任何其他模組,而 buffer
又依賴了下面兩個模組:base64-js
、 ieee754
。
{ "name": "buffer", "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }
那麼,執行 npm install
後,得到的 node_modules
中模組目錄結構就是下面這樣的:
這樣的方式優點很明顯, node_modules
的結構和 package.json
結構一一對應,層級結構明顯,並且保證了每次安裝目錄結構都是相同的。
但是,試想一下,如果你依賴的模組非常之多,你的 node_modules
將非常龐大,巢狀層級非常之深:
Windows
系統中,檔案路徑最大長度為260個字元,巢狀層級過深可能導致不可預知的問題。為了解決以上問題,NPM
在 3.x
版本做了一次較大更新。其將早期的巢狀結構改為扁平結構:
node_modules
根目錄。還是上面的依賴結構,我們在執行 npm install
後將得到下面的目錄結構:
此時我們若在模組中又依賴了 [email protected]
版本:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", "base64-js": "1.0.1", } }
node_modules
下安裝該模組。此時,我們在執行 npm install
後將得到下面的目錄結構:
對應的,如果我們在專案程式碼中參照了一個模組,模組查詢流程如下:
node_modules
路徑下搜素node_modules
路徑下搜尋node_modules
假設我們又依賴了一個包 buffer2@^5.4.3
,而它依賴了包 [email protected]
,則此時的安裝結構是下面這樣的:
所以 npm 3.x
版本並未完全解決老版本的模組冗餘問題,甚至還會帶來新的問題。
試想一下,你的APP假設沒有依賴 [email protected]
版本,而你同時依賴了依賴不同 base64-js
版本的 buffer
和 buffer2
。由於在執行 npm install
的時候,按照 package.json
裡依賴的順序依次解析,則 buffer
和 buffer2
在 package.json
的放置順序則決定了 node_modules
的依賴結構:
先依賴buffer2
:
先依賴buffer
:
另外,為了讓開發者在安全的前提下使用最新的依賴包,我們在 package.json
通常只會鎖定大版本,這意味著在某些依賴包小版本更新後,同樣可能造成依賴結構的改動,依賴結構的不確定性可能會給程式帶來不可預知的問題。
為了解決 npm install
的不確定性問題,在 npm 5.x
版本新增了 package-lock.json
檔案,而安裝方式還沿用了 npm 3.x
的扁平化的方式。
package-lock.json
的作用是鎖定依賴結構,即只要你目錄下有 package-lock.json
檔案,那麼你每次執行 npm install
後生成的 node_modules
目錄結構一定是完全相同的。
例如,我們有如下的依賴結構:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", "base64-js": "1.0.1", } }
在執行 npm install
後生成的 package-lock.json
如下:
{ "name": "my-app", "version": "1.0.0", "dependencies": { "base64-js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz", "integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=" }, "buffer": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" }, "dependencies": { "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" } } }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==" } } }
我們來具體看看上面的結構:
最外面的兩個屬性 name
、version
同 package.json
中的 name
和 version
,用於描述當前包名稱和版本。
dependencies
是一個物件,物件和 node_modules
中的包結構一一對應,物件的 key
為包名稱,值為包的一些描述資訊:
version
:包版本 —— 這個包當前安裝在 node_modules
中的版本resolved
:包具體的安裝來源integrity
:包 hash
值,基於 Subresource Integrity
來驗證已安裝的軟體包是否被改動過、是否已失效requires
:對應子依賴的依賴,與子依賴的 package.json
中 dependencies
的依賴項相同。dependencies
:結構和外層的 dependencies
結構相同,儲存安裝在子依賴 node_modules
中的依賴包。這裡注意,並不是所有的子依賴都有 dependencies
屬性,只有子依賴的依賴和當前已安裝在根目錄的 node_modules
中的依賴衝突之後,才會有這個屬性。
例如,回顧下上面的依賴關係:
我們在 my-app
中依賴的 [email protected]
版本與 buffer
中依賴的 base64-js@^1.0.2
發生衝突,所以 [email protected]
需要安裝在 buffer
包的 node_modules
中,對應了 package-lock.json
中 buffer
的 dependencies
屬性。這也對應了 npm
對依賴的扁平化處理方式。
所以,根據上面的分析, package-lock.json
檔案 和 node_modules
目錄結構是一一對應的,即專案目錄下存在 package-lock.json
可以讓每次安裝生成的依賴目錄結構保持相同。
另外,專案中使用了 package-lock.json
可以顯著加速依賴安裝時間。
我們使用 npm i --timing=true --loglevel=verbose
命令可以看到 npm install
的完整過程,下面我們來對比下使用 lock
檔案和不使用 lock
檔案的差別。在對比前先清理下npm
快取。
不使用 lock
檔案:
使用 lock
檔案:
可見, package-lock.json
中已經快取了每個包的具體版本和下載連結,不需要再去遠端倉庫進行查詢,然後直接進入檔案完整性校驗環節,減少了大量網路請求。
開發系統應用時,建議把 package-lock.json
檔案提交到程式碼版本倉庫,從而保證所有團隊開發者以及 CI
環節可以在執行 npm install
時安裝的依賴版本都是一致的。
在開發一個 npm
包 時,你的 npm
包 是需要被其他倉庫依賴的,由於上面我們講到的扁平安裝機制,如果你鎖定了依賴包版本,你的依賴包就不能和其他依賴包共用同一 semver
範圍內的依賴包,這樣會造成不必要的冗餘。所以我們不應該把package-lock.json
檔案發布出去( npm
預設也不會把 package-lock.json
檔案發布出去)。
在執行 npm install
或 npm update
命令下載依賴後,除了將依賴包安裝在node_modules
目錄下外,還會在原生的快取目錄快取一份。
通過 npm config get cache
命令可以查詢到:在 Linux
或 Mac
預設是使用者主目錄下的 .npm/_cacache
目錄。
在這個目錄下又存在兩個目錄:content-v2
、index-v5
,content-v2
目錄用於儲存 tar
包的快取,而index-v5
目錄用於儲存tar
包的 hash
。
npm 在執行安裝時,可以根據 package-lock.json
中儲存的 integrity、version、name
生成一個唯一的 key
對應到 index-v5
目錄下的快取記錄,從而找到 tar
包的 hash
,然後根據 hash
再去找快取的 tar
包直接使用。
我們可以找一個包在快取目錄下搜尋測試一下,在 index-v5
搜尋一下包路徑:
grep "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz" -r index-v5
然後我們將json格式化:
{ "key": "pacote:version-manifest:https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz:sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=", "integrity": "sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==", "time": 1575554308857, "size": 1, "metadata": { "id": "[email protected]", "manifest": { "name": "base64-js", "version": "1.0.1", "engines": { "node": ">= 0.4" }, "dependencies": {}, "optionalDependencies": {}, "devDependencies": { "standard": "^5.2.2", "tape": "4.x" }, "bundleDependencies": false, "peerDependencies": {}, "deprecated": false, "_resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz", "_integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=", "_shasum": "6926d1b194fbc737b8eed513756de2fcda7ea408", "_shrinkwrap": null, "bin": null, "_id": "[email protected]" }, "type": "finalized-manifest" } }
上面的 _shasum
屬性 6926d1b194fbc737b8eed513756de2fcda7ea408
即為 tar
包的 hash
, hash
的前幾位 6926
即為快取的前兩層目錄,我們進去這個目錄果然找到的壓縮後的依賴包:
以上的快取策略是從 npm v5 版本開始的,在 npm v5 版本之前,每個快取的模組在 ~/.npm 資料夾中以模組名的形式直接儲存,儲存結構是{cache}/{name}/{version}。
npm
提供了幾個命令來管理快取資料:
npm cache add
:官方解釋說這個命令主要是 npm
內部使用,但是也可以用來手動給一個指定的 package 新增快取。npm cache clean
:刪除快取目錄下的所有資料,為了保證快取資料的完整性,需要加上 --force
引數。npm cache verify
:驗證快取資料的有效性和完整性,清理垃圾資料。基於快取資料,npm 提供了離線安裝模式,分別有以下幾種:
--prefer-offline
: 優先使用快取資料,如果沒有匹配的快取資料,則從遠端倉庫下載。--prefer-online
: 優先使用網路資料,如果網路資料請求失敗,再去請求快取資料,這種模式可以及時獲取最新的模組。--offline
: 不請求網路,直接使用快取資料,一旦快取資料不存在,則安裝失敗。上面我們多次提到了檔案完整性,那麼什麼是檔案完整性校驗呢?
在下載依賴包之前,我們一般就能拿到 npm
對該依賴包計算的 hash
值,例如我們執行 npm info
命令,緊跟 tarball
(下載連結) 的就是 shasum
(hash
) :
使用者下載依賴包到本地後,需要確定在下載過程中沒有出現錯誤,所以在下載完成之後需要在本地在計算一次檔案的 hash
值,如果兩個 hash
值是相同的,則確保下載的依賴是完整的,如果不同,則進行重新下載。
好了,我們再來整體總結下上面的流程:
檢查 .npmrc
檔案:優先順序為:專案級的 .npmrc
檔案 > 使用者級的 .npmrc
檔案> 全域性級的 .npmrc
檔案 > npm 內建的 .npmrc
檔案
檢查專案中有無 lock
檔案。
無 lock
檔案:
npm
遠端倉庫獲取包資訊package.json
構建依賴樹,構建過程:node_modules
根目錄。node_modules
下放置該模組。npm
遠端倉庫下載包npm
快取目錄node_modules
node_modules
node_modules
lock
檔案有 lock
檔案:
package.json
中的依賴版本是否和 package-lock.json
中的依賴有衝突。上面的過程簡要描述了 npm install
的大概過程,這個過程還包含了一些其他的操作,例如執行你定義的一些生命週期函數,你可以執行 npm install package --timing=true --loglevel=verbose
來檢視某個包具體的安裝流程和細節。
yarn
是在 2016
年釋出的,那時 npm
還處於 V3
時期,那時候還沒有 package-lock.json
檔案,就像上面我們提到的:不穩定性、安裝速度慢等缺點經常會受到廣大開發者吐槽。此時,yarn
誕生:
上面是官網提到的 yarn
的優點,在那個時候還是非常吸引人的。當然,後來 npm
也意識到了自己的問題,進行了很多次優化,在後面的優化(lock
檔案、快取、預設-s...)中,我們多多少少能看到 yarn
的影子,可見 yarn
的設計還是非常優秀的。
yarn
也是採用的是 npm v3
的扁平結構來管理依賴,安裝依賴後預設會生成一個 yarn.lock
檔案,還是上面的依賴關係,我們看看 yarn.lock
的結構:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 [email protected]: version "1.0.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.0.1.tgz#6926d1b194fbc737b8eed513756de2fcda7ea408" integrity sha1-aSbRsZT7xze47tUTdW3i/Np+pAg= base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== buffer@^5.4.3: version "5.4.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== ignore@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
可見其和 package-lock.json
檔案還是比較類似的,還有一些區別就是:
package-lock.json
使用的是 json
格式,yarn.lock
使用的是一種自定義格式yarn.lock
中子依賴的版本號不是固定的,意味著單獨又一個 yarn.lock
確定不了 node_modules
目錄結構,還需要和 package.json
檔案進行配合。而 package-lock.json
只需要一個檔案即可確定。yarn
的緩策略看起來和 npm v5
之前的很像,每個快取的模組被存放在獨立的資料夾,資料夾名稱包含了模組名稱、版本號等資訊。使用命令 yarn cache dir
可以檢視快取資料的目錄:
yarn
預設使用prefer-online
模式,即優先使用網路資料,如果網路資料請求失敗,再去請求快取資料。
希望閱讀完本篇文章能對你有如下幫助:
pacakge.json
中的各項詳細設定從而對專案工程化設定有更進一步的見解npm
的版本管理機制,能合理設定依賴版本npm install
安裝原理,能合理運用 npm
快取、package-lock.json
更多node相關知識,請存取:!
以上就是深入解析npm的包管理機制的詳細內容,更多請關注TW511.COM其它相關文章!