forked from Louiszhai/Louiszhai.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
11485 lines (11249 loc) · 764 KB
/
atom.xml
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>louis blog</title>
<subtitle>坚持原著, 深度思考, 力求简单通俗叙事</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://louiszhai.github.io/"/>
<updated>2019-12-02T04:11:34.106Z</updated>
<id>http://louiszhai.github.io/</id>
<author>
<name>路易斯</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>webpack4编译代码如何完美适配IE内核</title>
<link href="http://louiszhai.github.io/2019/12/02/webpack4.ie/"/>
<id>http://louiszhai.github.io/2019/12/02/webpack4.ie/</id>
<published>2019-12-02T03:29:45.000Z</published>
<updated>2019-12-02T04:11:34.106Z</updated>
<content type="html"><![CDATA[<p>为了更好的帮助外部合作渠道开发游戏的营销活动,我们开发了一款 JS SDK。该 SDK 采用 webpack4 编译打包,用于支持移动端 H5 活动快速开发,随着业务拓展,端游活动的开发也需要支持,但<strong>由于一些用户 PC 系统版本较低,部分端游 webview 又采用默认 IE 内核、以及用户使用 IE 浏览器参与活动等原因、导致这些用户无法参与到活动中来</strong>,从而影响活动效果,那么,该如何完美的适配 IE 内核呢?</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie.icon.png" alt=""></p>
<a id="more"></a>
<p>对于 JS SDK 而言,适配 IE 内核至少面临 5 大难题:</p>
<ol>
<li>IE9及以下,XMLHttpRequest 不支持跨域请求,XDomainRequest 不支持 cookie 传递;</li>
<li>IE8 不遵循 W3C 规范,不支持 ECMAScript 5.1, CSS3 支持性低;</li>
<li>IE7 缺少更多 JS API 支持,postMessage 也不支持;</li>
<li>从 webpack2 起,IE8 及以下版本便不被支持;</li>
<li>SDK 提供的选择器引擎不支持 IE8、IE7;</li>
</ol>
<p>实际上,以上 5 大难题,并非全部,只是诸多难题中较为突出的部分,下面会重点来讲。</p>
<p>由于IE7、IE8、IE9 内核差异大,适配难度较大,所以我先从 IE9 开始适配,然后是 IE8、IE7。</p>
<h3 id="代码在IE9下运行"><a href="#代码在IE9下运行" class="headerlink" title="代码在IE9下运行"></a><strong>代码在IE9下运行</strong></h3><p>webpack4 打包的代码,在 IE9 下直接运行,果然,立即报错:提示 symbol 未定义,当然错误不止一个,这里不一一列举。</p>
<p>最终配置 .babelrc 如下所示,代码基本能在IE9上运行。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie01.png" alt=".babelrc配置"></p>
<p>如上图,transform-es2015-typeof-symbol 插件解决了symbol 未定义的问题。</p>
<p>这里有个问题需要说明下,为什么不使用 babel-polyfill,而使用 transform-runtime,有两点原因:</p>
<ol>
<li>babel-polyfill size 太大,达到了 60k,sdk 不能接受这么大的垫片包;</li>
<li>babel-polyfill 会污染全局变量,而且一旦 sdk 中引入了 babel-polyfill,使用 sdk 的项目,在不知情的情况下再次引入了 babel-polyfill, 有可能导致冲突;</li>
</ol>
<p>基于这两点,最终我使用了 transform-runtime 插件。</p>
<p>另外,稍微注意下,IE9 下 console 对象在不开 dev tool 时,并不存在,也要兼容。</p>
<h3 id="IE9如何支持跨域请求"><a href="#IE9如何支持跨域请求" class="headerlink" title="IE9如何支持跨域请求"></a><strong>IE9如何支持跨域请求</strong></h3><p>代码能在IE9上运行,起码开了个好头。接下来要面对的是第一个大难点:<strong>IE9及以下版本,XMLHttpRequest 不支持跨域请求,XDomainRequest 虽然支持跨域,却又不支持 cookie 传递</strong>。</p>
<p>解决这个问题之前,不得不提下 xhr 的发展历史。 </p>
<p>xhr 一共有两级标准,早在 IE5,微软就支持了 xhr1 标准,直到 IE10,xhr2 标准才得到支持。</p>
<p>xhr1 有如下缺点:</p>
<ul>
<li>仅支持文本数据传输,无法传输二进制数据</li>
<li>传输数据时,没有进度信息提示,只能提示是否完成</li>
<li>受浏览器 同源策略 限制,<strong>只能请求同域资源</strong></li>
<li>没有超时机制,超时了没办法处理</li>
</ul>
<p>很明显,xhr1 无法支持跨域请求。</p>
<p>xhr2 针对 xhr1 的缺点做了如下改进:</p>
<ul>
<li>支持二进制数据,可以上传文件,可以使用 FormData 对象管理表单</li>
<li>提供进度提示,可通过 xhr.upload.onprogress 事件回调方法获取传输进度</li>
<li>依然受 同源策略 限制,这个安全机制不会变。<strong>xhr2 新提供 Access-Control-Allow-Origin 等 headers,设置为 * 时表示允许任何域名请求,从而支持跨域 CORS 访问</strong></li>
<li>可以设置 timeout 及 ontimeout,方便设置超时时长和超时后续处理</li>
</ul>
<p>虽然,IE5 - IE9 都不支持 xhr2,但微软并没有闲着,而是在 IE8、IE9 中推出了支持 CORS 请求的 XDomainRequest,很遗憾,这是一个非成熟的解决方案,它有主要有以下限制:</p>
<ul>
<li>仅可用于发送 GET 和 POST 请求</li>
<li><strong>不支持跨域传输 cookie</strong></li>
<li>只能设置请求头的 Content-Type 字段,且不能访问响应头信息</li>
</ul>
<p>更多限制,不妨参考 <a href="https://blogs.msdn.microsoft.com/ieinternals/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds/" target="_blank" rel="external">XDomainRequest – Restrictions, Limitations and Workarounds</a>。</p>
<p>不能跨域传输 cookie,登录态怎么校验?到目前为止,通过纯 api 的方式,还没有找到跨域解决方案。最终我使用了 flash 的插件,在 IE9 及以下版本浏览器中加载 flash 插件,借助 flash 去发送跨域请求。</p>
<p>那么,是不是说 IE10、IE11 等浏览器 xhr 就没有问题了?并不尽然。xhr 规范细节一直在变化,参照 <a href="https://xhr.spec.whatwg.org/,最近一次更新是今年的" target="_blank" rel="external">https://xhr.spec.whatwg.org/,最近一次更新是今年的</a> 9 月 24 日,IE 不可能根据规范实时调整实现。即便是 IE11,它参照的也是 <a href="https://www.w3.org/TR/2011/WD-XMLHttpRequest2-20110816/#the-withcredentials-attribute" target="_blank" rel="external">w3c 2011</a>,甚至更早的规范,以下是规范的一段描述:</p>
<blockquote>
<p>On setting the <code>withCredentials</code> attribute these steps must be run:</p>
<ol>
<li>If the state is not <a href="https://www.w3.org/TR/2011/WD-XMLHttpRequest2-20110816/#dom-xmlhttprequest-opened" target="_blank" rel="external">OPENED</a> raise an <code>INVALID_STATE_ERR</code> exception and terminate these steps.</li>
<li>If the <a href="https://www.w3.org/TR/2011/WD-XMLHttpRequest2-20110816/#send-flag" target="_blank" rel="external"><code>send()</code> flag</a> is true raise an <code>INVALID_STATE_ERR</code> exception and terminate these steps.</li>
<li>If the <a href="https://www.w3.org/TR/2011/WD-XMLHttpRequest2-20110816/#anonymous-flag" target="_blank" rel="external">anonymous flag</a> is true raise an <code>INVALID_ACCESS_ERR</code> exception and terminate these steps.</li>
<li>Set the <code>withCredentials</code> attribute’s value to the given value.</li>
</ol>
</blockquote>
<p>这意味着,IE11 中,readyState 值为 <code>OPENED</code> 之前(即 open 方法调用前),为 xhr 对象设置 withCredentials 属性就会抛出 <code>INVALID_STATE_ERR</code> 错误。实际开发中,timeout 属性也必须在 open 方法之后调用。</p>
<h3 id="打包文件支持IE8"><a href="#打包文件支持IE8" class="headerlink" title="打包文件支持IE8"></a><strong>打包文件支持IE8</strong></h3><p>接下来,第二个需要解决的难题是: 从 webpack2 起,IE8 及以下版本便不被支持。为此我耗费了非常多的时间。</p>
<p>第一次在 IE8 上运行 webpack4 打包代码时,出现了各种各样的错误,远比 IE9 上遇到的多得多,挑重点讲,就是 IE8 下 default、catch 是关键字,而 webpack 打包代码,几乎无处没有 default,而用到 Promise 的地方,大多都会使用 catch 回调。</p>
<p>我们都知道,babel 将 es6 转成了 es5, 而 <code>es3ify-loader</code> 则可以继续将 es5 转成 es3。</p>
<pre><code class="js">function(t) { return t.default; } // 编译前
function(t) { return t["default"]; } // 编译后
{ catch:function(t){} } // 编译前
{ "catch":function(t){} } // 编译后
</code></pre>
<p>借助 <code>es3ify-loader</code> ,default、catch 等关键字编译后会被加上引号,避免 IE8 报缺少标识符错误。它的配置如下所示:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie03.png" alt="crossdomain.xml配置"></p>
<p>重新编译后运行,还是报了缺少标识符的问题。难道没效果?其实不是的。</p>
<p>原来 UglifyJsPlugin 在压缩时,默认将引号都去掉了。<code>es3ify-loader</code> 好不容易将引号加上,UglifyJsPlugin 顺手就移除了,深藏功与名。为此,需要将 <code>compress.properties</code> 设置为 false,避免使用点符号重写属性访问。如下:</p>
<pre><code class="js">t["default"] -> t.default // default true
t["default"] -> t["default"] // set false
</code></pre>
<p>另外,<code>output.quote_keys</code> 也需要设置为 true,从而保留对象 key 的引号,如下。</p>
<pre><code class="js">{ "catch": xx } -> { catch: xx } // default false
{ "catch": xx } -> { "catch": xx } // default false
</code></pre>
<p>重新编译后后运行,又报 “无法设置未定义或 null 引用的属性” 的错误。原来 UglifyJsPlugin 压缩时,<code>ie8</code> 设置默认为 false,这意味着它又顺手去掉了支持 IE8 的代码。那么,最终配置如下:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie04.png" alt="UglifyJsPlugin配置"></p>
<p>实际调试时,不妨增加 <code>mangle: false</code> 配置,关闭变量名混淆,debug 会更友好,更多配置介绍,请戳 <a href="https://github.com/LiPinghai/UglifyJSDocCN/blob/master/README.md" target="_blank" rel="external">UglifyJs 中文文档</a> 。</p>
<p>到此,我们解决了 IE8 不被 webpack 支持的问题。</p>
<h3 id="代码在IE8下运行"><a href="#代码在IE8下运行" class="headerlink" title=" 代码在IE8下运行"></a><strong> 代码在IE8下运行</strong></h3><p>很明显,不太可能这么容易在 IE8 下成功运行项目。这里还有两个大坑。</p>
<ol>
<li>webpack4 打包文件的开始部分就用到了 Function.prototype.bind 方法。</li>
<li>IE8 下 Object.defineProperty 实现与规范不同,它只能设置 dom 元素。</li>
</ol>
<h4 id="提供独立编译的bind垫片"><a href="#提供独立编译的bind垫片" class="headerlink" title="提供独立编译的bind垫片"></a><strong>提供独立编译的bind垫片</strong></h4><p>第一点,无论我在 index.js 入口文件中如何添加 bind 方法的垫片,都毫无效果,依然提示 bind 方法不存在。后来终于在编译文件的头部发现了 t.push.bind 的引用,如下。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie05.png" alt="bind方法被调用"></p>
<p>原来 bind 方法的调用如此之早,此时业务逻辑代码远没有开始执行,也就是说,垫片方法也还没有执行。为了解决这个问题,只能避开 webpack 打包,我不得不额外新增了 polyfill.js,用于承载 bind 方法垫片,然后使用 gulp 压缩 polyfill.js,再合并进 webpack 打包文件。gulp 脚本如下所示:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie06.png" alt="gulp配置"></p>
<h4 id="Object-defineProperty兼容方案"><a href="#Object-defineProperty兼容方案" class="headerlink" title="Object.defineProperty兼容方案"></a><strong>Object.defineProperty兼容方案</strong></h4><p>第二点,Object.defineProperty 缺陷是硬伤,即便引入了 es5-shim 垫片,由于 IE8 不支持访问器属性,依然会抛异常。那么,到底什么地方用到了 Object.defineProperty 呢?</p>
<p>问题出在 babel 身上,通常情况下,babel 对 export 解析不会设置访问器属性,没有问题,如下。</p>
<pre><code class="js">// 编译前
const a = 1;
export default a;
// 编译后
Object.defineProperty(exports, "__esModule", {
value: true
});
var a = 1;
exports.default = a;
</code></pre>
<p>但 babel 会把 export xx from yy 编译成 Object.defineProperty 形式,并且设置访问器属性,如下。</p>
<pre><code class="js">// 编译前
export { loadScript } from './util/loadScript'
// 编译后
Object.defineProperty(exports, "__esModule", {
value: true
});
var _loadScript = require('./util/loadScript');
Object.defineProperty(exports, 'loadScript', {
enumerable: true,
get: function get() {
return _loadScript.loadScript;
}
});
</code></pre>
<p>面对上面编译后的代码,es5-shim/es5-sham 会在如下位置抛出错误。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie07.png" alt="es5-sham报错位置"></p>
<p>当 supportsAccessors 为 false 且 get 或者 set 随便一个存在,即抛出 ERR_ACCESSORS_NOT_SUPPORTED 错误,supportsAccessors 用于判断是否支持访问器属性,它的实现如下:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie08.png" alt="es5-sham报错位置"></p>
<p>稍微转换下,即 <code>supportsAccessors = Object.prototype.hasOwnProperty("__defineGetter__")</code>,而 <code>__defineGetter__</code> IE11 才支持,虽然 IE9、IE10 同样不支持它,但它们已经支持 Object.defineProperty 方法,不需要垫片。另外,es-shim <a href="https://github.com/es-shims/es5-shim#may-fail" target="_blank" rel="external">官方文档</a>也提到了使用 Object.defineProperty 可能会失败。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie09.png" alt="Object.defineProperty可能失败"></p>
<p>如上图,描述符中存在 get 或 set,同时还不支持 defineGetter,将默默失败。</p>
<p>综上,问题出在 export xx from yy 这种写法上,换种写法,先 import 然后再 export 不就行了,当然可以,关键是目前有很多地方都用到了这种写法,贸然改写,然后还要立一个 flag,以后不要这样编写代码巴拉巴拉,确实不太优雅。</p>
<p>有这样的一个 babel 插件—<a href="https://www.npmjs.com/package/babel-plugin-transform-es2015-modules-simple-commonjs" target="_blank" rel="external">transform-es2015-modules-simple-commonjs</a> 可以使用(这个插件也出现在篇首 .babelrc 配置截图中第 20 行),它可以实现简单的 commonjs 语法转换,避开了访问器属性,一举解决了 Object.defineProperty 报错的问题,如下为它的编译结果。</p>
<pre><code class="js">Object.defineProperty(exports, "__esModule", {
value: true
});
var _loadScript = require('./util/loadScript');
exports.default = _loadScript.loadScript;
</code></pre>
<h4 id="垫片大小权衡"><a href="#垫片大小权衡" class="headerlink" title="垫片大小权衡"></a><strong>垫片大小权衡</strong></h4><p>babel 的 transform-runtime 插件虽好,却只会引入 es6 垫片,支持 IE8 还需要额外引入 es5 的垫片 es5-shim,而 es5-shim 又包含了 es5-shim.min.js 和 es5-sham.min.js,对于 IE8 而言,两者都依赖,前者 26k, 后者 6k,共计 32k。同样的问题再次面临抉择,无论如何,不能因为它方便,而去盲目增加 sdk 的 size,事实上,sdk 远远不需要集合型的垫片方案。</p>
<p>所以,我放弃了 es5-shim 垫片,最终选用了司徒正美提供的 <a href="https://github.com/RubyLouvre/object-defineproperty-ie8" target="_blank" rel="external">object-defineproperty-ie8</a> 解决了 Object.defineProperty 可能报错的问题。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie10.png" alt="Object.defineProperty-ie8"></p>
<p>如上图,具体思路就是,先判断是否支持访问器属性,不支持就忽略非 DOM 元素的访问器属性设置,只保证赋值成功,避免了报错导致执行中断。</p>
<p>到这里,webpack 不支持 IE8 的问题,基本解决了,接下来就需要与 IE8、IE7 斗智斗勇了。</p>
<h3 id="IE8-amp-IE7-各种缺陷"><a href="#IE8-amp-IE7-各种缺陷" class="headerlink" title="IE8 & IE7 各种缺陷"></a><strong>IE8 & IE7 各种缺陷</strong></h3><p>对于 IE8 & IE7 而言,由于不遵循 w3c 规范,不支持 ECMAScript 5.1,导致有很多 API 实现与标准不一致或没有实现,以下是 SDK 涉及到的一些缺陷介绍。(如未特别说明,即 IE8、IE7 都适用)</p>
<h4 id="缺少全局方法"><a href="#缺少全局方法" class="headerlink" title="缺少全局方法"></a><strong>缺少全局方法</strong></h4><ol>
<li>不支持 JSON 对象。</li>
<li>不支持 window.getComputedStyle 方法。</li>
<li>IE7 不支持 window.localStorage 方法。</li>
<li>IE7 不支持 window.querySelectorAll 方法。</li>
</ol>
<p>另外,IE8 下,document.head 也没有,需要做如下兼容。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie11.png" alt="document.head"></p>
<h4 id="缺少构造器方法"><a href="#缺少构造器方法" class="headerlink" title="缺少构造器方法"></a><strong>缺少构造器方法</strong></h4><ol>
<li>缺少 Object.keys、Object.create 等方法。</li>
<li>缺少 Array.isArray 方法。</li>
</ol>
<h4 id="原型方法缺陷"><a href="#原型方法缺陷" class="headerlink" title="原型方法缺陷"></a><strong>原型方法缺陷</strong></h4><ol>
<li>缺少 Function.prototype.bind 方法,webpack4 编译代码开头部分就用到了 bind 方法,这个前面解决了。</li>
<li>apply 方法第二个参数不能接受类数组对象,否则会报 “Function.prototype.apply: 缺少 Array 或 arguments 对象” 错误。</li>
<li>Array原型方法如 forEach,map,filter、indexOf … 这些方法都没有。</li>
<li>数组 toString 方法存在 bug,其它浏览器在调用数组的 toString 方法时,如果遇到继承于数组的就直接使用数组的 toString,如果遇到非数组对象时会切换到 Object 的 toString 上;而在 IE8 中只要非数组对象调用数组的 toString 方法,一律报错。</li>
</ol>
<h4 id="事件缺陷"><a href="#事件缺陷" class="headerlink" title="事件缺陷"></a><strong>事件缺陷</strong></h4><ol>
<li><p>只有全局事件,事件没有target属性,兼容如 <code>e = e || window.Event;target = e.target || e.srcElement</code> 。</p>
</li>
<li><p>事件回调内部 this 执行 window。</p>
</li>
<li><p>不支持 addEventListener API,可由 attachEvent 替代。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie12.png" alt="attachEvent"></p>
</li>
<li><p>IE7,不支持 stopPropagation、preventDefault 方法。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie13.png" alt="cancelBubble"></p>
</li>
</ol>
<h4 id="数据类型判断缺陷"><a href="#数据类型判断缺陷" class="headerlink" title="数据类型判断缺陷"></a><strong>数据类型判断缺陷</strong></h4><ol>
<li>typeof 判断原生方法时,会将其当作是 object,而不是 function,如下兼容。</li>
</ol>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie14.png" alt="typeof判断缺陷"></p>
<ol>
<li>不同 IE 版本,基本数据类型判断也存在差异,如下图。</li>
</ol>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie15.png" alt="基本数据类型判断缺陷"></p>
<h4 id="JS-异步加载缺陷"><a href="#JS-异步加载缺陷" class="headerlink" title="JS 异步加载缺陷"></a><strong>JS 异步加载缺陷</strong></h4><p>script load 不执行,需监听 onreadystatechange 事件,判断 readyState 是否等于 loaded 或 complete,兼容如下图。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie16.png" alt="script load兼容"></p>
<h4 id="DOM-API-缺陷"><a href="#DOM-API-缺陷" class="headerlink" title="DOM API 缺陷"></a><strong>DOM API 缺陷</strong></h4><ol>
<li><p>innerHTML 属性对于以下标签是只读的:col、colgroup、frameset、head、html、style、table、tbody、tfoot、thead、title、tr。除此之外,下拉框赋值,也非常困难,虽然其 innerHTML 可以写入,但新增的选项不会生效(清空又可以),推荐使用 options.add 方法 添加新选项,如果还不行,可以使用 outerHTML 赋值。</p>
</li>
<li><p>IE7 不支持 window.Element 对象</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie17.png" alt="Element兜底"></p>
</li>
<li><p>IE7 setAttribute 支持性也有问题</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie18.png" alt="setAttribute兼容"></p>
</li>
</ol>
<h4 id="CSS-API缺陷"><a href="#CSS-API缺陷" class="headerlink" title="CSS API缺陷"></a><strong>CSS API缺陷</strong></h4><ol>
<li>IE9- 不支持 element.classList 属性。</li>
<li>IE8-,CSS 部分属性动态修改后不生效,如:margin-left、margin-top 等。</li>
<li>IE 支持 currentStyle 方法,但只能获取表面的样式,无法获取计算后的,getComputedStyle 垫片也无法准确获取,可采用 element.getBoundingClientRect() 方法获取坐标,然后计算出宽高,如下所示。</li>
</ol>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie19.png" alt="获取宽度"></p>
<h4 id="透明度设置缺陷"><a href="#透明度设置缺陷" class="headerlink" title="透明度设置缺陷"></a><strong>透明度设置缺陷</strong></h4><ol>
<li>不支持 opacity 样式,可由 filter 滤镜替代。</li>
</ol>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie20.png" alt="透明度兼容"></p>
<ol>
<li>不支持 rgba 色值,可由 filter 滤镜替代。</li>
</ol>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie21.png" alt="rgba兼容"></p>
<p>上图中,颜色 “#19000000” 由两部分组成。</p>
<p>第一部是 # 号后面的 19 是 rgba 透明度 0.1 的 IEfilter 值。从 0.1 到 0.9 每个数字对应一个 IEfilter 值。</p>
<p>第二部分是 19 后面的六位。这个是六进制的颜色值,跟 rgb 函数中的取值一致。比如 rgb(0,0,0) 对应 #000000,也就是黑色。</p>
<p>rgba 透明度与 IEfilter 的对应关系如下所示。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie22.png" alt="rgba-IEfilter"></p>
<p>除了透明度外,CSS 动画也需要降级为 gif 实现,如果是 loading 动画,可以使用 <a href="https://loading.io/" target="_blank" rel="external">loading.io</a> 无缝转换。</p>
<p>到这,IE8、IE7 的兼容性问题基本解决,接下来只需要让选择器引擎支持到 IE7,改造便可完成。</p>
<h3 id="选择器引擎开发"><a href="#选择器引擎开发" class="headerlink" title="选择器引擎开发"></a><strong>选择器引擎开发</strong></h3><p>大家都知道,jquery 提供了非常棒的选择器引擎 <a href="https://github.com/jquery/sizzle/" target="_blank" rel="external">sizzle</a>,但它的 min 版本达到了 20 k,有点太大了。</p>
<p>对于 JS SDK 而言, 它不但内部使用了大量选择器,同时还需要将选择器的功能开放给第三方开发者,使其能在不使用 jquery、zepto 等类库的情况下,快速完成基本的 DOM 操作,所以搭载一款轻便的选择器引擎很有必要。</p>
<p>一个简单的选择器引擎,至少具备以下 4 点功能。</p>
<ol>
<li>选择元素,如 <code>$('#div')</code></li>
<li>包装 DOM 对象为 <code>$</code> 实例,如 <code>$(element);</code></li>
<li>包装字符串html模板为 <code>$</code> 实例,如 <code>$('<div></div>')</code></li>
<li>提供选择器 <code>$</code> 实例对象的原型方法,如 <code>$('#div').html('hello world')</code></li>
</ol>
<p>在如今,querySelectorAll 深入人心的背景下,实现一个选择器引擎,并不难,这里我参考的是 <a href="https://github.com/dengzhiqiang/balajs/tree/master/balajs" target="_blank" rel="external">balajs</a>,它主要代码如下所示。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie23.png" alt="balajs"></p>
<p>balajs 有如下 2 个问题:</p>
<ol>
<li>仅仅支持 IE9+ 浏览器。</li>
<li>原型是数组,不是对象,直接使用数组做原型,在 IE8 以下,tostring 方法调用时,会调用数组原型的tostring,从而报错(而标准浏览器,会调用对象的 tostring),由于继承数组,其实例 length 属性设置也会失效。</li>
</ol>
<p>为了解决这 2 个问题,我们需要更换选择器引擎的继承方式,如下代码(参考了 jquery 思想)。</p>
<pre><code class="js">function $(s, context) { // 选择器入口
if (s instanceof init) { // 如果是已经获取到的元素,直接返回
return s;
}
// 结合 balajs 可以先产生 elements 数组
const elements = ... // 先略去,后面讲
// 再将 elements 数组交由 init 方法产生类数组返回值
return new $.fn.init(s ? elements : []);
}
$.fn = {}; // 选择器对象原型由数组改为空对象
const init = $.fn.init = function (selector) {
selector.forEach((ele, i) => { // init 方法用于完成kv赋值操作,并产生类数组对象实例
this[i] = ele;
});
this.length = selector.length;
return this;
}
init.prototype = $.fn; // init 原型指定为 $.fn,使得 init 对象实例能够使用 $.fn 原型中的方法
Object.assign($.fn, { // 往原型中添加方法
click(handle) {...},
append(child) {...},
find(selector) {...},
...
})
</code></pre>
<p>现在,选择器引擎的雏形出来了,只要完成 elements 数组部分,便能 work。</p>
<p>elements 第一步筛选,可以使用上图 balajs 的主要代码,只需要将 querySelectorAll 垫片添加进去即可,如下图,主要修改了红框中的代码,并移除了 s 为 function 时,dom ready 部分实现(sdk 暂时用不到)。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ie25.png" alt="elements获取"></p>
<p>首先奉上 querySelectorAll 垫片。</p>
<pre><code class="js">if (!document.querySelectorAll) { // IE7 中没有 querySelectorAll
var style = document.createStyleSheet();
document.querySelectorAll = function (r, c, i, j, a) {
var a = document.all, c = [], r = r.replace(/\[for\b/gi, '[htmlFor').split(',');
for (i = r.length; i--;) {
style.addRule(r[i], 'k:v');
for (j = a.length; j--;) a[j].currentStyle.k && c.push(a[j]);
style.removeRule(0);
}
return c;
}
}
// 用于在 IE7 浏览器中,支持 Element.querySelectorAll 方法
window.querySelectorAll = (function () {
var idAllocator = 10000;
function qsaWorkerShim(element, selector) {
var needsID = element.id === "";
if (needsID) {
++idAllocator;
element.id = "__qsa" + idAllocator;
}
try {
return document.querySelectorAll("#" + element.id + " " + selector);
}
finally {
if (needsID) {
element.id = "";
}
}
}
function qsaWorkerWrap(element, selector) {
return element.querySelectorAll(selector);
}
return document.createElement('div').querySelectorAll ? qsaWorkerWrap : qsaWorkerShim;
})();
</code></pre>
<p>可以看出,elements 有可能返回类数组对象、NodeList 或 HTMLCollection 对象,第二步便需要将它们转换成数组,然后交由 init 方法处理。鉴于 IE8 apply 的 bug,需要做如下特殊处理。</p>
<pre><code class="js">const isDOM = obj => { // 是否 DOM 元素
if(typeof HTMLElement === 'object') {
return obj instanceof HTMLElement;
} else {
return obj && typeof obj === 'object' && obj.nodeType === 1;
}
};
const isArrayLike = collection => { // 是否类数组
var length = collection.length;
return typeof length == 'number' && length >= 0 && length <= (Math.pow(2, 53) - 1);
}
// IE8中继承数组的对象,toSting时默认会调用数组的toSting方法,这点与标准不一致,导致会报"缺少 Array 对象"错误,因此需要排除数组
const generalObj = elements instanceof Object && !(elements instanceof Array) && elements.toString() === '[object Object]' && !isArrayLike(elements) // 非类数组的普通对象
if (elements === window || elements === document || isDOM(elements) || generalObj) {
elements = [elements];
} else {
// 修复 bug:SCRIPT5028: Function.prototype.apply: 缺少 Array 或 arguments 对象
try {
elements = [].slice.apply(elements, [0]); // 类数组在这里完成转换
} catch (err) {
try {
elements = [].concat.apply([], elements); // NodeList 或 HTMLCollection 在这里完成转换
} catch (e) {
if (elements.length) { // 兜底
var tempArray = [];
for (var i = 0; i < elements.length; i++) {
tempArray[i] = elements[i];
}
elements = tempArray;
} else {
elements = [];
}
}
}
}
</code></pre>
<p>整合上述代码,简单的选择器引擎便开发完成。</p>
<p>到此,营销活动 JS SDK 终于适配完成。SDK 总 size 仅仅增大了 10k,其中 flash 垫片还占了增长的大部分。</p>
<h3 id="如何调试"><a href="#如何调试" class="headerlink" title="如何调试"></a><strong>如何调试</strong></h3><p>除了代码外,IE 内核的适配过程中,调试也是困难重重,调试问题主要集中在以下 4 个方面:</p>
<ol>
<li>如何使用 mac 调试 IE ?</li>
<li>选择哪种操作系统? windows 10 ? Windows 7 ? Windows xp?</li>
<li>选择哪种版本的 IE ?IE11?IE10?IE9?IE8?IE7?</li>
<li>选择什么样的代理? Fiddler ?Charles ? Whistle ?</li>
</ol>
<p>我本地安装的是 Parallels Desktop 虚拟机,基于它,又安装了 win7 sp1 及 win xp 两个操作系统,分别用于测试 IE9、IE10、IE11 及 IE7、IE8。</p>
<p>调试过程中,不要相信 IE 代理模式,效果不太好,很多错误都会被默默吃掉,需要老老实实把 IE 浏览器逐个装一遍,我经常调试完 IE8 发现 IE7 好像有个 bug,然后手动把 IE8 卸载,退回 IE7,测好了再升级,IE7 没有dev tool,调试不太方便,定位不到问题,只能先升到 IE8,这样的过程需要重复很多次。</p>
<p>漫长的调试过程中,代理是非常重要的基础设施,代理频繁断开,或请求不正常转发,https证书问题,都非常消耗时间和精力,上面提到的三个代理软件,我都使用过,Fiddler 很容易断开连接,不建议使用,Charles 兼容性最好,各个版本的 IE 都能正常代理,但配置不是很方便,做为备选,Whistle 配置很方便,基本作为日常代理工具,但在 IE7 及 IE8 下,由于证书问题,导致https 请求不能正常发送。</p>
<p>更多的IE适配问题,欢迎在评论区留言继续交流,谢谢。</p>
]]></content>
<summary type="html">
webpack4编译代码如何完美适配IE内核
</summary>
<category term="Tool" scheme="http://louiszhai.github.io/tags/Tool/"/>
</entry>
<entry>
<title>使用webpack4提升180%编译速度</title>
<link href="http://louiszhai.github.io/2019/01/04/webpack4/"/>
<id>http://louiszhai.github.io/2019/01/04/webpack4/</id>
<published>2019-01-04T02:49:08.000Z</published>
<updated>2019-12-02T03:28:32.899Z</updated>
<content type="html"><![CDATA[<p>对于现在的前端项目而言,编译发布几乎是必需操作,有的编译只需要几秒钟,快如闪电,有的却需要10分钟,甚至更多,慢如蜗牛。特别是线上热修复时,分秒必争,响应速度直接影响了用户体验,用户不会有耐心等那么长时间,让你慢慢编译;如果涉及到支付操作,产品损失更是以秒计,每提前哪怕一秒钟发布,在腾讯海量用户面前,都能挽回不小的损失。不仅如此,编译效率的提升,带来的最直观收益就是,开发效率与开发体验双重提升。</p>
<p>那么,到底是什么拖慢了webpack打包效率,我们又能做哪些提升呢?</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-logo.gif" alt="webpack"></p>
<a id="more"></a>
<p>webpack 是目前非常受欢迎的打包工具,截止6天前,webpack4 已更新至 <code>4.28.3</code> 版本,10 个月的时间,小版本更新达几十次之多,可见社区之繁荣。</p>
<p>webpack4 发布时,官方也曾表示,其编译速度提升了 60% ~ 98%。</p>
<h3 id="天下武功,唯快不破"><a href="#天下武功,唯快不破" class="headerlink" title="天下武功,唯快不破"></a><strong>天下武功,唯快不破</strong></h3><p>由于本地项目升级到 webpack4 有几个月了,为了获得测试数据,手动将 webpack 降级为 3.12.0 版本,其它配置基本不做改动。</p>
<p>测试时,Mac仅运行常用的IM、邮箱、终端、浏览器等,为了尽可能避免插件对数据的影响,我关闭了一些优化插件,只保留常用的loader、js压缩插件。</p>
<p>以下是分别在 [email protected] 及 [email protected] 两种场景下各测 5 次的运行截图。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-0-happyPack-0-dll-0-parallel.3~4.png" alt="webpack3~4x编译速度对比"></p>
<p>数据分析如下(单位ms):</p>
<table>
<thead>
<tr>
<th style="text-align:center"></th>
<th style="text-align:center">第1次</th>
<th style="text-align:center">第2次</th>
<th style="text-align:center">第3次</th>
<th style="text-align:center">第4次</th>
<th style="text-align:center">第5次</th>
<th style="text-align:center">平均</th>
<th style="text-align:center">速度提升</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">webpack3</td>
<td style="text-align:center">58293</td>
<td style="text-align:center">60971</td>
<td style="text-align:center">57263</td>
<td style="text-align:center">58993</td>
<td style="text-align:center">60459</td>
<td style="text-align:center">59195.8</td>
<td style="text-align:center">-</td>
</tr>
<tr>
<td style="text-align:center">webpack4</td>
<td style="text-align:center">42346</td>
<td style="text-align:center">40386</td>
<td style="text-align:center">40138</td>
<td style="text-align:center">40330</td>
<td style="text-align:center">40323</td>
<td style="text-align:center">40704.6</td>
<td style="text-align:center">45%</td>
</tr>
</tbody>
</table>
<p>纯粹的版本升级,编译速度提升为 <code>45%</code>,这里我选取的是成熟的线上运行项目,构建速度的提升只有建立在成熟项目上才有意义,demo 项目由于编译文件基数小,难以体现出构建环境的复杂性,测试时也可能存在较大误差。同时与官方数据的差距,主要是因为基于的项目及配置不同。</p>
<p>无论如何,近 50% 的编译速度提升,都值得你尝试升级 webpack4!当然,优化才刚刚开始,请继续往下读。</p>
<h3 id="新特性"><a href="#新特性" class="headerlink" title="新特性"></a><strong>新特性</strong></h3><p>为了更流畅的升级 webpack4,我们先要了解它。</p>
<p>webpack4 在大幅度提升编译效率同时,引入了多种新特性:</p>
<ol>
<li><p>受 Parcel 启发,支持 0 配置启动项目,不再强制需要 webpack.config.js 配置文件,默认入口 <code>./src/</code> 目录,默认entry <code>./src/index.js</code> ,默认输出 <code>./dist</code> 目录,默认输出文件 <code>./dist/main.js</code>。</p>
</li>
<li><p>开箱即用 WebAssembly,webpack4提供了wasm的支持,现在可以引入和导出任何一个 Webassembly 的模块,也可以写一个loader来引入C++、C和Rust。(注:WebAssembly 模块只能在异步chunks中使用)</p>
</li>
<li><p>提供mode属性,设置为 <code>development</code> 将获得最好的开发体验,设置为 <code>production</code> 将专注项目编译部署,比如说开启 Scope hoisting 和 Tree-shaking 功能。</p>
</li>
<li><p>全新的插件系统,提供了针对插件和钩子的新API,变化如下:</p>
<ul>
<li>所有的 hook 由 hooks 对象统一管理,它将所有的hook作为可扩展的类属性</li>
<li>添加插件时,你需要提供一个名字</li>
<li>开发插件时,你可以选择插件的类型(sync/callback/promise之一)</li>
<li>通过 this.hooks = { myHook: new SyncHook(…) } 来注册hook</li>
</ul>
<p>更多插件的工作原理,可以参考:<a href="https://medium.com/webpack/the-new-plugin-system-week-22-23-c24e3b22e95" target="_blank" rel="external">新插件系统如何工作</a>。</p>
</li>
</ol>
<h3 id="快上车,升级前的准备"><a href="#快上车,升级前的准备" class="headerlink" title="快上车,升级前的准备"></a><strong>快上车,升级前的准备</strong></h3><p>首先,webpack-dev-server 插件需要升级至最新,同时,由于webpack-cli 承担了webpack4 命令行相关的功能,因此 webpack-cli 也是必需的。</p>
<p>与以往不同的是,mode属性必须指定,否则按照 <code>约定优于配置</code> 原则,将默认按照 <code>production</code> 生产环境编译,如下是警告原文。</p>
<blockquote>
<p>WARNING in configuration<br>The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.<br>You can also set it to ‘none’ to disable any default behavior. Learn more: <a href="https://webpack.js.org/concepts/mode/" target="_blank" rel="external">https://webpack.js.org/concepts/mode/</a></p>
</blockquote>
<p>有两种方式可以加入mode配置。</p>
<ul>
<li><p>在package.json script中指定–mode:</p>
<pre><code class="js">"scripts": {
"dev": "webpack-dev-server --mode development --inline --progress --config build/webpack.dev.config.js",
"build": "webpack --mode production --progress --config build/webpack.prod.config.js"
}
</code></pre>
</li>
<li><p>在配置文件中加入mode属性</p>
<pre><code class="js">module.exports = {
mode: 'production' // 或 development
};
</code></pre>
</li>
</ul>
<p>升级至webpack4后,一些默认插件由 optimization 配置替代了,如下:</p>
<ul>
<li>CommonsChunkPlugin废弃,由 optimization.splitChunks 和 optimization.runtimeChunk 替代,前者拆分代码,后者提取runtime代码。原来的CommonsChunkPlugin产出模块时,会包含重复的代码,并且无法优化异步模块,minchunks的配置也较复杂,splitChunks解决了这个问题;另外,将 optimization.runtimeChunk 设置为true(或{name: “manifest”}),便能将入口模块中的runtime部分提取出来。</li>
<li>NoEmitOnErrorsPlugin 废弃,由 optimization.noEmitOnErrors 替代,生产环境默认开启。</li>
<li>NamedModulesPlugin 废弃,由 optimization.namedModules 替代,生产环境默认开启。</li>
<li>ModuleConcatenationPlugin 废弃,由 optimization.concatenateModules 替代,生产环境默认开启。</li>
<li>optimize.UglifyJsPlugin 废弃,由 optimization.minimize 替代,生产环境默认开启。</li>
</ul>
<p>不仅如此,optimization 还提供了如下默认配置:</p>
<pre><code class="js">optimization: {
minimize: env === 'production' ? true : false, // 开发环境不压缩
splitChunks: {
chunks: "async", // 共有三个值可选:initial(初始模块)、async(按需加载模块)和all(全部模块)
minSize: 30000, // 模块超过30k自动被抽离成公共模块
minChunks: 1, // 模块被引用>=1次,便分割
maxAsyncRequests: 5, // 异步加载chunk的并发请求数量<=5
maxInitialRequests: 3, // 一个入口并发加载的chunk数量<=3
name: true, // 默认由模块名+hash命名,名称相同时多个模块将合并为1个,可以设置为function
automaticNameDelimiter: '~', // 命名分隔符
cacheGroups: { // 缓存组,会继承和覆盖splitChunks的配置
default: { // 模块缓存规则,设置为false,默认缓存组将禁用
minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
priority: -20, // 优先级
reuseExistingChunk: true, // 默认使用已有的模块
},
vendors: {
test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
priority: -10
}
}
}
}
</code></pre>
<p>splitChunks是拆包优化的重点,如果你的项目中包含 element-ui 等第三方组件(组件较大),建议单独拆包,如下所示。</p>
<pre><code class="js">splitChunks: {
// ...
cacheGroups: {
elementUI: {
name: "chunk-elementUI", // 单独将 elementUI 拆包
priority: 15, // 权重需大于其它缓存组
test: /[\/]node_modules[\/]element-ui[\/]/
}
}
}
</code></pre>
<p>其更多用法,请参考以上注释或官方文档 <a href="https://webpack.js.org/plugins/split-chunks-plugin/" target="_blank" rel="external">SplitChunksPlugin</a>。</p>
<h3 id="升级避坑指南"><a href="#升级避坑指南" class="headerlink" title="升级避坑指南"></a><strong>升级避坑指南</strong></h3><blockquote>
<p>webpack4不再支持Node 4,由于使用了JavaScript新语法,Webpack的创始人之一,Tobias,建议用户使用Node版本 >= 8.94,以便使用最优性能。</p>
</blockquote>
<p>正式升级后,你可能会遇到各种各样的错误,其中,下面一些问题较为常见。</p>
<p>vue-loader v15 需要在 webpack 中添加 VueLoaderPlugin 插件,参考如下。</p>
<pre><code class="js">const { VueLoaderPlugin } = require("vue-loader"); // const VueLoaderPlugin = require("vue-loader/lib/plugin"); // 两者等同
//...
plugins: [
new VueLoaderPlugin()
]
</code></pre>
<p>升级到 webpack4 后,mini-css-extract-plugin 替代 extract-text-webpack-plugin 成为css打包首选,相比之前,它有如下优势:</p>
<ol>
<li>异步加载</li>
<li>不重复编译,性能更好</li>
<li>更容易使用</li>
</ol>
<p>缺陷,不支持css热更新。因此需在开发环境引入 css-hot-loader,以便支持css热更新,如下所示:</p>
<pre><code class="js">{
test: /\.scss$/,
use: [
...(isDev ? ["css-hot-loader", "style-loader"] : [MiniCssExtractPlugin.loader]),
"css-loader",
postcss,
"sass-loader"
]
}
</code></pre>
<p>发布到生产环境之前,css是需要优化压缩的,使用 optimize-css-assets-webpack-plugin 插件即可,如下。</p>
<pre><code class="js">const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
//...
plugins: [
new OptimizeCssAssetsPlugin({
cssProcessor: cssnano,
cssProcessorOptions: {
discardComments: {
removeAll: true
}
}
})
]
</code></pre>
<h3 id="持续加速"><a href="#持续加速" class="headerlink" title="持续加速"></a><strong>持续加速</strong></h3><p>文章开始,我曾提到,优化才刚刚开始。是的,随着项目越来越复杂,webpack也随之变慢,一定有办法可以进一步压榨性能。</p>
<p>经过很长一段时间的多个项目运行以及测试,以下几点经验<strong>非常有效</strong>。</p>
<ol>
<li><p>缩小编译范围,减少不必要的编译工作,即 modules、mainFields、noParse、includes、exclude、alias全部用起来。</p>
<pre><code class="js">const resolve = dir => path.join(__dirname, '..', dir);
// ...
resolve: {
modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索
resolve('src'),
resolve('node_modules'),
resolve(config.common.layoutPath)
],
mainFields: ['main'], // 只采用main字段作为入口文件描述字段,减少搜索步骤
alias: {
vue$: "vue/dist/vue.common",
"@": resolve("src") // 缓存src目录为@符号,避免重复寻址
}
},
module: {
noParse: /jquery|lodash/, // 忽略未采用模块化的文件,因此jquery或lodash将不会被下面的loaders解析
// noParse: function(content) {
// return /jquery|lodash/.test(content)
// },
rules: [
{
test: /\.js$/,
include: [ // 表示只解析以下目录,减少loader处理范围
resolve("src"),
resolve(config.common.layoutPath)
],
exclude: file => /test/.test(file), // 排除test目录文件
loader: "happypack/loader?id=happy-babel" // 后面会介绍
},
]
}
</code></pre>
</li>
<li><p>想要进一步提升编译速度,就要知道瓶颈在哪?通过测试,发现有两个阶段较慢:① babel 等 loaders 解析阶段;② js 压缩阶段。loader 解析稍后会讨论,而 js 压缩是发布编译的最后阶段,通常webpack需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时。如下图,编译就看似卡住。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-ParallelUglify.png" alt="ParallelUglify"></p>
<p>实际上,搭载 webpack-parallel-uglify-plugin 插件,这个过程可以倍速提升。我们都知道 node 是单线程的,但node能够fork子进程,基于此,webpack-parallel-uglify-plugin 能够把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而实现并发编译,进而大幅提升js压缩速度,如下是配置。</p>
<pre><code class="js">const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// ...
optimization: {
minimizer: [
new ParallelUglifyPlugin({ // 多进程压缩
cacheDir: '.cache/',
uglifyJS: {
output: {
comments: false,
beautify: false
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
}),
]
}
</code></pre>
<p>当然,我分别测试了五组数据,如下是截图:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-0-happyPack-0-dll-1-parallel.3~4.png" alt="ParallelUglifyPlugin插件启用后编译速度分析"></p>
<p>数据分析如下(单位ms):</p>
</li>
</ol>
<table>
<thead>
<tr>
<th style="text-align:center"></th>
<th style="text-align:center">第1次</th>
<th style="text-align:center">第2次</th>
<th style="text-align:center">第3次</th>
<th style="text-align:center">第4次</th>
<th style="text-align:center">第5次</th>
<th style="text-align:center">平均</th>
<th style="text-align:center">速度提升</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">webpack3</td>
<td style="text-align:center">58293</td>
<td style="text-align:center">60971</td>
<td style="text-align:center">57263</td>
<td style="text-align:center">58993</td>
<td style="text-align:center">60459</td>
<td style="text-align:center">59195.8</td>
<td style="text-align:center">-</td>
</tr>
<tr>
<td style="text-align:center">webpack3搭载ParallelUglifyPlugin插件</td>
<td style="text-align:center">44380</td>
<td style="text-align:center">39969</td>
<td style="text-align:center">39694</td>
<td style="text-align:center">39344</td>
<td style="text-align:center">39295</td>
<td style="text-align:center">40536.4</td>
<td style="text-align:center">46%</td>
</tr>
<tr>
<td style="text-align:center">webpack4</td>
<td style="text-align:center">42346</td>
<td style="text-align:center">40386</td>
<td style="text-align:center">40138</td>
<td style="text-align:center">40330</td>
<td style="text-align:center">40323</td>
<td style="text-align:center">40704.6</td>
<td style="text-align:center">-</td>
</tr>
<tr>
<td style="text-align:center">webpack4搭载ParallelUglifyPlugin插件</td>
<td style="text-align:center">31134</td>
<td style="text-align:center">29554</td>
<td style="text-align:center">31883</td>
<td style="text-align:center">29198</td>
<td style="text-align:center">29072</td>
<td style="text-align:center">30168.2</td>
<td style="text-align:center">35%</td>
</tr>
</tbody>
</table>
<p> 搭载 webpack-parallel-uglify-plugin 插件后,webpack3 的构建速度能够提升 46%;即使升级到 webpack4 后,构建速度依然能够进一步提升 35%。</p>
<ol>
<li><p>现在我们来看看,loader 解析速度如何提升。同 webpack-parallel-uglify-plugin 插件一样,HappyPack 也能实现并发编译,从而可以大幅提升 loader 的解析速度, 如下是部分配置。</p>
<pre><code class="js">const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const createHappyPlugin = (id, loaders) => new HappyPack({
id: id,
loaders: loaders,
threadPool: happyThreadPool,
verbose: process.env.HAPPY_VERBOSE === '1' // make happy more verbose with HAPPY_VERBOSE=1
});
</code></pre>
<p>那么,对于前面 <code>loader: "happypack/loader?id=happy-babel"</code> 这句,便需要在 plugins 中创建一个 <code>happy-babel</code> 的插件实例。</p>
<pre><code class="js">plugins: [
createHappyPlugin('happy-babel', [{
loader: 'babel-loader',
options: {
babelrc: true,
cacheDirectory: true // 启用缓存
}
}])
]
</code></pre>
<p>如下,happyPack开启了3个进程(默认为CPU数-1),运行过程感受下。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-happyPack.gif" alt="happyPack"></p>
<p>另外,像 vue-loader、css-loader 都支持 happyPack 加速,如下所示。</p>
<pre><code class="json">plugins: [
createHappyPlugin('happy-css', ['css-loader', 'vue-style-loader']),
new HappyPack({
loaders: [{
path: 'vue-loader',
query: {
loaders: {
scss: 'vue-style-loader!css-loader!postcss-loader!sass-loader?indentedSyntax'
}
}
}]
})
]
</code></pre>
<p>基于 webpack4,搭载 webpack-parallel-uglify-plugin 和 happyPack 插件,测试截图如下:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-1-happyPack-0-dll-1-parallel.w4.png" alt="搭载两款插件后编译速度分析"></p>
<p>数据分析如下(单位ms):</p>
</li>
</ol>
<table>
<thead>
<tr>
<th style="text-align:center"></th>
<th style="text-align:center">第1次</th>
<th style="text-align:center">第2次</th>
<th style="text-align:center">第3次</th>
<th style="text-align:center">第4次</th>
<th style="text-align:center">第5次</th>
<th style="text-align:center">平均</th>
<th style="text-align:center">速度提升</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">仅搭载ParallelUglifyPlugin</td>
<td style="text-align:center">31134</td>
<td style="text-align:center">29554</td>
<td style="text-align:center">31883</td>
<td style="text-align:center">29198</td>
<td style="text-align:center">29072</td>
<td style="text-align:center">30168.2</td>
<td style="text-align:center">35%</td>
</tr>
<tr>
<td style="text-align:center">搭载ParallelUglifyPlugin 和 happyPack</td>
<td style="text-align:center">26036</td>
<td style="text-align:center">25884</td>
<td style="text-align:center">25645</td>
<td style="text-align:center">25627</td>
<td style="text-align:center">25794</td>
<td style="text-align:center">25797.2</td>
<td style="text-align:center">17%</td>
</tr>
</tbody>
</table>
<p> 可见,在搭载 webpack-parallel-uglify-plugin 插件的基础上,happyPack 插件依然能够提升 17% 的编译速度,实际上由于 sass 等 loaders 不支持 happyPack,happyPack 的性能依然有提升空间。更多介绍不妨参考 <a href="http://taobaofed.org/blog/2016/12/08/happypack-source-code-analysis/" target="_blank" rel="external">happypack 原理解析</a>。</p>
<ol>
<li><p>我们都知道,webpack打包时,有一些框架代码是基本不变的,比如说 babel-polyfill、vue、vue-router、vuex、axios、element-ui、fastclick 等,这些模块也有不小的 size,每次编译都要加载一遍,比较费时费力。使用 DLLPlugin 和 DLLReferencePlugin 插件,便可以将这些模块提前打包。</p>
<p>为了完成 dll 过程,我们需要准备一份新的webpack配置,即 webpack.dll.config.js。</p>
<pre><code class="js">const webpack = require("webpack");
const path = require('path');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const dllPath = path.resolve(__dirname, "../src/assets/dll"); // dll文件存放的目录
module.exports = {
entry: {
// 把 vue 相关模块的放到一个单独的动态链接库
vue: ["babel-polyfill", "fastclick", "vue", "vue-router", "vuex", "axios", "element-ui"]
},
output: {
filename: "[name]-[hash].dll.js", // 生成vue.dll.js
path: dllPath,
library: "_dll_[name]"
},
plugins: [
new CleanWebpackPlugin(["*.js"], { // 清除之前的dll文件
root: dllPath,
}),
new webpack.DllPlugin({
name: "_dll_[name]",
// manifest.json 描述动态链接库包含了哪些内容
path: path.join(__dirname, "./", "[name].dll.manifest.json")
}),
],
};
</code></pre>
<p>接着, 需要在 package.json 中新增 dll 命令。</p>
<pre><code class="json">"scripts": {
"dll": "webpack --mode production --config build/webpack.dll.config.js"
}
</code></pre>
<p>运行 <code>npm run dll</code> 后,会生成 <code>./src/assets/dll/vue.dll-[hash].js</code> 公共js 和 <code>./build/vue.dll.manifest.json</code> 资源说明文件,至此 dll 准备工作完成,接下来在 webpack 中引用即可。</p>
<pre><code class="js">externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'vuex',
'elemenct-ui': 'ELEMENT',
'axios': 'axios',
'fastclick': 'FastClick'
},
plugins: [
...(config.common.needDll ? [
new webpack.DllReferencePlugin({
manifest: require("./vue.dll.manifest.json")
})
] : [])
]
</code></pre>
<p>dll 公共js轻易不会变化,假如在将来真的发生了更新,那么新的dll文件名便需要加上新的hash,从而避免浏览器缓存老的文件,造成执行出错。由于 hash 的不确定性,我们在 html 入口文件中没办法指定一个固定链接的 script 脚本,刚好,add-asset-html-webpack-plugin 插件可以帮我们自动引入 dll 文件。</p>
<pre><code class="js">const autoAddDllRes = () => {
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
return new AddAssetHtmlPlugin([{ // 往html中注入dll js
publicPath: config.common.publicPath + "dll/", // 注入到html中的路径
outputPath: "dll", // 最终输出的目录
filepath: resolve("src/assets/dll/*.js"),
includeSourcemap: false,
typeOfAsset: "js" // options js、css; default js
}]);
};
// ...
plugins: [
...(config.common.needDll ? [autoAddDllRes()] : [])
]
</code></pre>
<p>搭载 dll 插件后,webpack4 编译速度进一步提升,如下截图:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-1-happyPack-1-dll-1-parallel.w4.png" alt="搭载三款插件后编译速度分析"></p>
<p>数据分析如下(单位ms):</p>
</li>
</ol>
<table>
<thead>
<tr>
<th style="text-align:center"></th>
<th style="text-align:center">第1次</th>
<th style="text-align:center">第2次</th>
<th style="text-align:center">第3次</th>
<th style="text-align:center">第4次</th>
<th style="text-align:center">第5次</th>
<th style="text-align:center">平均</th>
<th style="text-align:center">速度提升</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">搭载ParallelUglifyPlugin 和 happyPack</td>
<td style="text-align:center">26036</td>
<td style="text-align:center">25884</td>
<td style="text-align:center">25645</td>
<td style="text-align:center">25627</td>
<td style="text-align:center">25794</td>
<td style="text-align:center">25797.2</td>
<td style="text-align:center">17%</td>
</tr>
<tr>
<td style="text-align:center">搭载ParallelUglifyPlugin 、happyPack 和 dll</td>
<td style="text-align:center">20792</td>
<td style="text-align:center">20963</td>
<td style="text-align:center">20845</td>
<td style="text-align:center">21675</td>
<td style="text-align:center">21023</td>
<td style="text-align:center">21059.6</td>
<td style="text-align:center">22%</td>
</tr>
</tbody>
</table>
<p> 可见,搭载 dll 后,webpack4 编译速度仍能提升 22%。</p>
<p> 综上,我们汇总上面的多次数据,得到下表:</p>
<table>
<thead>
<tr>
<th style="text-align:center"></th>
<th style="text-align:center">第1次</th>
<th style="text-align:center">第2次</th>
<th style="text-align:center">第3次</th>
<th style="text-align:center">第4次</th>
<th style="text-align:center">第5次</th>
<th style="text-align:center">平均</th>
<th style="text-align:center">速度提升</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">webpack3</td>
<td style="text-align:center">58293</td>
<td style="text-align:center">60971</td>
<td style="text-align:center">57263</td>
<td style="text-align:center">58993</td>
<td style="text-align:center">60459</td>
<td style="text-align:center">59195.8</td>
<td style="text-align:center">-</td>
</tr>
<tr>
<td style="text-align:center">webpack4</td>
<td style="text-align:center">42346</td>
<td style="text-align:center">40386</td>
<td style="text-align:center">40138</td>
<td style="text-align:center">40330</td>
<td style="text-align:center">40323</td>
<td style="text-align:center">40704.6</td>
<td style="text-align:center">45%</td>
</tr>
<tr>
<td style="text-align:center">搭载ParallelUglifyPlugin 、happyPack 和 dll</td>
<td style="text-align:center">20792</td>
<td style="text-align:center">20963</td>
<td style="text-align:center">20845</td>
<td style="text-align:center">21675</td>
<td style="text-align:center">21023</td>
<td style="text-align:center">21059.6</td>
<td style="text-align:center">181%</td>
</tr>
</tbody>
</table>
<p> 升级至 webpack4 后,通过搭载 ParallelUglifyPlugin 、happyPack 和 dll 插件,编译速度可以提升181%,整体编译时间减少了将近 2/3,为开发节省了大量编译时间!而且随着项目发展,这种编译提升越来越可观。</p>
<p> 实际上,为了获得上面的测试数据,我关闭了 babel、ParallelUglifyPlugin 的缓存,开启缓存后,第二次编译时间平均为 12.8s,由于之前缓存过,编译速度相对 webpack3 将提升362%,即使你已经升级到 webpack4,搭载上述 3 款插件后,编译速度仍能获得 218% 的提升!</p>
<h3 id="编译结果分析"><a href="#编译结果分析" class="headerlink" title="编译结果分析"></a><strong>编译结果分析</strong></h3><p>当然,编译速度作为一项指标,影响的更多是开发者体验,与之相比,编译后文件大小更为重要。webpack4 编译的文件,比之前版本略小一些,为了更好的追踪文件 size 变化,开发环境和生产环境都需要引入 webpack-bundle-analyzer 插件,如下图。</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-analyzer.png" alt="analyzer"></p>
<p>文件 size 如下图所示:</p>
<p><img src="http://louiszhai.github.io/docImages/webpack-analyzer.size.png" alt="analyzer.size"></p>
<h3 id="面向tree-shaking,约束编码"><a href="#面向tree-shaking,约束编码" class="headerlink" title="面向tree-shaking,约束编码"></a><strong>面向tree-shaking,约束编码</strong></h3><p><strong>sideEffects</strong></p>
<p>从 webpack2 开始,tree-shaking 便用来消除无用模块,依赖的是 ES Module 的静态结构,同时通过在. babelrc 文件中设置 <code>"modules": false</code> 来开启无用的模块检测,相对粗暴。webapck4 灵活扩展了无用代码检测方式,主要通过在 <code>package.json</code> 文件中设置 <code>sideEffects: false</code> 来告诉编译器该项目或模块是 pure 的,可以进行无用模块删除,因此,开发公共组件时,可以尝试设置下。</p>
<p>为了使得 tree-shaking 真正生效,引入资源时,仅仅引入需要的组件尤为重要,如下所示:</p>
<pre><code class="js">import { Button, Input } from "element-ui"; // 只引入需要的组件
</code></pre>
<h3 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a><strong>结尾</strong></h3><p>升级 webpack4 的过程,踩坑是必须的,关键是踩坑后,你能得到什么?</p>
<p>另外,除了文中介绍的一些优化方法,更多的优化策略,正在逐步验证中…</p>
]]></content>
<summary type="html">
webpack4 使用webpack4提升180%编译速度
</summary>
<category term="Tool" scheme="http://louiszhai.github.io/tags/Tool/"/>
</entry>
<entry>
<title>Alfred神器使用手册</title>
<link href="http://louiszhai.github.io/2018/05/31/alfred/"/>
<id>http://louiszhai.github.io/2018/05/31/alfred/</id>
<published>2018-05-31T03:46:17.000Z</published>
<updated>2019-12-02T03:28:32.871Z</updated>
<content type="html"><![CDATA[<p>我曾经耗费巨大的精力,试图在计算机的使用效率上找到一条优化的捷径,一直以来都收效甚微。直到遇上 alfred,它强大的工作流机制,<strong>彻底解决了输入输出的痛点,极大的减少了程序之间的切换成本和重复按键成本</strong>,这才让我明白,原来计算机可以这么玩。</p>
<p>神奇的魔法帽,alfred 初印象。</p>
<p><img src="http://louiszhai.github.io/docImages/alfred/alfred-icon.png" alt=""></p>
<a id="more"></a>
<h3 id="如何安装alfred"><a href="#如何安装alfred" class="headerlink" title="如何安装alfred"></a>如何安装alfred</h3><p>首先可以从 <a href="https://www.alfredapp.com/" target="_blank" rel="external">alfred官网</a> 自行下载安装,免费用户可以使用除 workflow 以外的其它功能,如需使用 workflow,则需要购买Powerpack。</p>
<p>不建议:自行搜索破解版,或者点个喜欢,留下邮箱找我要…</p>
<h3 id="一个例子说明为什么要用alfred"><a href="#一个例子说明为什么要用alfred" class="headerlink" title="一个例子说明为什么要用alfred"></a>一个例子说明为什么要用alfred</h3><p>以前,使用mac查询一个单词,或者翻译一个单词,我们要么经历五步:</p>
<ol>
<li>手动打开浏览器 </li>
<li>进入谷歌首页</li>
<li>选中输入框</li>
<li>输入或粘贴查询单词,然后空格并加上”翻译” 两个字,然后再回车</li>
<li>等待浏览器展示查询结果; </li>
</ol>
<p>要么经历四步:</p>
<ol>
<li>打开翻译应用(比如自带词典) </li>
<li>输入或粘贴查询单词</li>
<li>翻译应用输出查询结果</li>
<li>查询过后,一般都需要Cmd+Q退出应用(或者Cmd+H隐藏词典,亦或Cmd+Tab切换回上一个应用)</li>
</ol>
<p>查询单词这个场景中,我们至少需要兴师动众,切换或打开一个应用两次,定位输入框一次,输入或复制粘贴一次。且查询结果页也会挡住当前的工作区,使得我们分心,甚至忘记自己刚刚在做啥,总之,体验极不流畅。</p>
<p>alfred 工作流正是为了解决这个问题而设计的。使用 <a href="https://github.com/Louiszhai/tool/blob/master/workflow/youdao.alfredworkflow?raw=true" target="_blank" rel="external"><code>有道词典</code></a> workflow,<strong>最快只需两次按键便可查询单词</strong>. 举个栗子🌰:为了查询单词 “workflow”,我会选中单词所在区域,然后按住 Option+Y 键(我已将有道翻译的快捷键设置为 Option+Y),单词查询结果就出来了,不需要切换应用,同时查询结果也较少的挡住工作区。如下所示:</p>
<p><img src="http://louiszhai.github.io/docImages/alfred/alfred-youdao.png" alt=""></p>
<p>两次按键就能查询单词,这么好的应用为何不用呢?</p>
<h3 id="alfred能做什么?"><a href="#alfred能做什么?" class="headerlink" title="alfred能做什么?"></a>alfred能做什么?</h3><p>对于一个刚刚听说alfred的新手来说,迫切想知道的莫过于了解它能做什么?据我所知,公开的 alfred workflow 至少有 500+,有心网友甚至罗列了一张 [表格][<a href="http://www.alfredworkflow.com/]来管理它,表格的每一行都解锁了一项" target="_blank" rel="external">http://www.alfredworkflow.com/]来管理它,表格的每一行都解锁了一项</a> alfred 技能(注意并非所有的 workflow 都支持最新的 alfred 3.6.1版本)。你可以下载并免费使用其中任何一个 workflow,甚至,还可以基于一些不错的 workflow 样本,加入创意,改造成属于自己的 workflow(前提是已获得 powerpack license)。</p>
<p>默认情况下,alfred 至少能胜任 15 项工作:</p>
<ol>
<li>应用搜索</li>
<li>文件或目录搜索</li>
<li>文本内容搜索</li>
<li>标记搜索</li>
<li>快捷网页搜索</li>