敢問 9102 年的前端同窗們,上次你折騰依賴和構建配置是爲了什麼,又花了多少時間呢?對於如今前端項目中常使人詬病的開發環境穩定性問題,筆者認爲 NPM 的一個設計難辭其咎,那就是 devDependencies
。前端
一切都是從這條膾炙人口的命令開始的:vue
npm install
複製代碼
毫無疑問,這是條偉大的命令。少了 npm install
,估計能廢掉今天一個前端的八成功力。它的做用說簡單也很簡單,那就是把前端項目中始於 dependencies
和 devDependencies
的依賴,遞歸地安裝到 node_modules
目錄下。時至今日,相信你們拿到一個前端項目時,潛意識都是「先用一條命令裝好所有東西,而後再一條命令跑起開發環境」了吧。相比於刀耕火種的年代,這顯然是個巨大的進步。node
然而這個初看之下簡單易用的工具背後,槽點一直很多。好比很多同窗均可能看過這張生動鮮明的對比:react
你的項目有多大的 node_modules
呢?對於要正經上線的項目,這目錄裏的東西沒個 500M,恐怕都很差意思和人打招呼了吧。不過,依賴包體積並非本文的關注點——畢竟這是前端工程化水平飛速發展的最好證實嘛(認真臉求別誤傷),這裏想要首先指出的,是前端項目的一種特殊性。vue-cli
前端領域具有一個很是特別的性質,那就是構建項目所需的資源,不光種類繁多,並且碎片化程度極高。npm
何謂「資源種類繁多」呢?對於常見的編程語言,它們的包管理器都是專門爲這種語言定製的,從 Python 的 PIP 到 Java 的 Maven 再到 Rust 的 Cargo 無不如此。這些語言的文件格式也都是惟一肯定的。然而,前端項目中所須要承載的資源類型,基本上除了 JavaScript 以外,還會包括 CSS、HTML、SVG、字體、圖像……這裏每種資源的構建、轉譯、打包、優化……等工具,幾乎都是 JS 寫出來的,要經過 NPM 來安裝。因此說今天的前端項目裏,要經過 NPM 安裝依賴來搞定的資源種類,早就一應俱全了。編程
那麼,「碎片化程度極高」又是什麼概念呢?前端社區的奇妙之處,在於對上面提到的每種資源,都有一大堆百花齊放的解決方案:json
.vue
呢?別忘了還有什麼 EJS 啊 Pug 啊等等數不勝數的模板語法,任君選擇。這裏的麻煩之處,不在於如何挑選、對比或使用具體的某種工具(這對很多同窗來講,反而有着女人挑衣服般的快感),而是你必須作出高度碎片化的選擇。要知道,上面的每種解決方案(或者標準),幾乎都有一到多種構建工具,每種工具除了可能有本身的插件體系外,其版本還會不停更新。你的構建方案選型,幾乎註定是海量可能性之中的滄海一粟而已——因此,咱們不就正應該把這些碎片都放到 devDependencies
裏面去管理嗎?這不正是個很是合適的設計嗎?前端工程化
理論上確實是這樣沒錯。但最麻煩的地方在於,devDependencies
但是和 dependencies
共用同一份 node_modules
目錄的!瀏覽器
先想一想編寫其餘語言的時候,你是怎麼更新依賴的吧:更新某個 Python 爬蟲包的時候,你會想順帶更新 Python 解釋器的版本嗎?誰沒事這麼折騰本身啊——實際的業務邏輯代碼及其依賴,相比於用來構建項目的工具,幾乎歷來就是兩個互相獨立的東西。然而對於前端領域來講,因爲前端工具鏈一大部分都在 node_modules
裏面,所以在更新業務邏輯依賴的時候,很是容易影響到你的工具鏈。
可能不少同窗尚未意識到,node_modules
裏工具鏈類型的依賴,已經有多麼重了。以 React 和 Vue 爲例,社區的腳手架工具,分別會帶來多少 dependencies
和 devDependencies
呢?筆者作了個簡單的嘗試以下:
掐指一算不難發現,如今主流框架默認搭出來的前端項目裏,有 98% 的依賴是 devDependencies
啊!雖然實際項目中的業務依賴確定會更多,但瀏覽器端的業務基礎庫極少有複雜的重型依賴結構,反卻是根據項目須要改進構建配置的時候,很容易大幅增長工具鏈的總體體積。所以,認爲實際前端項目中半壁江山以上的依賴屬於 devDependencies
,應該是個合理的假設。這帶來的問題並不在於絕對的體積大小,而在於構建資源的高度碎片化會使得構建工具也須要快速迭代,帶來大量的依賴版本。這麼多依賴的版本一旦意外漂移,組裝出來的穩定程度未必讓人放心。這不是 JS 語言層面的問題,而是任何大型軟件在系統層面的問題。相信只要是折騰過一些激進 Linux 發行版圖形界面依賴的同窗,都應該能理解這一點吧。
對了,雖然 create-react-app 掩耳盜鈴地把
eject
後的全部依賴所有算在了dependencies
裏面,從應用與類庫的角度來看這也說得通,但這並不影響咱們的結論。
除了 npm update
以外,每次 npm install
和 yarn install
,均可能(注意是可能,不是必定)體貼地基於語義化的版本規範,幫你把工具鏈版本都更新一遍,進而引入潛在的不穩定問題。同一份 package.json 間隔幾天後再全量重裝一次,devDependencies
裏對應的構建工具版本幾乎確定會有些不同。你說有誰會平常勤奮地更新 GCC / XCode / Android Studio 這類玩意呢?許多前端項目裏偏偏就會發生這一點,由於 node_modules
常常變,而構建許多資源的核心工具都在裏面呢。
看到這裏確定不少同窗會坐不住了:不是有專治版本漂移的 Lock 文件嗎?沒錯,Lock 確實能鎖住版本,但別忘了 Lock 容易衝突但是天生的,一旦衝突該怎麼解決呢?刪掉重裝啊——恭喜你再次喜提全量更新大禮包。實際上對於下面這些場景,最終 devDependencies
的工具鏈都很容易被牽連到:
再說得過度一點,想要保證真實場景下任意兩次安裝都能生成一樣的 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 均可以有本身的 dependencies
和 devDependencies
,好像沒有問題吧?
咱們確實是能這麼作的。但問題在於,只要宏倉庫裏每一個包都引入了各自不一樣的 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
裏就行了呀……在這方面,很容易想到很多簡單方便的實踐,好比:
build
和 src
兩個路徑,讓兩者都具有本身的 package.json 文件,從而把 node_modules
隔離開。這樣它們也都不須要 devDependencies
了。devDependencies
提取出來,專門創建一個用來構建的頂層模塊。這個頂層模塊能夠將整個倉庫打包爲一個來正式發佈,而每一個小包則直接分發源碼便可。build
工具到全局,相似於很是特化的 Parcel——只要安裝它到全局,每一個項目裏甚至連 Babel 插件均可以沒必要安裝,只要裝幾個 depencencies
就夠了。這造輪子的感受豈不比素質三連更好嗎 XDbuild
類型的依賴,也有利於提升構建速度,以及提升增量構建的穩定性。devDependencies
裏,也相對較少形成使人困擾的構建問題。固然,把它的包隔離出去也是容易作到的,具體就見仁見智啦。總結來講,本文看似寫了不少東西,但其實真正重要的也就這麼幾點:
devDependencies
依賴很容易大幅膨脹。node_modules
,變得較爲脆弱而低效。node_modules
中分離出去,就很容易改善構建穩定性問題。這種分離實現起來也至關容易。說到底,devDependencies
也確實是有存在價值的。但今天咱們面對的複雜狀況,會使得它很容易超過設計時的負載。所以,前端開發環境的穩定性問題,很大程度上與項目的內在性質,以及咱們目前對工具鏈慣用的用法有關,並不應一言不合就怪到 JS 的弱類型頭上。但願本文能減小點平常的折騰,加強些你們對社區的信心吧 :D