作為前端開發者,應該每個人都用過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 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包快取
參考