npm install 的玄機

2021-02-14 JavaScript忍者秘籍

轉自https://juejin.cn/post/6902992287974817806#heading-1

背景

某次更新common包的時候筆者踩了很多坑,沒想到簡單的一個npm install命令背後還有這麼多的學問(keng),於是藉此機會學習一下,也將個人思考與大家分享。

更新歷史:

1、npm@3.0: 包的扁平化

2、[npm@5.0][npm_5.0]:install根據lock文件來,即使更改了package.json文件,只要有lock文件,那麼還是會根據lock文件安裝

3、[npm@5.1][npm_5.1]:install無視lock文件,只要json文件改了,那麼就根據json的來

4、[npm@5.4][npm_5.1]:lock和package文件都兼顧,如果package.json和lock文件不同,就根據package.json文件去更新包,並且更新lock文件,如果他們是一樣的,那麼會去根據lock文件更新包。

install過程

下面是步驟的簡單介紹,如果對整個流程有興趣的話可以看下[這個地方][Link 1]。

npm 向 registry 查詢模塊壓縮包的網址解壓壓縮包到當前項目的node_modules目錄源碼簡析

如果電腦上已經全局安裝了node,通過npm config get prefix獲取到npm安裝的全局路徑prefixUrl,然後就可以得到npm的代碼位置:${prefixUrl}/lib/node_modules/npm。在看install代碼時直接跳到到npm文件夾下install.js。看下面部分最好結合源碼部分一起看,這樣看起來更輕鬆。

初始化構造函數:

代碼截圖:

 

在ndTest文件夾,執行:npm i XXX

得到初始化變量

部分變量說明

dryrun : --dry-run: true不執行安裝,是否只列印信息currentTree:當前模塊的包含的信息,比如node_modules中中包構成的treeidealTree:安裝相應的模塊後,經過處理的包信息, 可以看成是目標 treeTodo: 用於存放運行的動作,在安裝過程中executeActions中看到它起的作用progress: 在run的newTracker過程中可以看到填充進去的processnoPackageJsonOk :沒有packageJson文件是否ok,變量的作用在readLocalPackageData使用到,用於如果直接執行npm install,那麼必須要有package.jsonautoPrune 布爾值,讀取配置文件 package-lock 判斷是否生成package-lock.json文件packageLockOnly 如果為true,不進行安裝動作,如果項目下面不存在lock文件,可以試一下這個命令npm i --package-lock-only體驗一把。[Chain][]

首先先看這個函數,因為這個函數幾乎貫穿了整個install的過程中,對於它的了解更有助於接下來的閱讀。

chain.first = {} ; chain.last = {}
function chain (things, cb) {
 var res = [];
  (function LOOP (i, len) {
  if (i >= len) return cb(null,res)
  if (Array.isArray(things[i]))
   things[i] = bindActor.apply(null,
    things[i].map(function(i){
     return (i===chain.first) ? res[0]
      : (i===chain.last)
       ? res[res.length - 1] : i }))
  if (!things[i]) return LOOP(i + 1, len)
  things[i](function (er, data) {
   if (er) return cb(er, res)
   if (data !== undefined) res = res.concat(data)
   LOOP(i + 1, len)
  })
 })(0, things.length) }
function bindActor () {
 var args = 
    Array.prototype.slice.call
    (arguments) *// jswtf.*
  , obj = null
  , fn
 if (typeof args[0] === "object") {
  obj = args.shift()
  fn = args.shift()
  if (typeof fn === "string")
   fn = obj[ fn ]
 } else fn = args.shift()
 return function (cb) {
  fn.apply(obj, args.concat(cb)) }
}

安裝過程(run)主要步驟

run的過程就是對上面那張圖的具體實現了,從構建邏輯樹到去網絡獲取包,從校驗包的完整性到解壓包到項目的node_modules文件夾,最後生成或者更新package-lock.json文件。

這裡先簡單介紹這些步驟,其中loadCurrentTree、loadIdealTree用於構建得到邏輯樹,generateActionsToTake比較兩棵樹生成需要執行的動作存儲到TODO中,executeActions真正的執行TODO中的動作。我們在install過程中如果使用verbose級別的log也可以看到各個階段的日誌。

loadCurrentTree

這個步驟主要經歷讀取PackageData和序列化讀取到的結果。

初步currentTree:讀node_modules裡面的包和npm-shrinkwrap.json構成初步tree。

readPackageTree(self.where, iferr(cb, function (currentTree) 從node_modules裡面讀包。以下是不存在node_modules時的截圖,這個時候得到的樹應該是最簡單的,如果存在node_modules,那麼這些信息會被放在children中。在讀取node_modules信息時,會過濾首字母為"."的文件夾,比如.bin。

在獲取到self.loadArgMetadata(cb如果是帶args的安裝,那麼會去獲取這個args的元信息,元信息的結果demo:

獲取args信息的過程中出現一個很重要代碼,如下,詳細可以看lib/fetch-package-metadata.js代碼,這塊代碼用於獲取指定包的信息,

pacote.manifest(dep, pacoteOpts({
 annotate: true,
 fullMetadata: opts.fullMetadata,
 log: tracker || npmlog,
 memoize: CACHE,
 where: where
})).then(

normalize之後:this.currentTree.isTop = true算是給idealTree打基礎

computeMetadata(this.currentTree)掛missingDeps和missingDevDeps:計算當前得到的currentTree中是否含有package.json中dependencies和devDependencies的包,如果沒有則把它放入對應的missing數組中。當然也會做其他的事情,比如computeMetadata中的resetMetadata會給它和所有的child節點初始化上,如下:

child.isTop = false
child.requiredBy = []
child.requires = []
child.missingDeps = {}
child.missingDevDeps = {}
child.phantomChildren = {}
child.location = null

如果執行命令時已經存在了node_modules,那麼會形成child和parent的循環引用,如圖:

loadIdealTree

這個步驟主要執行三個:cloneCurrentTreeToIdealTree、loadShrinkwrap、loadAllDepsIntoIdealTree。

cloneCurrentTreeToIdealTree:

this.idealTree = copyTree(this.currentTree):把currentTree複製給IdealTree,代碼比較短,是個遞歸調用,具體可以在install/copy-tree.js中看到。

loadShrinkwrap:

readShrinkwrap.andInflate(this.idealTree:依次讀取npm-shrinkwrap.json、package-lock.json、package.json,如果存在lock文件或者shrinkwrap文件,讀取到內容之後掛載到_shrinkwrap欄位,然後在install/inflate-shrinkwrap.js中重新構建一顆新的樹,形成了ideaTree的雛形。

具體代碼可以看install/inflate-shrink-wrap.js的inflatableChild方法:1、判斷已經存在包信息和lock文件中要求的是否一直,如果一直,就用這個包的信息,2、如果不存在,但是滿足構造信息的條件,比如存在integrity,那麼fake一個包信息,3、都不滿足,利用前面講的fetchPackageMetadata去獲取之後創建一個節點信息。

這裡也會運行computeMetadata(this.idealTree),類似上面normalize步驟,看lock文件中是否有dependencies和devDependencies的包信息,如果沒有那麼將信息存儲到對應的missing數組中。

loadAllDepsIntoIdealTree:這個步驟裡面還是嵌套了chain,根據是否制定了安裝參數鏈進去不同的步驟,不過做的事情其實算是大同小異吧,主要代碼在install/deps.js文件。這裡針對指定了參數的分支進行介紹:

首先開場部分,獲取到這次安裝的類型,如:optionalDependencies、devDependencies、、dependencies等等,這裡可以看見,如果沒有指定安裝類型,那麼默認作為dependencies。

再去校驗參數的有效性,將前面已經獲取到的args信息去validate-args.js文件校驗,這裡也體現了代碼的嚴謹性吧。

然後就是核心部分了,loadRequestedDeps將這個args加到idealTree上去,這個步驟會涉及到包的打平。可以將在args看成是一顆小tree,把小tree上的節點加載到idealTree上的過程中就會涉及到包比較:這個包信息是否已經存在idealTree上了或者滿足是符合semver規則,也可以看見安裝包的版本是怎樣變化的,所安裝包a的依賴b是不是和在開發包a時指定需要的依賴包b一樣,也可以看見怎麼為一個包找到它可以掛載的最高的父節點。下面是這個方法重要的幾個部分,大致是:slide包裡面asyncMap並行去處理參數,resolveWithNewModule處理小tree的最頂層節點,andForEachChild在之後對每個自節點進行處理,這個過程是比較耗時的*。*

debugTree、debugActions

這兩個步驟沒有進行很重要的處理,debugTree對currentTree和IdealTree使用[archy][]進行處理,對依賴形成一個樹形依賴結構,類似:

同樣,debugActions也是對得到的actions進行類似log的處理:

generateActionsToTake

這個步驟主要進行的動作有:

1、validateTree:遍歷idealTree,查看當前包進行安裝安裝的時候,是否滿足peer dependencies,如果不滿足那麼輸出warning;

2、diffTrees:比較currentTree和idealTree,首先將這兩棵樹使用flattenTree打平,從根結點的「/」key值開始,每個key都是相對於根結點的路徑,比如/@babel/plugin-proposal-dynamic-import。兩棵樹初步對比會得到那些包add、哪些包remove、哪些包update等等。如下圖:

然後把這些數據進行過濾,具體的過濾可看filterActions方法,得到的結果放入this.differences,這個步驟有個有意思的方法,function *pkgAreEquiv*(aa, bb) 比較兩個包是否相同。differences截圖示例,如果不存在node_modules的項目,那麼應該全是「add」,如果是安裝指定的包,那麼可能有「add」、"update"、"remove"、「move」等。

3、computeLinked:如果diff不為空,那麼檢測緩存,連結到合適的全局包,如果在全局存在,那麼將它從tree中移除

4、checkPermissions:如果diff不是空,檢測當前執行的動作在需要執行的目錄下面的權限,對於add、update\remove、move動作進行對應的權限檢測。

5、decomposeActions:如果diff不為空,對diff進行區別處理,根據cmd的不同,add、update、move、remove以及默認類型,對它進行不同處理之後加入到todo中。todo截圖示例:

startAudit

對安裝的包進行audit,function *generateFromInstall* (tree, diffs, install, remove)根據前面得到的actions和diffs,更新metaData。下面是auditData部分截圖:

並且返回 generateMetadata()這個promise,在後面executeActions步驟中使用。

executeActions

開始真正的安裝動作,執行todo裡面所有的動作。裡面的動作分:doParallelActions、doSerialActions、lock、rimraf四種。

執行過程部分輸出截圖demo:

這裡對install源碼進行簡單介紹,對於這種過程複雜的代碼使用調試手段輔助應該會更好,可以去[node官網看一圈][node]。介紹一種簡單的調試方法,新建一個node項目之後,在package.json的script中添加腳本:"npm-debug":「node --inspect-brk=5858 ${prefixUrl}/lib/node_modules/npm/bin/npm-cli.js install [option react@16.8.0]」。在調試的過程中一邊調試一邊看控制臺的輸出信息以及node_modules中的變化應該可以對安裝過程有更多的了解。

npm緩存文件夾

一直都比較好奇npm的緩存是把文件存在哪裡的,然後文件存放路徑選擇的依據是什麼,現在來根據緩存裡的數據倒推一下:npm的緩存文件夾.npm中存在_cacache文件夾,裡面存在content-v2、index-v5、tmp。index-v5索引文件夾,content-v2存放壓縮包的文件夾。

索引:在index-v5中搜索archy,獲取它的索引信息,得到哈希值:9323fd73c05db8792d37c9c0729f5f570e2d231672968fb966ba05443b446bc6,這個哈希值就是存儲的路徑了,不過為什麼會是這個哈希呢,下面找下原因:

打開文件index-v5/93/23/fd73c05db8792d37c9c0729f5f570e2d231672968fb966ba05443b446bc6:f819cc633198b457309887b93e50fe591aed55ec獲取到key:make-fetch-happen:request-cache:http://bnpm.byted.org/archy/download/archy-1.0.0.tgz,然後[實驗][Link 2]常用hash算法,得到使用sha256對這個key計算之後得到的索引存儲位置,也就是得到這個哈希值。

壓縮包:得到索引之後,將文件格式化得到:

觀察它們的值,貌似只有etag有可能是需要的,etag為f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40,那麼找到content-v2/sha1/f9/c8/c13757cc1dd7bc379ac77b2c62a5c2868c40果然看見了這個文件,顯示的信息和metadata裡面的其他信息符合。

解壓它:tar tf content-v2/sha1/f9/c8/c13757cc1dd7bc379ac77b2c62a5c2868c40,得到:

裡面的內容和archy相同。

在有package-lock的情況下,根據lock裡面的包名、版本、integrity拼一個key出來去找meta信息。沒有lock就只能去請求registry了,請求下來也有一樣的meta信息

相關焦點

  • npm指令無效、npm install失敗( 雲函數上傳失敗)解決方法
    在微信小程序雲開發中,部署和上傳雲函數是必不可少的一步,但經常會遇到一些問題,例如npm指令無效,npm install 安裝失敗, 雲函數上傳時間太長等問題
  • 你不應該只知道 npm install
  • npm 腳本命令學習筆記
    #查看當前項目的所有 npm 腳本命令#(其實也可以在package.json的看scripts對象裡有什麼屬性)npm runnpm的原理??每當執行npm run,就會自動新建一個 Shell,在這個 Shell 裡面執行指定的腳本命令。
  • NPM 發布 TS 包
    ,比如我的是 zmy-greetnpm init -y# 添加 gitignoreecho "node_modules" >> .gitignore# 安裝 tsnpm install --save-dev typescript# 為了使 ts 可以正常編譯,需要添加配置文件touch tsconfig.json
  • 你應該知道的 NPM 知識都在這!
    dependenices通過命令npm install/i packageName -S/--save把包裝在此依賴項裡。如果沒有指定版本,直接寫一個包的名字,則安裝當前npm倉庫中這個包的最新版本。如果要指定版本的,可以把版本號寫在包名後面,比如npm i vue@3.0.1 -S。
  • Facebook 新推 Yarn,或取代 npm 客戶端
    嘗試修改 npm 客戶端在開始階段,我們遵循了最佳實踐,在代碼倉庫中只跟蹤了 package.json 文件的變化,並要求工程師手動運行 npm install 命令安裝依賴。這種模式在開發人員的電腦上沒有問題,但在持續集成環境中遇到了困難,因為出於安全與可靠性的考慮,持續集成環境需要進行沙箱隔離,不能進行聯網,因此也無法安裝依賴。
  • 什麼是npm指令?它與express模塊有什麼關聯?
    Node中npm的使用是非常高的。使用npm引入各式各樣的模塊。今天就來說說npm是什麼以及用法。並介紹由npm引入的express,模塊。npm 是 JavaScript 世界的包管理工具,並且是 Node.js 平臺的默認包管理工具。
  • 前端工程化 - 剖析npm的包管理機制
    install package 安裝包時,npm 默認安裝當前最新版本,然後在所安裝的版本號前加 ^ 號。3.1 嵌套結構我們都知道,執行 npm install 後,依賴包被安裝到了 node_modules ,下面我們來具體了解下,npm 將依賴包安裝到 node_modules 的具體機制是什麼。
  • 【 Node.js】你應該知道的 NPM 知識都在這!
    dependenices通過命令npm install/i packageName -S/--save把包裝在此依賴項裡。如果沒有指定版本,直接寫一個包的名字,則安裝當前npm倉庫中這個包的最新版本。如果要指定版本的,可以把版本號寫在包名後面,比如npm i vue@3.0.1 -S。
  • 前端工程化 - 剖析npm的包管理機制(完整版)
    install package 安裝包時,npm 默認安裝當前最新版本,然後在所安裝的版本號前加 ^ 號。3.1 嵌套結構我們都知道,執行 npm install 後,依賴包被安裝到了 node_modules ,下面我們來具體了解下,npm 將依賴包安裝到 node_modules 的具體機制是什麼。
  • 你不知道的 Npm(Node.js 進階必備好文)
    dependenices通過命令npm install/i packageName -S/--save把包裝在此依賴項裡。如果沒有指定版本,直接寫一個包的名字,則安裝當前npm倉庫中這個包的最新版本。如果要指定版本的,可以把版本號寫在包名後面,比如npm i vue@3.0.1 -S。
  • 【 Node.js 進階】你應該知道的 NPM 知識都在這!
    dependenices通過命令npm install/i packageName -S/--save把包裝在此依賴項裡。如果沒有指定版本,直接寫一個包的名字,則安裝當前npm倉庫中這個包的最新版本。如果要指定版本的,可以把版本號寫在包名後面,比如npm i vue@3.0.1 -S。
  • npm v5.0.0 正式發布,改進了穩定性 - OSCHINA - 中文開源技術交流...
    沒有任何工具或意圖重新使用舊的緩存 (#15666)不要再使用大寫的 npm(npm will now scold you if you capitalize its name. seriously it will fight you.)
  • 使用CLI開發一個Vue3的npm庫
    前言前幾天寫了一個Vue的自定義右鍵菜單的npm庫,主要講了插件的設計思路以及具體的實現過程,插件的開發流程沒有細講。本文就跟大家分享下如何使用CLI從零開始開發一個支持Vue3的庫,並上傳至npm,歡迎各位感興趣的開發者閱讀本文。
  • NPM命令實用使用技巧總結
    使用npm install來安裝,你可以使用其簡寫npm i無需為你要安裝的每個模塊都輸入一遍npm i指令,像這樣:npm i gulp-pugnpm i gulp-debugnpm i gulp-sass你只需要輸入一行命令即可一次性批量安裝模塊npm i gulp-pug gulp-debug gulp-sass
  • 【第596期】npm scripts
    0. npm run & npm run-script這兩命令的效果都是一樣的,都能執行 package.json 文件 scripts 欄位下指定的任務。npm run 是 npm run-script 的縮寫,一般都使用 前者,但是後者可以更好地反應這個命令的本質。
  • 使同事羨慕不已的8個npm命令
    Installing a package:通常:npm install pkg簡寫:npm i pkgInstalling a package globally:通常:npm i --global pkg簡寫:
  • NPM 7:這才算是真正的更新
    工作區我最近分享了一篇文章,其中介紹了兩種 NPM 客戶端,它們都想要解決官方客戶端當前實現中的一個主要問題:npm_modules 文件夾已經成為了一個磁碟空間黑洞。https://blog.bitsrc.io/npm-clients-that-are-better-than-the-original-cd54ed0f5fe7這兩種選項都有自己的獨特解決方案,但總體來說它們都會將所有模塊保存在一個共享文件夾中,讓各個項目之間能夠更容易共享軟體包。
  • npm 發布 npm@6 包管理器,並提供新的安全保護
    npm 公司宣布推出 npm@6 軟體包管理器。 它將提供強大的新安全功能,例如,當開發人員嘗試使用具有已知漏洞的開原始碼時,它可以自動發出警告。
  • 【小心得】npm run多個script
    為大家帶來npm run多個script方面的「小心得」在使用npm script運行多個任務時,通常需要選擇任務是串行執行還是並行執行。串行執行可以保證命令執行的順序,並行執行可以提高運行速度。►串行執行只需要使用&&把多個npm script連接起來即可,例如:npm run s1 && npm run s2這樣就可以嚴格按照s1,s2