-
Notifications
You must be signed in to change notification settings - Fork 0
/
1-6-7.html
518 lines (516 loc) · 23.3 KB
/
1-6-7.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
<!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-7">前端模組化發展及專案規劃</h2>
<p>
日常生活還是其他科學領域,都離不開模組化的概念,它主要表現了可重複使用性、可組合性、中心化、獨立性等原則。
</p>
<p>
所謂程式的模組化,就是將一套可重複使用的邏輯封裝成一個實體,其內部完成共同的或類似的邏輯,透過對外暴露一些資料或呼叫方法,與外部完成整合。
</p>
<p>
每個檔案彼此獨立,容易開發和維護程式,模組之間也能夠互相呼叫和通訊,這就是現代化開發的基本模式。
</p>
<h3>模組化發展歷程</h3>
<p>前端模組化發展主要經歷了以下3個階段。</p>
<ul>
<li>早期「假」模組化時代</li>
<li>標準時代</li>
<li>ES 原生時代</li>
</ul>
<h4>早期「假」模組化時代</h4>
<p>
在早期,JavaScript
屬於執行在瀏覽器端的玩具指令稿,它只負責實現一些簡單的互動。隨著網際網路技術的演進,這樣的設計逐漸不能滿足業務的需求,這時開發者常常會從程式可讀性上借助函數作用域來模擬模組化,我稱其為函數模式,即將不同功能封裝成不同的函數。
</p>
<pre><code class="language-js">
function f1(){
//…
}
function f2(){
//…
}
</code></pre>
<p>
這樣的實現其實根本不算模組化:各個函數在同一個檔案中、混亂地互相呼叫,而且存在命名衝突的風險。這沒有從根本上解決問題,只是從程式撰寫的角度,將程式拆分成了更小的函數單元而已。
</p>
<p>
聰明的開發者很快就想出了第二種方式,姑且稱它為物件模式,即利用物件,實現命名空間的概念,使用這種方式的範例程式如下。
</p>
<pre><code class="language-js">
const module1 = {
foo: 'bar',
f11: function f11 () {//... },
f12: function f12 () {//... }
}
const module2 = {
data: 'data',
f21: function f21 () {//... },
f22: function f22 () {//... }
}
</code></pre>
<p>這裡模擬了簡單的 module1、module2 命名空間,因此可以在函數主體中呼叫以下敘述。</p>
<pre><code class="language-js">
module1.f11()
console.log(module2.data)
</code></pre>
<p>
可是,這樣做的問題也很明顯,module1 和 module2
中的資料並不安全,任何開發者都可以修改。舉例來說,像下面這樣直接修改設定值的程式。
</p>
<pre><code class="language-js">
module2.data = 'modified data'
</code></pre>
<p>這會使得物件內部成員可以隨意被改寫,極易出現 bug。</p>
<p>
閉包簡直就是一個天生解決資料存取性問題的方案。透過立即執行函數(IIFE),我們建置一個私有作用域,再透過閉包,將需要對外曝露的資料和介面輸出,我們稱之為
IIFE 模式。將立即執行函數與閉包結合使用的實現程式如下。
</p>
<pre><code class="language-js">
const module = (function () {
var foo = 'bar'
var fnl = function (){
// ...
}
var fn2 = function fn2 (){
// ...
}
return {
fn1: fn1,
fn2: fn2
}
})()
</code></pre>
<p>我們在呼叫 module 時,如果想要存取變數 foo,是存取不到實際資料的,程式如下所示。</p>
<pre><code class="language-js">
module.fn1()
module.foo
// undefined
</code></pre>
<p>
了解了這種模式,我們就可以在此基礎上「玩出另外一種花樣」來,這種模式的變種可以結合頂層
window 物件進行實現,程式如下。
</p>
<pre><code class="language-js">
(function (window) {
var data = 'data'
function foo () {
console.log(`foo executing, data is ${data}`)
}
function bar () {
data = 'modified data'
console.log(`bar executing, data is now ${data}`)
}
window.module1 = { foo, bar }
})(window)
</code></pre>
<p>
資料 data 完全做到了私有,外界無法修改 data 值。那麼如何存取 data
呢?這時就需要模組內部設計並曝露相關介面。上述程式只需要呼叫模組 module1
曝露給外界(window)的函數即可,呼叫方式如下。
</p>
<pre><code class="language-js">
module1.foo()
// foo executing, data is data
</code></pre>
<p>修改 data 值的途徑,也只能由模組 module1 提供。</p>
<pre><code class="language-js">
module1.bar()
// bar executing, data is now modified data
</code></pre>
<p>如此一來,程式已經初具「模組化」的實質了,實現了模組化所應該具備的初級功能。</p>
<p>
進一步思考,如果 module1 依賴外部模組 module2,該怎麼辦?可以將程式寫為如下所示的形式。
</p>
<pre><code class="language-js">
(function (window, $){
var data = 'data'
function foo () {
console.log(`foo executing, data is ${data}`)
console.log($)
}
function bar () {
data = 'modified data'
console.log( `bar executing, data is now ${data}` )
}
window.module1 = { foo, bar }
}) (window, jQuery)
</code></pre>
<p>
事實上,這就是現代模組化方案的基礎。至此,我們經歷了模組化的第一階段:「假」模組化時代。這種實現極具阿Q精神,它並不是語言原生層面上的實現,而是開發者利用語言,借助
JavaScript 特性,對類似的功能進行了模擬,為後續方案開啟了大門。
</p>
<h4>標準時代:CommonJS</h4>
<p>
Nodejs 無疑對前端的發展具有相當大的促進作用,它帶來的 CommonJs
模組化標準像一股「改革春風」:在 Nodejs
中,每一個檔案就是一個模組,具有單獨的作用域,對其他檔案是不可見的。關於 CommonJs
的標準,這裡不做過多介紹,讀者可自行了解其基礎內容,這裡只看一下它的幾個容易被忽略的特點。
</p>
<ul>
<li>檔案即模組,檔案內的所有程式都執行在獨立的作用域中,因此不會污染全域空間。</li>
<li>
模組可以被多次參考、載入。在第一次被較入時,會被快取,之後都從快取中直接讀取結果。
</li>
<li>載入某個模組,就是引用該模組的 module.exports 屬性</li>
<li>
module.exports
屬性輸出的是值的拷貝,一旦這個值被輸出,模組內再發生變化也不會影響到輸出的值。
</li>
<li>模組按照程式引用的順序進行載入。</li>
<li>注意 module.exports 和 exports 的區別</li>
</ul>
<p>
CommonJS 標準如何用程式在瀏覽器端實現呢?其實就是實現 module.exports 和 require 方法。
</p>
<p>
實現想法:根據 require
的檔案路徑載入檔案內容並執行,同時將對外介面進行快取。因此我們需要定義一個 module
物件,程式如下。
</p>
<pre><code class="language-js">
let module = {}
module.exports = {}
</code></pre>
<p>接著,借助立即執行函數,對 module 和 module.exports 物件進行設定值,如下。</p>
<pre><code class="language-js">
(function (module, exports){
// ...
}(module, module.exports))
</code></pre>
<h4>標準時代:AMD</h4>
<p>
由於 Node.js
執行於伺服器上,所有的檔案一般都已經儲存在本機硬碟中了,不需要額外的網路請求進行非同步載入,因此透過
CommonJS 標準載入模組是同步的。只有載入完成,才能執行後續操作。但是,如果 Node.js
在瀏覽器環境中執行,那麼由於需要從伺服器端取得模組檔案,所以此時採用同步的方式顯然就不合適了。為此,社區推出了
AMD 標準
</p>
<p>
AMD 標準的全稱為 Asynchronous Module Definition,看到
Asynchronous,我們就能夠知道它的模組化標準不同於 CommonJs
,按照該標準載入模組時是非同步的,這種標準是完全適用於瀏覽器的。
</p>
<p>
AMD
根據規定了如何定義模組,如何對外輸出,如何引用依賴。這一切都需要程式去實現,因此一個著名的函數庫一
require.js 應運而生, require.js 的實現很簡單:透過 define 方法,將程式定義為模組;透過
require 方法,實現程式的模組載入。
</p>
<p>define 和 require 就是 require.js 在全域植入的函數。</p>
<p>
在熟練使用 require.js
的基礎上,建議讀者閱讀一下其原始程式。事實上,require.js也是借助一個立即執行函數來賞現的,其中的程式如下。
</p>
<pre><code class="language-js">
var require, define;
(function(global, setTimeout) {
// ..
} (this,(typeof setTimeout ==='undefined'? undefined: setTimeout)));
</code></pre>
<p>
我們看到,require.js 在全域定義了 require 和 define
兩個方法,利用立即執行函數將全域物件(this)和 setTimeout 傳入函數本體內。define
方法的實作方式邏輯如下。
</p>
<pre><code class="language-js">
define = function (name, deps, callback) {
// ...
if (context) {
context.defQueue.push([name, deps, callback]) ;
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
}
</code></pre>
<p>
以上程式主要用於將依賴植入依賴佇列。require 的主要作用是完成 script
標籤的建立去請求對應的模組,並對模組進行載入和執行,其程式如下。
</p>
<pre><code class="language-js">
reg.load = function (context, moduleName, url) {
var config = (context && context.config) || {},
node;
if (isBrowser) {
// create a async script element
node = reg.createNode(config, moduleName, url);
// add Events[onreadystatechange, load, error]
//set url for loading
node.src = url;
// insert script element to head and start load
currentlyAddingScript = node;
if (baseElement){
head.insertBefore(node, baseElement) ;
} else {
head.appendChild(node) ;
}
currentlyAddingScript = null;
return node;
} else if (isWebWorker) {
// ...
}
}
req.createNode = function(config, moduleName, url){
var node = config.xhtml ?
document.createElementN(http://www.w3.0rg/1999/xhtml', html:script') :
document.createElement('script');
node.type = config.scriptType || 'text/javascript';
node.charset = 'utf-8';
node.async = true;
return node;
};
</code></pre>
<p>
了解了上面的程式,細心的讀者可能會有疑問:在我們使用 require.js 後,並沒有發現額外多出來的
script 標籤,這個秘密就在於 checkLoaded
方法會把已經載入完畢的指令稿刪除,因為我們需要的是模組內容,所以一旦載入完成,就沒有必要保留
script 標籤了。刪除 script 標籤的實作方式程式如下。
</p>
<pre><code class="language-js">
function removeScript(name) {
if(isBrowser){
each(scripts(), function (scriptNode) {
if(scriptNode.getAttribute('data-requiremodule') === name &&
scriptNode.getAttribute('data-requirecontext') === context.contextName) {
scriptNode.parentNode.removeChild(scriptNode) ; return true;
}
})
}
}
</code></pre>
<h4>標準時代:CMD</h4>
<p>
CMD 標準整合了 CommonJS 和 AMD標準的特點,它的全稱為 Common Module Definition,與
require.js 類似,CMD 標準的實現為 sea.js。
</p>
<p>AMD 和CMD的兩個主要區別如下。</p>
<ul>
<li>
AMD
需要非同步載入模組,而CMD在載入模組時,可以透過同步的形式(require),也可以透過非同步的形式(require.async)。
</li>
<li>
CMD
遵循依賴就近原則,AMD遵循依賴前置原則。也就是說,在AMD中,我們需要把模組所需要的依賴都提前宣告在依賴陣列中;而在CMD中,我們只需要在實際程式邏輯內,使用依賴前,引用依賴的模組即可。
</li>
</ul>
<h4>標準時代:UMD</h4>
<p>
UMD 的全稱為 Universal Module Definition,看到
Universal,我們可以猜到它允許在環境中同時使用 AMD 標準與 CommonJS
標準,相當於一個整合的標準。該標準的核心思想在於利用立即執行函數根據環境來判斷需要的參數類別,譬如,UMD
在判斷出目前模組遵循 CommonJs 標準時,模組化程式會以以下方式執行。
</p>
<pre><code class="language-js">
function(factory){
module.exports = factory();
}
</code></pre>
<p>
而如果 UMD 判斷出目前模組遵循 AMD 標準,則函數的參數就會變成 define,適用 AMD
標準,實際程式如下。
</p>
<pre><code class="language-js">
function (root, factory) {
if(typeof define === 'function' && define.amd){
// AMD標準
define(['b'], factory);
}else if (typeof module === 'object'&& module.exports){
// 類別 Node 環境,並不支援完全嚴格的 CommonJS 標準
// 但是屬於 CommonJS-like環境,支援 module.exports 用法
module.exports = factory(require('b'));
}else{
//瀏覽器環境
root.returnExports = factory(root.b);
}
}(this, function (b) {
//傳回值作為曝露內容
return {}
}))
</code></pre>
<p>
至此,我們便介紹完了模組化的 Node.js 和社區解決方案。這些方案充分利用了 JavaScript
的語言特性,並結合瀏覽器端的特點,加以實現。不同的實現方式表現了不同的設計哲學,但是它們的最後方向都指向了模組化的幾個原則:可重複使用性、可組合性、中心化、獨立性。下一節會繼續探討模組化這個主題,介紹ES
原生時代的解決方案。
</p>
<h3>ES 原生時代</h3>
<p>
ES
模組的設計思想是儘量靜態化,這樣能確保在編譯時就確定模組之間的依賴關係,每個模組的輸入和輸出變數也都是確定的;而
CommonJS 和 AMD 模組無法保證在編譯時就確定這些內容,它們都只能在執行時期確定。這是 ES
模組和其他模組標準最顯著的差別。第二個差別在於,CommonJS 模組輸出的是一個值的拷貝,ES
模組輸出的是值的參考。下面來看一個範例。
</p>
<pre><code class="language-js">
// data.js
export let data = 'data'
export function modifyData () {
data = 'modified data'
}
// index.js
import { data, modifyData } from './lib'
console.log(data) // data modifyData()
console.log(data) // modified data
</code></pre>
<p>
我們在 index.js 中呼叫了 modifyData 方法,之後查詢 data
值,獲得了最新的變化;而同樣的邏輯在 CommonJS 標準下的表現如下。
</p>
<pre><code class="language-js">
// data. js
var data = 'data'
function modifyData () {
data = 'modified data'
}
module.exports = {
data: data,
modifyData: modifyData
}
// index.js
var data = require('./data').data
var modifyData = require('./data').modifyData
console.log(data) // data
modifyData()
console.log(data) // data
</code></pre>
<p>
因為在 CommonJS 標準下輸出的是值的拷貝,而非參考,因此在呼叫 modifyData 之後,index.js 的
data 值並沒有發生變化,其值為一個全新的拷貝。
</p>
<h4>ES 模組為什麼要設計成靜態的</h4>
<p>
將 ES
模組設計成靜態的,一個明顯的優勢是,透過靜態分析,我們能夠分析出匯入的依賴。如果匯入的模組沒有被使用,我們便可以透過
Tree Shaking 等方法減少程式體積,進而提升執行效能。這就是基於 ESM 實現 tree shaking
的基礎。
</p>
<p>
下面從設計的角度分析這種標準的利弊。靜態性需更標準去強制保證,因此 ES 模組標準不像
CommonJS 標準那樣靈活,其靜態性會帶來以下一些限制。
</p>
<ul>
<li>只能在檔案頂部引用依賴。</li>
<li>匯出的變數類型受到嚴格限制。</li>
<li>變數不允許被重新綁定,引用的模組名稱只能是字串常數,即不可以動態確定依賴。</li>
</ul>
<p>
這樣的限制在語言層面帶來的便利之一是,我們可以透過分析作用域,得出程式中變數所屬的作用域及它們之間的參考關係,進而可以推導出變數和匯入依賴變數之間的參考關係,在沒有明顯參考時,可以對程式進行去容錯。
</p>
<h4>tree shaking</h4>
<p>
上面說到的「在沒有明顯參考時,可以對程式進行去容錯」,就是我們經常提到的 tree
shaking,它的目的是減少應用中沒有被實際運用的JavaScript
程式。對無用程式進行清除,表示可以獲得更小的程式體積,而程式體積的縮減對使用者體驗可以造成積極的作用
</p>
<p>
在電腦科學中,一個典型的去除無用程式、冗餘碼的方法是 DCE(Dead
CodeElimination,死碼消除)。那麼,tree shaking 和 DCE 有什麼區別?
</p>
<p>
Rollup 的主要貢獻者 Rich Haris
做過這樣的比喻:假設我們用雞蛋做蛋糕,顯然不需要蛋殼而只需要蛋清和蛋黃,那麼如何去除蛋殼呢?DCE
是這樣做的,直接把整個雞蛋放到碗裡攪拌,蛋糕做完後再慢慢地從裡面挑出蛋殼。
</p>
<p>
相反,與 DCE 不同,tree shaking 是一開始就把蛋殼剝離,留下蛋清和蛋黃。事實上,也可以將
tree shaking 了解為一種廣義上的 DCE,它在包裝時便會去掉用不到的程式。
</p>
<p>
當然:說到底, tree shaking 只是一種輔助方法,良好的模組拆分和設計才是減少程式體積的關鍵。
</p>
<p>
tree shaking 的使用也存在一些侷限,它還有很多不能清除無用程式的場景,舉例來說,Rollup的
tree Shaking 只能處理函數和頂層的 import/export
匯入的變數,不能把沒用到的類別的方法清除;具有副作用的指令稿無法被清除。
</p>
<p>
webpack 和 Rollup 建置工具目前在實現 tree shaking
方面都有成熟的方案,但並不建議將其馬上引入專案中。事實上,是否要在成熟的專案上立即實施
tree shaking 需要妥善考慮。
</p>
<h4>ES 的 export 和 export default</h4>
<p>
ES 模組化匯出有 export 和 export default 兩種。這裡建議減少使用 export default
匯出,一方面是因為 export default 會進出整體物件結果,不利於透過tree shaking
進行分析;另一方面是因為 export default 匯出的結果可以隨意命名變數,不利於團隊統一管理。
</p>
<h3>未來趨勢和思考</h3>
<p></p>
<h4>在瀏覽器中快速使用 ES 模組化</h4>
<p>
個人認為,ES 模組化是未來的發展趨勢,它的優點毫無爭議,舉例來說,具有開箱即用的 tree
shaking 和對未來瀏覽器的相容性支援。Node.js 的 CommonJS 模組化方案甚至會慢慢過渡到 ES
模組化上。如果你正在使用 webpack 建置應用專案,那麼 ES
模組化應該是首選方案;如果你的專案是一個前端函數庫,也建議你使用 ES
模組化。這麼看來,或許只有在撰寫 Node.js 程式時,才需要考慮使用 CommonJS 模組化方案。
</p>
<pre><code class="language-js">
<script type="module">
import module1 from './module1'
</script>
<script nomodule>
alert('你的瀏覽器不支援ES模組,請先升級!')
</script>
</code></pre>
<p>
使用 type="module" 的另一個作用是進行 ES Next 相容性偵測。因為支援了 ES
模組化的瀏覽器,所以能夠直接支援很多 ES 新特性。
</p>
<h4>在 Node.js 中使用 ES 模組化</h4>
<p>
Nodejs 從 9.0 版本開始支援 ES 模組化,執行指令稿啟動時需要加上
--experimental-modules,不過這一用法要求對應的檔案副檔名必須為,mjs,這樣就可以在 Node.js
中使用 ES 模組化了。
</p>
<pre><code class="language-js">
node --experimental-modules module1.mjs
import module1 from '/module1.mjs'
console.log(module1)
</code></pre>
<p>
另外,在Node.js 中使用ES模組化的另一種方式是,安裝 babel-cli 和 babel-
preset-env,設定,.babelrc 檔案後,執行 ./node_modules/.bin/babel-node 或 npx babel-node
。
</p>
<p>
在工具方面,webpack
本身維護了一套模組系統,這套模組系統相容了幾呼所有前端歷史處理程序下的模組標準,包含
AMD、CommonJS、ES模組等
</p>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>