NPM 與前端包管理
來自: http://web.jobbole.com/85067/
我們很清楚,前端資源及其依賴管理一直是 npm 的重度使用場景,同時這也一直是 Node.js 普及的重要推動力。但這類應用場景到底有多重度?這是一個很難回答的問題。這份 “ npm 最常下載的包的清單 ” 并不能提供有效的證據:因為像 async、minimist 和 request 這樣的包就像是 “生活必需品”,它們會被數以千計的其它包所 依賴 ,這樣一來它們當然會隨著那些依賴它們的包一起被不停地下載。
更有意義也更接近真相的問題是:哪些包是人們 主動 安裝的?所謂 “主動安裝”,就是指某個人(或某個機器人)以實際運行 npm install thispackage 命令的方式來安裝一個包。不久前,我們開始把日志數據加入到 Jut 中,隨后我們終于可以方便而快速地給出這個問題的答案了。最終,我們得到了 “ 最常主動安裝的 npm 包五十強榜單 ”,這份榜單畫風突變,很有意思。在五十強中有 32% 的包(它們產生了 50% 的實際下載量)都是前端的工具或框架,攜 Grunt、Bower 和 Gulp 一起遙遙領先(當然移動端也是一大重度應用,這里暫且不表)。此外,這些包的使用量也在穩步增長:
(客戶端工具的增長,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 當作你的數據庫或多媒體服務器來用)。對此如有疑問,請通過 推ter 或 Email 詢問,我們樂于討論。
npm 的哲學
npm 的愿景是幫助開發者減少摩擦。我們傾向于通過 “循蹤辟徑” 的方式來實現這一點。這句話的意思是說:我們不希望告訴用戶該怎么做;我們希望觀察用戶是怎么做的,然后把障礙掃清。如果很多人都是在以各自不同的方式在行事,那我們不會輕易地從中挑出一個勝者,除非最佳實踐已經昭然若揭。
那么,在前端包管理的領域中,用戶遇到的阻力究竟在哪里?用戶踩出的 “蹤” 又是怎樣的?
前端痛點
除了 GitHub issue 以及 IRC、推ter、技術會議和線下聚會中的用戶以外,我們還會跟一些大型前端包的開發者們直接對話——這其中包括 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 的完整設計還未定稿,但顯然會包含以下幾大部件:
- 一個用來從包倉庫中下載包的 API
- 一個可以在本地存儲、讀取并且解壓縮的 “緩存” API
- 一個安裝器 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、 推ter 和 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 動作雖慢,但斗轉星移,自己卻被推到浪潮之巔。這篇文章已無時效,但讀起來仍然很有意思,令我們有機會一窺這家公司的思維方式與價值觀。
</div>