NPM 與前端包管理

CSS魔法發表於2016-02-12

我們很清楚,前端資源及其依賴管理一直是 npm 的重度使用場景,同時這也一直是 Node.js 普及的重要推動力。但這類應用場景到底有多重度?這是一個很難回答的問題。這份 “npm 最常下載的包的清單” 並不能提供有效的證據:因為像 async、minimist 和 request 這樣的包就像是 “生活必需品”,它們會被數以千計的其它包所 依賴,這樣一來它們當然會隨著那些依賴它們的包一起被不停地下載。

更有意義也更接近真相的問題是:哪些包是人們主動安裝的?所謂 “主動安裝”,就是指某個人(或某個機器人)以實際執行 npm install thispackage 命令的方式來安裝一個包。不久前,我們開始把日誌資料加入到Jut 中,隨後我們終於可以方便而快速地給出這個問題的答案了。最終,我們得到了 “最常主動安裝的 npm 包五十強榜單”,這份榜單畫風突變,很有意思。在五十強中有 32% 的包(它們產生了 50% 的實際下載量)都是前端的工具或框架,攜 Grunt、Bower 和 Gulp 一起遙遙領先(當然移動端也是一大重度應用,這裡暫且不表)。此外,這些包的使用量也在穩步增長:

Client-side tools growth, Jan-October 2014

(客戶端工具的增長,2014 年 1 月~10 月)

另一個渠道也佐證了前端是重度使用場景的這一事實——我們從 npm 使用者和 web 開發者那裡收到了大量關於如何用 npm 來管理好客戶端依賴的提問(和故障反饋)。這些問題通常都伴有極其主觀的偏見,令我們感到相當詫異。好吧,那就讓我們嚴肅認真地來澄清一下:

1. “npm 只是為 CommonJS 服務的!”

不對。npm 希望成為 JavaScript 的包管理器,因此,只要是跟 JavaScript 相關的,都適合放入 npm 的包倉庫(registry)。雖然 Node.js 提供了一個 “CommonJS 式” 的模組環境,但 npm 對此並不關心。

2. “npm 只是為伺服器端的 JavaScript 服務的!”

同樣不對。你的包可以包含任何內容,不論是 ES6、客戶端 JS,還是 HTML 和 CSS。有很多東西天生就是跟 JavaScript 綁在一起的,那就把它們都放進來吧。

npm 的 《行為準則》 總結了一份非常簡短的列表,列出了我們認為不適合放進包裡的東西(簡單來說:不要把 npm 當作你的資料庫或多媒體伺服器來用)。對此如有疑問,請通過 Twitter 或 Email 詢問,我們樂於討論。

npm 的哲學

npm 的願景是幫助開發者減少摩擦。我們傾向於通過 “循蹤闢徑” 的方式來實現這一點。這句話的意思是說:我們不希望告訴使用者該怎麼做;我們希望觀察使用者是怎麼做的,然後把障礙掃清。如果很多人都是在以各自不同的方式在行事,那我們不會輕易地從中挑出一個勝者,除非最佳實踐已經昭然若揭。

那麼,在前端包管理的領域中,使用者遇到的阻力究竟在哪裡?使用者踩出的 “蹤” 又是怎樣的?

前端痛點

除了 GitHub issue 以及 IRC、Twitter、技術會議和線下聚會中的使用者以外,我們還會跟一些大型前端包的開發者們直接對話——這其中包括 Angular 和 Ember 的開發者(這兩者都位列五十強)。他們在解決方案上並不完全一致,但他們的痛點卻是大體相同的。接下來我們會一一展開,並討論如何攻克這些難題:

1. node_modules 目錄並不是按照前端包所需要的方式來組織的

這是一個非常明顯的問題。node_modules 目錄是預設情況下 npm 存放包的地方,它得名於 Node.js 的模組載入行為。根據你安裝的包的具體情況,所有包最終會被存放在目錄樹的不同位置。這對於 Node 來說一切良好,但對於 HTML 和 CSS 來說,不管怎樣,我們通常都期望所有東西可以彙總在同一個地方,比如/static/mypackage 這樣的目錄下。肯定有一些變通方法可以繞過這個問題,但還算不上是最佳方案。

2. 前端依賴在解決衝突方面具有截然不同的需求

Node 模組載入器的一個有意思的地方在於,它允許你同時使用同一個模組的多個不相容版本;而 npm 的一大有意思的地方在於,它可以將包的這些不同版本放置在合適的地方,從而做到在想要的地方載入想要的版本。這種方式對於避免 “依賴地獄” 有很大幫助,同時這也是 Node 的 “大量小模組” 的實踐模式如此實用且流行的原因之一。

但前端依賴卻是無法以這樣的方式來運作的。如果你在網頁中同時載入兩個版本的 jQuery,那其中只有一個會 “勝出”。如果你同時載入了兩個版本的 Bootstrap CSS 框架,它們會同時起作用,然後把頁面樣式搞得一團糟。在未來,HTML 將獲得新的特性(比如 web components 和 Shadow DOM),也許有助於解決這類問題;但在眼下,前端依賴會發生衝突。那我們如何優雅地判別並解決這個難題呢?

3. 同時維護多個包清單是很煩人的

前兩個問題其實已經有了一種解決方案,就是為前端包額外配備其它的包管理方案。但這會產生這樣一種局面——單個專案可能會同時包含一個 package.json 檔案、一個 bower.json、一個 component.json 等等。每當遇到哪怕是一丁點兒更新時,你都要把所有這些配置檔案通通編輯一遍。跟所有的資料冗餘一樣,這種情形不僅煩人,而且容易產生錯誤。

4. 找到相容瀏覽器的包很痛苦

npm 是為 JavaScript 服務的包倉庫,但目前庫中絕大多數的包都是 Node.js 包。在採用 Browserify 等工具做過適配之後,某些模組是可以在客戶端執行的,但還有很多仍然是不行的。目前,如果要判斷某個包是否在瀏覽器端可用,除了實測,似乎還沒有一種簡單易行的方法。

前端解決方案

在找出了以上四個難題之後,讓我們來逐一討論如何解決。

上面提到的最後一個難題是最容易克服的,我們已經開始為解決方案奠定基礎了。這個解決方案就是:生態圈

生態圈是指包倉庫的一些可搜尋的子集,這些子集是通過程式化地篩選庫中的所有包而產生的,篩選條件是諸如 “可在瀏覽器中執行” 或 “可在 Windows 上執行” 或 “相容 Express” 等數以百萬計種可能性。此功能一旦上線,必將會有一個叫作 “相容 Browserify” 的生態圈,而其它名稱比如 “對客戶端友好” 也肯定會出現。這將是一個非常棒的解決方案,我們對此非常樂觀。接下來,讓我們著手處理剩下的三個難題。

客戶端的包安裝與依賴解析

第三個問題——多套包管理系統——實際上是前兩個問題的副作用。現在已經有一些第三方工具試圖緩解客戶端的包安裝和依賴解析問題,它們通常需要建立各自獨立的包倉庫和配置檔案格式。這類解決方案層出不窮,每一種解決方案都有其長處和短處。不過,從上面的統計資料中可以看出,目前為止,在這方面最流行的解決方案是 Bower。那麼接下來,請允許我們暫時忽略其它優秀的包管理器,重點關注一下 Bower 是如何工作的。

Bower 的解決方案

Bower 可以通過名稱來安裝包,也可以通過 Git URL 或任意 HTTP URL 來安裝,這些都跟 npm 是一樣的。但跟 npm 不同的是,Bower 會把每個包都安裝到 bower_components 目錄下的獨立目錄中,整個目錄結構是扁平的。舉例來說,如果 backbone 依賴 underscore,那麼 bower install backbone 將會把 backbone 和 underscore 這兩者都放置在 bower_components 目錄下。這意味著,從一個 web 應用中引用一個元件是非常簡單的,因為它總是會被安裝在相同的地方——這跟 npm 不同,因為 npm 包的實際安裝路徑並不固定。

扁平的目錄結構存在一個問題,如果你試圖安裝同一個庫的兩個不相容版本(比如 jQuery 的 1.11.1 版和 2.1.1 版)時,它們將會被安裝到相同的位置,併發生衝突。如果發生了這種情況,Bower 會要求你手工選擇哪個版本是你想要的,並且可以決定是否把這次選擇的結果儲存到 bower.json 檔案中。這個過程存在不確定因素,它依賴人工干預,因此兩個人在安裝相同的依賴包時可能會得出不同的安裝結果。不過一旦你把你的選擇結果儲存到了 bower.json 中,就不存在變數了——任何人在安裝你的專案時都會得到相同的安裝結果。

這種體驗沒有 Node 環境那麼好,因為後者遇到的版本衝突可以在無需人工干預的情況下自動解決。總的來說,它照顧到了前端開發者的關注點,而且它確實也幹得挺不錯的。

現在還無法選出勝者,但我們還是想減少摩擦

我們並不想操之過急。儘管 Bower 已經十分流行了,但眼下仍然還有不少其它的包管理方案可用。同時,瀏覽器也在持續地快速演進,因此我們認為,現在就對前端包管理方案下結論還為時過早。正是基於這種考量,我們不久前在《npm 命令列介面(CLI)線路圖》一文中提出了以下重要策略。

我們計劃把 npm CLI 模組化,將其設計為各個分離的部件。這些部件不僅作為 npm 客戶端的一部分而存在,還可以獨立地被程式所呼叫。底層的目標是令其他人可以在 npm 這個基礎之上編寫工具——如果 npm 中已有對他們有用的部件,那他們就可以重用;如果沒有,他們也可以自行實現自己的解決方案。實現這個目標的方法,並不是把 npm 改造成配置選項、開關、生命週期鉤子所組成的一坨大雜燴,而是將其模組化。

模組化 CLI 的完整設計還未定稿,但顯然會包含以下幾大部件:

  1. 一個用來從包倉庫中下載包的 API
  2. 一個可以在本地儲存、讀取並且解壓縮的 “快取” API
  3. 一個安裝器 API,可以把包放置到你的專案中的合適位置

我們應該已經說得非常清楚了,相信任何前端包管理器都想用上第 1 和第 2 條,然後重新實現第 3 條。

使用 npm 來構建你自己的前端包管理系統

如果你打算在今天構建一個理想的前端包管理系統,那它會是什麼樣子的呢?

中期來看,我們所能想像到的官戶端包管理系統將是這個樣子的:

1. 別去運營你自己的包倉庫了,直接用我們的

這並不僅是自私自利:除了我們之外,還有一些人在運營著自己的包倉庫,但他們給我們的反饋都是再也不想繼續下去了。維持包倉庫的穩定、高效、以及必要的客戶支援都是十分昂貴、困難和耗費時間的。而且從任何意義上來說,“託管包” 都不是客戶端包管理器想要解決的問題。如果包是跟 JavaScript 相關的,那就託管到 npm 吧。一旦生態圈功能上線之後,就可以通過它來在全域性庫中建立 “微型庫”,通過自定義搜尋的索引來充實其內容,並顯示其特徵。(譯註:我其實不確定後半句在說什麼。)

2. 採用 package.json 作為配置檔案

如果你的工具需要一些配置資訊才能工作,那就把它放進 package.json 檔案中吧。似乎未經詢問就這樣做稍顯粗魯,但我們在此發出邀請:但做無妨。npm 的包倉庫是一個無模式限制的(schemaless)儲存空間,因此你新增的每個欄位都具有和其它欄位一樣的地位,我們既不會清除這些新欄位,也不會因為存在新欄位而報錯(只要新欄位沒有跟現有的欄位衝突就行)。

我們也意識到這可能會帶來一種風險,產生一堆互不相容的配置資訊,因此,請適度使用:千萬要抵禦住誘惑,不要試圖搶佔一些通用的欄位名,比如 "assets" 或 "frontend" 等等。用一個特定的、代表你的應用的標籤就好,比如 "mymanager-assets" 或 "mymanager-scripts"。在未來,如果我們決定更加明確地支援你的功能,併為你分配一個通用欄位,那也是很容易實現對舊欄位名的向後相容的。

3. 採用我們的快取模組

在規模化的情況下,解壓縮、儲存並快取包其實是一個非常複雜的問題。因此,如果你是在使用我們的包倉庫的話,那麼一旦快取模組可用,你就應該立即用上它。它將會節省你的精力、時間和頻寬。

4. 編寫你自己的前端包行為

你的使用場景肯定跟 npm 以 Node 為中心的行為大相徑庭,因此這是唯一一塊你需要自己搞定的部分。即便如此,我們還是會提供一些順手的模組來幫助你。你可以做到和 Bower 一樣的效果,比如把前端包下載並安裝到一個完全不同的目錄中,然後自行處理依賴關係。或者你可以讓 npm 把所有東西都安裝到node_modules 目錄中,然後利用一個 post-install 指令碼或一個執行時鉤子來解析依賴,或以上策略的某種組合。我們不確定哪條路是最佳選擇,這也是我們鼓勵大家在此深入探索的原因。

我什麼時候可以開始動手?

一旦我們講清楚了這個計劃之後,接下來每個人都會問出這個問題。我們只能說:可能是明年(譯註:2015 年)的某個時候。將 npm 改造成上述效果所需要的工作早已啟動了,但 npm 公司的首要任務是得先讓自己成為一個自給自足的實體,這也是為什麼我們會在 2015 年早期專注於釋出 私有包 服務。在此之後,我們的下一個專注點應該就是擴充套件包倉庫自身的實用性了,屆時將是客戶端包管理功能的登場之時。

我現在可以做什麼?

我們將對此提供支援,這確實沒錯,但這個問題現在就橫在你的面前啊!那你眼下可以做些什麼呢?

1. 使用我們的包倉庫

沒有理由不這麼做。它很快,它的可用性高達 99.99%,而且它對開源專案是(並且永遠都將是)免費的。

2. 採用 package.json 作為配置檔案

同樣,沒有理由不這麼做。它是你的包,就用你想要的方式來描述它吧。要注意避免資料重複(不要另外弄出一個你自己的 "name" 欄位),並且避免通用的欄位名,除此以外,你就放手去做吧。如果你發覺自己對package.json 的使用方式有些怪異或複雜,隨時可以通過 IRC、Twitter 和 Email 找到我們——如果你想先跟我們通個氣的話。

3. 給你的包打標籤

目前 npm 的 "keywords" 欄位在某種程度上利用得還不夠,其實它可以用來清晰地宣告包與某個生態圈的從屬關係或相容性,即使這個生態圈還不存在也沒關係。舉個例子,如果我給一個包打上 “ecosystem:hapi” 的標籤,那你就可以用這個標籤搜到它了。這種方式明顯不能像一個真正的生態圈那樣好用,因為它不具備(將來生態圈功能將會提供的)自動的驗證機制,但這總比模糊不清的關鍵字要好。

4. 使用生命週期指令碼,以及 Browserify

使用 生命週期指令碼 來管理那些通過 npm 安裝的客戶端資源,並不是一個完美的解決方案,但我們認為這個方向值得探索。比如說,你可以設定一個 "postinstall" 指令碼,用來把 npm 安裝的包移動到一個扁平的目錄結構中,並處理依賴關係。這種方式肯定不夠完美,但如果你把它作為救命稻草來用,我們會樂於關注你在這條路上能走多遠,而你的痛點也將為我們接下來的行動帶來啟發。

我們還認為 Browserify 是非常棒的工具,但遠沒有得到充分利用。如果在安裝時把它作為一個端到端的解決方案來使用,將是一個非常有創意的想法。(請查閱 Browserify 的 溫馨手冊,那裡有非常棒的文件,會告訴你如何用好它。)

請再堅持一下

前端開發者希望不再同時使用多個包管理器。包倉庫的運營者們也已經厭倦。目前 npm 對前端包管理的支援確實還不夠好。我們知道、我們同意、我們承諾會讓事情變得更好。前端開發者們,npm 愛你們,而且我們關心你們的使用場景。我們自己也使用 npm 來構建自己的網站,我們也有著同樣的痛點。因此,請繼續向我們提供反饋和建議。我們正在為之努力。

最終,勝者必現

我們的最終觀點是有必要明確一下的:我們期望有一個解決方案能浮出水面,它是如此直觀、如此易用,以致於我們可以 “仰慕” 它,甚至把它內建到 npm 中或將它繫結為 npm 的一部分。當我們這樣做的時候,不希望人們認為我們是在偷換概念,因為我們曾許諾要維護一個良性的競爭生態,但結果我們又挑出了一位勝者(我們知道這個做法在其它公司身上曾出過問題)。但事實上最終必將出現一位勝者:我們只不過是到了那個時候才知道它是誰,而已。

如果你已經強烈地預感到那個終極方案是個什麼樣子,就去實現並推廣它吧,這比在 GitHub issue 裡寫長篇評論要強一萬倍;同樣,對於每個處在 Node 社群的人來說,這也是極其受用的。因此,大步向前,去構建解決方案吧,我們會密切關注的!


譯者後記

最初同事將這篇文章推薦給我時,我沒有讀下去。當江湖傳聞 Bower “要完” 時,我再次翻出了這篇文章,並將它翻譯了出來。

但譯完之後,坦白地說,我有些失望。npm 在這篇文章中並沒有提供任何有效的解決方案,只是期望 “美好的事情必將發生”。這篇文章發表於 2014 年底,但直到現在 npm 也拿出文中提到的 “生態圈” 功能;這一年多來,前端包管理領域也沒有浮現任何真命天子般的終級解決方案。

不過,在前端開發者這一端,包管理的實踐風向倒是發生了不小的轉變。最明顯的潮流就是 “放棄 Bower,直接採用 npm”。這背後的推力,一方面是越來越多的 npm 包採用 UMD 作為釋出方式,網頁直接使用也無壓力(當然我們也可以認為這一點與上述潮流互為因果);另一方面,前端資源的構建過程已成常態,在頁面中通過 <script> 標籤直接引入指令碼的情況越來越少了,Bower 的獨有價值也就少了很多。此外,npm3 的扁平化目錄結構也進一步瓦解了前端開發者的心理防線。

如此看來,npm 動作雖慢,但斗轉星移,自己卻被推到浪潮之巔。這篇文章已無時效,但讀起來仍然很有意思,令我們有機會一窺這家公司的思維方式與價值觀。

相關文章