-
Notifications
You must be signed in to change notification settings - Fork 0
/
1-6-8.html
1346 lines (1320 loc) · 59.9 KB
/
1-6-8.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
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!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-8">1-6-8 webpack 前端知識點</h2>
<h3>CommonJS 標準下的包裝結果</h3>
<p>
如何者手分析 CommonJS 標準下的包裝結果?首先,如下所示,建立並切入到專案,進行初始化。
</p>
<pre><code class="language-bash">
mkdir webpack-demo
cd webpack-demo
npm init -y
</code></pre>
<p>接著,安裝 webpack 的最新版本,安裝指令如下。</p>
<pre><code class="language-bash">
npm install --save-dev webpack
npm install --save-dev webpack-cli
</code></pre>
<p>在根目錄下建立 index.html,其中的程式如下。</p>
<pre><code class="language-html">
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./dist/main.js"></script>
</body>
</html>
</code></pre>
<p>
然後建立 ./src 資料夾,在 src
資料夾中,因為我們要研究模組化打包產出,這有關依賴關係,所以要在 ./src 目錄下建立 hello.js
和 index.js。index.js 為入口指令稿,它將依賴 hello.js,實際程式如下。
</p>
<pre><code class="language-js">
const sayHello = require('./hello')
console.log(sayHello('lucas'))
</code></pre>
<p>hello.js 中的內容如下。</p>
<pre><code class="language-js">
module.exports = function(name) {
return 'hello' + name
}
</code></pre>
<p>
這裡為了示範,採用了 CommonJS 標準,也沒有加入 Babel
編譯環節。直接執行以下指令,可以獲得產出,產出內容出現在 ./dist 檔案中。
</p>
<pre><code class="language-bash">
node_modules/.bin/webpack --mode development
</code></pre>
<p>開啟 ./dist/main.js,可以看到最後的編譯結果。</p>
<pre><code class="language-js">
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/hello.js":
/*!**********************!*\
!*** ./src/hello.js ***!
\**********************/
/***/ ((module) => {
eval("module.exports = function(name) {\r\n return 'hello' + name\r\n}\n\n//# sourceURL=webpack://webpack-demo/./src/hello.js?");
/***/ }),
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\r\nconsole.log(sayHello('lucas'))\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/
/******/ })()
;
</code></pre>
<p>把以上程式最核心的骨架分析出來,會發現它其實就是一個 IIFE(立即呼叫函數運算式)。</p>
<p>
Ben Cherry 的著名文章 JavaScript Module Pattern: In-Depth 介紹了用 IIFE
實現模組化的多種進階嘗試,阮一峰老師在其部落格中也提到了相關內容。對用 IIFE
實現模組化並不陌生。 深入研究上述程式結果(已增加註釋),可以提煉出以下幾個關鍵點。
</p>
<ul>
<li>
webpack 的包裝結果就是一個 IIFE:一般被稱為 WebpackBootstrap。這個 IIFE 接收一個物件
modules 作為參數,modules 物件的 key 是依賴路徑,value
是經過簡單處理後的指令稿(它不完全等於我撰寫的業務指令稿。而是被 webpack
包裹後的內容)。
</li>
<li>包裝結果中定義了一個重要的模組載入函數 __webpack_require__。</li>
<li>首先使用模組載入函數 __webpack_require__ 去載入入口模組, ./src/index.js。</li>
<li>
載入函數 __webpack_require__ 使用了閉包變數
installedModules,它的作用是將已載入過的模組結果儲存在記憶體中。
</li>
</ul>
<h4>ES 標準下的包裝結果</h4>
<p>
以上是以 CommonJS 標準為基礎的模組化寫法,而業務中的程式常常遵循 ESNext 模組化標準,並透過
Babel 進行編譯。
</p>
<p>首先,安裝依賴,程式如下。</p>
<pre><code class="language-bash">
npm install --save-dev webpack
npm install --save-dev webpack-cli
npm install --save-dev babel-loader
nom install --save-dev @babel/core
npm install --save-dev @babel/preset-env
</code></pre>
<p>同時設定 package.json,即在 package.json 檔案中寫入以下內容。</p>
<pre><code class="language-js">
// V5 已棄用: --display-modules --colors --display-reasons
// --progress 可加可不加結果相同
"scripts": {
"build": "webpack --mode development"
},
</code></pre>
<p>
設定 npm script 以便執行 webpack 的建置流程程式,同時在 package.json 中加入如下所示的
Babel 設定。
</p>
<pre><code class="language-js">
"babel": {
"presets": ["@babel/preset-env"]
}
</code></pre>
<p>將 index.js 和 hello.js 改寫為 ESM 形式,如下。</p>
<pre><code class="language-js">
// hello.js
const sayHello = name => `hello ${name}`
export default sayHello
// index.js
import sayHello from './hello.js'
console.log(sayHello('lucas'))
</code></pre>
<p>執行以下程式,獲得的包裝主體與之前的內容基本一致。</p>
<pre><code class="language-bash">
npm run build
</code></pre>
<p>但是在細節上,發現在執行指令稿中多了以下敘述。</p>
<pre><code class="language-js">
__webpack_require __.r(webpack_exports__)
</code></pre>
<p>
實際上,__webpack_require __.r 這個方法是用來給模組的 exports 物件加上 ES
模組化標準的標記的。
</p>
<p>
實際標記方式為:如果目前環境支援 Symbol 物件,則可以透過 Object.defineProperty 為 exports
物件的 Symbol.toStringTag 屬性設定值 Module,這樣做的結果是 exports 物件在呼叫 toString
方法時會傳回 Module,同時將 exports.__esModule 設定值為 true。
</p>
<p>
除了 CommonJS 和 ES Module 標準,webpack 和樣支援 AMD
標準,可以在此標準下對程式重新包裝來觀察這3種標準之間的差別。
</p>
<h4>隨選載入下的包裝結果</h4>
<p>
在現代化的業務中,尤其是在單頁應用中,我們常常會使用隨選載入的方式,那麼在這種相對較新的載入方式下,webpack
又會產出什麼樣的程式呢?
</p>
<p>首先安裝 Babel 外掛程式,以支援動態引用(dynamic import)。</p>
<pre><code class="language-bash">
npm install --save-dev babel-plugin-dynamic-import-webpack
</code></pre>
<p>在 webpack.config.js 中増加相關外掛程式設定。</p>
<pre><code class="language-js">
module.exports={
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader:"babel-loader",
options: {
"plugins": ["dynamic-import-webpack"]
}
}
]
}
}
</code></pre>
<p>
同時,在 index.js 檔案中使用動態引用的方式隨選載入 ./hello 檔案,實現隨選載入的程式如下。
</p>
<pre><code class="language-js">
import('./hello').then(sayHello =>{
console.log(sayHello('lucas'))
})
</code></pre>
<p>最後執行以下程式。</p>
<pre><code class="language-bash">
npm run build
</code></pre>
<p>
這樣一來,發現重新建置後會輸出兩個檔案,分別是執行入口檔案 main.js 和非同步載入檔案
src_hello_js.js,因為非同步隨選載入時,顯然不能把所有的程式再包裝到一個 bundle 中了。
</p>
<p>src_hello_js.js檔案中的內容如下。</p>
<p>"webpack": "^5.91.0"</p>
<pre><code class="language-js">
"use strict";
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
(self["webpackChunkwebpacktest"] = self["webpackChunkwebpacktest"] || []).push([["src_hello_js"],{
/***/ "./src/hello.js":
/*!**********************!*\
!*** ./src/hello.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nvar sayHello = function sayHello(name) {\n return \"hello \".concat(name);\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sayHello);\n\n//# sourceURL=webpack://webpacktest/./src/hello.js?");
/***/ })
}]);
</code></pre>
<p>main.js 檔案中的內容也與之前相比變化較大。</p>
<pre><code class="language-js">
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("new Promise(function (resolve) {\n __webpack_require__.e(/*! require.ensure */ \"src_hello_js\").then((function (require) {\n resolve(__webpack_require__(/*! ./hello */ \"./src/hello.js\"));\n }).bind(null, __webpack_require__))['catch'](__webpack_require__.oe);\n}).then(function (sayHello) {\n console.log(sayHello(\"lucas\"));\n});\n\n//# sourceURL=webpack://webpacktest/./src/index.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/ensure chunk */
/******/ (() => {
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/get javascript chunk filename */
/******/ (() => {
/******/ // This function allow to reference async chunks
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
/******/ return "" + chunkId + ".js";
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/global */
/******/ (() => {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/load script */
/******/ (() => {
/******/ var inProgress = {};
/******/ var dataWebpackPrefix = "webpacktest:";
/******/ // loadScript function to load a script via script tag
/******/ __webpack_require__.l = (url, done, key, chunkId) => {
/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ var script, needAttach;
/******/ if(key !== undefined) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ for(var i = 0; i < scripts.length; i++) {
/******/ var s = scripts[i];
/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/ }
/******/ }
/******/ if(!script) {
/******/ needAttach = true;
/******/ script = document.createElement('script');
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/
/******/ script.src = url;
/******/ }
/******/ inProgress[url] = [done];
/******/ var onScriptComplete = (prev, event) => {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var doneFns = inProgress[url];
/******/ delete inProgress[url];
/******/ script.parentNode && script.parentNode.removeChild(script);
/******/ doneFns && doneFns.forEach((fn) => (fn(event)));
/******/ if(prev) return prev(event);
/******/ }
/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ script.onload = onScriptComplete.bind(null, script.onload);
/******/ needAttach && document.head.appendChild(script);
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/publicPath */
/******/ (() => {
/******/ var scriptUrl;
/******/ if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
/******/ var document = __webpack_require__.g.document;
/******/ if (!scriptUrl && document) {
/******/ if (document.currentScript)
/******/ scriptUrl = document.currentScript.src;
/******/ if (!scriptUrl) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ if(scripts.length) {
/******/ var i = scripts.length - 1;
/******/ while (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;
/******/ }
/******/ }
/******/ }
/******/ // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
/******/ // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
/******/ if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
/******/ scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
/******/ __webpack_require__.p = scriptUrl;
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/ __webpack_require__.f.j = (chunkId, promises) => {
/******/ // JSONP chunk loading for javascript
/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ if(true) { // all chunks have JS
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ var loadingEnded = (event) => {
/******/ if(__webpack_require__.o(installedChunks, chunkId)) {
/******/ installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/ if(installedChunkData) {
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/ var realSrc = event && event.target && event.target.src;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ installedChunkData[1](error);
/******/ }
/******/ }
/******/ };
/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/ }
/******/ }
/******/ }
/******/ };
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ // no on chunks loaded
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkwebpacktest"] = self["webpackChunkwebpacktest"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/
/******/ })()
;
</code></pre>
<p>
相比正常打包產出的結果,隨選載入下包裝的產出結果變化較大,也更加複雜,下面歸納了兩點變化。
</p>
<ul>
<li>多了一個 __webpack_require__.e</li>
<li>多了一個 webpackJsonp。</li>
</ul>
<p>
其中, __webpack_require__.e 實現非常重要,它初始化了一個 Promise 陣列, 使用
Promise.all() 非同步插入 script 指令稿;webpackJsonp 會掛載到全域物件 window
上,進行模組安裝。
</p>
<p>
熟悉 webpack 的可能會知道 CommonsChunkPlugin 外掛程式(在webpackV4
版本中已經被取代),這個外掛程式用來分割協力廠商依賴或公共函數庫的式,將業務邏輯和穩定的函數庫指令稿分離,以達到最佳化程式體積、合理使用快取的目的。實際上,這樣的想法和上述隨選載入的想法不謀而合,實作方式也一致。可以推測,開發者使用
CommonsChunkPlugin 外掛程式對程式進行包裝後的結構和上面的程式結構類似,都存在
webpack_require.e 和
webpackJsonp,因為分析公共程式和非同步載入本質上都是建置時進行程式分割,再在必要時進行載入。實作方式可以觀察
webpack_require.e 和 webpackJsonp 。
</p>
<p>
至此,分析了業務中幾乎所有的包裝方式及 webpack
產出結果。雖然這些內容較為晦澀,原始程式冗長而難以閱讀,但是這對了解 webpack
內的工作原理,以及撰寫 webpack loader、webpack
外掛程式意義重大。只有分析過這些最基本的編譯後的程式,才能對上線後的程式品質做到心裡有底。在出現問題時,能夠幫輕就熟。這也是進階
Web 工程師所必備的素養。
</p>
<p>
細節實現相對包裝思想設計來說並沒有那麼重要。也許試著去設計一個模組系統,了解一下
require.js 或 sea.js 的實現,就不會覺得這些內容那麼高深了。
</p>
<h3>webpack 工作基本原理</h3>
<p>webpack 工作流程圖</p>
<img src="./assets/image/1-6-8/1.jpeg" alt="" />
<p>簡單歸納起來,流程如下:</p>
<ul>
<li>
首先:webpack 會讀取專案中由開發者定義的 webpack.config.js 設定檔,或從 shell
敘述中獲得必要的參數。這是 webpack 內部接收業務設定資訊的
方式。這樣就完成了設定讀取的初步工作。
</li>
<li>
接著,將所需的 webpack 外掛程式產生實體,在 webpack
事件流上掛載外掛程式鉤子,這樣在合適的建置過程中,外掛程式就具備了改動產出結果的能力。
</li>
<li>
同時,根據設定所定義的入口檔案,從入口檔案(可以不只一個)開始,進行依賴收集,對所有依賴的檔案進行編譯,這個編譯過程依賴
loaders,不同類型的檔案根據開發者定義的不同loader 進行解析。編譯好的內容使用 acorn
或其他抽象語法樹能力,解析產生抽象語法樹,分析檔案依賴關係,將不同模組化語法(如
require)等取代為 __webpack_require__,即使用 webpack 自己的載入器進行模組化實現。
</li>
<li>上述步驟完成後,產出結果,根據開發者設定,將結果包裝到對應目錄。</li>
</ul>
<p>
值得一提的是,在整個包裝過程中,webpack
和外掛程式都採用以事件流為基礎的發佈/訂閱模式,監聽某些關鍵過程,並在這些環節中執行外掛程式任務。最後,所有檔案的編譯和轉化都已經完成,輸出最後資源。
</p>
<p>
如果深入剖析原始程式,則上述過程可以用更加專業的術語歸納為:模組會經歷載入(loaded)、封存(sealed)、最佳化(optimized)、分段(chunked)、雜湊(hashed)和重新建立(restored)這幾個經典步驟。
</p>
<h4>抽象語法樹</h4>
<p>
在電腦科學中,抽象語法樹(Abstract Syntax Tree ,AST)
是原始程式語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始程式中的一種結構和表達。
</p>
<p>
之所以說語法是抽象的,是因為這裡的語法並不會表示出真寶語法中出現的每個細節。舉例來說,類似
if-condition-then 這樣的條件跳躍陳述式可以用帶有兩個分支的節點來表示。
</p>
<p>
AST 並不會被電腦所識別,更不會被執行,它是對裡式語言的一種表達,為程式分析提供了基礎。
webpack 將檔案轉換成 AST
的目的就是方便開發者分析模組檔案中的關鍵資訊。這樣一來,就可以知曉開發者到底寫了什麼東西,也就可以根據這些寫出的東西進行分析和擴充。在程式層面,可以把
AST 了解為一個object。舉例來說,下面這句簡單的設定陳述式:
</p>
<pre><code class="language-js">
var ast = 'AST demo'
</code></pre>
<p>轉為 AST 後的程式如下所示。</p>
<pre><code class="language-js">
{
"type": "Program",
"start": 0,
"end": 20,
"body": [{
"type": "VariableDeclaration",
"start": 0,
"end": 20,
"declarations": [{
"type": "VariableDeclarator",
"start": 4,
"end": 20,
"id": {
"type": "Identifier",
"start": 4,
"end": 7,
"name": "ast"
},
"init": {
"type": "Literal",
"start": 10,
"end": 20,
"value": "AST demo",
"raw": "'AST demo'",
}
}],
"kind": "var"
}],
"sourceType": "module"
}
</code></pre>
<p>
從以上程式中可以看出,AST
結果精確地表明了這是一筆變數宣告敘述,敘述起始於哪裡、設定值結果是什麼等資訊都被表達出來。
</p>
<p>有了這樣的語法樹,開發者便可以針對原始檔案進行一些分析、加工或轉換操作了。</p>
<h3>compiler 和 compilation</h3>
<p>
compiler 和 compilation 這兩個物件是 webpack 核心原理中最重要的概念。它們是了解 webpack
工作原理、loader 和外掛程式工作的基礎。
</p>
<p>
compiler 物件:它的實例包含了完整的 webpack設定,且全域只有一個compiler
實例,因此它就像webpack 的骨架或神經中樞。當外掛程式被產生實體的時候,就會收到一個
compiler 物件,透過這個物件可以造訪 webpack 的內部環境。
</p>
<p>
compilation 物件:當 webpack 以開發模式執行時期,每當檢測到檔案變化時,一個新的
compilation
物件就會被建立。這個物件包含了目前的模組資源、編譯產生資源、變化的檔案等資訊。也就是說,所有建置過程中讀取設定產生的建置資料都會被儲存在該物件上,它也掌控著建置過程中的每一個環節。該物件還提供了很多事件回呼供外掛程式做擴充。
</p>
<p>兩者的關係可以透過圖2 來說明。</p>
<img src="./assets/image/1-6-8/2.jpeg" alt="" />
<p>
webpack 的建置過程是透過 compiler 控制流程,透過 compilation
進行程式解析的。在開發外掛程式時,我們可以從 compiler 物件中獲得所有與 webpack
主環授相關的內容,包含事件鉤子。
</p>
<p>
compiler 物件和 compilation 物件都繼承自 tapable
函數庫,該函數庫曝露了所有和事件相關的發佈/訂閱的方法。webpack 中以事件流為基礎的 tapable
函數庫不僅能確保外掛程式的有序性,還能使整個系統擴充性更好。
</p>
<h3>撰寫 webpack loader</h3>
<p>
熟悉了概念,下面就來進行實戰,了解如何撰寫一個 webpack loader。事實上,在 webpack
中,loader 是真正發生魔法的階段:Babel 將 ES Next 編譯成 ES5,sass-loader 將 SCSS/Sass
編譯成 CSS,等等,都是由相關的 loader 或 plugin 完成的。因此,從直觀上了解,loader
的工作就是接收原始檔案,對原始檔案進行處理,並傳回編譯後的檔案,如圖3所示。
</p>
<img src="./assets/image/1-6-8/3.jpeg" alt="" />
<p>
可以看到,loader
秉承了單一職責,完成了最小單元的檔案轉換。當然,一個原始檔案可能需要經歷多步轉換才能正常使用,舉例來說,Sass
檔案會先透過 sass-loader 輸出 CSS,之後將內容交給 css-loader 處理,甚至還需要將 css-loader
輸出的內容交給 style-loader 處理,並轉換成透過指令稿載入的 JavaScript 程式,使用方式如下。
</p>
<pre><code class="language-js">
module.exports = {
//...
module: {
rules: [{
test:/\.less$/,
use: [{
loader:'style-loader' // 透過 Javascript 字串建立 style node
},{
loader:'css-loader' // 編譯 CSS 使其符合 CommonJS 標準
},{
loader:'less-loader'// 將 Less編譯為 CSS
}]
}]
}
}
</code></pre>
<p>
當串聯地呼叫多個 loader 去轉換一個檔案時,每個 loader 都會鏈式地循序執行。在 webpack
中,在同一檔案存在多個比對 loader 的情況下,各個 loader 的執行過程會遵循以下原則。
</p>
<ul>
<li>
loader 的執行順序和設定順序是相反的,即設定的最後一個 loader 最先執行,第一個 loader
最後執行。
</li>
<li>
第一個執行的 loader 接收原始檔案中的內容作為參數,其他 loader 接收前一個執行的 loader
的傳回值作為參數。最後執行的 loader 會傳回最後結果。
</li>
</ul>
<p>下圖所示的流程就對應了上面程式中的設定內容。</p>
<img src="./assets/image/1-6-8/4.jpeg" alt="" />
<p>
因此,在開發一個 loader 時,只需關心輸入和輸出,但需要注意保持其職責的單一性。
不難了解,loader 的本質就是函數,其最簡單的結構如下所示。
</p>
<pre><code class="language-js">
module.exports = function(source){
// some magic...
return content
}
</code></pre>
<p>
loader 就是一個以 CommonJS
標準為基礎的函數模組,它接收內容(這裡的內容可能是原始檔案,也可能是經過其他 loader
處理後的結果),並傳回新的內容。
</p>
<p>
更進一步,我們知道在設定 webpack 時,可以對 loader 增加一些設定,例如著名的 babel-loader
的簡單設定,如下所示。
</p>
<pre><code class="language-js">
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"plugins": [
"dynamic-import-webpack"
]
}
}]
}
</code></pre>
<p>
這樣一來,上文中簡單的 loader 寫法便不能滿足需求了,因為撰寫 loader 時,除了撰寫 source
內容,還需要根據開發者設定的 options
資訊進行建置訂製化處理,以輸出最後的結果。那麼,如何取得 options 呢?這時就需要用到
loader-utils 模組了。
</p>
<pre><code class="language-js">
const loaderUtils = require("loader-utils")
module.exports = function(source) {
// 取得開發者設定的 options
const options = loaderUtils.getOptions(this)
// some magic...
return content
}
</code></pre>
<p>
另外,對於 loader 傳回的內容,在實際開發中,單純對 content
進行改寫並傳回改寫後的內容,也許是不夠的。
</p>
<p>
舉例來說,我們想對 loader 處理過程中的錯誤進行捕捉,或想匯出 sourceMap
等資訊時,該如何做呢?
</p>
<p>
這種情況需要用 loader 中的 this.callback 來傳回內容。this.callback 中可以傳入 4
個參數,分別如下。
</p>
<ul>
<li>error: 當 loader 出錯時向外拋出一個 error。</li>
<li>content: 經過 loader 編譯後需要匯出的內容。</li>
<li>sourceMap: 為方便偵錯編譯後的 source map。</li>
<li>
ast: 本次編譯產生的抽象語法樹。之後執行的 loader 可以直接使用這個 AST ,進而省去重複產生
AST 的過程。
</li>
</ul>
<p>
使用 this.callback 後,我們的 loader
程式就變得更加複雜了,同時能夠處理更加多樣的需求,舉例來說,下面的程式可用於取得開發者傳入的設定資訊,並根據資訊做出處理。
</p>
<pre><code class="language-js">
module.exports = function (source) {
//取得開發者設定的 options
const options = loaderUtils.getOptions(this)
// some magic...
// return content
this.callback(null, content)
}
</code></pre>
<p>
注意,當使用 this.callback傳 回內容時,該 loader 必須傳回 undefined ,這樣webpack 就知道該
loader 傳回的結果在 this.callback 中,而不在 return 中。
</p>
<p>
這裡的 this 指向誰?事實上,這裡的 this 指向的是一個叫 loaderContext 的 loader-runner
特有物件。如果根究底,就要細讀 webpack loader 部分的相關原始程式了。
</p>
<p>
預設情況下,webpack 傳給 loader 的內容來源都是 UTF-8 格式編碼的字串。 但 file-loader
這個常用的 loader 不是處理文字檔的,而是處理二進位檔案的,在這種情況下,可以透過 source
instanceof Buffer === true 來判斷內容來源類型,範例如下。
</p>
<pre><code class="language-js">
module.exports = function (source) {
source instanceof Buffer === true
return source
}
</code></pre>
<p>如果自訂的 loader 也會傳回二進位檔案,則需要在檔案中顯性註明,如下所示。</p>
<pre><code class="language-js">
module.exports.raw = true
</code></pre>
<p>
當然,還會有使用非同步 loader 的情況,即並不能同步完成對 source
的處理的情況,這時使用簡單的 async-await即可,程式如下。
</p>
<pre><code class="language-js">
module.exports = async function(source){
function timeout(delay){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(source)
}, delay)
})
}
const content = await timeout(1000)
this.callback(null, content)
}
</code></pre>
<p>
另一種非同步 loader 的解決方案是使用 webpack 提供的 this.async,呼叫 this.async
會傳回一個回呼函數,可以在非同步作業完成後進行呼叫。上面的範例程式可以改寫為以下形式。
</p>
<pre><code class="language-js">
module.exports = async function(source){
function timeout(delay){
return new Promise((resolve, reject) => {
setTimeout(() =>{
resolve(source)
}, delay)
});
}
const callback = this.async()
timeout(1000).then(data =>{
callback(null, data)
})
}
</code></pre>
<p>
實際上,less-loader,翻看其原始程式,就能發現它的核心是利用 Less 這個函數庫來解析 less
樣式程式,less 函數庫解析後會傳回一個 Promise,因此 less-loader 是非同步的,其正是運用了
this.async() 來實現的。
</p>
<p>
工趕師想要進階,就一定要學以致用,解決實際問題。下面就來撰寫一個 path-replace-loader
來實際演練一下。這個loader 將允許把 require 敘述中的 base path 取代為動態指定的
path,使用和設定方式如下。
</p>
<pre><code class="language-js">
module.exports = {
module: {
rules: [{
test: /\.js$/,
loader: 'path-replace-loader',
exclude: /(mode_modules)/,
options: {
path:'ORIGINAL_PATH'/,
replacePath: 'REPLACE_PATH'
}
}]
}
}
</code></pre>
<p>根據上面所介紹的內容,列出 path-replace-loader 原始程式,如下所示。</p>
<pre><code class="language-js">
const fs = require('fs')
const loaderUtils = require('loader-utils')
module.exports = function (source) {
this.cacheable && this.cacheable()
const callback = this.async()
const options = loaderUtils.getOptions(this)
if(this.resourcePath.indexOf(options.path)>-1){
const newPath = this.resourcePath.replace(options. path, options.replacePath)
fs.readFile(newPath, (err, data) => {
if (err) {
if (err.code === 'ENOENT') return callback(null, source)
return callback(err)
}
this.addDependency(newPath)
callback (null, data)
})
} else {
callback (null, source)
}
}
module.exports.raw = true
</code></pre>
<p>
這只是一個簡單的實例,但卻涵蓋了撰寫 loader
時需要注意的不少內容,下面就來膳單分析一下。由於以上所撰寫的是一個非同立 loader
因此可以使用下面的傳回方式。
</p>
<pre><code class="language-js">
const callback = this.async()
// ...
callback(null, data)
</code></pre>
<p>
透過以下敘述,可以取得開發者的設定資訊,並透過比較開發者設定的路徑與
this.resourcePath(目前資源檔路徑)來進行路徑取代。
</p>
<pre><code class="language-js">
const options = loaderUtils.getOptions(this)
// ...
const newPath = this.resourcePath.replace(options.path, options.replacePath)
</code></pre>
<p>該實例對錯誤的處理也很簡單:如果新的目標路徑檔案不存在,則傳回原路徑檔案,程式如下。</p>
<pre><code class="language-js">
if(err.code === 'ENOENT') return callback(null, source)
</code></pre>
<p>其他錯誤也一併是透過 return callback(err) 抛出的。</p>
<p>
該實例的主邏輯使用了 this.addDependency(newPath) 將新的檔案加入 webpack 依賴中,並透過
callback(null, data) 傳回內容。