npm基本用法及原理(10000+)

木子草明發表於2021-06-08

   作為前端開發者,應該每個人都用過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 vuenpm install eslint --save-dev,會發現專案會有以下變化:

  • 新增了目錄node_modules
    安裝的包和包的依賴都存放在這裡,引入的時候,會自動到此目錄下找。
  • package.json檔案自動新增了如下配置:
      "dependencies": {
        "vue": "^2.6.13"
      },
      "devDependencies": {
        "eslint": "^7.27.0"
      }
    
    npm 在安裝包的同時,會把包的名稱和版本加入到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
    打包依賴,釋出當前包的時候,會把此配置下的依賴包也一起打包。必須先在 dependenciesdevDependencies 宣告過,否則打包會報錯。

包版本說明
npm採用semver作為包版本管理規範。此規範規定軟體版本由三個部分組成:

  • 主版本號做了不相容的重大變更
  • 次版本號做了向下相容的功能新增
  • 補丁版本號做了向下相容的bug修復

除了版本號之外,還有一些版本修飾,後面可以帶上數字:

  • alpha內測版 eg:3.0.0-alpha.1
  • beta公測版 eg:3.0.0-beta.10
  • rc正式版本的候選版 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.03.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-v5content-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包快取

參考

相關文章