與你項目相關的npm知識總結

每次克隆下別人的代碼後,執行的第一步就是npm install安裝依賴包,安裝成功後全部的包都會放在項目的node_modules文件夾下,也會自動生成package-lock.json文件。有沒有好奇過node_modules下的文件都是啥?package-lock.json文件的做用是啥?前端

本文主要解決如下幾個問題:vue

  1. package.json中的dependenciesdevDependencies的區別是啥,peerDependenciesbundledDependenciesoptionalDependencies又是啥?
  2. 爲何有的命令寫在package.json中的script中就能夠執行,可是經過命令行直接執行就不行?
  3. 爲何須要package-lock.json文件?
  4. 一個包在項目中有可能須要不一樣的版本,最後安裝到根目錄node_modules中的具體是哪一個版本?

帶着這幾個問題,咱們先從package.json文件提及。node

package.json

最靠譜的官方文檔請點這裏ios

官方文檔中列出了好多屬性,感興趣的能夠一個個看一遍。下面只列出其中幾個比較經常使用且重要的屬性。web

name & version

若是想要發佈一個npm包,nameversion屬性是必須的。他們兩個組合會造成一個惟一的標識來表名當前包。之後每更新一次包,version就須要進行相應的更改。若是你不打算髮布包,只想在本地使用,這兩個字段不是必須的。算法

name字段命名的規則以下:shell

  • 長度不能超過214個字符(對於有scoped的包,該限制包括scoped字段)(什麼是Scoped packages?
  • 有做用域的包名字能夠以.或者_開頭,沒有做用域限制的不可
  • 不能含有大寫字母
  • 不能含有非URL安全的字符

version字段npm

版本號須要符合semver(語義化版本號)規則,具體版本格式爲:主版本號.次版本號.修訂號, 如1.1.0。json

  • 主版本號(major):作了不兼容的 API 修改
  • 次版本號(minor):作了向下兼容的功能性新增
  • 修訂號(patch):作了向下兼容的問題修正

當有一些先行版本須要發佈時,能夠在主版本號.次版本號.修訂號以後加上一個中劃線和標識符如alpha(內部版本)、beta(公測版本)、rc(候選版本)等來代表。axios

以vue的版本爲例:

  • 最新的穩定版本:3.0.5
  • 最新的rc版本:3.0.0-rc.13
  • 最新的beta版本:3.0.0-beta.24
  • 最新的alpha版本:3.0.0-alpha.13

能夠經過npm install semver來檢查一個包的命名是否符合semver規則。有關semver具體的說明能夠看這裏

dependencies & devDependencies

dependenciesdevDependencies你們應該都不陌生,經過npm install xx --save安裝的包會寫入dependencies中,經過npm install xx --save-dev安裝的包會寫入devDependencies

dependencies中的包是生產環境的依賴,屬於線上代碼的一部分,好比vueaxiosveui等。devDependencies中的包是開發環境的依賴,只是在本地開發的時候須要依賴這裏的包,好比 vue-loadereslint等。

咱們平時用的npm install命令既會安裝dependencies中的包,也會安裝devDependencies中的包。若是隻想安裝dependencies中包,可使用npm install --production或者將NODE_ENV環境變量設置爲production,一般在生成環境咱們會這麼用。

須要注意的是,一個模塊會不會被打包取決於咱們在項目中是否引入了該模塊,跟該模塊放在dependencies中仍是devDependencies並無關係。

peerDependencies & bundledDependencies & optionalDependencies

這三個屬性在平時咱們的項目開發中都用不到。不一樣於dependencies & devDependencies面向的是包的使用者,peerDependencies & optionalDependencies & bundledDependencies這三個屬性是面向包的發佈者。

peerDependencies

咱們在一些node_modules包的package.json中能夠看到peerDependencies,它用來代表若是你想要使用此插件,此插件要求宿主環境所安裝的包。好比項目中用到的veui1.0.0-alpha.24版本中:

"peerDependencies": {
    "vue": "^2.5.16"
 }

這代表若是你想要使用veui1.0.0-alpha.24版本,所要求的vue版本須要知足>=2.5.16<3.0.0

npm3.x以上版本中,若是安裝結束後宿主環境沒有知足peerDependencies中的要求,會在控制檯打印出警告信息。

bundledDependencies

當咱們想在本地保留一個npm完整的包或者想生成一個壓縮文件來獲取npm包的時候,會用到bundledDependencies。本地使用npm pack打包時會將bundledDependencies中依賴的包一同打包,當npm install時相應的包會同時被安裝。須要注意的是,bundledDependencies中的包不該該包含具體的版本信息,具體的版本信息須要在dependencies中指定。

例如一個package.json文件以下:

{
  "name": "awesome-web-framework",
  "version": "1.0.0",
  "bundledDependencies": [
    "renderized", 
    "super-streams"
  ]
}

當咱們執行npm pack後會生成awesome-web-framework-1.0.0.tgz文件。該文件中包含renderizedsuper-streams這兩個依賴,當執行npm install awesome-web-framework-1.0.0.tgz下載包時,這兩個依賴會被安裝。

當咱們使用npm publish來發布包的話,這個屬性不會起做用。

optionalDependencies

從名字上就能夠看出,這是可選依賴。若是有包寫在optionalDependencies中,即便npm找不到或者安裝失敗了也不會影響安裝過程。須要注意的是,optionalDependencies中的配置會覆蓋dependencies中的配置,因此不要將同一個包同時放在這兩個裏面。

若是使用了optionalDependencies,必定記得要在項目中作好異常處理,獲取不到的狀況下應該怎麼辦。

scripts

定義在scripts中的命令,咱們經過npm run <command>就能夠執行。npm run <command>npm run-script <command>的簡寫。若是不加command,則會列出當前目錄下可執行的全部腳本。

teststartrestartstop這幾個命令執行時能夠不加run,直接npm testnpm startnpm restartnpm stop調用便可。

env是一個內置的命令,能夠經過npm run env能夠獲取到腳本運行時的全部環境變量。自定義的env命令會覆蓋內置的env命令。

以前開發中遇到一種狀況,好比咱們想本地經過http-server啓動一個服務器,若是事先沒有全局安裝過http-server包,只是安裝在對應項目的node_modules中。在命令行中輸入http-server會報command not found,可是若是咱們在scripts中增長以下一條命令就能夠執行成功。

scripts: {
  "server": "http-server",
  "eslint": "eslint --ext .js"
}

爲何一樣的命令寫在scripts中就能夠成功,可是在命令行中執行就不行呢?這是由於npm run命令會將node_modules/.bin/加入到shell的環境變量PATH中,這樣即便局部安裝的包也能夠直接執行而不用加node_modules/.bin/前綴。當執行結束後,再將其刪除。

是否是仍是沒明白,下面咱們來具體分析一下。

首先要明確什麼是環境變量。環境變量就是系統在執行一個程序,可是沒有明確代表該程序所在的完整路徑時,須要去哪裏尋找該程序。

對於局部安裝的包,拿eslint來講,npm會在本地項目./node_modules/.bin目錄下建立一個指向./node_moudles/eslint/bin/eslint.js名爲eslint的軟連接,即執行./node_modules/.bin/eslint其實是執行./node_moudles/eslint/bin/eslint.js。而當咱們執行npm run eslint的時候,node_modules/.bin/會被加入到環境變量PATH中,實際上執行的是./node_modules/.bin/eslint,這樣就串起來了。

理論說完以後,咱們來實際驗證一下。

首先看一下系統的環境變量。直接執行env便可。

而後在當前項目目錄下經過npm run env查看腳本運行時的環境變量。

經過對比能夠發現,運行時的PATH多了兩個環境變量。即npm指令的路徑和項目/node_modules/.bin的路徑。

以上就是package.json中經常使用 & 重要的幾個屬性,接下來咱們來看一看package-lock.json

package-lock.json

對於npmpackage.json文件能夠當作它的輸入,node_modules能夠作爲它的輸出。在理想狀況下,npm應該是一個純函數,不管什麼時候執行相同的package.json文件都應該產生徹底相同的node_modules樹。在一些狀況下,這確實能夠作到。可是在大多狀況下,都實現不了。主要有如下幾個緣由:

  • 使用者的npm版本有可能不一樣,不一樣的npm版本有着不一樣的安裝算法
  • 自上次安裝以後,有些符合semver-range的包已經有新的版本發佈。這樣再有別人安裝的時候,會安裝符合要求的最新版本。好比引入vue包:vue:^2.6.1。A小夥伴下載的時候是2.6.1,過一陣有另外一個小夥伴B入職在安裝包的時候,vue已經升級到2.6.2,這樣npm就會下載2.6.2的包安裝在他的本地
  • 針對第二點,一個解決辦法是固定本身引入的包的版本,可是一般咱們不會這麼作。即便這樣作了,也只能保證本身引入的包版本固定,也沒法保證包的依賴的升級。好比vue其中的一個依賴lodashlodash:^4.17.4,A下載的是4.17.4, B下載的時候有可能已經升級到了4.17.21

爲了解決上述問題,npm5.x開始增長了package-lock.json文件。每當npm install執行的時候,npm都會產生或者更新package-lock.json文件。package-lock.json文件的做用就是鎖定當前的依賴安裝結構,與node_modules中下全部包的樹狀結構一一對應。

有了這個package-lock.json文件,就能保證團隊每一個人安裝的包版本都是相同的,不會出現有些包升級形成我這好使別人那很差使的兼容性問題。

下面是lesspackage-lock.json文件結構:

"less": {
    "version": "3.13.1",
    "resolved": "https://registry.npmjs.org/less/-/less-3.13.1.tgz",
    "integrity": "sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw==",
    "dev": true,
    "requires": {
      "copy-anything": "^2.0.1",
      "errno": "^0.1.1",
      "graceful-fs": "^4.1.2",
      "image-size": "~0.5.0",
      "make-dir": "^2.1.0",
      "mime": "^1.4.1",
      "native-request": "^1.0.5",
      "source-map": "~0.6.0",
      "tslib": "^1.10.0"
    },
    dependencies: {
        "copy-anything": {
          "version": "2.0.3",
          "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz",
          "integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==",
          "dev": true,
          "requires": {
            "is-what": "^3.12.0"
          }
          }
    }
 }
  • version: 包的版本信息
  • resoloved: 包的安裝源
  • integrity:一個hash值,用來校驗包的完整性
  • dev:布爾值,若是爲true,代表此包若是不是頂層模塊的一個開發依賴(寫在devDependencies中),就是一個傳遞依賴(如上面less中的copy-anything)。
  • requires: 對應子依賴的依賴,與依賴包的package.jsondependencies的依賴項相同
  • dependencies:結構與外層結構相同,存在於包本身的node_modules中的依賴(不是全部的包都有,當子依賴的依賴版本與根目錄的node_modules中的依賴衝突時,纔會有)

經過分析上面的package-lock.json文件,也許會有一個問題。爲何有的包能夠被安裝在根目錄的node_modules中,有的包卻只能安裝在本身包下面的node_modules中?這就涉及到npm的安裝機制。

npm從3.x開始,採用了扁平化的方式來安裝node_modules。在安裝時,npm會遍歷整個依賴樹,不論是項目的直接依賴仍是子依賴的依賴,都會優先安裝在根目錄的node_modules中。遇到相同名稱的包,若是發現根目錄的node_modules中存在可是不符合semver-range,會在子依賴的node_modules中安裝符合條件的包。

具體的安裝算法以下:

  • 從磁盤加載node_modules
  • 克隆node_modules
  • 獲取package.json文件和分類完畢的元數據信息並把元數據信息插入到克隆樹中
  • 遍歷克隆樹,檢測是否有丟失的依賴。若是有,把他們添加到克隆樹中,依賴會盡量的添加到最高層
  • 比較原始樹和克隆樹,列出將原始樹轉換爲克隆樹所要採起的具體步驟
  • 執行,包括install, update, remove and move

npm官網的例子舉例,假設package{dep}結構表明包和包的依賴,現有以下結構:A{B,C}, B{C}, C{D},按照上述算法執行完畢後,生成的node_modules結構以下:

A
+-- B
+-- C
+-- D

對於B,C被安裝在頂層很好理解,由於是A的直接依賴。可是B又依賴C,安裝C的時候發現頂層已經有C了,因此不會在B本身的node_modules中再次安裝。C又依賴D,安裝D的時候發現根目錄並無D,因此會把D提高到頂層。

換成A{B,C}, B{C,D@1}, C{D@2}這樣的依賴關係後,產生的結構以下:

A
+-- B
+-- C
   `-- D@2
+-- D@1

B又依賴了D@1,安裝時發現根目錄的node_modules沒有,因此會把D@1安裝在頂層。C依賴了D@2,安裝D@2時,由於npm不容許同層存在兩個名字相同的包,這樣就與跟目錄node_modulesD@1衝突,因此會把D@2安裝在C本身的node_modules中。

模塊的安裝順序決定了當有相同的依賴時,哪一個版本的包會被安裝在頂層。首先項目中主動引入的包確定會被安裝在頂層,而後會按照包名稱排序(a-z)進行依次安裝,跟包在package.json中寫入的順序無關。所以,若是上述將B{C,D@1}換成E{C,D@1},那麼D@2將會被安裝在頂層。

有一種狀況,當咱們項目中所引用的包版本較低,好比A{B@1,C},而C所須要的是C{B@2}版本,如今的結構應該以下:

A
+-- B@1
+-- C
   `-- B@2

有一天咱們將項目中的B升級到B@2,理想狀況下的結構應該以下:

A
+-- B@2
+-- C

可是如今package-lock.json文件的結構倒是這樣的:

A
+-- B@2
+-- C
   `-- B@2

B@2不只存在於根目錄的node_modules下,C下也一樣存在。這時須要咱們手動執行npm dedupe進行去重操做,執行完成後會發現C下面的B@2會消失。你們能夠在本身的項目中試一試,優化一下package-lock.json文件的結構。

如下是在個人項目中執行npm dedupe的結果:

removed 41 packages, moved 15 packages and audited 1994 packages in 18.538s

npm5.x以前,能夠手動經過npm shrinkwrap生成npm-shrinkwrap.json文件,與package-lock.json文件的做用相同。當項目中同時存在npm-shrinkwrap.jsonpackage-lock.json,將以npm-shrinkwrap.json爲主。

本文只是一些理論基礎,以後會介紹一些npm源碼相關的知識。

參考文章

  1. npm官網
  2. 前端工程化 - 剖析npm的包管理機制
  3. 前端工程化(5):你所須要的npm知識儲備都在這了
  4. semver
相關文章
相關標籤/搜索