譯者前言
這兩天被 Yarn 刷屏了?對於新工具,範範地道聽途說或淺嘗輒止,不如靜下心來聽聽作者的心路歷程。我讀了多篇文章,感覺說得最清楚的還是 Facebook 發表的這篇官方介紹,於是翻譯出來分享給大家。
在談論 Yarn 時,有人在談論前端圈的 “造輪子” 風氣,有人在談論 Facebook 工程師的 PR 能力,有人在談論網路速度……但讀完這篇文章之後,我看到的是前端工程、是開發體驗、是思維方式。相信不論是一線工程師還是架構師,都會從 Facebook 團隊的思考中找到啟發。
在 JavaScript 社群裡,開發者們共享出了成千上萬的程式碼,這令我們不必自己從頭開始編寫最基礎的元件、類庫或框架。這些程式碼之間通常層層依賴,而這些依賴關係正是由包管理器來管理的。當前最流行的 JavaScript 包管理器是 npm 客戶端,通過它,我們可以獲取 npm 倉庫中的 30 多萬個包。超過 500 萬名開發者在使用 npm 倉庫,每個月產生 50 多億次下載。
在 Facebook,我們已經成功地使用 npm 客戶端多年了,但隨著程式碼體積的不斷增長和團隊規模的不斷壯大,我們在一致性、安全性和效能方面遇到了問題。我們曾嘗試逐一解決這些問題,但最終,我們決定自己打造一套全新的解決方案,以一種更加可靠的方式來管理依賴。我們的工作成果叫作 Yarn——作為 npm 客戶端的替代器,它更加快速、可靠、安全。
今天,我們很榮幸地宣佈,Yarn 以開源的方式釋出,它是由 Exponent、 Google、Tilde 與我們合作完成的。在使用 Yarn 時,開發者們還像以前一樣從 npm 倉庫那裡獲取資源,但安裝速度更快,不同的機器的安裝結果完全一致,甚至還可以在安全的離線環境中使用。Yarn 令開發者可以更加迅捷和從容地享受前人栽種的果實,進而集中精力打造自己的產品——這才是更加重要的事情。
JS 包管理方案在 Facebook 的演變
在包管理器出現之前,JavaScript 開發者平時所用的依賴並不多,這些依賴通常會直接存放在專案中,或通過 CDN 引入。npm 是第一個正式的 JavaScript 包管理器,在 Node.js 誕生後不久它就建立起來了,隨後迅速成為全世界最流行的包管理器。從此,開源專案如雨後春筍般湧現,盛況空前。
Facebook 的很多專案,比如 React,就依賴了 npm 倉庫中的程式碼。但是,隨著內部規模的擴張,我們遇到了很多問題:不同機器或不同人所得到的安裝結果並不一致,安裝依賴所花費的時間也無法忽視;由於 npm 客戶端在安裝依賴包時會自動執行其中的指令碼,安全性也令我們顧慮重重。我們不斷嘗試針對這些問題建立解決方案,但這些解決方案往往又不斷引發新的問題。
關於理順 npm 客戶端的一系列嘗試
最開始,我們遵循官方推薦的最佳實踐,只把 package.json
檔案提交到程式碼庫,然後要求工程師們手動執行 npm install
來安裝依賴。這種方法對工程師來說運作良好,但在我們的持續整合(CI)環境下就不靈了,因為 CI 環境需要執行在沙箱中,基於安全性和可靠性的考慮,它需要切斷與網際網路的連線。
後來我們改變了對策,把所有 node_modules
簽入程式碼庫。這種方法也執行了一段時間,但它也讓一些原本簡單的操作變得困難起來。舉例來說,更新 Babel 的一個次版本號會產生一次 “80 萬行” 級別的提交動作,這種情況很難處理,而且會觸發一系列無意義的 lint 操作(諸如檢查 UTF-8 位元組序列、Windows 換行符、未優化的 PNG 圖片等等)。把 node_modules
的變更合併進來往往要花費工程師一整天的時間。我們的版本控制團隊也指出我們提交的 node_modules
目錄直接導致後設資料體積的飆升。比如 React Native 專案的 package.json
只列出了 68 個依賴,但在跑過了 npm install
之後,node_modules
目錄下會產生 121,358 個檔案。
為了讓 npm 的這一套工作流適應 Facebook 的團隊人數和程式碼量,我們動用了最後一招。我們決定把整個 node_modules
目錄打成一個壓縮包,上傳到專用的 CDN 上,這樣工程師和 CI 伺服器都可以下載、解壓並得到完全一致的結果了。這個方案一上,我們就可以從版本控制系統中清掉成千上萬的檔案了;但這同時也導致工程師不論是在拉取新程式碼時,還是在編寫新程式碼時,都需要保持網路連線。
我們還不得不應付 npm 的 shrinkwrap 功能所帶來的問題,因為我們使用 shrinkwrap 來鎖定依賴版本。Shrinkwrap 檔案並不是預設生成的,一旦工程師在提交程式碼前忘記生成這個檔案,則這個檔案跟程式碼就不同步了。為此我們還特意寫了一個工具,用來校驗 shrinkwrap 檔案的內容跟 node_modules
的內容是否匹配。這個 shrinkwrap 檔案是由一堆亂序欄位所組成的巨型 JSON 資料,因此每次修改都會產生一次巨型的、無法 review 的提交。為了緩解這個問題,我們又需要寫一個額外的指令碼來把所有的欄位排好序。
這還沒完呢。在 npm 中,更新一項單獨的依賴其實還會按照 “語義化版本”(semantic versioning)的規則更新一堆無關的依賴。這使得每次程式碼變更都比預期的要多,而我們所能做的也就是上述這一系列的下策。
構建新的客戶端
痛定思痛,我們決定不再圍繞 npm 客戶端來構建基礎設施,而是從一個更高的高度來審視整件事情。如果我們針對當下的痛點來打造一款全新的客戶端會怎麼樣?我們倫敦辦事處的 Sebastian McKenzie 同學開始推進這個想法,很快我們就發現這條道路充滿光明。
在推進這個計劃的同時,我們也開始跟業內的工程師們廣泛交談。我們發現大家遇到的問題其實都差不多,也都嘗試過差不多的解決方案,效果也差不多都是按下個葫蘆浮起個瓢。接下來的事情就變得水到渠成了——我們集合社群的力量來解決共通的問題,產出一個適用於每個人的解決方案。由此我們收到了來自 Exponent、Google 和 Tilde 公司的工程師的幫助,共同開發出了 Yarn 客戶端;為了確保它也適用於 Facebook 之外的使用場景,我們還在所有主流的 JS 框架身上驗證了它的效能。今天,我們懷著激動的心情,將它推薦給整個社群。
Yarn 介紹
Yarn 是一款新的包管理器,它將取代原有的基於 npm 客戶端(或其它包管理器)的工作流,但同時又保留了與 npm 倉庫的相容性。它具備原有工作流的所有功能,但相比之下更加快速、安全、可靠。
所有包管理器的核心功能都是從一個通用倉庫中獲取包,然後安裝到開發者的本地環境中。所謂 “包”,就是一套解決特定任務的程式碼。一個包可能依賴、也可能不依賴其它包。一個典型的專案可能會在它的依賴樹中涉及數十、數百甚至數千個包。
這些依賴包都是有版本的,它們按照 “語義化版本”(semver)的規則被安裝進來。Semver 定義了一套版本描述規範,用於表明每個版本的變更型別:是 API 行為出現了向後不相容的破壞性變更、是增加了一個新功能、還是修復了一個 bug。但是,semver 是否奏效,取決於包的開發者是否遵守規則。在理想情況下,即使依賴包沒有鎖定版本,包的不同型別的變更版本都會在 semver 的約定下以不同的方式被正確地安裝進來。
架構設計
在 Node 的生態系統中,依賴會被安裝到你的專案中的 node_modules
目錄中。不過,其內部的檔案結構和實際的依賴關係樹並不完全對應,因為安裝過程存在重複依賴的合併機制。npm 客戶端在把依賴安裝到 node_modules
目錄時存在不確定性。這種 “不確定性” 是指,由於安裝依賴的順序不同,你得到的 node_modules
目錄的內部結構可能跟別人不一樣。這種差異可能會導致 “我電腦上是好的” 之類的 bug,而這類 bug 往往是極難定位的。
Yarn 解決上述版本問題和不確性問題的方案是引入 lockfile(鎖定檔案),並啟用了一套新的安裝演算法,以此達到一致、可靠的結果。這個 lockfile 會把所有已安裝的依賴鎖定在一個固定的版本上,確保每次安裝所產生的 node_modules
目錄的檔案結構在不同機器上總是一致的。這個 lockfile 採用一種簡明的格式來書寫,其欄位是有序的,以確保每次更新都是最小化的、易於 reivew 的。
整個安裝過程被分解為以下三個步驟:
- 解析:Yarn 首先開始解析依賴關係。它向包倉庫發出請求,並遞迴地查詢各層依賴。
- 獲取:接下來,Yarn 會在一個全域性的快取目錄中查詢當前所需的包是不是已經下載過了。如果還沒有,Yarn 會把這個包的 tarball 拉下來,並把它存放在全域性快取中,這樣它下次就可以離線安裝了,無需重複下載。依賴包也可以以 tarball 的形式存放到版本控制系統中,以實現完全的離線安裝。
- 連結:最後,Yarn 會把所需的所有檔案從快取中複製到本地的
node_modules
目錄中,這樣所有東西就連結為一個整體了。
由於我們把安裝過程清晰地拆解開來,消滅了安裝結果的不確定性,Yarn 天生具備並行操作的能力。這種並行能力可以最大化資源的利用率,提升安裝速度。在 Facebook 的某些專案中,Yarn 可以帶來一個數量級的效能提升,安裝耗時從幾分鐘縮短到幾秒。Yarn 內建互斥特性,以確保同時執行多個 CLI 例項也不會相互衝突、相互汙染。
在整個安裝過程中,Yarn 還提供了嚴格的安全保障。你可以精確控制某個包的某個生命週期指令碼是否執行。包的校驗資訊(checksum)也會儲存在 lockfile 中,確保你每次安裝得到的都是同一個包。
其它特性
除了讓包的安裝變得更加快速和可靠以外,Yarn 還提供瞭如下特性,進一步簡化了依賴管理的工作流。
- 同時相容 npm 和 Bower 工作流,支援混用多種倉庫型別。
- 可以限制依賴包的授權型別,並且可以輸出依賴包的授權資訊。
- 暴露一個穩定的 JS API,提供抽象化的日誌資訊以便與其它構建工具整合。
- 提供可讀的、最小化的、美觀的 CLI 輸出資訊。
應用到生產環境
在 Facebook,我們已經將 Yarn 用於生產環境了,而且它跑得也確實不錯。我們的很多 JavaScript 專案都是由它來完成依賴安裝和包管理的。每當一個專案完成遷移後,工程師們都會獲得離線工作的能力,進而加快工作流。我們提供了一個頁面(https://yarnpkg.com/en/compare),用來展示不同條件下 Yarn 和 npm 在安裝耗時方面的對比。
如何上手
最簡單的上手方式就是執行以下命令:
npm install -g yarn yarn
新的 yarn
CLI 將會在你的開發工作流中替代 npm
。在各種場景下,Yarn 要麼提供了一個對等的命令,要麼提供了一個功能相似的新命令:
npm install
→yarn
在不加任何引數的情況下,yarn
命令將會讀取你的package.json
檔案,從 npm 倉庫拉取包,然後存放到你的node_modules
目錄中。其效果等同於直接執行npm install
。npm install --save
→yarn add
我們去掉了npm install
命令產生 “隱形依賴” 的行為,只保留了顯式安裝行為。執行yarn add
將等同於執行npm install --save
。
未來
我們這一群人走到一起構建了 Yarn,目的在於解決社群共通的問題;我們的願景也很明確,希望 Yarn 成為一個真正的社群專案,人人均可從中受益。Yarn 已經在 GitHub 開源,我們也已經準備好迎接來自 Node 社群的幫助:開始使用、交換想法、編寫文件、互相扶持,共同建立一個強大的社群來推動它的發展。我們相信 Yarn 已經邁出了堅實的第一步,而且你的參與會讓它變得更好!