前端(vue)入門到精通課程:進入學習
現如今,前端開發的同學已經離不開npm
這個包管理工具,其優秀的包版本管理機制承載了整個繁榮發展的NodeJS
社區,理解其內部機制非常有利於加深我們對模組開發的理解、各項前端工程化的配置以加快我們排查問題(相信不少同學收到過各種依賴問題的困擾)的速度。
本文從三個角度: 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
規範的標準版本號碼採用XYZ
的格式,其中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('abc') // 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
)再來看看後面兩個,版本號中引用了~
和^
符號:
~
: 當安裝依賴時獲取到有新版本時,安裝到xyz
中z
的最新的版本。即保持主版本號、次版本號不變的情況下,保持修訂號的最新版本。^
: 當安裝依賴時取得到有新版本時,安裝到xyz
中y
和z
都為最新版本。 即保持主版本號不變的情況下,保持次版本號、修訂版本號為最新版本。在package.json
檔案中最常見的應該是"yargs": "^14.0.0"
這種格式的依賴, 因為我們在使用npm install package
安裝套件時, npm
預設安裝目前最新版本,然後在所安裝的版本號前加^
號。
請注意,當主版本號為0
的情況,會被認為是一個不穩定版本,情況與上面不同:
0
: ^0.0.z
、 ~0.0.z
都被當作固定版本,安裝依賴時均不會變更。0
: ^0.yz
表現和~0.yz
相同,只保持修訂號為最新版本。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/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWXwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== 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
其和
json
yarn.lock
package-lock.json
訂格式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