作為前端開發者,應該每個人都用過npm,那么npm到底是什么東西呢?npm run,npm install的時候發生了哪些事情呢?下面做詳細說明。
1.npm是什么
npm是JavaScript語言的包管理工具,它由三個部分組成:
- npm網站 進入
npm官網上可以查找包,查看包信息。 - 注冊表
一個巨大的數據庫,存放包的信息 - 命令行工具npm-cli
開發者運行npm命令的工具
這三者中,與我們打交道最多的就是npm-cli,其實我們所說的npm的使用,就是指這個工具的使用,那它到底是個什么東西呢?我們先來看看它被放在哪里,在系統命令行(window cmd)工具中輸入 where npm(安裝node會自帶npm),就能找到它的位置:

然后根據路徑找到npm文件打開:

從標紅的地方可以看出,這其實就是一個腳本,它最終執行的是: node npm-cli.js
所以到目前為止,我們可以知道當在命令行輸入npm時,其實是在node環境中,執行了一段npm-cli.js代碼,這是對npm的一個直觀的認識。
至於npm-cli.js里面的邏輯是什么,就是研究源碼層面的事了,這里不涉及。我們主要來看npm的用法和功能層面的原理。首先來看npm的配置文件package.json。
2.package.json文件
當我們運行命令npm init,根據提示輸入一些信息后(npm init -y不需輸入信息),會在當前目錄下生成一個package.json文件:
{
"name": "testNpm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
這里就是一個npm包的基本信息,包括包名name,版本version,描述description,作者author,主文件main,腳本scripts等等, 這里先主要來看下main:
2.1 入口文件 main
main配置項的值是一個js文件的路徑,它將作為程序的主入口文件。也就是說當別人引用了這個包時import testNpm from 'testNpm',其實引入的就是testNpm/index.js文件所export出的模塊。
2.2 腳本 scripts
npm scripts 腳本應該是我們打交道最多的一個配置項了,它一個json的對象,由腳本名稱和腳本內容組成:
"scripts":{
"star":"echo star npm",
"echo":"echo hello npm"
}
一般用npm run xxx來運行,但是一些關鍵命令比如:start,test,stop,restart等等,可以直接npm xxx來執行。那scripts是如何執行腳本的呢?又可以執行哪些腳本呢?
npm 腳本可以執行的命令
其實當我們npm run xxx的時候,就是把xxx的內容生成了一個shell腳本,然后執行腳本,那么npm的shell具體是什么呢?我們可以運行npm config get -l來查看npm的全部配置:

可能個人的系統和配置不同,以我個人電腦配置為例,其實就是cmd.exe,其實就是window系統的cmd命令行工具。所以在cmd中可以執行的命令,在npm的scripts中都可以執行,舉例說明:
"scripts":{
/*系統命令*/
"echo":"echo hello npm",
"dir":"dir",
"ip":"ipconfig"
}
像dir,ipconfig,echo這些都是可以直接在cmd命令行中執行的命令,在npm的scripts中都可以通過npm run xxx來執行。這一類是系統cmd的內部命令,不需要安裝額外的插件,就可以直接執行。
還有一種就是我們在cmd還可以執行外部命令,比如我們如果安裝了node,git等客戶端,可以直接在cmd窗口執行(需配置了系統的環境變量):

這一類的命令npm也可以執行:
"scripts":{
/*系統命令*/
"echo":"echo hello npm",
"dir":"dird",
"ip":"ipconfig",
/*全局外部命令*/
"git":"git --version",
"node":"node -v",
}
這是全局引入的外部命令,還有些項目內部才有的命令,比如我們在項目下安裝eslint: npm install eslint --save-dev,在scripts中配置了腳本的話,我們可以直接運行npm run eslint
"scripts":{
/*系統命令*/
"echo":"echo hello npm",
"dir":"dird",
"ip":"ipconfig",
/*全局外部命令*/
"git":"git --version",
"node":"node -v",
/*項目內外部命令*/
"eslint":"eslint -v"
}
但是如果我們直接在cmd窗口執行eslint -v,則會報錯,

這是因為系統找不到eslint的位置(沒有配系統環境變量),但是既然cmd室npm 腳本執行的環境,為什么npm run eslint可以執行呢?
這是因為當我們通過npm run xxx執行腳本的時候,會把當前目錄的'node_modules/.bin'加入到環境變量,也就是說npm執行腳本的時候,會自動到node_modules/.bin目錄下找,如果找到則可以正常執行,我們來看一下:

在node_modules/.bin目錄下果然是eslint.cmd腳本的,而它作的其實就是node eslint.js,用node來執行eslint.js的代碼。
npm 腳本可以執行的命令總結:
- cmd內部命令,例如dir,ipconfig...
- 外部命令
- 全局命令,加入了系統環境變量
- 項目下命令,這部分會放在node_modules/.bin目錄下,而npm會自動鏈接到此目錄。
2.3 npm腳本其他配置
路徑通配符
我們在寫腳本命令的時候,常常要匹配文件,這就要用到路徑的通配符。
總的來說*表示任意字符串,在目錄中表示1級目錄,**表示0級或多級目錄,例如:
src/*:src目錄下的任意文件,匹配 src/a.js; src/b.json;不匹配src/aa/a.js
src/*.js:src目錄下任何js文件,匹配 src/a.js; 不匹配 src/b.json;src/aa/a.js
src/*/*.js:src目錄下一級的任意js文件,匹配 src/aa/a.js; 不匹配src/a.js;src/a/aa/a.js
src/**/*.js:src目錄下的任意js文件,匹配 src/a.js; src/a/a.js; src/a/aa/a.js
命令參數
關於npm的參數,我們先來看一段代碼:
node代碼:
//index.js
console.log(process.env.npm_package_name)
console.log(process.env.npm_config_env)
console.log(process.argv)
npm配置:
//package.json
{
"name": "npm",
"version": "1.0.0",
"scripts": {
"node":"node index.js --name=node age=28",
},
}
然后我們執行命令npm run node --env=npmEnv,結果為:

下面來做下說明,其實npm的參數都是指node環境下的參數,用node的全局變量process來獲取。
- npm內部變量
當我們在執行npm命令的時候,就會把package.json的參數加上npm_package_前綴,加入到process.env的變量中,所以在上面的node代碼可以通過process.env.npm_package_name獲取到package.json里面配置的name屬性。 - 命令參數
當我們在運行npm命令時,帶上以雙橫線為后綴的參數:npm 命令 --xx=xx,npm就會把xx加上npm_config_前綴,加入到process.env變量中,如果原來有同名的,命令參數的優先級最高,會覆蓋掉原來的,所以在上面的node代碼可以通過process.env.npm_config_env獲取到npm run node --env=npmEnv命令里的參數env的值,如果參數沒有賦值:npm run node --env,則默認值為true - 腳本參數
這個其實要根據腳本的內容來看,比如我們上面的腳本是node index.js --env=node,這其實是純粹的node命令了,可以通過process.argv來獲取node的命令參數,這是個數組,第一個為node命令路徑,第二個為執行文件路徑,后面的值為用空格隔開的其他參數,如上面打印的結果所示。
執行順序
npm腳本的執行順序分為兩部分:
- 命令鈎子
npm腳本有pre,post兩類鈎子,一個是執行前,一個是執行后。比如,當我們執行npm run start時,會按照以下順序執行npm run prestart->npm run start->npm run poststart - 多任務並行
如果要執行多個腳本,可以用&或&&來連接npm run aa & npm run bb並行執行,沒有先后關系npm run aa && npm run bb串行執行,先執行完aa再執行bb
3.npm 包管理
npm做完包管理工具,主要的作用還是包的安裝及管理。
3.1 安裝包 npm install xxx
npm install xxx 命令用於安裝包。
我們先來運行npm install vue和npm install eslint --save-dev,會發現項目會有以下變化:
- 添加了目錄node_modules
安裝的包和包的依賴都存放在這里,引入的時候,會自動到此目錄下找。 - package.json文件自動添加了如下配置:
npm 在安裝包的同時,會把包的名稱和版本加入到"dependencies": { "vue": "^2.6.13" }, "devDependencies": { "eslint": "^7.27.0" }dependencies配置中,這表明這是項目必需的包。
如果帶上參數--save-dev,則加入到devDependencies配置中,這表明這是項目開發時才需要的工具包,不是項目必需的。 - 添加了package-lock.json文件
鎖定包的版本和依賴結構。
3.2 從package.json配置文件安裝包
包依賴類型
現在把node_modules目錄和package-lock.json文件都刪除,然后運行npm install,會發現項目會自動安裝vue和eslint包。
如果我們執行npm install --production則表明我們只是想安裝項目必須的包,用於生產環境,這是就只會安裝dependencies對象下的包。
其實npm包除了這兩種還有其他包的依賴類型:
dependencies
業務依賴,是項目的必須包,是項目線上代碼的一部分。npm install --production只會安裝此配置下的包。devDependencies
開發環境依賴,只在開發環境需要。npm install --save-dev安裝包並添加到此配置下。peerDependencies
同行依賴,當運行npm install,會提示安裝此配置下的包。注意只是警告提示,不會自動安裝。optionalDependencies
可選依賴,表明即使安裝失敗,也不影響項目的安裝過程。會覆蓋掉dependencies中的同名包。bundledDependencies
打包依賴,發布當前包的時候,會把此配置下的依賴包也一起打包。必須先在dependencies和devDependencies聲明過,否則打包會報錯。
包版本說明
npm采用semver作為包版本管理規范。此規范規定軟件版本由三個部分組成:
主版本號做了不兼容的重大變更次版本號做了向下兼容的功能添加補丁版本號做了向下兼容的bug修復
除了版本號之外,還有一些版本修飾,后面可以帶上數字:
alpha內測版 eg:3.0.0-alpha.1beta公測版 eg:3.0.0-beta.10rc正式版本的候選版 eg:3.0.0-rc.3
版本匹配
*/x:匹配任意值
1.1.*=>=1.1.0 <1.2.0
1.x=>=1.0.0 <2.0.0^xxx: 最左側非0版本號不變,不小於xxx
^1.2.3=>=1.2.3 <2.0.0主版本號不變
^0.1.2=>=0.1.2 <0.2.0主、次版本號不變
^0.0.2== 0.0.2主、次、補丁版本號都不變~xxx:如果列出了次版本號,則次版本號不變,如果沒有列出次版本號,則主版本號不變,均不小於xxx
~1.2.3=>=1.2.3 <1.3.0主、次版本號不變
~1=>=1.0.0 <2.0.0主版本號不變
3.3 package-lock.json作用
固定版本
當我們安裝包的時候,會自動添加package-lock.json文件,那么這個文件的作用是什么呢?在這個問題之前,先來看看npm install的安裝原理:
//package.json
{
"name": "npm",
"version": "1.0.0",
"dependencies": {
"vue": "^2.5.1"
},
"devDependencies": {
"eslint": "^7.0.0"
}
}
有上面一份npm配置文件,當npm install時會安裝兩個包:vue ^2.5.1,eslint ^7.0.0 ,符合所配置版本的包是一個范圍多個,npm會會安裝符合版本配置的最新版本。比如:
vue ^2.5.1 = >=2.5.1 <3.0.0, npm會選擇安裝2.6.13,因為它在匹配版本范圍內,且是目前最新的vue2的版本,它不會選擇2.5.0和3.0.0。
那么如果只有一份package.json文件,就很可能導致項目依賴的版本不一樣。比如開發時候vue2的最新版本是2.6.13,過了幾個月項目要上線,部署的時候vue2的最新版本已經是2.7.0了,那么線上就會安裝最新的版本。如果2.7.0有一些不兼容2.6.13的地方,或者有bug,那就會導致我們開發的一個經典問題:開發環境沒問題,一上線就壞。如果項目是多個人協同開發,甚至會導致開發環境都不一樣。
那么我們來看看package-lock.json文件怎么解決這個問題的:
//package-lock.json
{
"name": "npm",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"vue": {
"version": "2.6.13",
"resolved": "https://registry.nlark.com/vue/download/vue-2.6.13.tgz?cache=0&sync_timestamp=1622664849693&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue%2Fdownload%2Fvue-2.6.13.tgz",
"integrity": "sha1-lLLBsx/d8d/MNPKOyEi6jwHqTFs="
},
.....
}
}
我們看到package-lock.json文件里直接記錄了vue的固定版本號和下載地址。
npm在執行install的時候,會把每個需要安裝的包先在package-lock.json里查找,如果找到並且版本符合package.json的配置范圍(在范圍內就行,不需要最新),就會直接按照package-lock.json里的地址安裝。如果沒找到或者不符合范圍,則安裝原本的邏輯安裝(符合版本要求的最新版)。
這樣就確保,不管時間過了多久,只要package-lock.json文件不變,npm install安裝的包的版本都是一致的,避免代碼運行的依賴環境不同。
固定依賴結構
我們的一個項目通常會有很多依賴包,而這些依賴包很可能又會依賴其他的包,那如何來避免重復安裝呢?
比如:
//package.json
{
"name": "npm",
"version": "1.0.0",
"dependencies": {
"esquery": "^1.4.0",
"esrecurse": "^4.3.0",
"eslint-scope": "^5.1.1"
}
}
依賴關系如下:
- esquery : ^1.4.0,
estraverse : ^5.1.0
esrecurse : ^4.3.0estraverse : ^5.2.0
- eslint-scope :^5.1.1
esrecurse : ^4.3.0estraverse :^5.2.0
estraverse :^4.1.1
如果按照這個嵌套結構來安裝包的話也是可以的,而且npm原來的版本就是這么做的,這樣可以保證每個包都安裝完整,但是問題是會導致一些包重復安裝,如果這個依賴很多的話,重復的數量也會很多。那npm是怎么處理的呢?
npm采用的是用扁平結構,包的依賴,不管是直接依賴,還是子依賴的依賴,都會優先放在第一級。
如果第一級有找到符合版本的包,就不重復安裝,如果沒找到,則在當前目錄下安裝。
比如上面的包會被安裝成如下的結構:
- esquery :1.4.0,
estraverse : 5.2.0
- esrecurse : 4.3.0
estraverse : 5.2.0
- eslint-scope : 5.1.1
- estraverse : 4.3.1
包安裝的數量從開始的8個減少到了6個,雖然還是有重復,但是因為這個json的結構,又是以包名為鍵名,所以同一級下只能有一個同名的包,就像 estraverse : 5.2.0不能放在外層,因為外層已經有了以estraverse 為名的對象:estraverse : 4.3.1。
package-lock.json記錄的就是上面的依賴結構(上面只是簡寫,每一項還包含一些其他的信息,比如下載地址),這也是node_modules里面包的結構。
所以一個項目只要package-lock.json不變,它的依賴結構就不變,而且npm不用重新解析包的結構了,直接從package-lock.json文件就可以安裝完整且正確的包依賴,也提高了重新安裝的效率。
3.4 包緩存
npm安裝包不是每一次都從服務器直接下載,而是有緩存機制。當npm安裝包時,會在本地的緩存一份。執行npm config get cache可以查看緩存目錄:

按照路徑打開文件夾,會發現_cacache緩存文件夾,打開文件夾會有index-v5和content-v2兩個目錄。
其中index-v5存放的是包的索引,而content-v2則存放的是緩存的壓縮包。
緩存查找
那么npm是如何找到緩存包的呢?以vue包為例:
- 1.首先安裝vue包:
npm install vue - 2.查看package-lock.json文件,根據包信息獲取
resolved,integrity字段,構造字符串:
pacote:range-manifest:{resolved}:{integrity} - 3.把上面字符串按
SHA256加密,得到加密字符串:
2686ae12fd03809c9e5704cd01db518f1d7d07efe5ab61e6ef386e95b8481360 - 4.上面加密字符串的前4位就是
_cacache/index-v5目錄的下兩級,索引文件的位置:
_cacache/index-v5/26/86/ae12fd03809c9e5704cd01db518f1d7d07efe5ab61e6ef386e95b8481360 - 5.打開按照上面路徑找到的索引文件,在索引文件中找到
_shasum字段:
94b2c1b31fddf1dfcc34f28ec848ba8f01ea4c5b - 6.上面符串就是緩存包的位置,其前4位就是
_cacache/content-v2/sha1目錄的下兩級,包位置:
_cacache/content-v2/sha1/94/b2/c1b31fddf1dfcc34f28ec848ba8f01ea4c5b - 7.把按照上面路徑找到的文件的拓展名改為
.tgz,然后解壓,會得到vue.tar包,再解壓,就是我們熟悉的vue包了。
3.5 npm install 原理流程圖
把npm install原理總結為下面的流程圖:

4.npm常用命令
npm init [-y]創建package.json文件 [直接創建]npm run xxx [--env]運行腳本 [參數]npm config get [-l]查看npm配置 [全部配置]npm install xxx [--save-dev] [-g]安裝npm包 [添加到開發依賴] [全局安裝]npm uninstall xxx [-g]刪除包 [刪除全局包]npm info xxx查看包信息npm view xxx version查看包最新版本npm update [-g] xxx更新包 [全局包]npm root [-g]npm包安裝的目錄 [全局包安裝目錄]npm ls [-g]查看項目安裝的包 [全局安裝的包]npm install [--production]安裝項目 [只安裝項目依賴]npm ci安裝項目,不對比package.json,只從package-lock.json安裝,並且會先刪除node_modules目錄npm config get cache查看緩存目錄npm cache clean --force清除npm包緩存
參考
