今天碰到了一些NPM相關(guān)的問題, 花了些時間搞清楚, 記錄一下.
關(guān)于NPM包依賴的扁平化 (flatten)
接手的項目里面使用到了包flatten-packages, 這個包可以將npm包的嵌套依賴壓扁. 對這方面之前沒了解過, 于是做了一下調(diào)研.
為什么需要扁平化
在早期, npm包是以下形式保存的:
.
|--app
|--node_modules
|--sub_module
|--node_modules
|--sub_sub_module
|-- ... and so on
即你的項目依賴包A, 那么node_modules里面就有一個A的文件夾, 在這個文件夾中, 又有一個node_modules, 所有A的依賴都放這個node_modules中, 以此類推. npm的包依賴就會成為一個非常深的文件夾結(jié)構(gòu), 深到有時你在windows系統(tǒng)直接刪除該文件夾會失敗, 因為文件目錄超過了windows限制的256個字符. 此時你可以打開Git Bash什么的rm -rf.
這會導(dǎo)致一個問題: 不同的包(A和B)可能依賴同一個包(C), 這樣npm install的時候, 安裝A的時候會下載一次C, 安裝B的時候又會下載一次C. 無形中多了很多沒必要的下載, 導(dǎo)致npm install的速度變慢很多.
所以這也就有了扁平化包依賴的需求 -- 讓重復(fù)的依賴盡量合并, 以加速包管理的速度.
扁平化的方法
flatten-packages
flatten-packages這個包可以解決這個問題. 跑一下flatten-packages即可.
但是, 新版本的npm會進行自動扁平化處理, 所以flatten-packages已經(jīng)沒有用了, 這個包也已經(jīng)停止更新了. 而且在解決包沖突方面也不如npm dedupe. 相關(guān)Issue
npm3的自動扁平化
npm第3版會自動進行包的扁平化. 詳見npm v3, 搜索flat.
npm3會盡可能地扁平化包依賴, 絕大多數(shù)情況下你的依賴包都會直接存在于node_modules里. 唯一的例外是, 某兩個包依賴互相沖突的時候.
為什么包依賴會有沖突?
簡單的例子就是同一個包C, A依賴于C 1.0.0, B依賴于C 2.0.0. 這樣如果你的app同時依賴于A和B, 那就沒法直接在node_modules里面放一個版本C來同時滿足A和B的依賴了. 這里的版本號是SemVer.
SemVer
SemVer (Semantic Versioning, 語義化版本).
SemVer最基本的結(jié)構(gòu)是major.minor.patch, 如1.2.3.
其中,
- major為主版本號, 當(dāng)有非向后兼容(即breaking change)的時候, 更新major.
- minor為次版本號, 當(dāng)有向后兼容的時候, 更新minor.
- patch為補丁號, 當(dāng)有向后兼容的bug fix時, 更新patch.
npm使用SemVer來標(biāo)注包的版本, 這些配置寫入到了package.json中.
除了指定固定的版本號, package.json中還可以指定版本號范圍.
-
1.2.x(x也可以用*代替), 相當(dāng)于>=1.2.0 <1.3.0 -
1.x.x, 相當(dāng)于>=1.0.0 <2.0.0 - 波浪線(Tilde),
~1.2.3相當(dāng)于>=1.2.3 <1.3.0, 即minor不能增加. - 破折號(Caret),
^1.2.3相當(dāng)于>=1.2.3 <2.0.0, 即major不能增加.
包去重 (dedupe)
想要去重只需運行npm dedupe
既然npm3自動進行了扁平化, 為什么還需要去重? 這個npm用了一篇文章npm3 Duplication and Deduplication進行講解, 我就不翻譯了. 簡單來說就是:
- 由于歷史原因, 某兩個包A和B依賴于同一個包C的不同版本1.0.0和2.0.0, 這個沖突導(dǎo)致C的依賴無法被合并.
- 后來A和B的某次更新使得他們依賴于同一個版本的C2.0.0, 但是仍然由于你的app直接依賴C1.5.0, 導(dǎo)致A和B依賴的C2.0.0依然無法被合并, 只能各自存放在A和B的目錄下.
(問題: 如果這時候運行npm dedup是什么效果?) - 后來你把直接依賴C1.5.0更新成了C2.0.0, 這會導(dǎo)致你有三個重復(fù)的C2.0.0, 需要運行
npm dedupe去重.
(問題: 為什么不能更新版本的時候自動運行dedupe呢?)
包依賴鎖定(shrinkwrap/lock)
package.json中的依賴包的版本號可能是版本范圍, 或者依賴包的依賴可能使用了版本范圍, 這會導(dǎo)致一個問題: 你今天npm install安裝了包A1.0.0, 一段時間后你的同事運行npm install, 可能就會安裝A1.2.0, 導(dǎo)致你們的運行環(huán)境不完全一樣. 想要將包依賴的版本號完全鎖定住, 就需要shrinkwrap/lock.
package-lock.json
npm5引入了package-lock.json, 即在你運行npm install的時候自動會生成package-lock.json文件, 這是一個描述依賴樹的文件, 它的好處是鎖定了所有依賴的版本甚至下載地址, 而且結(jié)構(gòu)清晰人能讀懂(相對于錯綜復(fù)雜的node_modules目錄結(jié)構(gòu)).
示例package-lock.json
{
"name": "A",
"version": "0.1.0",
...metadata fields...
"dependencies": {
"B": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/B/-/B-0.0.1.tgz",
"integrity": "sha512-DeAdb33F+"
"dependencies": {
"C": {
"version": "git://github.com/org/C.git#5c380ae319fc4efe9e7f2d9c78b0faa588fd99b4"
}
}
}
}
}
如果有package-lock.json, 安裝過程會變成:
- 按照package-lock.json重建依賴包的樹形結(jié)構(gòu). 如果有"resolved"字段則使用該字段指向的文件下載文件, 否則使用"version".
- 若最后還有缺失的依賴包, 則使用普通的package.json安裝方法.
注意: npm install, npm rm, npm update都會自動更新package-lock.json. 如果你不想更新, 可以使用以下命令行參數(shù):
--no-save: 不更新package.json也不更新package-lock.json
--no-shrinkwrap: 更新package.json, 不更新package-lock.json和npm-shrinkwrap.json.
npm非常建議將package-lock.json存入版本控制, 以確保組內(nèi)所有成員, 持續(xù)集成(Continuous Integration, CI)和部署環(huán)境是用完全一致的依賴包.[package-locks]
npm-shrinkwrap.json
你可以使用npm shrinkwrap指令, 在package-lock.json的基礎(chǔ)上生成一個名為npm-shrinkwrap.json的文件.
package-lock.json和npm-shrinkwrap.json的內(nèi)容完全一樣, 唯一區(qū)別是: 當(dāng)發(fā)布包的時候, package-lock.json不會被包含在內(nèi), 但是npm-shrinkwrap.json會被一同發(fā)布. 如果兩者同時存在, package-lock.json會被完全忽略.