28.前端工程化背后的項(xiàng)目組織設(shè)計(jì)(下)

承接上一節(jié)的內(nèi)容构拳,本節(jié)來繼續(xù)學(xué)習(xí)前端工程化中依賴關(guān)系相關(guān)的內(nèi)容。在此之前侨颈,先回顧一下「項(xiàng)目組織」主題的知識點(diǎn):


說到項(xiàng)目中的依賴關(guān)系署穗,我們往往會想到使用 yarn/npm 解決依賴問題。依賴關(guān)系大體上可以分為:

  • 嵌套依賴
  • 扁平依賴

項(xiàng)目中趁舀,我們引用了三個包:PackageA赖捌、PackageB、PackageC矮烹, 它們都依賴了 PackageD 的不同版本越庇。那么在安裝時,如果 PackageA奉狈、PackageB卤唉、PackageC 在各自的 node_modules 目錄中分別含有 PackageD,那么我們將其理解為嵌套依賴:

PackageA
   node_modules/PackageD@v1.1
PackageB
   node_modules/PackageD@v1.2
PackageC
   node_modules/PackageD@v1.3

如果在安裝時仁期,先安裝了 PackageA桑驱,那么 PackageA 依賴的 PackageD 版本成為主版本竭恬,它和 PackageA、PackageB熬的、PackageC 一起平級出現(xiàn)痊硕,我們認(rèn)為這是扁平依賴。此時 PackageB押框、PackageC 各自的 node_modules 目錄中也含有各自的 PackageD 版本:

PackageA
PackageD@v1.1
PackageB
   node_modules/PackageD@v1.2
PackageC
   node_modules/PackageD@v1.3

npm 在安裝依賴包時岔绸,會將依賴包下載到當(dāng)前的 node_modules 目錄中。對于嵌套依賴和扁平依賴的話題强戴,npm 給出了不同的處理方案:npm3 以下版本在依賴安裝時亭螟,非常直接挡鞍,它會按照包依賴的樹形結(jié)構(gòu)下載到本地 node_modules 目錄中骑歹,也就是說,每個包都會將該包的依賴放到當(dāng)前包所在的 node_modules 目錄中墨微。

這么做的原因可以理解:它考慮到了包依賴的版本錯綜復(fù)雜的問題道媚,同一個包因?yàn)楸灰蕾嚨年P(guān)系原因會出現(xiàn)多個版本,保證樹形結(jié)構(gòu)的安裝能夠簡化和統(tǒng)一對于包的安裝和刪除行為翘县。這樣能夠簡單地解決多版本兼容問題最域,可是也帶來了較大的冗余。

npm3 則采用了扁平結(jié)構(gòu)锈麸,但是更加智能镀脂。在安裝時,按照 package.json 里聲明的順序依次安裝包忘伞,遇到新的包就把它放在第一級 node_modules 目錄薄翅。后面再進(jìn)行安裝時,如果遇到一級 node_modules 目錄已經(jīng)存在的包氓奈,那么會先判斷包版本翘魄,如果版本一樣則跳過安裝,否則會按照 npm2 的方式安裝在樹形目錄結(jié)構(gòu)下舀奶。

npm3 這種安裝方式只能夠部分解決問題暑竟,比如:項(xiàng)目里依賴模塊 PackageA、PackageB育勺、PackageC但荤、PackageD, 其中 PackageC、PackageB 依賴模塊 PackageD v2.0涧至,A 依賴模塊 PackageD v1.0腹躁。那么可能在安裝時,先安裝了 PackageD v1.0化借,然后分別在 PackageC潜慎、PackageB 樹形結(jié)構(gòu)內(nèi)部分別安裝 PackageD v2.0。這也是一定程度的冗余。為了解決這個問題铐炫,因此也就有了 npm dedupe 命令垒手。

npm 和 yarn 的內(nèi)容足以單獨(dú)開講,我們這里不再展開倒信。

另外科贬,為了保證同一個項(xiàng)目中不同團(tuán)隊(duì)成員安裝的版本依賴相同,我們往往使用 package-lock.json 或 yarn-lock.json 這類文件通過 git 上傳以共享鳖悠。在安裝依賴時榜掌,依賴版本將會鎖定。

這些內(nèi)容與開發(fā)息息相關(guān)乘综,但是往往被開發(fā)者所忽視憎账。依賴問題說小很小,說復(fù)雜卻也很復(fù)雜卡辰,我們再來看一個循環(huán)依賴的問題胞皱。

復(fù)雜依賴關(guān)系分析和處理

前端項(xiàng)目,安裝依賴非常簡單:

npm install / yarn add

安裝一時爽九妈,而帶來的依賴關(guān)系慢慢地會讓人頭大反砌。依賴關(guān)系的復(fù)雜性帶來的主要副作用有就是循環(huán)依賴。

這里我們來重點(diǎn)說一下萌朱。簡單來說宴树,循環(huán)依賴就是模塊 A 和模塊 B 相互引用,在不同的模塊化規(guī)范下晶疼,對于循環(huán)依賴的處理不盡相同酒贬。

Node.js 中,我們制造一個簡單的循環(huán)引用場景冒晰。

模塊 A:

exports.loaded = false
const b = require('./b')
module.exports = {
   bWasLoaded: b.loaded,
   loaded: true
}

模塊 B:

exports.loaded = false
const a = require('./a')
module.exports = {
   aWasLoaded: a.loaded,
   loaded: true
}

在 index.js 中調(diào)用:

const a = require('./a');
const b = require('./b')
console.log(a)
console.log(b)

這種情況下揪罕,并未出現(xiàn)死循環(huán)崩潰的現(xiàn)象宪彩,而是輸出:

{ bWasLoaded: true, loaded: true }
{ aWasLoaded: false, loaded: true }

因是模塊加載過程的緩存機(jī)制:Node.js 對模塊加載進(jìn)行了緩存少辣。按照執(zhí)行順序厂抽,第一次加載 a 時,走到 const b = require('./b')蒋情,這樣直接進(jìn)入模塊 B 當(dāng)中埠况,此時模塊 B 中 const a = require('./a'),模塊 A 已經(jīng)被緩存棵癣,因此模塊 B 返回的結(jié)果為:

{
   aWasLoaded: false,
   loaded: true
}

模塊 B 加載完成辕翰,回到模塊 A 中繼續(xù)執(zhí)行,模塊 A 返回的結(jié)果為:

{
   aWasLoaded: true,
   loaded: true
}

據(jù)此分析狈谊,我們不難理解最終的打印結(jié)果喜命。也可以總結(jié)為:

Node.js沟沙,或者 CommonJS 規(guī)范,得益于其緩存機(jī)制壁榕,在遇見循環(huán)引用時矛紫,程序并不會崩潰。但這樣的機(jī)制牌里,仍然會有問題:它只會輸出已執(zhí)行部分颊咬,對于未執(zhí)行部分,export 內(nèi)容為 undefined牡辽。

ES 模塊化與 CommonJS 規(guī)范不同喳篇,ES 模塊不存在緩存機(jī)制,而是動態(tài)引用依賴的模塊态辛。

《Exploring ES6》 一文中的示例很好地闡明了這樣的行為:

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
   bar(); // (ii)
}

//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
   if (Math.random()) {
       foo(); // (iv)
   }
}

這樣的代碼麸澜,如果在 commonJS 規(guī)范中:

//------ a.js ------
var b = require('b');
function foo() {
   b.bar();
}
exports.foo = foo;

//------ b.js ------
var a = require('a');
function bar() {
   if (Math.random()) {
       a.foo();
   }
}
exports.bar = bar;

如果模塊 a.js 先被執(zhí)行,a.js 依賴 b.js因妙,在 b.js 中痰憎,因?yàn)?a.js 此刻還并沒有暴漏出任何內(nèi)容,因此如果在 b.js 中攀涵,對于頂層 a.foo() 的調(diào)用,會得到報(bào)錯洽沟。但是如果 a.js 模塊執(zhí)行完畢后以故,再調(diào)用 b.bar(),b.bar() 當(dāng)中的 a.foo() 可以正常運(yùn)行裆操。

但是這樣的方式的局限性:

如果 a.js 采用 module.exports = function () { ··· } 的方式怒详,那么 b.js 當(dāng)中的 a 變量在賦值之后不會二次更新。

ESM 不會存在這樣的局限性踪区。ESM 加載的變量昆烁,都是動態(tài)引用其所在的模塊。只要引用是存在的缎岗,代碼就能執(zhí)行静尼。回到:

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
   bar(); // (ii)
}

//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
   if (Math.random()) {
       foo(); // (iv)
   }
}

代碼传泊,第 ii 行和第 iv 行鼠渺,bar 和 foo 都指向原始模塊數(shù)據(jù)的引用。ESM 的設(shè)計(jì)目的之一就是支持循環(huán)引用眷细。

ES 的設(shè)計(jì)思想是:盡量靜態(tài)化拦盹,這樣在編譯時就能確定模塊之間的依賴關(guān)系。這也是 import 命令一定要出現(xiàn)在模塊開頭部分的原因溪椎。在模塊中普舆,import 實(shí)際上不會直接執(zhí)行模塊恬口,而是只生成一個引用。在模塊內(nèi)真正引用依賴邏輯時沼侣,再到模塊里取值楷兽。這樣的設(shè)計(jì)非常有利于 tree shaking 技術(shù)的實(shí)現(xiàn),我們在《深入淺出模塊化相關(guān)話題(含 tree shaking)》課程中繼續(xù)展開华临。

在工程實(shí)踐中芯杀,循環(huán)引用的出現(xiàn)往往是由設(shè)計(jì)不合理造成的。如果使用 webpack 進(jìn)行項(xiàng)目構(gòu)建雅潭,可以使用 webpack 插件 circular-dependency-plugin 來幫助檢測項(xiàng)目中存在的所有循環(huán)依賴揭厚。循環(huán)依賴這個問題說大不大,說小不小扶供,我們應(yīng)該盡可能在設(shè)計(jì)源頭規(guī)避筛圆。

另外復(fù)雜的依賴關(guān)系還會帶來以下等問題:

  • 依賴版本不一致
  • 依賴丟失
    對此,需要開發(fā)者根據(jù)真實(shí)情況進(jìn)行處理椿浓,同時太援,合理使用 npm/yarn 工具,也能起到非常關(guān)鍵的作用扳碍。

筆者團(tuán)隊(duì)中通過:

"scripts": {
   // ...
   "analyzeDeps": "scripts analyzeDeps",
   "graph": "scripts graph",
   // ...
}

yarn run analyzeDeps

來對依賴進(jìn)行分析提岔。具體流程是 analyzeDeps 腳本會對依賴版本沖突和依賴丟失的情況進(jìn)行處理,這個過程依賴 missingDepsAnalyze 和 versionConflictsAnalyze 兩個任務(wù):

其中 missingDepsAnalyze 依賴 depcheck笋敞,depcheck 可以找出哪些依賴是沒有用到的碱蒙,或者對比 package.json 聲明中缺少的依賴項(xiàng)。

同時 missingDepsAnalyze 會讀取 lerna.json 配置夯巷,獲得項(xiàng)目中所有 package赛惩,接著對所有 package 中的 package.json 進(jìn)行遍歷,檢查是否存在相關(guān)依賴趁餐,如果不存在則自動執(zhí)行 yarn add XXXX 進(jìn)行安裝喷兼。

versionConflictsAnalyze 任務(wù)類似,只不過在獲得每個 package 的 package.json 中定義的依賴之后后雷,檢查同一個依賴是否有重復(fù)聲明且存在版本不一致的情況季惯。對于版本沖突,采用交互式命令行喷面,讓開發(fā)者選擇正確的版本星瘾。
相關(guān)代碼并不難實(shí)現(xiàn),感興趣的讀者可以在評論區(qū)交流或者向我提問惧辈,出于隱私原因琳状,這里不再貼出。

使用 yarn workspace 管理依賴關(guān)系

monorepo 項(xiàng)目中依賴管理問題值得重視『谐荩現(xiàn)在我們來看一下非常流行的 yarn workspace 如何處理這種問題念逞。

workspace 的定位為:

It allows you to setup multiple packages in such a way that you only need to run yarn install once to install all of them in a single pass.

翻譯過來困食,workspace 能幫助你更好地管理有多個子 package 的 monorepo。開發(fā)者既可以在每個子 package 下使用獨(dú)立的 package.json 管理依賴翎承,又可以享受一條 yarn 命令安裝或者升級所有依賴的便利硕盹。

引入 workspace 之后,在根目錄執(zhí)行:

yarn install / yarn updrade XX

所有的依賴都會被安裝或者更新叨咖。

當(dāng)然瘩例,如果只想更新某一個包內(nèi)的版本,可以通過以下代碼完成:

yarn workspace upgrade XX

在使用 yarn 的項(xiàng)目中甸各,如果想使用 yarn workspace垛贤,我們不需要安裝其他的包,只要簡單更改 package.json 便可以工作:

// package.json
{
 "private": true,
 "workspaces": ["workspace-1", "workspace-2"]
}

需要注意的是趣倾,如果需要啟用 workspace聘惦,那么這里的 private 字段必須設(shè)置成 true。 同時 workspaces 這個字段值對應(yīng)一個數(shù)組儒恋,數(shù)組每一項(xiàng)是個字符串善绎,表示一個 workspace(可以理解為一個 repo)。

接著诫尽,我們可以在 workspace-1 和 workspace-2 項(xiàng)目中分別添加 package.json 內(nèi)容:

{
 "name": "workspace-1",
 "version": "1.0.0",

 "dependencies": {
   "react": "16.2.3"
 }
}

以及:

{
 "name": "workspace-2",
 "version": "1.0.0",

 "dependencies": {
   "react": "16.2.3",
   "workspace-1": "1.0.0"
 }
}

執(zhí)行 yarn install 之后禀酱,發(fā)現(xiàn)項(xiàng)目根目錄下的 node_modules 內(nèi)已經(jīng)包含所有聲明的依賴,且各個子 package 的 node_modules 里面不會重復(fù)存在依賴箱锐,只會有針對根目錄下 node_modules 中的 React 引用比勉。

我們發(fā)現(xiàn),yarn workspace 跟 Lerna 有很多共同之處驹止,解決的問題也部分重疊。下面我們對比一下** workspace **和 Lerna观蜗。

  • yarn workspace 寄存于 yarn臊恋,不需要開發(fā)者額外安裝工具,同時它的使用也非常簡單墓捻,只需要在 package.json 中進(jìn)行相關(guān)的配置抖仅,不像 Learn 那樣提供了大量 API
  • yarn workspace 只能在根目錄中引入,不需要在各個子項(xiàng)目中引入

事實(shí)上砖第,Lerna 可以與 workspace 共存撤卢,搭配使用能夠發(fā)揮更大作用。在我們團(tuán)隊(duì)中:Lerna 負(fù)責(zé)版本管理與發(fā)布梧兼,依靠其強(qiáng)大的 API 和設(shè)置放吩,做到靈活細(xì)致;workspace 負(fù)責(zé)依賴管理羽杰,整個流程非常清晰渡紫。

在 Lerna 中使用 workspace到推,首先需要修改 lerna.json 中的設(shè)置:

{
   ...
   "npmClient": "yarn",
   "useWorkspaces": true,
   ...
}

然后將根目錄下的 package.json 中的 workspaces 字段設(shè)置為 Lerna 標(biāo)準(zhǔn) packages 目錄:

{
 ...
 "private": true,
 "workspaces": [
   "packages/*"
 ],
 ...
}

注意:如果我們開啟了 workspace 功能,lerna.json 中的 packages 值便不再生效惕澎。原因是 Lerna 會將 package.json 中 workspaces 中所設(shè)置的 workspaces 數(shù)組作為 lerna packages 的路徑莉测,也就是各個子 repo 的路徑。換句話說唧喉,Lerna 會優(yōu)先使用 package.json 中的 workspaces 字段捣卤,在不存在該字段的情況下,再使用 lerna.json 中的 packages 字段八孝。如果未開啟 workspace 功能董朝,lerna.json 配置為:

{
 "npmClient": "yarn",
 "useWorkspaces": false,
 "packages": [
   "packages/11/*",
   "packages/12/*"
 ]
}

根目錄下的 package.json 配置為:

{
   "private": true,
   "workspaces": [
     "packages/21/*",
     "packages/22/*",
   ],
     ...
}

那么這就意味著使用 yarn 管理的是 package.json 中 workspaces 所對應(yīng)的項(xiàng)目路徑下的依賴:packages/21/* 以及 packages/22/。而 Leran 管理的是 lerna.json 中 packages 所對應(yīng)的 packages/11/ 以及 packages/12/* 的項(xiàng)目唆阿。

總結(jié)

本節(jié)主要拋出了大型前端項(xiàng)目的組織選型問題益涧,著重分析了 monorepo 方案,內(nèi)容注重實(shí)戰(zhàn)驯鳖。對于大型代碼庫的組織闲询,本節(jié)梳理出一條完善的工作流程。找到適合自己團(tuán)隊(duì)的風(fēng)格浅辙,是一名合格的開發(fā)者所需要具備的技能扭弧。

但是關(guān)于 npm 和 yarn 以及所牽扯出的依賴問題、monorepo 設(shè)計(jì)問題仍然將是挑戰(zhàn)记舆,其中的話題仍然值得深挖和系統(tǒng)展開鸽捻。具體工程化項(xiàng)目的代碼組織選型和設(shè)計(jì),開發(fā)者一定要通過動手來理解泽腮。在此學(xué)習(xí)過程中御蒲,有任何疑問和想法,都?xì)g迎與我交流诊赊,也希望能有更多機(jī)會和大家交流厚满。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市碧磅,隨后出現(xiàn)的幾起案子碘箍,更是在濱河造成了極大的恐慌,老刑警劉巖鲸郊,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丰榴,死亡現(xiàn)場離奇詭異,居然都是意外死亡秆撮,警方通過查閱死者的電腦和手機(jī)四濒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人峻黍,你說我怎么就攤上這事复隆。” “怎么了姆涩?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵挽拂,是天一觀的道長。 經(jīng)常有香客問我骨饿,道長亏栈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任宏赘,我火速辦了婚禮绒北,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘察署。我一直安慰自己闷游,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布贴汪。 她就那樣靜靜地躺著脐往,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扳埂。 梳的紋絲不亂的頭發(fā)上业簿,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機(jī)與錄音阳懂,去河邊找鬼梅尤。 笑死,一個胖子當(dāng)著我的面吹牛岩调,可吹牛的內(nèi)容都是我干的巷燥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼号枕,長吁一口氣:“原來是場噩夢啊……” “哼矾湃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起堕澄,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霉咨,沒想到半個月后蛙紫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡途戒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年坑傅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喷斋。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡唁毒,死狀恐怖蒜茴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浆西,我是刑警寧澤粉私,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站近零,受9級特大地震影響诺核,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜久信,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一窖杀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裙士,春花似錦入客、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至酥诽,卻和暖如春鞍泉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肮帐。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工咖驮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人训枢。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓托修,卻偏偏與公主長得像,于是被迫代替她去往敵國和親恒界。 傳聞我的和親對象是個殘疾皇子睦刃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容