-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path1-6-9.html
698 lines (687 loc) · 30.5 KB
/
1-6-9.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="./public/favicon.ico" />
<meta http-equiv="cache-control" content="no-cache" />
<title></title>
<link rel="stylesheet" href=" https://necolas.github.io/normalize.css/8.0.1/normalize.css" />
<link rel="stylesheet" href="./hightlight/default.min.css" />
<link rel="stylesheet" href="./css/main.css" />
<link rel="stylesheet" href="./css/copybutton.css" />
<link rel="stylesheet" href="./css/hightlight.css" />
<script src="./hightlight/hightlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEVZJDBC7Z"></script>
<script src="./js/gtag.js"></script>
</head>
<body>
<header>
<nav>
<h1>
<span id="toggle-menu"></span>
<a href="index.html"></a>
</h1>
</nav>
</header>
<main>
<aside>
<nav></nav>
</aside>
<article>
<h2 id="1-6-9">1-6-9 前端專案化背後的專案組織設計</h2>
<h3>大型前端專案的組織設計</h3>
<p>
隨著業務複雜度的直線上升,前端專案不管是從程式量上,還是從依賴關係上都呈爆炸式增長。同時,由於團隊中一般不只有一個業務專案,所以「多個專案之間如何配合」、「如何維護相互關係」、「公司自己的公共函數庫版本如何管理」這些問題隨著業務擴充紛紛浮出水面。一名合格的進階前端工程師,必需能在巨觀上妥善處理這些問題。
</p>
<p>
當然,不是每個開發者都有機會接觸專案設計。如果讀者沒有面對過上述問題,也許並不容易了解這些問題究竟表示什麼。舉個實例,團隊主業務專案名稱為
App-project ,這個倉庫依賴了元件函數庫 Component-lib,因此 App-Project 專案的 package.json
檔案中會有類似下面的程式。
</p>
<pre><code class="language-js">
{
"name": "App-project",
"version": "1.0.0",
"description": "This is our main app project",
"scripts": {
"test": "echo \\"Error: no test specified\\" && exit 1"
},
"main": "index.js",
"dependencies":{
"Component-lib":"^1.0.0"
}
}
</code></pre>
<p>
針對以上情況,產品經理提出要更改 Component-lib 元件函數庫中的 modal
元件樣式及互動行為,那麼作為開發者,我們需要切換到 Component.lib
專案,進行相關需求開發,開發完畢後進行測試。這裡的測試包含 Component.lib
中的單元測試,當然也包含在實際專案中進行的效果驗收。為了方便偵錯,有經驗的開發者也許會使用
npm link/yarn link 來開發和偵錯效果。當確認一切沒問題後,我們還需要發佈 Component-lib
專案的新版本,並將 App-project 專案中的 Component-lib 版本提升為
1.0.1。所有這些都順利完成後,才能在 App-project 專案中進行升級。
</p>
<pre><code class="language-js">
{
// ...
"dependencies": {
"Component-lib": "^1.0.1"
}
}
</code></pre>
<p>
這個過程已經比較複雜了。中間環節出現任何紕漏都要重複上述所有步驟。另外,這裡只存在單一依賴關係,現實中的
App-project 不可能只依賴
Component-lib。這種專案管理的方式無疑是低效且痛苦的。那麼在專案設計哲學上,有更好的方式嗎?答案是一定的。下面就對管理組織程式的兩種主要方式(monorepo
和 multirepo)説明。
</p>
<h4>monorepo 和 multirepo</h4>
<p>
multirepo,顧名思義,就是將應用按照模組分別在不同的倉庫中進行管理,即上述 App-project 和
Component-lib 專案的管理模式;而 monorepo
就是將應用中所有的模組一股腦兒全部放在同一個專案中,這樣自然就完全避開了前文描述的困擾,不需要單獨發送包裝、測試,且所有程式都在一個專案中管理,一同部署上線,能夠在開發階段更早地複現
bug,曝露問題。
</p>
<p>
這就是專案釋式在組線上的不同哲學:一種宣導分而治之,一種宣導集中管理。究竟是把雞蛋放在同一個籃子裡,還是宣導多元化,這就要根據團隊的風格及面臨的實際場景進行選型了。
</p>
<p>下面試著從 multirepo 和 monorepo 兩種處理方式的弊端說起,multirepo 存在以下問題。</p>
<ul>
<li>開發偵錯及版本更新效率不佳。</li>
<li>
團隊技術選型分散,不同函數庫的實現風格可能存在較大差異(例如有的函數庫依賴 Vue,有的依賴
React)。
</li>
<li>changelog 整理困難,Issues 管理混亂(對開放原始碼函數庫來說)。</li>
</ul>
<p>而 monorepo 缺點也非常明顯,實際如下。</p>
<ul>
<li>庫體積超大,目錄結構複雜度上升。</li>
<li>需要使用維護 monorepo 的工具,這就表示學習成本比較高。</li>
</ul>
<p>清楚了不同專案組織管理的缺點,再來看一下社區上的經典選型案例。</p>
<p>
Babel 和 React 都是典型的 monorepo,其 Issues 和 Pull requests
都集中在唯一的專案中,changelog 可以簡單地從一份 commits 列表中整理出來。先來看一下 React
專案倉庫,從如下所示的目錄結構中即可看出其強烈的 monorepo 風格。
</p>
<pre><code class="language-js">
react-16.2.0/
packages/
react/
react-art/
react-.../
</code></pre>
<p>
因此:react和 react-dom 在 npm 上是兩個不同的函數庫,它們只不過是在 React 專案中透過
monorepo 的方式進行管理的。至於為什麼 react 和 react-dom
是兩個套件,讀者可以自行思考一下。
</p>
<h3>使用 Lerna 實現 monorepo</h3>
<p>
Lerna 是Babel 管理本身專案的開放原始碼工具:這網對 Lerna 的定位非常商單直接:A tool for
managing JavaScript projects with multiple
packages.(Lerna是一個管理多套件共存問題的JavaScript 專案工具。)
</p>
<p>來建立一個簡單的 demo。首先安裝依賴,並建立專案,程式如下。</p>
<pre><code class="language-bash">
mkir new-monorepo & cd new-monorepo
npm init -Y
#有需要的話要執行sudo
npm i -g lerna
git init new-monorepo
lerna init
</code></pre>
<p>建立成功後,Lerna 會在 new-monorepo 專案下自動增加以下3個檔案。</p>
<ul>
<li>packages</li>
<li>lerna.json</li>
<li>package.json</li>
</ul>
<p>透過以下程式增加第一個專案 module-1。</p>
<pre><code class="language-js">
cd packages
mkdir module-1
cd module-1
npm init -y
</code></pre>
<p>此時,可以自行觀察 new-monorepo 專案下的目錄結構,如下所示。</p>
<pre><code class="language-js">
packages/
module-1/
package.json
module-2/
package.json
module-3/
package.json
</code></pre>
<p>透過以下程式增加第一個專案 module-1。</p>
<pre><code class="language-bash">
cd packages
mkdir module-1
cd module-1
npm init -y
</code></pre>
<p>
這樣便在 ./packages 目錄下新增了第一個專案 module-1,並在 module-1
中增加了一些依賴,使模擬的場景更加真實。接下來,用同樣的方式建立 module-2 及 module-3 。
</p>
<p>此時,可以自行觀察 new-monorepo 專案下的目錄結構,如下所示。</p>
<pre><code class="language-bash">
packages/
module-1/
package.json
module-2/
package.json
module-3/
package.json
</code></pre>
<p>然後,退到根目錄下安裝依賴。</p>
<pre><code class="language-bash">
cd..
lerna bootstrap
</code></pre>
<p>
關於該指令的作用,官網上的直述為:Bootstrap the packages in the current Lerna repo.
Installs all of their dependencies and links any cross-dependencies.
</p>
<p>
也就是說,假設我們在 module-1 專案中增加了依賴 module-2,那麼執行 lemna bootstrap
指令後,就會在 module-1 專案的 node_modules 目錄下建立軟連結直接指向 module-2
目錄。也就是說,lerna bootstrap
指令會建立整個專案內子應用模組之間的依賴關係,這種建立方式不是透過硬安裝,而是透過軟連結指向相關依賴的。
</p>
<p>在正確連接了 Git 遠端倉庫後,就可以透過以下指令來發佈專案了。</p>
<pre><code class="language-bash">
lerna publish
</code></pre>
<p>
這行指令可以將各個 package 一步步發佈到 npm 中。Lerna 還可以支援自動產生 changelog
等功能。
</p>
<p>至此,你可能覺得 Lerna 還挺簡單。但其實裡面還有更多學問,如 Lerna 支援下面兩種模式。</p>
<ul>
<li>1.Fixed/Locked 模式</li>
</ul>
<p>
Babel 便採用了這樣的模式。這個模式的特點是,開發者執行 Lerna publish 指令後, Lerna 會在
lerna.json
中找到指定的版本編號。如果這一次發佈包含某個專案的更新,那麼就會自動更新版本編號。對於各個專案相連結的場景。
</p>
<ul>
<li>2.Independent 模式</li>
</ul>
<p>
下同於 Fixed/Locked 模式,在 Independent
模式下,各個專案相互獨立。開發首總要獨立管理多個套件(package)的版本更新。也就是說,我們可以實際到更新每個套件的版本。每次發佈時,Lerna
都會配合 Git 檢查相關套件權案的變動,只發佈有改動的套件。
</p>
<p>開發者可以根據團隊需求進行模式選擇。</p>
<p>我們也可以使用 Lerna 安裝依賴,該指令可以在專案下的任何資料夾中執行。</p>
<pre><code class="language-bash">
lerna add dependencyName
</code></pre>
<p>Lerna 預設支援 hoist 選項,即預設在 lerna.json 檔案中有以下設定內容。</p>
<pre><code class="language-js">
{bootstrap: { hoist: true }}
</code></pre>
<p>
這樣一來,專案中所有套件下的 package.json 檔案中都會出現 dependencyName
套件宣告敘述,內容如下。
</p>
<pre><code class="language-js">
packages/
module-1/
package.json(+ dependencyName)
node_modules
module-2/
package.json(+ dependencyName)
node_modules
module-3/
package.json(+ dependencyName)
node_modules
node_modules
dependencyName
</code></pre>
<p>
這種方式會在父資料夾的 node_modules 中高效安裝 dependencyName 依賴 (Node.js
會向上在祖先資料夾中尋找依賴) 。對於未開啟 hoist 的情況,執行 lerna add
後需要執行以下指令。
</p>
<pre><code class="language-bash">
lerna bootstrap --hoist
</code></pre>
<p>
如果想有選擇地升級某個依賴,例如只想為 module-1 升級 dependencyName 依賴的版本,則可以使用
scope 參數,如下所示。
</p>
<pre><code class="language-bash">
lerna add dependencyName --scope-module-1
</code></pre>
<p>
這時:module-1 資料夾下會有一個 node_modules 檔案,其中包含了 dependencyName
依賴的最新版本。
</p>
<h3>分析一個專案遷移案例</h3>
<p>
接下來會選取一個正在線上執行的 multirepo 專案,來示範使用 Lerna 將其選移到 monorepo
的過程。
</p>
<h4>背景介紹</h4>
<p>
該專案使用 TypeScript 和 Rollup 工具進行開發,並使用 TypeDoc 產生標準化文件。在使用 Lerna
進行 monorepo
化之前,這樣的技術方案帶來的困擾顯而易見,下面就來分析一下目前技術堆疊的弊端,以及
monorepo化能為這些專案帶來哪些好處。
</p>
<ul>
<li>
如果 @mitter-io/core 中出現任何一處改動,其他所有的套件就都需要升級到 @mitter-io/core
最新版本,不管這些改動是為了發佈新特性還是為了修復問題,所要花費的成本都比較大。
</li>
<li>如果這些套件都能共同分享版本,那麼帶來的收益也是非常大的。</li>
<li>
對這些不同的倉庫來說,由於技術堆疊相似,一些建置指令稿大致相同,部署流程也都一致,所以如果能夠將這些指令稿統一抽象,也將帶來便利。
</li>
</ul>
<h4>遷移步驟</h4>
<p>使用 Lerna 建置 monorepo 專案,指令如下。</p>
<pre><code class="language-bash">
mkdir my-new-monorepo & cd my-new-monorepo
git init .
lerna init
</code></pre>
<p>
不同於之前的範例,這次是在新的專案 my-new-monorepo 中匯入已有專案 my-single-repo-package-1
並完成 my-new-monorepo 的 monorepo 化設計,因此可以使用以下指令。
</p>
<pre><code class="language-bash">
lerna import ~/projects/my-single-repo-package-1 --flatten
</code></pre>
<p>
這行指令不僅可以匯入專案,同時會將已有專案中的 commit 記錄一併搬遷過來。可以放心地在新
monorepo 倉庫中使用 git blame 指令來進行回溯。
</p>
<p>如此一來,便可以獲得如下所示的專案結構。</p>
<pre><code class="language-js">
packages/
core/
models/
node/
react-native/
web/
lerna.json
package.json
</code></pre>
<p>接下來,執行以下程式進行依賴維護和發佈。</p>
<pre><code class="language-bash">
lerna bootstrap
lerna publish
</code></pre>
<p>
注意,並不是每次都需要執行 lerna
bootstrap,在第一次切換到專案,安裝所有依賴時執行一次即可。
</p>
<p>對每一個 package 來說,其 package.json 檔案中都有如下所示的 npm script 宣告。</p>
<pre><code class="language-js">
"scripts": {
"prepare": "yarn run build",
"prepublishOnly": "./../../ci-scripts/publish-tsdocs.sh"
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc
-out docs --target es6 --theme minimal --mode file src"
}
</code></pre>
<p>
受益於 monorepo,所有專案得以集中管理在一個倉庫中,這樣我們便可以將所有 package 中公共的
npm 指令稿移到 ./scripts 檔案中,且可以在單一 monorepo
專案的不同套件之間共用建置指令稿了。
</p>
<p>
執行公共指令搞時,有時侯有必要知道目前執行的專案資訊,npm 能夠讀取到每個 package.json
檔案中的資訊,因此,可以在每個套件的 package.json 的檔案中增加以下資訊。
</p>
<pre><code class="language-js">
{
"name": "@mitter-io/core",
"version": "0.6.28"
"repository": {
"type": "git"
}
}
</code></pre>
<p>之後,以下變數就都可以被 npm script 使用了。</p>
<pre><code class="language-js">
npm_package_name = @mitter-io/core
npm_package_version = 0.6.28
npm_package_repository_type = git
</code></pre>
<h3>流程最佳化</h3>
<p>
團隊中的正常開發流程是,每個程式設計師新增一個git 分支,透過程式審核後進行合併。整套流程在
monorepo 架構下非常清晰,下面來整理一下。
</p>
<ul>
<li>開發完成後,我們計畫進行版本升級,只需要執行 lerna version 指令。</li>
<li>Lerna 會提供互動式 prompt,對下一版本進行序號升級。</li>
</ul>
<pre><code class="language-bash">
lerna version --force-publish
lerna notice cli v3.8.1
lerna info current version 0.6.2
lerna info Looking for changed packages since v0.6.2
? Select a new version (currently 0.6.2) (Use arrow keys)
Patch (0.6.3)
Minor (0.7.0)
Major (1.0.0)
Prepatch (0.6.3-alpha. 0)
Preminor (0.7.0-alpha. O)
Premajor (1.0.0-alpha. 0)
Custom Prerelease
Custom Version
</code></pre>
<p>
選定新版本之後,Lerna 會自動改變每個套件的版本編號,在遠端倉庫中建立一個新的
tag,並將所有的改動發送到 GitLab 實例中。
</p>
<p>接下來,透過 CI (Continuous Integration ,持續整合)進行建置實際上只為要以下兩步。</p>
<ul>
<li>build,即建置。</li>
<li>publish,即發佈。</li>
</ul>
<p>建置實際上就是執行以下程式。</p>
<pre><code class="language-bash">
lerna bootstrap
lerna run build
</code></pre>
<p>而發佈也不複雜,只需要執行以下程式即可。</p>
<pre><code class="language-bash">
git checkout master
lerna bootstrap
git reset --hard
lerna publish from-package --yes
</code></pre>
<p>
注意,這裡使用了 lerna publish from-package,而非簡單的 lerna
publish。因為開發者在本機已經執行了 lerna version,這時再執行 lerna publish
會收到「目前放本已經發佈」的提示。
</p>
<p>而 from- package參數會告訴 Lerna 發佈所有非目前 npm package 版本的專案。</p>
<h3>依賴關係簡介</h3>
<p>
說到專案中的依賴關係,常會想到使用 yarn/npm
解決依賴問題。依賴關係大致上可以分為巢狀結構依賴和扁平依賴。
</p>
<p>
在專案中,參考了3個套件:PackageA、PackageB、PackageC,它們都依賴了 PackageD
的不同版本。那麼在安裝時,如果 PackageA、PackageB、PackageC 在各自的 node_modules
目錄中分別含有 PackageD ,那麼就將其了解為巢狀結構依賴,範例如下。
</p>
<pre><code class="language-js">
PackageA
node_modules/PackageD@v1.1
PackageB
node_modules/PackageD@v1.2
PackageC
node_modules/PackageD@v1.3
</code></pre>
<p>
如果在安裝時,先安裝了 PackageA,那麼 PackageA 依賴的 PackageD
版本就會成為主版本,但它又和 PackageA、PackageB、PackageC
一起出現,所以認為這是扁平依賴。此時 PackageB、PackageC 各自的 node_modules
目錄中也含有各自的 PackageD 版本。
</p>
<pre><code class="language-js">
PackageA
PackageD@v1.1
PackageB
node_modules/PackageD@v1.2
PackageC
node_modules/PackageD@v1.3
</code></pre>
<p>
npm 在安裝依賴套件時,會將依賴套件下載到目前的 node_modules 目錄中。
對於巢狀結構依賴和扁平依賴的話題,npm 列出了不同的處理方案。npm3
以下的版本在安裝依賴時非常直接,它會按照套件依賴的樹狀結構將其下載到本機 node_modules
目錄中,也就是說,每個套件都會將該套件的依賴放到目前套件所在的 node_modules 目錄中。
</p>
<p>
這麼做是因為考慮到了套件依賴的版本錯綜複雜,同一個套件因為被依賴的關係會出現多個版本,確保樹狀結構的安裝能夠簡化和統一對於套件的安裝和刪除行為。這樣能夠簡單地解決多版本相容問題,但也帶來了較大的容錯。
</p>
<p>
npm3 採用了扁平結構,在安裝依賴套件時更加智慧,實際表現在:在安裝依賴套件時,npm3 會按照
package.json 中宣告的順序依次安裝套件,遇到新的套件就把它放在第一級 node_modules
目錄中。後面再進行安裝時,如果遇到一級 node_modules
目錄已經存在的套件,就會先判斷套件版本,如果版本一樣則跳過安裝,否則會按照 npm2
的方式安裝在樹狀目錄結構下。
</p>
<p>
npm3
這種安裝方式只能夠部分解決依賴重複的問題,對一些場景,依然無法做到將依賴去重。舉例來說,專案中有
PackageA、PackageB、 PackageC 、PackageD、PackageB、PackageC 依賴模組 PackageD v2.0 -
PackageA 依賴模組 PackageD v1.0,如果在安裝時先安裝了 PackageD v1.0,然後分別在 PackageB、
PackageC 樹狀結構內部安裝了 PackageD
v2.0,則會造成菜權程度的容錯。為了解決這個問題,就有了 npm dedupe 指令。
</p>
<p>
另外,為了確保同一個專案中不同團隊或不同團隊成員安裝的版本依賴相同,常會使用
package-lock.json 或 yarn-lock.json 這種檔案,並透過 Git 上傳 package-lock.json 或
yarn-lock.json 以共用指定版本的依賴。
</p>
<p>
這些內容與開發息息相關,但是常常會被開發者忽視。依賴問題說小很小,説複雜也很複雜,下面再來看一個循環依賴的問題。
</p>
<h3>複雜依賴關係分析和處理</h3>
<p>
簡單來說,循環依賴就是模組A 和模組B
相互參考。在不同的模組化標準下,對循環依賴的處理不盡相同。
</p>
<p>下面在 Node.js 中製造一個簡單的循環參考場景。</p>
<pre><code class="language-js">
// 假設模組A 的內容如下。
exports.loaded = false
const D = 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 檔案中呼叫以下程式。
const a = require('./a');
const b = require('./b');2
console.log(a)
console.log(b)
</code></pre>
<p>在這種情況下執行程式,並未出現無窮迴圈當機的現象,而是輸出以下結果。</p>
<pre><code class="language-js">
{ bWasLoaded: true, loaded: true }
{ aWasLoaded: false, loaded: true }
</code></pre>
<p>
這多虧了模組載入過程中的快取機制,使 Node.js
對模組載入進行了快取。按照執行順序,第一次載入 a 時會執行 const b =
require('./b'),所以可以直接進入模組 B 中,而此時在模組 B 中又會執行 const a =
require('./a'),由於模組A 已經被快取,因此模組B 傳回的結果如下。
</p>
<pre><code class="language-js">
{aWasLoaded: false, loaded: true}
</code></pre>
<p>模組B 載入完成後,回到模組A 中繼續執行,模組A傳回的結果如下。</p>
<pre><code class="language-js">
{aWasLoaded: true, loaded: true}
</code></pre>
<p>
整體來說,Node.js 或 CommonJS
標準得益於其快取機制,可以使程式在遇見循環參考時不會當機。但是,這樣的機制仍然會有問題:它只會輸出已執行部分,對未執行部分
export 來說,其內容為 undefined。
</p>
<p>
ES 模組與 CommonJS 標準不同,ES 模組不存在快取機制,而是動態参考依賴的模組。 ES
模組的設計思想是,儘量靜態化,這樣在編譯時就能確定模組之間的依賴關係。這也是 import
指令一定要出現在模組開頭部分的原因:在模組中,import
實際上不會直接執行模組:而是只產生一個参考。在模組內真正参考依賴邏輯時,模組會從依賴中進行設定值。這樣的設計非常有利於
Tree shaking 技術的實現。
</p>
<p>
在專案實作中,循環參考的出現常常是由設計不合理造成的。如果使用 webpack
進行專案建置,則可以使用 webpack 外掛程式 circular-dependency-plugin
來幫助檢測專案中存在的所有循環依賴。循環依賴這個問題說大不大,說小不小,應該盡可能在設計之初避開。
</p>
<p>另外,複雜的依賴關係至少還會帶來以下問題。</p>
<ul>
<li>依賴版本不一致</li>
<li>依賴遺失</li>
</ul>
<p>
對此,開發者需要根據實際情況進行處理,同時,合理使用 npm/yarn 工具也能造成十分重要的作用。
</p>
<h3>使用yarn workspace 管理依賴關係</h3>
<p>
monorepo 專案中的依賴管理問題值得重視。現在來看一下非常流行的 yarn workspace
是如何處理這種問題的。
</p>
<p>
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.
</p>
<p>
翻譯過來的意思就是,workspace 能幫助你更進一步地管理有多個子套件的 monorepo
:開發者既可以在每個子套件下使用獨立的 package.json 管理依賴,又可以享受透過一行 yarn
指令安裝或升級所有依賴的便利。
</p>
<p>引用 workspace 後,在根目錄下執行以下指令,所有的依賴都會被安裝或更新。</p>
<pre><code class="language-js">
yarn install / yarn upgrade XX
</code></pre>
<p>當然,如果只想更新某一個套件內的版本,則可以透過以下程式完成。</p>
<pre><code class="language-js">
yarn workspace <workspace-name> upgrade XX
</code></pre>
<p>
在使用 yarn 的專案中,使用 yarn workspace 時不需要安裝其他的珍件,只需要簡單更改
package.json 便可以工作。
</p>
<pre><code class="language-js">
// package. json
{
"private": true,
"workspaces": ["workspace-1", "workspace-2"]
}
</code></pre>
<p>
需要注意的是,如果需要啟用 workspace,則必須將這裡的 private 欄位設定為
true。同時,workspaces 這個欄位的值對應一個陣列,陣列中的每一項都是字串,分別表示一個
workspace(可以視為一個 repo)。
</p>
<p>
接著,可以在 workspace-1 和 workspace-2 專案中分別増加 package.json
檔案中的內容,如下所示。
</p>
<pre><code class="language-js">
// workspace-1 專案:
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"react": "16.2.3"
}
}
// workspace-2 專案:
{
"name": "workspace-2",
"version": "1.0.0",
"dependencies": {
"react" : "16.2.3",
"workspace-1": "1.0.0"
}
}
</code></pre>
<p>
執行 yarn install 指令後,可以發現專案根目錄下的 node_modules
內已經包含了所有宣告的依賴,而且各個子套件的 node_modules
中不會重複存在依賴:只會參考根目錄下 node_modules 中的 React 套件。
</p>
<p>
yarn workspace和 Lerna 有很多共同之處,解決的問題也有部分重疊。下面比較 yarn workspace 和
Lerna。
</p>
<ul>
<li>
yarn workspace 暫存於 yarn
中,不需要開發者額外安裝工具即可使用,使用起來也非常簡單,只需要在 package.json
中進行相關的設定,但不像 Lerna 那樣提供了大量 API。
</li>
<li>yarn workspace 只能在根目錄中引用,不需要在各個子專案中引用。</li>
</ul>
<p>
事實上,Lerna 可以與 yarn workspace
共存,兩者搭配使用能夠發揮更大的作用。在團隊中,Lerna負責版本管理與發佈,其強大的 API
和設定可以在開發時做到靈活細緻;workspace 負責依賴管理,使整個流程非常清晰。
</p>
<p>在 Lerna 中使用 workspace,首先需要修改 lerna.json 檔案中的設定。</p>
<pre><code class="language-js">
{
// ...
"npmClient": "yarn",
"useWorkspaces": true,
// ...
}
</code></pre>
<p>
然後,將根目錄下 package.json 檔案中的 workspaces 欄位設定為 Lerna 標準的 packages 目錄。
</p>
<pre><code class="language-js">
{
"private": true,
"workspaces": [
"packages/*'
],
// ...
}
</code></pre>
<p>
注意,如果開啟了 workspace 功能,則 lerna.json 中的 packages 值便不再生效。原因是 Lerna
會將 package.json 檔案的 workspaces 中所設定的 workspaces 陣列作為 lerna packages
的路徑,也就是各個子倉庫的路徑。換句話說 Lerna 會優先使用 package.json 中的 workspaces
模組,在不存在該欄位的情況下使用 lerna.json 中的 packages 欄位。如果未開啟 workspace
功能,則 lerna.json 檔案中的設定如下所示。
</p>
<pre><code class="language-js">
{
"npmClient": "yarn",
"useWorkspaces": false,
"packages":[
"packages/11/",
"packages/12/"
]
}
</code></pre>
<p>根目錄下 package.json 檔案中的設定如下所示。</p>
<pre><code class="language-js">
{
"private": true,
"workspaces": [
"packages/21/*"
"packages/22/*",
]
}
</code></pre>
<p>
那麼,這就表示使用 yarn 管理的是 package.json 檔案中 workspaces
所對應的專案路徑下的依賴:packages/21/* 及 packages/22/*;而 Lerna 管理的是 lerna.json
檔案中 packages 所對應的 packages/11/* 及 packages/12/*。
</p>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>