在npm、yarn等包管理工具的幫助下,前端工程化模塊化的進程日益加快,項目的依賴包也日益增大,特別是若項目採用webpack來構建用到許多webpack的插件、一些輔助開發如(eslint、postcss、dev-server之類的庫)以及一些單元測試(jest、mocha、enzyme)可能須要用到的插件,項目中的node_module就會變的十分龐大。javascript
如圖:css
若是搭配這種狀況是否是很絕望:前端
所以團隊開發者每次從新初始化項目進行npm install會十分緩慢,而且若佔用大量空間,更進一步來講項目中每每不少前端團隊會用到一臺構建服務器,對前端項目的代碼獲取、打包、構建、部署進行管理(例如筆者本身所在的團隊就在使用jenkins對前端項目的打包、構建、部署進行自動化託管)。npm這樣的項目級依賴管理的特性就會形成大量的時間以及資源的消耗。vue
同時筆者也想在文章開始以前代表一個態度,npm自己portable的模塊化設計,使每一個項目互不幹系,也是一種它設計的精華所在,本文僅是針對實際使用中遇到的一些小困擾進行解讀,但願提供一個新的思路。java
補充一下,實際yarn已經有了解決方案 - workspaces:node
Workspaces in Yarnreact
本文暫不把yarn容納到討論範圍內(實際思路很類似,筆者也是寫完以後才發現的)。webpack
此本篇文章就在這樣的背景下誕生了。純粹是筆者在項目中積累的一些經驗,如有不足望指出。git
github倉庫地址:github.com/Roxyhuang/n…github
一般現代前端的標準工程每每具有如下功能,爲了讓這個討論更貼近實際開發,而且更直觀,所以規定示例項目基本具有如下這些能力:
(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中紅色標註的react-redux,redux版本以及使用了mobx react-mobx更換
本地開發和測試所需的模塊容量與文件數(devDependencies):
無變化
實際這種狀況和狀況一相似只有dependencies中部分模塊的依賴有差別或者版本不一樣。固然再進一步,這個部分每一個項目確實可能出現較大的差別,同時着部分差別可能會引起devDependencies也出現變化(如:React Vue 可能用到webpack Loader eslint plugins等,以後會說起這塊)。
實際依然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個依賴重疊)。
這種是目前最複雜的狀況,存在下列幾個狀況
(1) 前端構件方案存在多個, 例子中爲webpack 和 parcel
(2) 生產中所需的模塊存在差別,甚至技術棧不一樣
(3) 本地開發和測試所需的包依賴重複,但有略微不一樣,且存在版本不一樣
如圖:
這種狀況咱們也囊括了狀況二的差別,能夠看到同一個構件工具下存在本地開發和測試所需的包依賴重疊的狀況依然是較多的。
所以實際若是出現這種狀況,咱們仍須要考慮以及解決。
結合狀況一、狀況二、狀況三、狀況四的例子能夠得出幾點小小的「規律」:
(1) devDependencies不管從模塊所佔體積仍是文件數量都在總體依賴模塊中所佔的比較較大
(2) dependencies下的包,可能會由於項目不一樣差別較大,同時也會致使devDependencies下模塊有差別
(3) 即便devDependencies可能項目存在差別,可是仍有可能一部分重疊
(4) 多個模塊化方案,可能會致使項目devDependencies差別較大
咱們能夠發現其中無論前三種狀況的哪種devDependencies都會有一部分的依賴模塊是重疊的。那既然存在重複,那咱們就有必要考慮「複用」
同時咱們發現多個模塊化方案(webpack, parcel)devDependencies下的模塊差別會較大,這也是咱們以後須要解決的。
非本文重點,僅簡述一下:
這個問題社區探討的老問題,鎖不鎖都有支持的一方,然而這邊筆者不代表態度,可是同時也但願咱們以後的依賴管理的優化和「瘦身」可以具有鎖定依賴的能力。
使用語義版本控制來鎖定版本:
關於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中引入模塊,須要經歷以下3個步驟:
咱們利用的則是在路徑分析中自定義文件模塊(第三方npm包)的查找:
自定義文件模塊查找順序爲:
有點相似於JS的原型鏈查找,文件路徑越深,模塊查找越耗時,同時也是它慢的緣由。
固然這是默認狀況下,實際通暢咱們能夠經過配置NODE_PATH的方式,在遞歸至根目錄後,若依然沒法找到,給到一個路徑或多個路徑找到具體模塊。
方案一實際就是利用了配置NODE_PATH,一般通常會將其配製成npm i -g
所在的全局模塊目錄(實際能夠改,本例暫定這個目錄)。
狀況1、狀況2、狀況三均可以轉化成以下結構:
以狀況三下爲例子 - 如圖:
在對應系統的環境變量配置文件中增長環境變量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,所以沒法鎖定依賴內部循環。結合以前問題分析的咱們得出的結論,所以實際上咱們把項目中devDependencies依賴重疊的模塊,在項目的父目錄存放node_modules便可將依賴提高。便可以進行項目間的共享。
目前筆者的理想方案應該可以達到下列幾個目的:
能夠將項目構建類似技術棧的項目統一在一塊兒,共享依賴
能夠實現對公共模塊的維護和管理,並實現增量安裝
對原來npm install流程改動不會太大
實際須要實現只要將項目目錄改成如下結構
----|---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/
複製代碼
狀況1、狀況2、狀況3、狀況四能夠轉化成以下結構:
以狀況四下爲例子 - 如圖:
說說目前筆者探索的兩個實踐:
下一步提及來也十分簡單:
實際只要在,維護一個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中的內容維護便可。
「對原來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提高。
那具體來看一下步驟:
"scripts": {
"preinstall": "node build/scripts/preinstall.js",
"start": "webpack-dev-server --mode development --hot --progress --colors --port 3000 --open"
}
複製代碼
這裏我會在項目的build目錄下的scripts執行preinstall.js這個文件內的代碼
這裏我僅進行了一個最簡單的示範,其原理是在項目的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}`);
}
});
複製代碼
(此處就不貼圖片了,你們能夠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的開發環境下的各類問題,好比:
(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) 如何更標準化的管理公共依賴,使其能夠自動化標準化
這裏挖個坑下篇會更新這部份內容。
通過上述操做,咱們即可以將前端項目根據框架進行依賴管理以及劃分了,其實如此改進仍並不是最完美以及優化,不足之處仍很是之多,期待更加優雅的解決方案,使咱們前端項目的架構更佳健壯和靈活。也歡迎你們和我探討和討論,謝謝各位大佬能耐心看完。