Leftpad事件 我們是不是早已忘記該如何好好地編程?
多年前的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 ."
}