你也許不須要 devDependencies

敢問 9102 年的前端同窗們,上次你折騰依賴和構建配置是爲了什麼,又花了多少時間呢?對於如今前端項目中常使人詬病的開發環境穩定性問題,筆者認爲 NPM 的一個設計難辭其咎,那就是 devDependencies前端

一切都是從這條膾炙人口的命令開始的:vue

npm install
複製代碼

毫無疑問,這是條偉大的命令。少了 npm install,估計能廢掉今天一個前端的八成功力。它的做用說簡單也很簡單,那就是把前端項目中始於 dependenciesdevDependencies 的依賴,遞歸地安裝到 node_modules 目錄下。時至今日,相信你們拿到一個前端項目時,潛意識都是「先用一條命令裝好所有東西,而後再一條命令跑起開發環境」了吧。相比於刀耕火種的年代,這顯然是個巨大的進步。node

然而這個初看之下簡單易用的工具背後,槽點一直很多。好比很多同窗均可能看過這張生動鮮明的對比:react

你的項目有多大的 node_modules 呢?對於要正經上線的項目,這目錄裏的東西沒個 500M,恐怕都很差意思和人打招呼了吧。不過,依賴包體積並非本文的關注點——畢竟這是前端工程化水平飛速發展的最好證實嘛(認真臉求別誤傷),這裏想要首先指出的,是前端項目的一種特殊性。vue-cli

前端項目的特殊性

前端領域具有一個很是特別的性質,那就是構建項目所需的資源,不光種類繁多,並且碎片化程度極高npm

何謂「資源種類繁多」呢?對於常見的編程語言,它們的包管理器都是專門爲這種語言定製的,從 Python 的 PIP 到 Java 的 Maven 再到 Rust 的 Cargo 無不如此。這些語言的文件格式也都是惟一肯定的。然而,前端項目中所須要承載的資源類型,基本上除了 JavaScript 以外,還會包括 CSS、HTML、SVG、字體、圖像……這裏每種資源的構建、轉譯、打包、優化……等工具,幾乎都是 JS 寫出來的,要經過 NPM 來安裝。因此說今天的前端項目裏,要經過 NPM 安裝依賴來搞定的資源種類,早就一應俱全了。編程

那麼,「碎片化程度極高」又是什麼概念呢?前端社區的奇妙之處,在於對上面提到的每種資源,都有一大堆百花齊放的解決方案:json

  • 你想寫 JS?光是官方標準就有 ES2015/2017/2018/2019……這麼多,還有各類 Stage 的魔改語法,更不要說以 TypeScript 爲表明的各類 XXScript 了。
  • 你想寫 CSS?據說 Less、Sass、Stylus 都過期了,如今到底流行的是 PostCSS 仍是 CSS Modules 呀?哦好像還有個 Styled Components 好像也能用?
  • 你想寫 HTML?來看看你是喜歡 React 黨的 JSX 仍是 Vue 幫的 .vue 呢?別忘了還有什麼 EJS 啊 Pug 啊等等數不勝數的模板語法,任君選擇。

這裏的麻煩之處,不在於如何挑選、對比或使用具體的某種工具(這對很多同窗來講,反而有着女人挑衣服般的快感),而是你必須作出高度碎片化的選擇。要知道,上面的每種解決方案(或者標準),幾乎都有一到多種構建工具,每種工具除了可能有本身的插件體系外,其版本還會不停更新。你的構建方案選型,幾乎註定是海量可能性之中的滄海一粟而已——因此,咱們不就正應該把這些碎片都放到 devDependencies 裏面去管理嗎?這不正是個很是合適的設計嗎?前端工程化

理論上確實是這樣沒錯。但最麻煩的地方在於,devDependencies 但是和 dependencies 共用同一份 node_modules 目錄的瀏覽器

devDependencies 的脆弱性

先想一想編寫其餘語言的時候,你是怎麼更新依賴的吧:更新某個 Python 爬蟲包的時候,你會想順帶更新 Python 解釋器的版本嗎?誰沒事這麼折騰本身啊——實際的業務邏輯代碼及其依賴,相比於用來構建項目的工具,幾乎歷來就是兩個互相獨立的東西。然而對於前端領域來講,因爲前端工具鏈一大部分都在 node_modules 裏面,所以在更新業務邏輯依賴的時候,很是容易影響到你的工具鏈。

可能不少同窗尚未意識到,node_modules 裏工具鏈類型的依賴,已經有多麼重了。以 React 和 Vue 爲例,社區的腳手架工具,分別會帶來多少 dependenciesdevDependencies 呢?筆者作了個簡單的嘗試以下:

  • 單獨安裝 React 和 ReactDOM,只佔用 3.9M 空間。
  • 單獨安裝 Vue,也只佔用 3.6M 空間。
  • 使用 create-react-app 建立一個空白 React 項目,佔用 189.6M 空間。
  • 使用 vue-cli 建立一個空白 Vue 項目,佔用 164.5M 空間。

掐指一算不難發現,如今主流框架默認搭出來的前端項目裏,有 98% 的依賴是 devDependencies 啊!雖然實際項目中的業務依賴確定會更多,但瀏覽器端的業務基礎庫極少有複雜的重型依賴結構,反卻是根據項目須要改進構建配置的時候,很容易大幅增長工具鏈的總體體積。所以,認爲實際前端項目中半壁江山以上的依賴屬於 devDependencies,應該是個合理的假設。這帶來的問題並不在於絕對的體積大小,而在於構建資源的高度碎片化會使得構建工具也須要快速迭代,帶來大量的依賴版本。這麼多依賴的版本一旦意外漂移,組裝出來的穩定程度未必讓人放心。這不是 JS 語言層面的問題,而是任何大型軟件在系統層面的問題。相信只要是折騰過一些激進 Linux 發行版圖形界面依賴的同窗,都應該能理解這一點吧。

對了,雖然 create-react-app 掩耳盜鈴地把 eject 後的全部依賴所有算在了 dependencies 裏面,從應用與類庫的角度來看這也說得通,但這並不影響咱們的結論。

除了 npm update 以外,每次 npm installyarn install,均可能(注意是可能,不是必定)體貼地基於語義化的版本規範,幫你把工具鏈版本都更新一遍,進而引入潛在的不穩定問題。同一份 package.json 間隔幾天後再全量重裝一次,devDependencies 裏對應的構建工具版本幾乎確定會有些不同。你說有誰會平常勤奮地更新 GCC / XCode / Android Studio 這類玩意呢?許多前端項目裏偏偏就會發生這一點,由於 node_modules 常常變,而構建許多資源的核心工具都在裏面呢。

看到這裏確定不少同窗會坐不住了:不是有專治版本漂移的 Lock 文件嗎?沒錯,Lock 確實能鎖住版本,但別忘了 Lock 容易衝突但是天生的,一旦衝突該怎麼解決呢?刪掉重裝啊——恭喜你再次喜提全量更新大禮包。實際上對於下面這些場景,最終 devDependencies 的工具鏈都很容易被牽連到:

  • 項目依賴了內部的 NPM 包,並常常須要在不一樣開發分支上並行更新時
  • 項目須要 checkout 到某個老版本修復問題,從新同步業務依賴時
  • 團隊內須要使用特殊的私有倉庫配置時
  • 團隊成員使用的包管理工具版本不徹底一致時

再說得過度一點,想要保證真實場景下任意兩次安裝都能生成一樣的 Lock,簡直就跟要求保證兩臺型號一致的手機要得到一致的跑分數值同樣難辦。倒不是說 Lock 設計得很差,只是按照如今的使用方式來講,即使基於它來保證工具鏈的穩定,仍是存在很多意外可能性的。

因此咱們已經知道,無論有沒有使用 Lock,只要是和業務依賴混在一塊兒的 devDependencies,都容易被意外更新,從而引入不穩定性。可是,這裏「脆弱性」還不止體如今工具鏈版本容易波動上而已,還有其它麻煩的問題。

例如,devDependencies 可能影響宏倉庫的開發體驗。所謂宏倉庫 (Mono-Repo),也就是把一堆 package 按這種形式放到一塊兒管理的倉庫:

my-mono-repo
├── package.json
└── packages
    ├── A
    │   ├── package.json
    │   ├── node_modules
    │   └── src
    ├── B
    │   ├── package.json
    │   ├── node_modules
    │   └── src
    └── C
        ├── package.json
        ├── node_modules
        └── src
複製代碼

假設咱們本身維護了 A B C 三個包,這些包之間也有相互的依賴。那麼通常來講,基於 npm link 命令或者更自動化的 Lerna 工具,咱們能夠經過軟連接的方式,把它們之間的依賴關係維護好。若是須要你本身來維護這些包,那麼一個很天然的想法就是,A B C 均可以有本身的 dependenciesdevDependencies,好像沒有問題吧?

咱們確實是能這麼作的。但問題在於,只要宏倉庫裏每一個包都引入了各自不一樣的 devDependencies,這些包每一個都會帶來龐大的 node_modules,不只會引入大量的冗餘,還會減慢倉庫的初始化過程,讓連接關係更加脆弱,就像在幾臺巨大的機器之間搭飛線同樣。若是倉庫裏的包還須要被連接到其它項目,那就更麻煩了。在咱們的實踐中,若是宏倉庫裏的每一個包都各自依賴了 Babel 這樣的大型構建工具,它們之間的微妙區別會使得不少時候都不得不從新配置連接關係,而後帶來大量無心義的 Lock 文件改動。除非是爲了整合老項目而臨時處理,不然儘可能不要這麼幹噢。

某種意義上,宏倉庫裏折騰的 Link 操做,是 NPM 爲了簡單性付出的代價。NPM 基於文件系統的目錄結構來映射依賴結構,你能夠在開發時直接修改 node_modules 裏 Webpack 和 Vue 這些基礎庫的代碼看到效果,方便你爲社區貢獻 PR(逃)。Link 則就是個軟連接,能在任意兩個目錄之間創建依賴關係。然而,須要本身 Link 來管理的依賴,基本都是以私有依賴和業務依賴爲主,這時候它們也要和大量用於構建的依賴複用同一個 node_modules「容器」,自己就是一種不穩定因素。

除了宏倉庫和 Link 外,膨脹的 devDependencies 對於 CI 構建也是有影響的。對於本身維護業務 NPM 包的團隊,業務依賴極可能被快速地更新。這也就意味着,在 CI 上執行 npm install 的時候,常常要爲了微不足道的業務依賴 bugfix 升級,去驚動一整個混沌的 node_modules,影響構建的速度和穩定性。固然,主流的構建系統都具有了構建緩存,但咱們仍是在生產環境中遇到過 Yarn 對依賴的依賴使用了錯誤的緩存版本,而致使線上出現問題的意外。這時候怎麼辦呢?清空構建緩存從頭再來吧……對了,前段時間咱們構建用的的某臺 dev 機器,其磁盤 inode 索引甚至已經被刷到歸零了呢。固然,你能夠說這些問題是構建緩存、Lock 和 Linux 的鍋,但對於動輒帶來幾萬行 Lock 文件的 devDependencies,你等於……你也有責任吧。爲何要把雞蛋整個打進水裏拌勻,而後再把蛋殼挑出來呢?

當前流行的腳手架工具,某種程度上也加重了這種環境配置的不穩定性。自從 create-react-app 帶頭示範了 eject 這個拔屌無情的玩法以後,主流的腳手架工具基本都是比較管生無論養的。很多公司內部的「統一腳手架」工具,也仍是以「複製一坨東西 -> install -> run」的素質三連爲主。項目少的時候這確實也挺方便,但項目建出來以後的依賴管理,就比較棘手了。

咱們能夠作什麼

寫到這裏,咱們已經說了不少 devDependencies 的行爲所帶來的問題了。那麼咱們能作什麼呢?其實很是簡單,把 devDependencies 單獨丟到另外一個 node_modules 裏就行了呀……在這方面,很容易想到很多簡單方便的實踐,好比:

  • 對簡單的前端應用項目,直接用全局的 Parcel 甚至瀏覽器原生的 ES Module 就足夠了,無需專門配置打包工具。
  • 對常見的前端應用項目,能夠區分 buildsrc 兩個路徑,讓兩者都具有本身的 package.json 文件,從而把 node_modules 隔離開。這樣它們也都不須要 devDependencies 了。
  • 對於宏倉庫類型的前端項目,能夠將每一個包裏沉重的 devDependencies 提取出來,專門創建一個用來構建的頂層模塊。這個頂層模塊能夠將整個倉庫打包爲一個來正式發佈,而每一個小包則直接分發源碼便可。
  • 對於存在必定規範的多個前端應用項目,除了腳手架外,還能夠考慮爲它們封裝一個特化而穩定的 build 工具到全局,相似於很是特化的 Parcel——只要安裝它到全局,每一個項目裏甚至連 Babel 插件均可以沒必要安裝,只要裝幾個 depencencies 就夠了。這造輪子的感受豈不比素質三連更好嗎 XD
  • 對於須要 CI 的項目來講,專門隔離出 build 類型的依賴,也有利於提升構建速度,以及提升增量構建的穩定性。
  • 噢可別忘了測試工具。它們確實很適合歸屬在 devDependencies 裏,也相對較少形成使人困擾的構建問題。固然,把它的包隔離出去也是容易作到的,具體就見仁見智啦。

總結來講,本文看似寫了不少東西,但其實真正重要的也就這麼幾點:

  • 由於資源的複雜性,用於構建的 devDependencies 依賴很容易大幅膨脹。
  • 這些構建用的大量依賴,會與項目的實際依賴共用 Lock 和 node_modules,變得較爲脆弱而低效。
  • 只要把這部分臃腫但能夠長期穩定的依賴從 node_modules 中分離出去,就很容易改善構建穩定性問題。這種分離實現起來也至關容易。

說到底,devDependencies 也確實是有存在價值的。但今天咱們面對的複雜狀況,會使得它很容易超過設計時的負載。所以,前端開發環境的穩定性問題,很大程度上與項目的內在性質,以及咱們目前對工具鏈慣用的用法有關,並不應一言不合就怪到 JS 的弱類型頭上。但願本文能減小點平常的折騰,加強些你們對社區的信心吧 :D

相關文章
相關標籤/搜索