一、引子
在npm、yarn等包管理工具的幫助下,前端工程化模組化的程式日益加快,專案的依賴包也日益增大,特別是若專案採用webpack來構建用到許多webpack的外掛、一些輔助開發如(eslint、postcss、dev-server之類的庫)以及一些單元測試(jest、mocha、enzyme)可能需要用到的外掛,專案中的node_module就會變的十分龐大。
如圖:
如果搭配這種情況是不是很絕望:
因此團隊開發者每次重新初始化專案進行npm install會十分緩慢,並且若佔用大量空間,更進一步來說專案中往往很多前端團隊會用到一臺構建伺服器,對前端專案的程式碼獲取、打包、構建、部署進行管理(例如筆者自己所在的團隊就在使用jenkins對前端專案的打包、構建、部署進行自動化託管)。npm這樣的專案級依賴管理的特性就會造成大量的時間以及資源的消耗。
同時筆者也想在文章開始之前表明一個態度,npm本身portable的模組化設計,使每個專案互不幹系,也是一種它設計的精華所在,本文僅是針對實際使用中遇到的一些小困擾進行解讀,希望提供一個新的思路。
補充一下,實際yarn已經有了解決方案 - workspaces:
本文暫不把yarn容納到討論範圍內(實際思路很相似,筆者也是寫完之後才發現的)。
此本篇文章就在這樣的背景下誕生了。純粹是筆者在專案中積累的一些經驗,若有不足望指出。
二、先來說說示例
github倉庫地址:github.com/Roxyhuang/n…
通常現代前端的標準工程往往具備以下功能,為了讓這個討論更貼近實際開發,並且更直觀,因此規定示例專案基本具備以下這些能力:
(1) 提供dev-server
(2) 提供語法轉譯(babel)
(3) 能夠解析樣式
(4) js程式碼風格外掛以及樣式風格檢查
(5) 單元測試
(6) 提供樣式相容處理
當然以上部分只體現的依賴層面。因此可能有些依賴的引入可能出現錯誤或者不合理。
並假設目前團隊使用了webpack和parcel兩套前端構建方案,團隊專案中有使用了React和Vue。
三、問題的總結
若不對專案做任何優化以及設定正常專案的依賴的結構應該是這樣的:
如圖:
然後我們再看一下node_modules整體所佔的空間:
情況一:多個依賴模組完全相同的專案
這種情況相對很常見,實際就是相同依賴的專案有多個
如圖:
可以想象這樣模組的容量便是162.2mb x 3,同時檔案數也會是1072 x 3。
由此可見實際我們對容量的消耗是是十分大的,筆者做了非精確統計實際:
生產中所需的模組容量與檔案數(dependencies):
本地開發和測試所需的模組容量與檔案數(devDependencies):
實際可以看到devDependencies才是我們整體依賴體積與數量如此龐大的“元凶”。
當然有部分生產中全域性所需的庫(如:React Vue)可以採用外部化,通過CDN來引入。(即圖上CDN模組的複用)
情況二:僅生產中所需的模組依賴依賴重複 (dependencies)
如圖:
生產中所需的模組容量與檔案數(dependencies):
僅圖中dependencies中紅色標註的react-redux,redux版本以及使用了mobx react-mobx更換
本地開發和測試所需的模組容量與檔案數(devDependencies):
無變化
實際這種情況和情況一類似只有dependencies中部分模組的依賴有差異或者版本不同。當然再進一步,這個部分每個專案確實可能出現較大的差異,同時著部分差異可能會引發devDependencies也出現變化(如:React Vue 可能用到webpack Loader eslint plugins等,之後會提及這塊)。
實際依然devDependencies才是我們整體依賴體積與數量如此龐大的“元凶”。
情況三:本地開發和測試所需的包依賴重複且,但不存在版本差異 (devDependencies)
如圖:
這種情況我們也囊括了情況二的差異(由於dependencies產生的差異,同時也會影響devDependencies的)
生產中所需的模組容量與檔案數(dependencies):
如圖所示,除了情況中出現的react-redux,redux版本有差異,由於出現了webpack + vue的專案,專案中dependencies的依賴需要引入vue或vue-router,除此都是一致的。
本地開發和測試所需的模組容量與檔案數(devDependencies):
由於我們需要引入vue,導致我們devDependencies需要產生一些變化包括移除一些webpack與react相關的外掛,eslint與react的外掛等,變更為webpack與vue相關的外掛,eslint與vue的外掛等。
如圖筆者非精確的統計一下這種情況僅devDependencies下的容量與檔案數:
還是依然很大,並且實際還是存在大量的依賴重疊(例子中有17個依賴重疊)。
情況四:多個構件工具,存在本地開發和測試所需的包依賴重複,且存在在版本差異, (devDependencies)
這種是目前最複雜的情況,存在下列幾個情況
(1) 前端構件方案存在多個, 例子中為webpack 和 parcel
(2) 生產中所需的模組存在差異,甚至技術棧不同
(3) 本地開發和測試所需的包依賴重複,但有略微不同,且存在版本不同
如圖:
這種情況我們也囊括了情況二的差異,可以看到同一個構件工具下存在本地開發和測試所需的包依賴重疊的情況依然是較多的。
因此實際如果出現這種情況,我們仍需要考慮以及解決。
小小的總結一下:
結合情況一、情況二、情況三、情況四的例子可以得出幾點小小的“規律”:
(1) devDependencies無論從模組所佔體積還是檔案數量都在整體依賴模組中所佔的比較較大
(2) dependencies下的包,可能會因為專案不同差異較大,同時也會導致devDependencies下模組有差異
(3) 即使devDependencies可能專案存在差異,但是仍有可能一部分重疊
(4) 多個模組化方案,可能會導致專案devDependencies差異較大
我們可以發現其中不管前三種情況的哪一種devDependencies都會有一部分的依賴模組是重疊的。那既然存在重複,那我們就有必要考慮“複用”
同時我們發現多個模組化方案(webpack, parcel)devDependencies下的模組差異會較大,這也是我們之後需要解決的。
鎖定依賴模組的能力:
非本文重點,僅簡述一下:
(1) lock or dont'lock
這個問題社群探討的老問題,鎖不鎖都有支援的一方,然而這邊筆者不表明態度,但是同時也希望我們之後的依賴管理的優化和“瘦身”能夠具備鎖定依賴的能力。
(2) npm 鎖定依賴以及鎖定依賴的內部迴圈的方式
-
使用語義版本控制來鎖定版本:
關於semantic-versioning,這裡我們不贅述,有興趣可以看一下npm文件:
但是通過semantic-versioning來鎖定版本,無法鎖定依賴的內部迴圈(即依賴的依賴)。
-
npm-package-locks來鎖定
關於npm-package-locks,這裡我們不贅述,有興趣的可以看一下:
我們關注的重點是我們需要依賴package-lock.json,來協助鎖定依賴的版本以及依賴內部迴圈的版本。
再總結一下:
(1) devDependencies無論從模組所佔體積還是檔案數量都在整體依賴模組中所佔的比較較大
(2) 專案的dependencies可能會因為專案不同差異較大,同時一部分可以外部化,因此不在本文想要探討“瘦身管理”的範圍內,列出主要是以便更接近現實狀況
(3) 即使devDependencies可能專案存在差異,但是仍有大量的依賴重複與重疊,根據實際包的分析他們是目前依賴“瘦身管理”的主要目標
(4) 即使dependencies有差異的專案,devDependencies大部分可能相似(如情況三所示),因此我們仍可以提煉出一部分相同的devDependencies進行“瘦身管理”。
(5) 多個模組化方案,可能會導致專案devDependencie差異較大(如例子中webpack和parcel)可能會有一部分devDependencies可能版本也會不同,不建議兩個專案差異極大的node_modules共享。
(6) 同一個模組可能會存在版本差異,因此我們可能會面對同時面對一個庫存在多版本的情況
(7) 無論依賴鎖定是否必要,但是希望提供的方案能夠具備。
(8) 依賴鎖定,需要依靠package-lock.json實現(shrinkwrap.json亦可)
四、優化方案以及存在的一些問題
下面的優化方案都依賴了NodeJS模組載入機制,因此先粗略的聊一下,這裡不描述完整的過程,感興趣的朋友可以查詢官方文件或可以看一下樸靈大大的深入淺出NodeJS 。
如何利用NodeJS模組載入機制
簡單來說 - 在NodeJS中引入模組,需要經歷如下3個步驟:
- 路徑分析
- 副檔名分析
- 編譯執行
我們利用的則是在路徑分析中自定義檔案模組(第三方npm包)的查詢:
自定義檔案模組查詢順序為:
- 當前目錄下node_modules目錄
- 父目錄下node_modules目錄
- 向上逐級遞迴直到根目錄下下node_modules目錄
- 遞迴至根目錄
有點類似於JS的原型鏈查詢,檔案路徑越深,模組查詢越耗時,同時也是它慢的原因。
當然這是預設情況下,實際通暢我們可以通過配置NODE_PATH的方式,在遞迴至根目錄後,若依然無法找到,給到一個路徑或多個路徑找到具體模組。
1. 配置NODE_PATH(方案一)
方案一實際就是利用了配置NODE_PATH,通常一般會將其配製成npm i -g
所在的全域性模組目錄(實際可以改,本例暫定這個目錄)。
結構變動
情況一、情況二、情況三都可以轉化成如下結構:
以情況三下為例子 - 如圖:
具體實現:
在對應系統的環境變數配置檔案中增加環境變數NODE_PATH,例如在MacOS中
vi /ect/profile
# 或/etc/bashrc或~/.bashrc
# 此處不贅述配置問題、載入順序以及原理
複製程式碼
-> export PATH=$PATH:
# 將 /usr/bin 追加到 PATH 變數中
-> export NODE_PATH="/usr/lib/node_modules;/usr/local/lib/node_modules"
# 指定 NODE_PATH 變數
複製程式碼
那 NODE_PATH 就是NODE中用來尋找模組所提供的路徑註冊環境變數。我們可以使用上面的方法指定NODE_PATH環境變數。並且用;分割多個不同的目錄。
關於 node 的包載入機制我就不在這裡贅述了。NODE_PATH中的路徑被遍歷是發生,從專案的根位置遞迴搜尋 node_modules 目錄,直到檔案系統根目錄的 node_modules,如果還沒有查詢到指定模組的話,就會去 NODE_PATH中註冊的路徑中查詢。
存在問題:
(1) 不會生成package.json,因此對依賴管理比較繁瑣,實現增量安裝比較繁瑣
- 當然其實可以不需要
npm i -g
去處理,可以手動自己維護一個目錄包括package.json,以及package-lock.json,但如果這麼只會形成只是一個線性關係,而非一個樹狀關係。
(2) 不支援同一模組同時存在不同版本,因此如果依賴出現版本差異,沒有解決方案
- 因為形成的是一個線性關係,而非一個樹狀關係實際專案若存同一依賴版本差異,就會有一個優先順序的問題存在。
(3) 全域性安裝模組,無法生成package-lock.json無法鎖定依賴內部迴圈(依賴模組的依賴)
- 預設如果使用
npm i -g
安裝,同樣不會生成package-lock.json,因此無法鎖定依賴內部迴圈。
2. 提升node_modules目錄(方案二)
結合之前問題分析的我們得出的結論,因此實際上我們把專案中devDependencies依賴重疊的模組,在專案的父目錄存放node_modules即可將依賴提升。即可以進行專案間的共享。
目前筆者的理想方案應該能夠達到下列幾個目的:
-
可以將專案構建相似技術棧的專案統一在一起,共享依賴
-
可以實現對公共模組的維護和管理,並實現增量安裝
-
對原來npm install流程改動不會太大
(1)結構變化後的依賴結構圖
實際需要實現只要將專案目錄改為以下結構
----|---wwwroot/ # 工作目錄或部署目錄
|
|---webpack/--|---webpack-react/---|---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
| | |
| |--- webpack-vue/--- |---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
| |---node_modules/ |
| |---package.json |
| |---package.json-lock|
| | |
|---parcel/---|--- parcel-react/---|---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
| | |
| |--- parcel-vue/ --- |---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
複製程式碼
情況一、情況二、情況三、情況四可以轉化成如下結構:
以情況四下為例子 - 如圖:
說說目前筆者探索的兩個實踐:
(2)手工管理package.json
下一步說起來也十分簡單:
實際只要在,維護一個package.json即可,實際常常只需要devDependencies,然後實際需要安裝時只需要進行一次npm install即可,並且會生成package-lock.json。
即上面說到的這幾個位置
|---webpack/--|---webpack-react/---|---package.json
| |--- webpack-vue/--- |---package.json
|---parcel/---|--- parcel-react/---|---package.json
| |--- parcel-vue/ --- |---package.json
複製程式碼
實際只要在devDependencies按正常package.json中的內容維護即可。
(3)利用 preinstall
“對原來npm install流程改動不會太大”,這一點上面對於手工管理方式實際並並不滿足。並且手工進行管理十分繁瑣,因此我們是否可以在npm install之前把devDependencies內重疊模組的node_modules進行提升呢?
答案是:可以的,主要利用的是npm script中的preinstall,關於npm script以及preinstall可以通過:docs.npmjs.com/misc/script…來了解。
npm的preinstall這個hook實際會在安裝軟體包之前執行。實際我們就可以通過preinstall去執行一些node.js的程式碼讓我們的devDependencies內重疊模組的node_modules提升。
那具體來看一下步驟:
a.在專案package.json中增加preinstall
"scripts": {
"preinstall": "node build/scripts/preinstall.js",
"start": "webpack-dev-server --mode development --hot --progress --colors --port 3000 --open"
}
複製程式碼
這裡我會在專案的build目錄下的scripts執行preinstall.js這個檔案內的程式碼
b.切換至專案父級目錄(即約定的技術棧目錄)
這裡我僅進行了一個最簡單的示範,其原理是在專案的package.json裡增加devDependenciesGlobal 項:
實際可以看一下我提供的實現具體程式碼
實際最為關鍵的為:
const execPath = path.resolve('../');
const preListObj = preList.devDependenciesGlobal; // 專案package.json自己維護的devDependencies那些重疊依賴
const currentMd5 = md5(JSON.stringify(preListObj)); // 簡單舉例生成一個md5,實際應用中可以搭配其他機制
...
fs.writeFileSync(`${execPath}/package.json`, JSON.stringify({"md5": currentMd5,dependencies: preListObj})); // 在上級目錄建立一個package.json檔案並,寫入md5以及devDependenciesGlobal的內容
let script = `cd ${execPath} && npm i`;
// 切換至父級目錄並執行npm install
exec(script, function (err, stdout, stderr) {
console.log(stdout);
if (err) {
console.log('error:' + stderr);
} else {
console.log(stdout);
console.log('package init success');
console.log(`The global install in ${execPath}`);
}
});
複製程式碼
c.實際所佔空間的變化:
(此處就不貼圖片了,大家可以clone例項自己試一下)
|---webpack/--|---webpack-react/---|---node_modules/ # 共21.1 MB,369項
| |--- webpack-vue/--- |---node_modules/ # 共17.8 MB, 336項
| |---node_modules/ # 共153.3 MB,994項
| |
|---parcel/---|--- parcel-react/---|---node_modules/ # 89.4 MB,540項
| |--- parcel-vue/ --- |---node_modules/ # 71.5 MB,491項
| |---node_modules/ # 共21.1 MB,118項
複製程式碼
實際非常簡單,即得執行目錄的上級目錄,此處為了舉例我的例子裡會在執行preinstall時根據devDependenciesGlobal生成一個md5,以便下次install時比對,若一致不執行preinstall內的流程。當然這並不是一個最佳實踐(之後的各種實踐方案在下篇中進一步給出)
當然實際這部分存在“無限”的可能性,可以根據自己的需求來完善(比如服務端獲取devDependenciesGlobal以及md5)。
這僅僅是一個簡單示範,實際可以通過配合一些服務端以及docker進一步提升。
存在問題:
(1) windows相容性
筆者所在的團隊在使用的過程中,有遇到windows的開發環境下的各種問題,比如:
- win10下自定義的preinstall流程中的install執行十分緩慢,或無法執行
- babel或丟擲一些錯誤(這裡不具體說明)
(2) 使用者無法做到無感知
也是最大的缺點,即使用的時候使用者會有感知,開發專案的時候要求使用一定是主動必須按特定的目錄結構來安排自己的workspace。
(3) 本地npm版本,不一致,也可能對依賴造成的影響較大
(4) npm script相關的必須在devDependencies引入 (當然也可以通過一些方式避開,比如dev-server不直接使用npm script啟動)
這僅僅是一個開始...待續
利用node模組載入機制,我們其實已經可以很大程度改進我們的依賴了,但如上所述我們還存在這些問題:
(1) windows相容性
上文有提到過windows下,筆者實際實踐發現會出現一些問題,是不是有辦法可以統一環境呢?
(2) 無法做到讓使用者到無感知
(3) 本地npm版本,對依賴造成的影響較大
(5) 管理公共依賴沒有標準化和自動化
因此下篇筆者可能會主要進一步通過其他方案來配合這個解決方案
(1) 本文舉例的完善版本 (通過shell遠端下載公共依賴的package.json,也是筆者目前團隊在使用的)
(2) 通過搭配docker進一步完善方案 (是筆者想進一步增強的)
(3) 搭配私有npm倉庫
(4) 如何更標準化的管理公共依賴,使其可以自動化標準化
這裡挖個坑下篇會更新這部分內容。
結語以及本文不足之處
經過上述操作,我們便可以將前端專案根據框架進行依賴管理以及劃分了,其實如此改進仍並非最完美以及優化,不足之處仍非常之多,期待更加優雅的解決方案,使我們前端專案的架構更佳健壯和靈活。也歡迎大家和我探討和討論,謝謝各位大佬能耐心看完。