作為前端開發者,應該每個人都用過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.0
estraverse : ^5.2.0
- eslint-scope :^5.1.1
esrecurse : ^4.3.0
estraverse :^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包緩存
參考