引言
作為
node
自帶的包管理器工具,在
nodejs
社群和
web 前端工程化
領域發展日益龐大的背景下,
npm
已經成為每位前端開發同學必備的工具。
每天,無數的開發人員使用
npm
來建構項目,
npm init
、
npm install
等方式幾乎成為了建構項目的首選方式,但是大多數同學對于 npm 的使用卻隻停留在了
npm install
這裡。(相信删除
node_modules
檔案夾,重新執行
npm install
這種事情你應該做過吧)
本篇文章主要是結合我以往的經驗,帶大家更深層次的了解一下
npm
。
npm 中的依賴包
依賴包類型
npm 目前支援一下幾種類型的依賴包管理
-
dependencies
-
devDependencies
-
peerDependencies
-
optionalDependencies
-
bundledDependencies / bundleDependencies
"dependencies": {
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
"koa-redis": "^4.0.0",
},
"devDependencies": {
"babel-eslint": "^10.0.3",
"cross-env": "^6.0.3",
"lint-staged": "^9.5.0",
"mysql2": "^2.1.0",
"nodemon": "^1.19.1",
"precommit": "^1.2.2",
"redis": "^2.8.0",
"sequelize": "^5.21.3",
},
"peerDependencies": {},
"optionalDependencies": {},
"bundledDependencies": []
複制
dependencies
dependencies
應用依賴,或者叫做業務依賴,是我們最常用的一種。這種依賴是應用釋出後上線所需要的,也就是說其中的依賴項屬于線上代碼的一部分。比如架構
react
,第三方的元件庫
ant-design
等。可通過下面的指令來安裝:
npm i ${packageName} -S
複制
devDependencies
devDependencies
開發環境依賴。這種依賴隻在項目開發時所需要,比如建構工具
webpack
、
gulp
,單元測試工具
jest
、
mocha
等。可通過下面的指令來安裝:
npm i ${packageName} -D
複制
peerDependencies
peerDependencies
同行依賴。這種依賴的作用是提示宿主環境去安裝插件在
peerDependencies
中所指定依賴的包,用于解決插件與所依賴包不一緻的問題。
聽起來可能沒有那麼好了解,舉個例子來說明下。
隻是提供了一套基于
react
的
ui
元件庫,但它要求宿主環境需要安裝指定的
react
版本,是以你可以看到 node_modules 中 antd 的
package.json
中有這麼一項配置:
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
},
複制
它要求宿主環境安裝大于等于
16.0.0
版本的
react
,也就是
antd
的運作依賴宿主環境提供的該範圍的
react
安裝包。
在安裝插件的時候,peerDependencies 在和
npm 2.x
中表現不一樣。npm2.x 會自動安裝同等依賴,npm3.x 不再自動安裝,會産生警告!手動在
npm 3.x
檔案中添加依賴項可以解決。
package.json
optionalDependencies
optionalDependencies
可選依賴。這種依賴中的依賴包即使安裝失敗了,也不影響整個安裝的過程。需要注意的是,
optionalDependencies
會覆寫
dependencies
中的同名依賴包,是以不要在兩個地方都寫。
在實際項目中,如果某個包已經失效,我們通常會尋找它的替代方案。不确定的依賴會增加代碼判斷和測試難度,是以這個依賴項還是盡量不要使用。
bundledDependencies / bundleDependencies
bundledDependencies / bundleDependencies
打包依賴。如果在打包釋出時希望一些依賴包也出現在最終的包裡,那麼可以将包的名字放在
bundledDependencies
中,bundledDependencies 的值是一個字元串數組,如:
{
"name": "sequelize-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"mysql2": "^2.1.0"
},
"devDependencies": {
"sequelize": "^5.21.3"
},
"bundledDependencies": [
"mysql2",
"sequelize"
]
}
複制
執行打包指令
npm pack
,在生成的
sequelize-test-1.0.0.tgz
包中,将包含
mysql2
和
sequelize
。
需要注意的是,在 bundledDependencies
中指定的依賴包,必須先在 dependencies 和 devDependencies 聲明過,否則打包會報錯。
語義化版本控制
為了在軟體版本号中包含更多意義,反映代碼所做的修改,産生了語義化版本,軟體的使用者能從版本号中推測軟體做的修改。npm 包使用語義化版控制,我們可安裝一定版本範圍的 npm 包,npm 會選擇和你指定的版本相比對 的 (
latest
)最新版本安裝。
npm 采用了
semver
規範作為依賴版本管理方案。版本号由三部分組成:
主版本号
、
次版本号
、
更新檔版本号
。變更不同的版本号,代表不同的意義:
-
:軟體做了不相容的變更(主版本号(major)
重大變更)breaking change
-
:添加功能或者廢棄功能,向下相容次版本号(minor)
-
:bug 修複,向下相容更新檔版本号(patch)
下面讓我們來看下常用的幾個版本格式:
-
"compression": "1.7.4"
表示精确版本号。任何其他版本号都不比對。在一些比較重要的線上項目中,建議使用這種方式鎖定版本。
-
"typescript": "^3.4.3"
表示相容更新檔和小版本更新的版本号。官方的定義是
能夠相容除了最左側的非 0 版本号之外的其他變化
。這句話不是很好了解,舉幾個例子大家就明白了:
"^3.4.3" 等價于 `">= 3.4.3 < 4.0.0"。即隻要最左側的 "3" 不變,其他都可以改變。是以 "3.4.5", "3.6.2" 都可以相容。
"^0.4.3" 等價于 ">= 0.4.3 < 0.5.0"。因為最左側的是 "0",那麼隻要第二位 "4" 不變,其他的都相容,比如 "0.4.5" 和 "0.4.99"。
"^0.0.3" 等價于 ">= 0.0.3 < 0.0.4"。大版本号和小版本号都為 "0" ,是以也就等價于精确的 "0.0.3"。
複制
-
"mime-types": "~2.1.24"
表示隻相容更新檔更新的版本号。關于
~
的定義分為兩部分:如果列出了小版本号(第二位),則隻相容更新檔(第三位)的修改;如果沒有列出小版本号,則相容第二和第三位的修改。我們分兩種情況了解一下這個定義:
"~2.1.24" 列出了小版本号 "1",是以隻相容第三位的修改,等價于 ">= 2.1.24 < 2.2.0"。
"~2.1" 也列出了小版本号 "2",是以和上面一樣相容第三位的修改,等價于 ">= 2.1.0 < 2.2.0"。
"~2" 沒有列出小版本号,可以相容第二第三位的修改,是以等價于 ">= 2.0.0 < 3.0.0"
複制
-
"underscore-plus": "1.x"
"uglify-js": "3.4.x"
除了上面的
x
、
X
還有
*
和(
空
),這些都表示使用通配符的版本号,可以比對任何内容。具體來說:
"*" 、"x" 或者 (空) 表示可以比對任何版本。
"1.x", "1.*" 和 "1" 表示比對主版本号為 "1" 的所有版本,是以等價于 ">= 1.0.0 < 2.0.0"。
"1.2.x", "1.2.*" 和 "1.2" 表示比對版本号以 "1.2" 開頭的所有版本,是以等價于 ">= 1.2.0 < 1.3.0"。
複制
-
"css-tree": "1.0.0-alpha.33"
"@vue/test-utils": "1.0.0-beta.29"
有時候為了表達更加确切的版本,還會在版本号後面添加标簽或者擴充,來說明是預釋出版本或者測試版本等。常見的标簽有:
标簽 | 含義 | 補充 |
---|---|---|
demo | demo 版本 | 可能用于驗證問題的版本 |
dev | 開發版 | 開發階段用的,bug 多,體積較大等特點,功能不完善 |
alpha | α 版本 | 預覽版,或者叫内部測試版;一般不向外釋出,會有很多 bug;一般隻有測試人員使用。 |
beta | 測試版(β 版本) | 測試版,或者叫公開測試版;這個階段的版本會一直加入新的功能;在 alpha 版之後推出。 |
gamma | (γ)伽馬版本 | 較 α 和 β 版本有很大的改進,與穩定版相差無幾,使用者可使用 |
trial | 試用版本 | 本軟體通常都有時間限制,過期之後使用者如果希望繼續使用,一般得交納一定的費用進行注冊或購買。有些試用版軟體還在功能上做了一定的限制。 |
csp | 内容安全版本 | js 庫常用 |
rc | 最終測試版本 | 可能成為最終産品的候選版本,如果未出現問題則可釋出成為正式版本 |
latest | 最新版本 | 不指定版本和标簽,npm 預設安最新版 |
stable | 穩定版 |
npm install 原理分析
我們都知道,執行
npm install
後,依賴包被安裝到了
node_modules
中。雖然在實際開發中我們無需十分關注裡面具體的細節,但了解
node_modules
中的内容可以幫助我們更好的了解
npm
安裝依賴包的具體機制。
嵌套結構
在 npm 的早期版本中,npm 處理依賴的方式簡單粗暴,以遞歸的方式,嚴格按照
package.json
結構以及子依賴包的
package.json
結構将依賴安裝到他們各自的
node_modules
中。
舉個例子,我們的項目
ts-axios
現在依賴了兩個子產品:
axios
、
body-parser
:
{
"name": "ts-axios",
"dependencies": {
"axios": "^0.19.0",
"body-parser": "^1.19.0",
}
}
複制
axios
依賴了
follow-redirects
和
is-buffer
子產品:
{
"name": "axios",
"dependencies": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
}
複制
body-parser
依賴了
bytes
和
content-type
等子產品:
{
"name": "body-parser",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
...
}
}
複制
那麼,執行 npm install 後,得到的 node_modules 中子產品目錄結構就是下面這樣的:

這樣的方式優點很明顯,
node_modules
的結構和
package.json
結構一一對應,層級結構明顯,并且保證了每次安裝目錄結構都是相同的。
但是,試想一下,如果你依賴的子產品非常之多,你的 node_modules 将非常龐大,嵌套層級非常之深:
從上圖這種情況,我們不難得出嵌套結構擁有以下缺點:
- 在不同層級的依賴中,可能引用了同一個子產品,導緻大量備援
- 嵌套層級過深可能導緻不可預知的問題
扁平結構
為了解決以上問題,npm 在
3.x
版本做了一次較大更新。其将早期的嵌套結構改為扁平結構。
安裝子產品時,不管其是直接依賴還是子依賴的依賴,優先将其安裝在
node_modules
根目錄。
還是上面的依賴結構,我們在執行 npm install 後将得到下面的目錄結構:
此時我們若在子產品中又依賴了
版本:
{
"name": "ts-axios",
"dependencies": {
"axios": "^0.19.0",
"body-parser": "^1.19.0",
"is-buffer": "^2.0.1"
}
}
複制
當安裝到相同子產品時,判斷已安裝的子產品版本是否符合新子產品的版本範圍,如果符合則跳過,不符合則在目前子產品的
node_modules
下安裝該子產品。
此時,我們在執行
npm install
後将得到下面的目錄結構:
對應的,如果我們在項目代碼中引用了一個子產品,子產品查找流程如下:
- 在目前子產品路徑下搜尋
- 在目前子產品
路徑下搜尋node_modules
- 在上級子產品的
路徑下搜尋node_modules
- ...
- 直到搜尋到全局路徑中的
node_modules
假設我們又依賴了一個包
axios2@^0.19.0
,而它依賴了包
is-buffer@^2.0.3
,則此時的安裝結構是下面這樣的:
是以 npm 3.x 版本并未完全解決老版本的子產品備援問題,甚至還會帶來新的問題。
我們在
package.json
通常隻會鎖定大版本,這意味着在某些依賴包小版本更新後,同樣可能造成依賴結構的改動,依賴結構的不确定性可能會給程式帶來不可預知的問題。
package-lock.json
為了解決
npm install
的不确定性問題,在
npm 5.x
版本新增了
package-lock.json
檔案,而安裝方式還沿用了
npm 3.x
的扁平化的方式。
package-lock.json
的作用是鎖定依賴結構,即隻要你目錄下有
package-lock.json
檔案,那麼你每次執行
npm install
後生成的
node_modules
目錄結構一定是完全相同的。
例如,我們有如下的依賴結構:
{
"name": "ts-axios",
"dependencies": {
"axios": "^0.19.0",
}
}
複制
在執行
npm install
後生成的
package-lock.json
如下:
{
"name": "ts-axios",
"version": "0.1.0",
"dependencies": {
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"is-buffer": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
}
}
複制
最外面的兩個屬性
name
、
version
同
package.json
中的
name
和
version
,用于描述目前包名稱和版本。
dependencies
是一個對象,對象和
node_modules
中的包結構一一對應,對象的
key
為包名稱,值為包的一些描述資訊:
-
: 包唯一的版本号version
-
: 安裝來源resolved
-
: 表明包完整性的 hash 值(驗證包是否已失效)integrity
-
: 依賴包所需要的所有依賴項,與子依賴的requires
中package.json
的依賴項相同。dependencies
-
: 依賴包dependencies
中依賴的包,與頂層的node_modules
一樣的結構dependencies
這裡注意,并不是所有的子依賴都有
dependencies
屬性,隻有子依賴的依賴和目前已安裝在根目錄的
node_modules
中的依賴沖突之後,才會有這個屬性。
通過以上幾個步驟,說明
package-lock.json
檔案和
node_modules
目錄結構是一一對應的,即項目目錄下存在
package-lock.json
可以讓每次安裝生成的依賴目錄結構保持相同。
在開發一個應用時,建議把
package-lock.json
檔案送出到代碼版本倉庫,進而讓你的團隊成員、運維部署人員或
CI
系統可以在執行
npm install
時安裝的依賴版本都是一緻的。
npm scripts 腳本
腳本功能
是 npm 最強大、最常用的功能之一。
npm 允許在
package.json
檔案中使用
scripts
字段來定義腳本指令。以
vue-cli3
為例:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
},
複制
這樣就可以通過
npm run serve
腳本替代
vue-cli-service serve
腳本來啟動項目,而無需每次都敲一遍冗長的腳本。
原理
這裡我們參考一下阮老師的文章:
npm 腳本的原理非常簡單。每當執行 npm run,就會自動建立一個,在這個 Shell 裡面執行指定的腳本指令。是以,隻要是 Shell(一般是 Bash)可以運作的指令,就可以寫在 npm 腳本裡面。 比較特别的是,npm run 建立的這個 Shell,會将目前目錄的
Shell
子目錄加入 PATH 變量,執行結束後,再将 PATH 變量恢複原樣。
node_modules/.bin
傳入參數
在原有腳本後面加上
--
分隔符, 後面再加上參數,就可以将參數傳遞給 script 指令了,比如 eslint 内置了代碼風格自動修複模式,隻需給它傳入
-–fix
參數即可,我們可以這樣寫:
"scripts": {
"lint": "vue-cli-service lint --fix",
},
複制
除了第一個可執行的指令,以空格分割的任何字元串(除了一些 shell 的文法)都是參數,并且都能通過
process.argv
屬性通路。
屬性傳回一個數組,其中包含當啟動 Node.js 程序時傳入的指令行參數。 第一個元素是
process.argv
,表示啟動 node 程序的可執行檔案的絕對路徑名。第二個元素為目前執行的 JavaScript 檔案路徑。剩餘的元素為其他指令行參數。
process.execPath
執行順序
如果 npm 腳本裡面需要執行多個任務,那麼需要明确它們的執行順序。
如果是串行執行,即要求前一個任務執行成功之後才能執行下一個任務。使用
&&
符号連接配接。
npm run script1 && npm run script2
複制
串行指令執行過程中,隻要一個指令執行失敗,則整個腳本将立刻終止。
如果是并行執行,即多個任務可以同時執行。使用
&
符号來連接配接。
npm run script1 & npm run script2
複制
鈎子
這裡的鈎子和
vue
或
react
裡面的生命周期有點相似。
npm 腳本有
pre
和
post
兩個鈎子。在執行 npm scripts 指令(無論是自定義還是内置)時,都經曆了 pre 和 post 兩個鈎子,在這兩個鈎子中可以定義某個指令執行前後的指令。
比如,在使用者執行
npm run build
的時候,會自動按照下面的順序執行。
npm run prebuild && npm run build && npm run postbuild
複制
當然,如果沒有指定
prebuild
、
postbuild
,會默默的跳過。如果想要指定鈎子,必須嚴格按照 pre 和 post 字首來添加。
環境變量
npm 腳本有一個非常強大的功能,就是可以使用 npm 的内部變量。
在執行
npm run
腳本時,npm 會設定一些特殊的
env
環境變量。其中 package.json 中的所有字段,都會被設定為以
npm_package_
開頭的環境變量。比如 package.json 中有如下字段内容:
{
"name": "sequelize-test",
"version": "1.0.0",
"description": "sequelize測試",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"mysql2": "^2.1.0",
"sequelize": "^5.21.3"
}
}
複制
那麼,變量
npm_package_name
傳回
sequelize-test
,變量
npm_package_description
傳回
sequelize測試
。也就是:
console.log(process.env.npm_package_name) // sequelize-test
console.log(process.env.npm_package_description) // sequelize測試
複制
npm 配置
優先級
npm 從以下來源擷取配置資訊(優先級由高到低):
指令行
指令行
npm run dev --foo=bar
複制
執行上述指令,會将配置項
foo
的值設為
bar
,通過
process.env.npm_config_foo
可以通路其配置值。這個時候的 foo 配置值将覆寫所有其他來源存在的 foo 配置值。
環境變量
環境變量
如果 env 環境變量中存在以
npm_config_
為字首的環境變量,則會被識别為 npm 的配置屬性。比如,環境變量中的
npm_config_foo=bar
将會設定配置參數
foo
的值為
"bar"
。
如果隻指定了參數名卻沒有指定任何值的配置參數,其值将會被設定為
true
。
npmrc檔案
npmrc檔案
通過修改
npmrc
檔案可以直接修改配置。系統中存在多個 npmrc 檔案,這些 npmrc 檔案被通路的優先級從高到低的順序為:
-
項目配置檔案
隻作用在本項目下。在其他項目中,這些配置不生效。通過建立這個.npmrc 檔案可以統一團隊的 npm 配置規範。路徑為
/path/to/my/project/.npmrc
。
-
使用者配置檔案
預設為
~/.npmrc/
,可通過
npm config get userconfig
檢視存放的路徑。
-
通過全局配置檔案
可以檢視具體存放的路徑。npm config get globalconfig
-
npm 内置的配置檔案
這是一個不可更改的内置配置檔案,為了維護者以标準和一緻的方式覆寫預設配置。
mac
下的路徑為
/path/to/npm/npmrc
。
預設配置
預設配置
通過
npm config ls -l
檢視 npm 内部的預設配置參數。如果指令行、環境變量、所有配置檔案都沒有配置參數,則使用預設參數值。
npm config 指令
set
set
npm config set <key> <value> [-g|--global]
npm config set registry <url> # 指定下載下傳 npm 包的來源,預設為 https://registry.npmjs.org/ ,可以指定私有源
複制
設定配置參數 key 的值為 value,如果省略 value,key 會被設定為
true
。
get
get
npm config get <key>
複制
檢視配置參數 key 的值。
delete
delete
npm config delete <key>
複制
删除配置參數 key。
list
list
npm config list [-l] [--json]
複制
檢視所有設定過的配置參數。使用
-l
檢視所有設定過的以及預設的配置參數。使用
--json
以 json 格式檢視。
edit
edit
npm config edit
複制
在編輯器中打開
npmrc
檔案,使用
--global
參數打開全局 npmrc 檔案。
最後
以上就是我關于 npm 的一些
深度挖掘
,當然有很多方面沒有總結到位,後續我會在實戰的過程中,不斷總結,随時更新。也歡迎大佬随時來
吐槽
!