天天看點

你不知道的npm

引言

作為

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

應用依賴,或者叫做業務依賴,是我們最常用的一種。這種依賴是應用釋出後上線所需要的,也就是說其中的依賴項屬于線上代碼的一部分。比如架構

react

,第三方的元件庫

ant-design

等。可通過下面的指令來安裝:

npm i ${packageName} -S
           

複制

devDependencies

開發環境依賴。這種依賴隻在項目開發時所需要,比如建構工具

webpack

gulp

,單元測試工具

jest

mocha

等。可通過下面的指令來安裝:

npm i ${packageName} -D
           

複制

peerDependencies

同行依賴。這種依賴的作用是提示宿主環境去安裝插件在

peerDependencies

中所指定依賴的包,用于解決插件與所依賴包不一緻的問題。

聽起來可能沒有那麼好了解,舉個例子來說明下。

[email protected]

隻是提供了一套基于

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

npm 3.x

中表現不一樣。npm2.x 會自動安裝同等依賴,npm3.x 不再自動安裝,會産生警告!手動在

package.json

檔案中添加依賴項可以解決。

optionalDependencies

可選依賴。這種依賴中的依賴包即使安裝失敗了,也不影響整個安裝的過程。需要注意的是,

optionalDependencies

會覆寫

dependencies

中的同名依賴包,是以不要在兩個地方都寫。

在實際項目中,如果某個包已經失效,我們通常會尋找它的替代方案。不确定的依賴會增加代碼判斷和測試難度,是以這個依賴項還是盡量不要使用。

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)

    :添加功能或者廢棄功能,向下相容
  • 更新檔版本号(patch)

    :bug 修複,向下相容

下面讓我們來看下常用的幾個版本格式:

  • "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 中子產品目錄結構就是下面這樣的:

你不知道的npm

這樣的方式優點很明顯,

node_modules

的結構和

package.json

結構一一對應,層級結構明顯,并且保證了每次安裝目錄結構都是相同的。

但是,試想一下,如果你依賴的子產品非常之多,你的 node_modules 将非常龐大,嵌套層級非常之深:

你不知道的npm

從上圖這種情況,我們不難得出嵌套結構擁有以下缺點:

  • 在不同層級的依賴中,可能引用了同一個子產品,導緻大量備援
  • 嵌套層級過深可能導緻不可預知的問題

扁平結構

為了解決以上問題,npm 在

3.x

版本做了一次較大更新。其将早期的嵌套結構改為扁平結構。

安裝子產品時,不管其是直接依賴還是子依賴的依賴,優先将其安裝在

node_modules

根目錄。

還是上面的依賴結構,我們在執行 npm install 後将得到下面的目錄結構:

你不知道的npm

此時我們若在子產品中又依賴了

[email protected]

版本:

{
  "name": "ts-axios",
  "dependencies": {
    "axios": "^0.19.0",
    "body-parser": "^1.19.0",
    "is-buffer": "^2.0.1"
  }
}
           

複制

當安裝到相同子產品時,判斷已安裝的子產品版本是否符合新子產品的版本範圍,如果符合則跳過,不符合則在目前子產品的

node_modules

下安裝該子產品。

此時,我們在執行

npm install

後将得到下面的目錄結構:

你不知道的npm

對應的,如果我們在項目代碼中引用了一個子產品,子產品查找流程如下:

  • 在目前子產品路徑下搜尋
  • 在目前子產品

    node_modules

    路徑下搜尋
  • 在上級子產品的

    node_modules

    路徑下搜尋
  • ...
  • 直到搜尋到全局路徑中的

    node_modules

假設我們又依賴了一個包

axios2@^0.19.0

,而它依賴了包

is-buffer@^2.0.3

,則此時的安裝結構是下面這樣的:

你不知道的npm

是以 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

    : 安裝來源
  • integrity

    : 表明包完整性的 hash 值(驗證包是否已失效)
  • 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 裡面執行指定的腳本指令。是以,隻要是 Shell(一般是 Bash)可以運作的指令,就可以寫在 npm 腳本裡面。 比較特别的是,npm run 建立的這個 Shell,會将目前目錄的

node_modules/.bin

子目錄加入 PATH 變量,執行結束後,再将 PATH 變量恢複原樣。

傳入參數

在原有腳本後面加上

--

分隔符, 後面再加上參數,就可以将參數傳遞給 script 指令了,比如 eslint 内置了代碼風格自動修複模式,隻需給它傳入

-–fix

參數即可,我們可以這樣寫:

"scripts": {
    "lint": "vue-cli-service lint --fix",
  },
           

複制

除了第一個可執行的指令,以空格分割的任何字元串(除了一些 shell 的文法)都是參數,并且都能通過

process.argv

屬性通路。

process.argv

屬性傳回一個數組,其中包含當啟動 Node.js 程序時傳入的指令行參數。 第一個元素是

process.execPath

,表示啟動 node 程序的可執行檔案的絕對路徑名。第二個元素為目前執行的 JavaScript 檔案路徑。剩餘的元素為其他指令行參數。

執行順序

如果 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 檔案可以統一團隊的 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

npm config set <key> <value> [-g|--global]
npm config set registry <url>  # 指定下載下傳 npm 包的來源,預設為 https://registry.npmjs.org/ ,可以指定私有源
           

複制

設定配置參數 key 的值為 value,如果省略 value,key 會被設定為

true

get

npm config get <key>
           

複制

檢視配置參數 key 的值。

delete

npm config delete <key>
           

複制

删除配置參數 key。

list

npm config list [-l] [--json]
           

複制

檢視所有設定過的配置參數。使用

-l

檢視所有設定過的以及預設的配置參數。使用

--json

以 json 格式檢視。

edit

npm config edit
           

複制

在編輯器中打開

npmrc

檔案,使用

--global

參數打開全局 npmrc 檔案。

最後

以上就是我關于 npm 的一些

深度挖掘

,當然有很多方面沒有總結到位,後續我會在實戰的過程中,不斷總結,随時更新。也歡迎大佬随時來

吐槽