作為前端工程師應(yīng)該都知道這個東東际长,在我的電腦里存有幾年積攢下來的各種工程約一百G,現(xiàn)在換了新電腦兴泥,導(dǎo)資料真麻煩工育,因為這個東東太大且文件太多,復(fù)制很慢搓彻。
node_modules 目錄結(jié)構(gòu)
npm install 執(zhí)行完畢后如绸,我們可以在 nodemodules 中看到所有依賴的包。雖然使用者無需關(guān)注這個目錄里的文件夾結(jié)構(gòu)細節(jié)旭贬,只管在業(yè)務(wù)代碼中引用依賴包即可怔接,但了解 nodemodules 的內(nèi)容可以幫我們更好理解 npm 如何工作,了解從 npm 2 到 npm 5 有哪些變化和改進稀轨。
為簡單起見扼脐,我們假設(shè)應(yīng)用目錄為 app, 用兩個流行的包 webpack, nconf 作為依賴包做示例說明。并且為了正常安裝靶端,使用了“上古” npm 2 時期的版本 webpack@1.15.0, nconf@0.8.5.
npm 2 - 樹狀結(jié)構(gòu)
npm 2 在安裝依賴包時谎势,采用簡單的遞歸安裝方法。執(zhí)行 npm install 后杨名,npm 2 依次遞歸安裝 webpack和 nconf 兩個包到 nodemodules 中。執(zhí)行完畢后猖毫,我們會看到 ./nodemodules 這層目錄只含有這兩個子目錄台谍。
node_modules/
├── nconf/
└── webpack/
進入更深一層 nconf 或 webpack 目錄,將看到這兩個包各自的 nodemodules 中吁断,已經(jīng)由 npm 遞歸地安裝好自身的依賴包趁蕊。包括 ./node_modules/webpack/node_modules/webpack-core , ./node_modules/conf/node_modules/async 等等。而每一個包都有自己的依賴包仔役,每個包自己的依賴都安裝在了自己的 nodemodules 中掷伙。依賴關(guān)系層層遞進,構(gòu)成了一整個依賴樹又兵,這個依賴樹與文件系統(tǒng)中的文件結(jié)構(gòu)樹剛好層層對應(yīng)任柜。
最方便的查看依賴樹的方式是直接在 app 目錄下執(zhí)行 npm ls 命令。
app@0.1.0
├─┬ nconf@0.8.5
│ ├── async@1.5.2
│ ├── ini@1.3.5
│ ├── secure-keys@1.0.0
│ └── yargs@3.32.0
└─┬ webpack@1.15.0
├── acorn@3.3.0
├── async@1.5.2
├── clone@1.0.3
├── ...
├── optimist@0.6.1
├── supports-color@3.2.3
├── tapable@0.1.10
├── uglify-js@2.7.5
├── watchpack@0.2.9
└─┬ webpack-core@0.6.9
├── source-list-map@0.1.8
└── source-map@0.4.4
這樣的目錄結(jié)構(gòu)優(yōu)點在于層級結(jié)構(gòu)明顯沛厨,便于進行傻瓜式的管理:
- 例如新裝一個依賴包宙地,可以立即在第一層 node_modules 中看到子目錄
- 在已知所需包名和版本號時,甚至可以從別的文件夾手動拷貝需要的包到 node_modules 文件夾中逆皮,再手動修改 package.json 中的依賴配置
- 要刪除這個包宅粥,也可以簡單地手動刪除這個包的子目錄,并刪除 package.json 文件中相應(yīng)的一行即可
實際上电谣,很多人在 npm 2 時代也的確都這么實踐過秽梅,的確也都可以安裝和刪除成功抹蚀,并不會導(dǎo)致什么差錯。
但這樣的文件結(jié)構(gòu)也有很明顯的問題:
- 對復(fù)雜的工程, node_modules 內(nèi)目錄結(jié)構(gòu)可能會太深企垦,導(dǎo)致深層的文件路徑過長而觸發(fā) windows 文件系統(tǒng)中环壤,文件路徑不能超過 260 個字符長的錯誤
- 部分被多個包所依賴的包,很可能在應(yīng)用 node_modules 目錄中的很多地方被重復(fù)安裝竹观。隨著工程規(guī)模越來越大镐捧,依賴樹越來越復(fù)雜,這樣的包情況會越來越多臭增,造成大量的冗余懂酱。
——在我們的示例中就有這個問題, webpack 和 nconf 都依賴 async 這個包誊抛,所以在文件系統(tǒng)中列牺,webpack 和 nconf 的 node_modules 子目錄中都安裝了相同的 async 包,并且是相同的版本拗窃。
npm 3 - 扁平結(jié)構(gòu)
主要為了解決以上問題瞎领,npm 3 的 node_modules 目錄改成了更加扁平狀的層級結(jié)構(gòu)。文件系統(tǒng)中 webpack, nconf, async 的層級關(guān)系變成了平級關(guān)系随夸,處于同一級目錄中九默。
雖然這樣一來 webpack/nodemodules 和 nconf/nodemodules 中都不再有 async 文件夾,但得益于 node 的模塊加載機制宾毒,他們都可以在上一級 node_modules 目錄中找到 async 庫驼修。所以 webpack 和 nconf 的庫代碼中 require('async') 語句的執(zhí)行都不會有任何問題。
這只是最簡單的例子诈铛,實際的工程項目中乙各,依賴樹不可避免地會有很多層級,很多依賴包幢竹,其中會有很多同名但版本不同的包存在于不同的依賴層級耳峦,對這些復(fù)雜的情況, npm 3 都會在安裝時遍歷整個依賴樹,計算出最合理的文件夾安裝方式焕毫,使得所有被重復(fù)依賴的包都可以去重安裝蹲坷。
npm 文檔提供了更直觀的例子解釋這種情況:
假如 package{dep} 寫法代表包和包的依賴,那么 A{B,C}, B{C}, C{D} 的依賴結(jié)構(gòu)在安裝之后的 node_modules 是這樣的結(jié)構(gòu):
A
+-- B
+-- C
+-- D
這里之所以 D 也安裝到了與 B C 同一級目錄咬荷,是因為 npm 會默認會在無沖突的前提下冠句,盡可能將包安裝到較高的層級。
如果是 A{B,C}, B{C,D@1}, C{D@2} 的依賴關(guān)系幸乒,得到的安裝后結(jié)構(gòu)是:
A
+-- B
+-- C
`-- D@2
+-- D@1
這里是因為懦底,對于 npm 來說同名但不同版本的包是兩個獨立的包笙隙,而同層不能有兩個同名子目錄挥唠,所以其中的 D@2 放到了 C 的子目錄而另一個 D@1 被放到了再上一層目錄。
很明顯在 npm 3 之后 npm 的依賴樹結(jié)構(gòu)不再與文件夾層級一一對應(yīng)了。想要查看 app 的直接依賴項糕韧,要通過 npm ls 命令指定 --depth 參數(shù)來查看:
npm ls --depth 1
PS: 與本地依賴包不同酱吝,如果我們通過 npm install--global 全局安裝包到全局目錄時禽车,得到的目錄依然是“傳統(tǒng)的”目錄結(jié)構(gòu)艇搀。而如果使用 npm 3 想要得到“傳統(tǒng)”形式的本地 node_modules 目錄,使用 npm install--global-style 命令即可亲桦。
npm 5 - package-lock 文件
npm 5 發(fā)布于 2017 年也是目前最新的 npm 版本崖蜜,這一版本依然沿用 npm 3 之后扁平化的依賴包安裝方式,此外最大的變化是增加了 package-lock.json 文件客峭。
package-lock.json 的作用是鎖定依賴安裝結(jié)構(gòu)豫领,如果查看這個 json 的結(jié)構(gòu),會發(fā)現(xiàn)與 node_modules 目錄的文件層級結(jié)構(gòu)是一一對應(yīng)的舔琅。
以依賴關(guān)系為: app{webpack} 的 'app' 項目為例, 其 package-lock 文件包含了這樣的片段等恐。
"express": {
"version": "4.15.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.15.4.tgz",
"integrity": "sha1-Ay4iU0ic+PzgJma+yj0R7XotrtE=",
"requires": {
"accepts": "1.3.3",
"array-flatten": "1.1.1",
"content-disposition": "0.5.2",
"content-type": "1.0.2",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.8",
"depd": "1.1.1",
"encodeurl": "1.0.1",
"escape-html": "1.0.3",
"etag": "1.8.0",
"finalhandler": "1.0.4",
"fresh": "0.5.0",
"merge-descriptors": "1.0.1",
"methods": "1.1.2",
"on-finished": "2.3.0",
"parseurl": "1.3.1",
"path-to-regexp": "0.1.7",
"proxy-addr": "1.1.5",
"qs": "6.5.0",
"range-parser": "1.2.0",
"send": "0.15.4",
"serve-static": "1.12.4",
"setprototypeof": "1.0.3",
"statuses": "1.3.1",
"type-is": "1.6.15",
"utils-merge": "1.0.0",
"vary": "1.1.1"
}
},
看懂 package-lock 文件并不難,其結(jié)構(gòu)是同樣類型的幾個字段嵌套起來的备蚓,主要是 version, resolved, integrity, requires, dependencies 這幾個字段而已课蔬。
- version, resolved, integrity 用來記錄包的準確版本號、內(nèi)容hash郊尝、安裝源的二跋,決定了要安裝的包的準確“身份”信息
- 假設(shè)蓋住其他字段,只關(guān)注文件中的 dependencies:{} 我們會發(fā)現(xiàn)流昏,整個文件的 JSON 配置里的 dependencies 層次結(jié)構(gòu)與文件系統(tǒng)中 node_modules 的文件夾層次結(jié)構(gòu)是完全對照的
- 只關(guān)注 requires:{} 字段又會發(fā)現(xiàn)同欠,除最外層的 requires 屬性為 true 以外, 其他層的 requires 屬性都對應(yīng)著這個包的 package.json 里記錄的自己的依賴項
因為這個文件記錄了 nodemodules 里所有包的結(jié)構(gòu)、層級和版本號甚至安裝源横缔,它也就事實上提供了 “保存” nodemodules 狀態(tài)的能力。只要有這樣一個 lock 文件衫哥,不管在那一臺機器上執(zhí)行 npm install 都會得到完全相同的 node_modules 結(jié)果茎刚。
這就是 package-lock 文件致力于優(yōu)化的場景:在從前僅僅用 package.json 記錄依賴,由于 semver range 的機制撤逢;一個月前由 A 生成的 package.json 文件膛锭,B 在一個月后根據(jù)它執(zhí)行 npm install 所得到的 node_modules 結(jié)果很可能許多包都存在不同的差異,雖然 semver 機制的限制使得同一份 package.json 不會得到大版本不同的依賴包蚊荣,但同一份代碼在不同環(huán)境安裝出不同的依賴包初狰,依然是可能導(dǎo)致意外的潛在因素。
相同作用的文件在 npm 5 之前就有互例,稱為 npm shrinkwrap 文件奢入,二者作用完全相同,不同的是后者需要手動生成媳叨,而 npm 5 默認會在執(zhí)行 npm install 后就生成 package-lock 文件腥光,并且建議你提交到 git/svn 代碼庫中关顷。
package-lock.json 文件在最初 npm 5.0 默認引入時也引起了相當(dāng)大的爭議。在 npm 5.0 中武福,如果已有 package-lock 文件存在议双,若手動在 package.json 文件新增一條依賴,再執(zhí)行 npm install, 新增的依賴并不會被安裝到 node_modules 中, package-lock.json 也不會做相應(yīng)的更新捉片。這樣的表現(xiàn)與使用者的自然期望表現(xiàn)不符平痰。在 npm 5.1 的首個 Release 版本中這個問題得以修復(fù)。這個事情告訴我們伍纫,要升級宗雇,不要使用 5.0。
——但依然有反對的聲音認為 package-lock 太復(fù)雜翻斟,對此 npm 也提供了禁用配置:
npm config set package-lock false
清除所有
了解了node_modules目錄結(jié)構(gòu)后逾礁,知道這東東都是一些重復(fù)的依賴包,干脆全部刪除便于復(fù)制工程項目访惜,手動刪除好像不太現(xiàn)實嘹履,太多了,還要一個個找债热,通過命令可以一鍵完成砾嫉。
find . -name "node_modules" -print | xargs rm -rf
命令前部分是通過find
遞歸找到當(dāng)前目錄下所有的node_modules
目錄并打印出來,后部分是通過管道將前部分查找的結(jié)果當(dāng)作參數(shù)并強制刪除窒篱。