多年前的Leftpad 撤包事件使得React 、 Babel 和許多流行的npm模組都受到波及,無法正常執行。
這些受到影響的模組都引入了一個叫做 left-pad 的模組。
以下就是這十一行程式碼:
module.exports = leftpad;
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
而其中的原因大概是這樣:作者 Azer 寫了一個叫 kik 的工具和某個公司同名了,這天公司的律師要求其刪掉這個模組,把 kik 這個名字“讓”給他們,作者不答應,律師就直接找 NPM 了,而 NPM 未經作者同意就把包的許可權轉移給了這家公司。於是,Azer 一怒衝冠,將他所有的 NPM 包全部刪掉了。
有意思的是,社群中許多的模組都選擇引入這個十一行的模組,而不是花上兩分鐘的時間自己去實現這個簡單的字串填充功能。
這不是npm包管理第一次出問題,也不會是最後一次。
Leftpad撤包事件、event-stream投毒事件、Ant Design彩蛋時間,使得我們不得不開始重新思考npm生態真的存在的問題,甚至去問自己:我們是不是早已忘記該如何好好地程式設計?
- NPM模組粒度
- 程式碼風格
- 程式碼質量/效率
- 過度依賴
這種過度依賴其他npm模組的做法是不是解決問題的正確方式呢?現在,一個空白專案模板一裝好就要引入兩萬八千多個檔案、依賴成百上千個其他的npm模組。這太瘋狂了、而且過度複雜。
那麼我們可以做些什麼?把命運掌握在自己手裡
- 在釋出前“凍結”依賴模組的版本號。這讓我們對安裝的依賴有信心,依賴模組的版本都是我們驗證、測試過的。
- 在釋出前“打包”依賴模組到自己專案。這讓我們可以坦然面對我們依賴的某個模組“沒有了”這樣的囧境。
凍結依賴模組:
凍結依賴模組的版本號最簡單的辦法就是直接在 package.json 裡面寫死版本號,但是這解決不了深度依賴的問題。我們來看個例子。 假設有下面這樣的依賴:
A@0.1.0
└─┬ B@0.0.1
└── C@0.0.1
A 模組依賴了 B 模組,B 模組又依賴了 C 模組。我們可以將 B 模組的依賴寫死成 0.0.1 版本,但是如果 B 模組對 C 模組的依賴寫的是 C@0.0.1
,會怎樣?
這時候 C 模組更新到了 0.0.2 版本,雖然我們安裝的 B 模組是 B@0.0.1
,但是安裝的 C 模組卻是 C@0.0.2
。如果不巧這個 C@0.0.2
剛好有 bug,那我們的模組很有可能就不能正常工作了。 實際上,NPM 提供了一個叫做npm shrinkwrap
的命令來解決這個問題:
NAME
npm-shrinkwrap -- Lock down dependency versions
SYNOPSIS
npm shrinkwrap
DESCRIPTION
This command locks down the versions of a package's dependencies so that you can control exactly which versions of each dependency will be used when your package is installed.
這條命令會根據目前我們 node_modules
目錄下的模組來生成一份“凍結”住的模組依賴(npm-shrinkwrap.json)。
還是上面的例子,我們在模組 A 的根目錄執行 npm shrinkwrap
後,生成的 npm-shrinkwrap.json
檔案內容大概是下面這樣:
{
"name": "A",
"dependencies": {
"B": {
"version": "0.0.1",
"resolved": "http://registry.npmjs.com/B-0.0.1.tgz",
"dependencies": {
"C": {
"version": "0.0.1",
"resolved": "http://registry.npmjs.com/C-0.0.1.tgz"
}
}
}
}
}
然後,當我們執行 npm install
時,依賴查詢的“來源”不再是 package.json
,而是我們生成的 npm-shrinkwrap.json
,再也不會突然裝上什麼 C@0.0.2
了,依賴裡面的模組版本都是我們驗證、測試後的版本,讓人安心。
注:npm shrinkwrap
預設只會生成 dependencies
的依賴,不會生成 devDependencies
的依賴,如果你真的需要,可以加 --dev
引數。
打包依賴模組:
我們解決了依賴模組版本號的問題,但是每次安裝時其實還是會去 NPM 的 registry 獲取模組的 tgz 包然後進行安裝。我們需要將這些依賴都打包進我們的專案。這可能會帶來一些問題(比如:專案體積的增大),但是好處也是顯而易見的。
上面生成的 npm-shrinkwrap.json
裡面有個 resolved
欄位,表示模組所在的位置,實際上這個欄位完全可以寫一個檔案路徑。所以,我們可以遞迴的遍歷 npm-shrinkwrap.json
檔案,將所有的 tgz 包先下載到我們專案的某個目錄,然後改寫 resolved
欄位為對應的檔案路徑。這樣的功能有開發者已經實現了,我們可以直接享用:https://github.com/JamieMason/shrinkpack
於是,我們以後再進行 npm install --loglevel=http
時會發現依賴模組的安裝根本沒有網路請求了(因為依賴都在我們自己的倉庫裡了嘛)。
可能有人會說,為啥不直接把 node_modules
目錄提交進倉庫算了?原因主要是這樣:
- 有些模組需要編譯,編譯是和環境有關的,你當前的環境編譯可用,其他環境直接使用該模組不一定能用。
node_modules
目錄裡面啥東西都有,太凌亂,很容易把提交給攪亂。diff 時突然 diff 出node_modules
下的原始碼、README,你應該不想這樣吧?
只儲存模組的 tgz 包,安裝編譯的過程交給 NPM 命令更明智。
新方式
於是,現在我們使用 NPM 模組的正確姿勢應該是這樣了:
- 本地安裝、更新需要的模組,測試、驗證
- 執行
npm shrinkwrap
將依賴模組的版本凍結 - 執行
shrinkpack .
將依賴模組打包進倉庫 - 提交程式碼(注意要將
npm-shrinkwrap.json
和node_shrinkpack
一起提交哦) - 釋出模組或者部署應用
如果你覺得這樣很繁瑣,可以定義一個 NPM 命令:
"scripts": {
"pack": "npm shrinkwrap & shrinkpack ."
}