Leftpad事件 我們是不是早已忘記該如何好好地程式設計?

辜負寒徹骨發表於2023-03-12

多年前的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 模組的正確姿勢應該是這樣了:

  1. 本地安裝、更新需要的模組,測試、驗證
  2. 執行 npm shrinkwrap 將依賴模組的版本凍結
  3. 執行 shrinkpack . 將依賴模組打包進倉庫
  4. 提交程式碼(注意要將 npm-shrinkwrap.json 和 node_shrinkpack 一起提交哦)
  5. 釋出模組或者部署應用

如果你覺得這樣很繁瑣,可以定義一個 NPM 命令:

"scripts": {
  "pack": "npm shrinkwrap & shrinkpack ."
}

相關文章