-
Notifications
You must be signed in to change notification settings - Fork 31
/
atom.xml
2255 lines (2141 loc) · 473 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><![CDATA[Taobao FED | 淘宝前端团队]]></title>
<subtitle><![CDATA[淘宝前端团队(FED)]]></subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://taobaofed.org/"/>
<updated>2019-03-19T09:18:13.000Z</updated>
<id>http://taobaofed.org/</id>
<author>
<name><![CDATA[Taobao FED]]></name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title><![CDATA[Web端H.265播放器研发解密]]></title>
<link href="http://taobaofed.org/blog/2019/03/18/web-player-h265/"/>
<id>http://taobaofed.org/blog/2019/03/18/web-player-h265/</id>
<published>2019-03-18T08:59:39.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB1lEw2MwHqK1RjSZFgXXa7JXXa-900-500.png" alt="Web端H.265播放器研发解密"></p>
<p>音视频编解码对于前端工程师是一个比较少涉足的领域,涉及到流媒体技术中的文本、图形、图像、音频和视频多种理论知识的学习,才能够应用到具体实践中,我们自研web播放器并支持h.265解码,在码率优化的大背景下(保持画质不变情况下,应用图像增强、roi区域检测、智能场景分类和h265编解码等多种技术能力,将码流降低50%。达到减少带宽成本,提升视频服务QoE的目的),真正做到了h265解码播放的全域覆盖。本文主要分享了我们基于WebAssembly实现H.265格式的解封装、解码和播放。</p>
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p><strong>H.265</strong>又称HEVC(全称<strong>High Efficiency Video Coding</strong>,高效率视频编码),是ITU-T H.264/MPEG-4 AVC标准的继任者。相比H.264,H.265拥有更高的压缩率,也就意味着同样<strong>码率</strong>(又称比特率是指每秒传送的比特(bit)数。单位为bps(Bit Per Second),比特率越高,每秒传送数据就越多,画质就越清晰),H.265的画质会更清晰,更高的压缩率就能使用更低的存储和传输成本。</p>
<ul>
<li><strong>带宽成本</strong>:在有限带宽下H.265能传输更高质量的网络视频,理论上,H.265最高只需H.264编码的一半带宽即可传输相同质量视频。更低的带宽可以更好的降低存储及传输成本,并为未来基于短视频及直播领域更多更复杂好玩的互动玩法做铺垫。</li>
<li><strong>转码成本</strong>:但是当前主流浏览器均不支持H.265原生视频播放,因此通常视频生产端需要针对浏览器做一次H.264视频的转码来适配浏览器端如PC场景的播放,而增加了<strong>转码成本</strong>。如在淘宝直播中,假设以每天5万场直播计算,每场直播转码成本20元,一天就是100万的转码成本。<br>为此,我们团队对浏览器端H.265视频播放的可行性及兼容性进行了一次探索,为移动端及PC端全量H.265做准备,也对浏览器端视音频处理、WebAssembly实践进行一次深入的尝试。</li>
</ul>
<h2 id="H-264-vs-H-265"><a href="#H-264-vs-H-265" class="headerlink" title="H.264 vs H.265"></a>H.264 vs H.265</h2><p>H.264是当下用的最为广泛的视频编码格式,H.265标准围绕着现有的视频编码标准H.264,保留原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置。H.265和H.264都是基于块的视频编码技术,主要的差别在于<strong>编码单元的大小</strong>以及一些编码算法细节,H.265将图像划分为“编码树单元(coding tree Unit, CTU)”,而不是像H.264那样的16×16的宏块。根据不同的编码设置,编码树单元的尺寸可以被设置为64×64或有限的32×32或16×16。一般来说区块尺寸越大,压缩效率就越好。具体的算法及相关细节这里不具体展开了,还有一些其他的压缩算法如因为H.265专利限制而生的开放编码格式如AV1等,读者可以参考其他相关文章。<br><img src="https://img.alicdn.com/tfs/TB1rDSbJgHqK1RjSZFgXXa7JXXa-1178-442.png" alt="h264vsh265.png"> </p>
<p>如下图,可以看到同样主观画面质量,H.265(500K)仅需H.264(800K)一半左右的带宽码率。<br><img src="https://img.alicdn.com/tfs/TB1yUiiJcbpK1RjSZFyXXX_qFXa-545-509.jpg" alt="h264vsh265.png"> </p>
<h2 id="浏览器现状"><a href="#浏览器现状" class="headerlink" title="浏览器现状"></a>浏览器现状</h2><p>如下图,因为H.265专利及硬解支持情况不完善的原因,主流现代浏览器均不兼容H.265编码的视频播放(Edge新版本以插件方式支持),但是因为Apple对H.265的支持(这里作者认为这可能是一个很重要的标志,因为技术的发展很多时候不光是这个技术本身所决定的,而是很多因素共同作用的结果,<strong>商业</strong>也是其中很重要的一个因素),移动端ios safari在11.0版本以上支持原生播放。<br><img src="https://gw.alicdn.com/tfs/TB1OP1lJhjaK1RjSZFAXXbdLFXa-1256-378.png" alt="h265comp"> </p>
<p>想要在浏览器端播放H.265视频原生的 <code><video /></code> 标签没有办法支持,但是因为视频格式本身是<strong>连续图像画面</strong>和<strong>音频</strong>的集合,参考了chromium的源码及video标签内部的实现原理,可以通过 <code><canvas /> + Web Audio API</code> 的结合来模拟实现一个虚拟的video标签来实现播放器功能。</p>
<h2 id="demo"><a href="#demo" class="headerlink" title="demo"></a>demo</h2><p>因为直播流时效性的缘故,发布了一个播放<a href="https://gw.alicdn.com/bao/uploaded/LB1l2iXISzqK1RjSZFjXXblCFXa.mp4?file=LB1l2iXISzqK1RjSZFjXXblCFXa.mp4" target="_blank" rel="noopener">H.265 mp4视频</a>(该视频地址直接在浏览器中播放只有声音而没有画面)的在线demo,读者可以有一个直观感受。<br>地址:<a href="https://g.alicdn.com/videox/mp4-h265/1.0.2/index.html" target="_blank" rel="noopener">https://g.alicdn.com/videox/mp4-h265/1.0.2/index.html</a><br>效果:<br><img src="https://img.alicdn.com/tfs/TB1uRatJhYaK1RjSZFnXXa80pXa-600-341.gif" alt="demo.gif"></p>
<h1 id="前期调研"><a href="#前期调研" class="headerlink" title="前期调研"></a>前期调研</h1><h1 id="播放器整体架构"><a href="#播放器整体架构" class="headerlink" title="播放器整体架构"></a>播放器整体架构</h1><p>基于传统播放器的架构,我们设计的播放器架构如下:<br><img src="https://img.alicdn.com/tfs/TB15IM4LYrpK1RjSZTEXXcWAVXa-1617-856.png" alt></p>
<h2 id="视音频基础"><a href="#视音频基础" class="headerlink" title="视音频基础"></a>视音频基础</h2><p>因为前端领域对视频领域的涉及场景不多,一个 <code><video /></code> 标签就可以满足大部分场景,但是经历了这几年直播和短视频的爆发,视频的需求和功能也变得越来越复杂,开发之前阅读了很多视音频领域相关的书籍和文章,在此先对视音频基础进行一个简单的介绍。</p>
<p>视频中我们通常说的视频的格式,比如 .mp4, .mov, .wmv, .m3u8, .flv 等等被称为 <code>container</code>。在一个视频文件中音频、视频数据是分开存储的,使用的压缩算法也不一样。其中container作为容器主要包含了video数据、audio数据、metadata(用于检索视音频payload格式等信息)。每个格式的封装格式不一样,比如FLV格式的基本单元是Tag,而MP4格式的基本单元是Box,辅助的meta信息用于检索找到对应的原始数据。</p>
<p>而平时听到的H.264, H.265等视频编码标准被称为 <code>codec</code> (COmpress and DECompress )。一个视频格式比如mp4可以使用任何标准化的压缩算法,这些信息都会被包含在一个视频文件的meta信息中来告诉播放器该用什么编解码算法来播放。</p>
<h2 id="客户端播放器"><a href="#客户端播放器" class="headerlink" title="客户端播放器"></a>客户端播放器</h2><p>一个传统的客户端播放器播放一个视频流经过了如下各个环节:</p>
<p>拉取数据 => 解封装 => 音视频解码 => 采样/像素数据发送到设备进行渲染。<br><img src="https://img.alicdn.com/tfs/TB1VaB_JmzqK1RjSZFLXXcn2XXa-1024-768.png" alt="client"></p>
<p>对于流媒体,播放器客户端通过拉流以数据源(音视频流)为中心,进行管道式的传输。在此期间,对视频流的读取,转换,分类,复制等一系列操作处理,以封装的mp4流为例,需要对流进行解封装、解码、渲染等步骤:<br><img src="https://img.alicdn.com/tfs/TB1aridJkvoK1RjSZFDXXXY3pXa-1055-524.png" alt="stream"></p>
<h2 id="浏览器video标签"><a href="#浏览器video标签" class="headerlink" title="浏览器video标签"></a>浏览器video标签</h2><p>在探究的过程中,为了了解主流浏览器不支持H.265视频播放的原因,以及浏览器端实现播放器原理的了解,通过对Chromium浏览器<a href="https://github.com/chromium/chromium/tree/master/media#playback" target="_blank" rel="noopener">官方文档</a>及video标签实现源码的阅读,整理了一个流程图。<br><img src="https://img.alicdn.com/tfs/TB1UxCyJhjaK1RjSZKzXXXVwXXa-1502-1008.png" alt="chromium"></p>
<p>可以看到浏览器内部对视频流播放的实现,在经过了PipelineController等数据传输管道的处理后利用FFmpeg软解或者Gpu硬解之后交给视频设备及音频设备进行同步及渲染。其中H.265的视频因为硬解支持情况不完善,软解可能有性能风险,所以在chrome中被关闭了不支持,在chromium中可以通过参数打开。我们就依照这个思路,利用浏览器提供的接口来实现一个模拟的video标签。</p>
<h1 id="设计过程"><a href="#设计过程" class="headerlink" title="设计过程"></a>设计过程</h1><h2 id="开发思路"><a href="#开发思路" class="headerlink" title="开发思路"></a>开发思路</h2><p>开发思路按照从简单到复杂的过程,对任务进行拆分,来完成H.265视频点播及直播等各个场景的覆盖,以mp4短视频出发完成播放流程,再覆盖直播场景,考虑如网络抖动、内存控制等复杂因素,再针对直播m3u8等回放文件进行播放并开发视频seek、倍速等功能。</p>
<p>mp4播放=>flv播放=>hls播放=>加入seek、倍速等功能</p>
<h2 id="可行性分析"><a href="#可行性分析" class="headerlink" title="可行性分析"></a>可行性分析</h2><ul>
<li>思路:在最开始进行可行性分析时,参考结合了已有工具videoconverter.js和libde265.js对H.265视频ffmpeg的编译提取了hevc文件及mp3音频文件在浏览器端进行了播放。</li>
<li>demo地址:<a href="https://sparkmorry.github.io/mse-learning/h265/" target="_blank" rel="noopener">https://sparkmorry.github.io/mse-learning/h265/</a></li>
<li>表现:将720P的mp4视频进行视频和音频的分离,通过 <code><canvas /></code> 绘制图像,通过 <code><audio /></code> 标签播放音频,画面在Macbook Pro上Chrome浏览器下在23fps左右。</li>
<li>问题:<ul>
<li><strong>不能达到解码性能标准</strong>: 720P的视频在Macbook Pro上仅在<strong>23fps</strong>左右,而原视频是<strong>25fps</strong>,不能达到解码性能标准,无法流畅播放。</li>
<li><strong>无法做到音画同步</strong>: 该方案因为直接提取了hevc裸流文件,无法获取视频和音频每帧的pts时间戳,无法做到严格的音画同步。</li>
</ul>
</li>
<li>解决方案:<ul>
<li>性能:因为libde265.js是asm.js,通过对libde265.js开源库的改造,打包WebAssembly测试性能情况</li>
<li>音画同步:参考flv.js、hls.js等开源视频库的方案,根据曾经的实践经历,js在解封装方面的性能能够完成视频流文件解封装,获取每帧视频、音频播放的pts及原始数据交给解码器进行解码再渲染。</li>
</ul>
</li>
<li>方案调整:<br><img src="https://img.alicdn.com/tfs/TB17KKAJiLaK1RjSZFxXXamPFXa-1024-768.jpg" alt="1"></li>
</ul>
<h2 id="MP4点播流播放"><a href="#MP4点播流播放" class="headerlink" title="MP4点播流播放"></a>MP4点播流播放</h2><ul>
<li>思路:根据上一过程调整的解决方案,通过js对mp4流进行解封装,因为音频解码的复杂度不高,也先用js进行解码,仅将视频解码模块用已有的三方模块libde265并替换为wasm解决性能问题,音视频解码模块都自身维护一段缓存区,负责存储解封装模块传过来的packet数据,解决音画同步的问题。</li>
<li>表现:通过开源libde265实现的视频解码模块,针对于720p的视频流,平均解码时间是45ms,不能满足每一帧音频播放时间间隔(40ms)。</li>
<li>问题:视频解码性能仍然不够。</li>
<li>解决方案:<ul>
<li><strong>丢帧</strong>:保证了音频同步,丢掉部分非参考帧,但损失了部分体验。所以提升解码性能和改善播放策略才能有可能满足当前方案的可行性。提升解码性能和改善播放策略。</li>
<li><strong>提升解码性能</strong>:用解码性能更好的ffmpeg替换掉libde265。</li>
<li><strong>改善播放流程</strong>:因为每个requestAnimationFrame循环任务都是同步的,边解码边播放。引入用WebWorker线程。通过改善视频解码模块,解码器内部开启循环解码,当外部的视频播放设备需要播放下一帧时,直接从解码器解码完的帧缓存中读取下一帧数据。实现了worker和主线程并行执行。</li>
</ul>
</li>
<li>方案调整:<ul>
<li>demo地址:<a href="https://static-assets.cyt-rain.cn/h265/index.html" target="_blank" rel="noopener">https://static-assets.cyt-rain.cn/h265/index.html</a></li>
<li>设计流程<br><img src="https://img.alicdn.com/tfs/TB1_emeJkvoK1RjSZFDXXXY3pXa-1024-768.jpg" alt="2"></li>
</ul>
</li>
</ul>
<h2 id="FLV直播流播放"><a href="#FLV直播流播放" class="headerlink" title="FLV直播流播放"></a>FLV直播流播放</h2><ul>
<li>思路:mp4视频流畅播放,但在直播场景(如FLV视频流)中,客户端需要和服务端建立长链接,不断接收流消息,借用FFmpeg本身对流媒体的支持,对视频数据进行解封装及解码。</li>
<li>表现:无法编译FFmpeg网络库,TCP无法建立连接。</li>
<li>问题:<br>无法编译FFmpeg网络库:TCP建立连接创建Socket时报错,Emscripten工具无法编译TCP连接相关配置<br>codec不支持:FLV官方协议不支持H.265。</li>
<li>解决方案:<ul>
<li>无法编译FFmpeg网络库:主线程利用fetch方法进行拉流,放到FFmpeg自定义缓冲区进行解封装及解码。因为直播流长时间播放需要不停的开辟、释放内存空间,采用环形的数据缓冲区。</li>
<li>FLV官方协议不支持H.265:对FFmpeg及编码端对H.265进行扩展,因为FFmpeg内部数据结构嵌套较深,替换js解封装函数直接用FFmpeg的解封装函数。</li>
</ul>
</li>
<li>方案调整:<ul>
<li>设计流程<br><img src="https://img.alicdn.com/tfs/TB1HgiaJb2pK1RjSZFsXXaNlXXa-1024-768.jpg" alt="3"></li>
</ul>
</li>
</ul>
<h1 id="当前方案"><a href="#当前方案" class="headerlink" title="当前方案"></a>当前方案</h1><h2 id="播放流程"><a href="#播放流程" class="headerlink" title="播放流程"></a>播放流程</h2><p><img src="https://img.alicdn.com/tfs/TB1zsS3JhnaK1RjSZFtXXbC2VXa-1024-768.png" alt="current"></p>
<ol>
<li>因为FFmpeg支持多种格式解封装,只需要在在主线程中通过浏览器API(通常是fetch方法)拉取原始流数据并放到缓存中,等初始缓存到一个阈值时开启Worker进行解封装及解码;</li>
<li>在子线程(Worker)中通过主线程fetch方法触发的数据回调接收数据存入环形缓冲区(内存环)中;</li>
<li>子线程将读取到的音频帧输送到主线程中,通过Web Audio API缓存音频数据,根据已解码的视频帧缓存队列循环解码保证缓存中一直缓存10帧rgba图像数据;</li>
<li>主线程中canvas根据音频播放回调的pts消费并渲染视频图像;</li>
<li>循环以上操作直到fetch接口返回流已结束。</li>
</ol>
<h2 id="解码器编译"><a href="#解码器编译" class="headerlink" title="解码器编译"></a>解码器编译</h2><p>通过Emscripten工具可以把C语言编写的FFmpeg库编译成wasm并在浏览器中应用到视音频解码中。<br>我们的视频解码场景和通常的播放器一样,通过依赖FFmpeg的通用接口来实现解封装和解码的工作。先通过emscripten编译ffmpeg库,再通过静态库的方式依赖到解封装和解码入口程序中。<br><img src="https://img.alicdn.com/tfs/TB10qJ.JkvoK1RjSZPfXXXPKFXa-1024-768.png" alt="compile"></p>
<h1 id="测试表现"><a href="#测试表现" class="headerlink" title="测试表现"></a>测试表现</h1><h2 id="性能测试"><a href="#性能测试" class="headerlink" title="性能测试"></a>性能测试</h2><h3 id="测试视频"><a href="#测试视频" class="headerlink" title="测试视频"></a>测试视频</h3><p>因为flv直播视频受时效性影响较大,拿720P高清的H.265 mp4视频作为<strong>稳定输入</strong>测试</p>
<ul>
<li>地址:<a href="https://gw.alicdn.com/bao/uploaded/LB1l2iXISzqK1RjSZFjXXblCFXa.mp4?file=LB1l2iXISzqK1RjSZFjXXblCFXa.mp4" target="_blank" rel="noopener">https://gw.alicdn.com/bao/uploaded/LB1l2iXISzqK1RjSZFjXXblCFXa.mp4?file=LB1l2iXISzqK1RjSZFjXXblCFXa.mp4</a></li>
<li>视频参数:</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'https://gw.alicdn.com/bao/uploaded/LB1l2iXISzqK1RjSZFjXXblCFXa.mp4?file=LB1l2iXISzqK1RjSZFjXXblCFXa.mp4':</span><br><span class="line"> Metadata:</span><br><span class="line"> major_brand : isom</span><br><span class="line"> minor_version : 512</span><br><span class="line"> compatible_brands: isomiso2mp41</span><br><span class="line"> encoder : www.aliyun.com - Media Transcoding</span><br><span class="line"> Duration: 00:01:00.10, start: 0.000000, bitrate: 907 kb/s</span><br><span class="line"> Stream #0:0(und): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, bt709, progressive), 1280x720, 854 kb/s, 25 fps, 25 tbr, 12800 tbn, 25 tbc (default)</span><br><span class="line"> Metadata:</span><br><span class="line"> handler_name : VideoHandler</span><br><span class="line"> Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 48 kb/s (default)</span><br><span class="line"> Metadata:</span><br><span class="line"> handler_name : SoundHandler</span><br></pre></td></tr></table></figure>
<h3 id="测试机器"><a href="#测试机器" class="headerlink" title="测试机器"></a>测试机器</h3><ul>
<li>lenovo ThinkPad T430<ul>
<li>CPU: Intel(R) Core(TM) i5-3230M <a href="mailto:CPU@2.60GHz" target="_blank" rel="noopener">CPU@2.60GHz</a> x64处理器</li>
<li>内存: 8 GB</li>
<li>系统: windows 10</li>
</ul>
</li>
<li>MacBook Pro (Retina, 15-inch, Mid 2015)<ul>
<li>CPU: 2.2 GHz Intel Core i7</li>
<li>内存: 16 GB</li>
<li>系统: macOS 10.14.2</li>
</ul>
</li>
</ul>
<h3 id="性能情况"><a href="#性能情况" class="headerlink" title="性能情况"></a>性能情况</h3><ul>
<li>MBP下表现</li>
</ul>
<table>
<thead>
<tr>
<th>decoder.wasm大小</th>
<th>decoder.js大小</th>
<th>平均每帧解码时长</th>
<th>内存占用</th>
<th>cpu占用</th>
</tr>
</thead>
<tbody>
<tr>
<td>1.4M</td>
<td>168K</td>
<td>26.79ms</td>
<td>27M</td>
<td>17~25%</td>
</tr>
</tbody>
</table>
<ul>
<li>针对两个pc笔记本进行了测试,平均每帧解码(包含yuv420转rgba)时长在各个浏览器的表现情况如下:<br><strong><em>注:此处Native(原生)表示针对mac系统原生编译的FFmpeg作为依赖的解码器(相对不考虑具体如x86、arm等计算机架构的WebAssembly的跨平台方案而言)</em></strong>。</li>
</ul>
<table>
<thead>
<tr>
<th>设备</th>
<th>Chrome</th>
<th>Safari</th>
<th>FireFox</th>
<th>Edge</th>
<th>Native</th>
</tr>
</thead>
<tbody>
<tr>
<td>MacOS(i7)</td>
<td>26.79ms</td>
<td>22.19ms</td>
<td>24.77ms</td>
<td>-</td>
<td>5.08ms</td>
</tr>
<tr>
<td>windows(i5)</td>
<td>33.51ms</td>
<td>-</td>
<td>36.74ms</td>
<td>86.72ms</td>
<td>未测试</td>
</tr>
</tbody>
</table>
<p>意味着最高能提供720P高清视频如下帧率视频流畅播放的能力:</p>
<table>
<thead>
<tr>
<th>设备</th>
<th>Chrome</th>
<th>Safari</th>
<th>FireFox</th>
<th>Edge</th>
<th>视频基准</th>
<th>Native</th>
</tr>
</thead>
<tbody>
<tr>
<td>MacOS(i7)</td>
<td>37fps</td>
<td>45fps</td>
<td>40fps</td>
<td>-</td>
<td><strong>25fps</strong></td>
<td>196fps</td>
</tr>
<tr>
<td>windows(i5)</td>
<td>30fps</td>
<td>-</td>
<td>27fps</td>
<td>12fps</td>
<td><strong>25fps</strong></td>
<td>未测试</td>
</tr>
</tbody>
</table>
<p>可以看到这两台机器中,在非高速运动等普通的如电商场景25fps帧率的高清720p视频已经能达到生产环境的标准,但是距离原生的速度还有一定距离。</p>
<h2 id="浏览器兼容性"><a href="#浏览器兼容性" class="headerlink" title="浏览器兼容性"></a>浏览器兼容性</h2><p>主要用到了WebAssembly及WebWorker的支持,实际测试中主流浏览器Chrome、Safari、Firefox、Edge均能通过兼容性测试。</p>
<ul>
<li><p>WebAssembly<br><img src="https://img.alicdn.com/tfs/TB1uyl9JbPpK1RjSZFFXXa5PpXa-1224-436.png" alt="wasm"></p>
</li>
<li><p>WebWorker<br><img src="https://img.alicdn.com/tfs/TB1bHp.JkvoK1RjSZPfXXXPKFXa-1226-411.png" alt="worker"></p>
</li>
</ul>
<h1 id="TODO"><a href="#TODO" class="headerlink" title="TODO"></a>TODO</h1><p>当前的技术方案已经能在大部分机器的主流浏览器上流畅的播放720P的高清直播流,但是在Edge浏览器及性能稍差的机器上还是存在高清视频解码性能不能满足流畅播放的风险,针对WebAssembly达到native速度的目标还有一定距离,尤其是汇编并行计算的支持,在视音频及大规模数据处理中是很常见的性能优化策略,作者整理了几个优化的方向,在未来还有更多探索的空间:</p>
<ul>
<li><p>汇编<br>FFmpeg中解码有较多利用汇编进行并行计算的优化,但是汇编指令是cpu specific的(比如x86指令和arm指令),而wasm是跨平台的基于栈的虚拟机。Emscripten不支持汇编的编译,考虑用clang等llvm前端将FFmpeg的.c和汇编.asm文件编译成LLVM IR(LLVM Intermediate Representation),然后通过fastcomp或者其他后端来编译测试。</p>
</li>
<li><p>硬解<br>FFmpeg3.3以上开始支持自动硬解探测,支持的硬件设备根据不同操作系统及硬件会有不同的支持,具体参考:<a href="https://trac.ffmpeg.org/wiki/HWAccelIntro" target="_blank" rel="noopener">https://trac.ffmpeg.org/wiki/HWAccelIntro</a> 。因为wasm是跨平台的虚拟指令集,支持程度还要待进一步探究。</p>
</li>
<li><p>多线程<br>FFmpeg内部解码有多线程来提高解码性能,通过pthread可以支持跨平台的多线程支持的,但是如果不支持共享内存,则线程之间的数据传输会有很多性能消耗(深拷贝或者Transfered Object)。浏览器端共享内存通过SharedArrayBuffer来实现,因为有安全隐患,大部分主流浏览器关闭了SharedArrayBuffer、Chrome67+开始恢复。考虑到兼容性多线程的支持还要再进行尝试。</p>
</li>
<li><p>WebGL渲染<br>解码平均时长中有4ms左右(15%)在yuv转rgba上,通过WebGL可以用gpu加速图像的计算,但是同时与WebGL的数据交换又会产生一定的性能损耗,需要再测试查看性能结果</p>
</li>
</ul>
<h1 id="未来展望"><a href="#未来展望" class="headerlink" title="未来展望"></a>未来展望</h1><p>通过H.265视频播放将开源视音频库<strong>FFmpeg</strong>的能力<strong>及WebAssembly</strong>性能的优势在浏览器端视音频处理上有了一次深入的尝试。视频作为一种多媒体形式,相比现有的文字、图像、音频都能有更生动及更丰富信息的表现。尤其经过了直播和短视频的爆发增长后,成为了一种基础的多媒体形式,也是网络及移动端手机性能等技术发展的体现。未来随着5G及更高性能的硬件设备的发展会被更广泛的应用到各个领域。浏览器在这场视频革命中也是不可或缺的一个环节,通过这次的探索,为未来浏览器端扩展视音频处理的通用能力提供了想象的空间,同时也在浏览器端通过WebAssembly向native性能及能力靠近的路上做了一个落地的尝试,虽然从测试情况看现在的表现还不如native,但是随着标准及技术的演进,为未来对性能要求比较高的图形图像及人工智能等相关方向在浏览器端处理一定会渐渐被广泛的应用起来,比如如下几个方向:</p>
<ul>
<li><p><strong>扩展浏览器端视频播放能力</strong><br>借助FFmpeg强大的编解码能力,除了H.265视频的播放,未来还可以在浏览器端兼容各种格式及编码类型的视频播放。如不同的编码格式AV1、不同的容器格式mov格式等等。</p>
</li>
<li><p><strong>扩展浏览器端视音频处理能力</strong><br>借助FFmpeg及其他语言框架的现有能力,还可以在视音频领域做更多复杂的操作如视频滤镜、视频剪切、视频格式转换等功能,减少网络传输及存储的成本。</p>
</li>
<li><p><strong>基于WebAssembly的高性能web应用</strong><br>借助WebAssembly的跨平台优势,可以将传统的其他语言的开源框架如图形相关开源库OpenGL、SDL等的能力移植到浏览器上来。借助性能上的优势也可以将传统的图像、3D等运算能力要求较高的应用扩展到浏览器端。</p>
</li>
</ul>
<h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul>
<li>Chromium媒体元素源码: <a href="https://github.com/chromium/chromium/tree/master/media" target="_blank" rel="noopener">https://github.com/chromium/chromium/tree/master/media</a></li>
<li>WebAssembly: <a href="https://webassembly.org/" target="_blank" rel="noopener">https://webassembly.org/</a></li>
<li>优秀的开源视音频处理框架FFmpeg: <a href="https://www.ffmpeg.org/" target="_blank" rel="noopener">https://www.ffmpeg.org/</a></li>
<li>基于LLVM编译的WebAssembly打包工具集Emscripten:<a href="https://emscripten.org/index.html" target="_blank" rel="noopener">https://emscripten.org/index.html</a></li>
<li>基于WebAssembly的ogg播放器:<a href="https://github.com/brion/ogv.js" target="_blank" rel="noopener">https://github.com/brion/ogv.js</a></li>
<li>基于FFmpeg的简单播放器:<a href="https://github.com/leixiaohua1020/simplest_ffmpeg_player" target="_blank" rel="noopener">https://github.com/leixiaohua1020/simplest_ffmpeg_player</a></li>
</ul>
<h1 id="后言"><a href="#后言" class="headerlink" title="后言"></a>后言</h1><p>本文介绍了我们在Web端H.265播放器上研发的过程和进展,后续还有很多继续优化和深入的点。对相关知识感兴趣的同学欢迎沟通交流,附上我们前端团队的介绍:</p>
<p>淘宝技术部内容与开放平台前端团队是淘系核心的商业变革阵地,相对于横向资源前端团队,我们更深入在内容电商、音视频技术领域,探寻创新商业模式及业界领先技术。</p>
<p>本团队目前正火热招聘20届优秀毕业生,欢迎有志之士加入!<br>有意向者可直接联系团队负责人: 灵玉 <a href="mailto:lingyu.csh@taobao.com" target="_blank" rel="noopener">lingyu.csh@taobao.com</a></p>
<blockquote>
<p>题图出处:<a href="https://unsplash.com/photos/oTjFWTHDRZQ" target="_blank" rel="noopener">https://unsplash.com/photos/oTjFWTHDRZQ</a></p>
</blockquote>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB1lEw2MwHqK1RjSZFgXXa7JXXa-900-500.png" alt="Web端H.265播放器研发解密"></p>
<p>音视频编解码对于前端工程师是一个比较少涉足的领域,涉及到流]]>
</summary>
<category term="player" scheme="http://taobaofed.org/tags/player/"/>
<category term="h265" scheme="http://taobaofed.org/tags/h265/"/>
<category term="WebAssembly" scheme="http://taobaofed.org/tags/WebAssembly/"/>
<category term="Web开发" scheme="http://taobaofed.org/categories/Web%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[淘宝前端团队 2019 年实习生内部推荐通道已开启]]></title>
<link href="http://taobaofed.org/blog/2019/03/13/fed-2019/"/>
<id>http://taobaofed.org/blog/2019/03/13/fed-2019/</id>
<published>2019-03-13T02:22:25.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://img.alicdn.com/tfs/TB1uzLwLQzoK1RjSZFlXXai4VXa-900-500.jpg" alt="淘宝前端团队 2019 年实习生内部推荐通道已开启"></p>
<p>随着阿里巴巴 2019 年实习生招聘的启动,淘宝前端团队(Taobao FED)实习生内部推荐也已经拉开序幕。这是一个有着十多年历史与荣耀的团队,相信你已经从各个渠道听到过我们的名字。选择一家公司进行实习,对于大学生来说是一个关键的抉择;找到有潜力的实习生进行培养,对于一个团队来说更是求贤若渴。</p>
<p>在这里,请让我向你介绍我们在做的一些事情以及我的小伙伴们。如果你感到有兴趣,并且认为自己足够优秀,不要吝啬你的才华,直接邮件简历给他们,也许他们将会成为你职业生涯的第一个老板,以及你全新人生阶段的良师益友。</p>
<p>以下是我们团队的几个技术场景和负责的小伙伴,你可以选择跟随这些业界大牛学习和成长。</p>
<h3 id="Node-架构体系"><a href="#Node-架构体系" class="headerlink" title="Node 架构体系"></a>Node 架构体系</h3><p>从 Node.js 应用框架到全链路监控以及故障演练,从基于 TypeScript 的 IoC 容器到 Serverless Node.js 运行时,这里有一帮 maker 组成的团队。2020 年毕业的喜欢动手的 maker/创客们欢迎投递简历至:<a href="mailto:qinian.wmq@taobao.com" target="_blank" rel="noopener">qinian.wmq@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1f_A0LQzoK1RjSZFlXXai4VXa-750-150.jpg" alt="七念"></p>
<h3 id="中后台体系"><a href="#中后台体系" class="headerlink" title="中后台体系"></a>中后台体系</h3><p>如果你有志于通过打造桌面工具帮助开发者极速构建前端应用,亦或是参与智能界面设计平台的建设以革新前端的开发模式,还是参与到 Github 1w+ star 开源项目 ICE,简历投递至:<a href="mailto:yuanyan.cao@alibaba-inc.com" target="_blank" rel="noopener">yuanyan.cao@alibaba-inc.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1BOxFLSrqK1RjSZK9XXXyypXa-750-150.jpg" alt="元彦"></p>
<h3 id="前端智能化"><a href="#前端智能化" class="headerlink" title="前端智能化"></a>前端智能化</h3><p>专注前端智能化,提效与业务密切相关的开发场景效率,基于计算机视觉、深度学习等,从设计稿(Sketch、PSD、静态图片)一键智能生成高可维护性代码,在这里,我们持续探索智能化的世界。欢迎对前端智能化感兴趣的极客们投递简历至:<a href="mailto:miaojing@taobao.com" target="_blank" rel="noopener">miaojing@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1oyxFLSrqK1RjSZK9XXXyypXa-750-150.jpg" alt="妙净"></p>
<h3 id="终端架构"><a href="#终端架构" class="headerlink" title="终端架构"></a>终端架构</h3><p>坐落于北京的淘宝前端团队,覆盖淘宝 H5 主站、前端数据体系、海报搭建平台、个性化定制、跨端解决方案等场景,我们专注于挖掘业务价值、提升性能体验、拓展技术边界;欢迎 2020 年毕业的精英们投递简历至:<a href="mailto:haiwen.lpy@taobao.com" target="_blank" rel="noopener">haiwen.lpy@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1628yLMTqK1RjSZPhXXXfOFXa-750-150.jpg" alt="海文"></p>
<h3 id="前端搭建体系"><a href="#前端搭建体系" class="headerlink" title="前端搭建体系"></a>前端搭建体系</h3><p>从支持双十一、双十二活动页面的搭建系统,到炙手可热的小程序搭建系统,前端技术-搭建服务团队承担着淘宝页面搭建服务的重任,实现可视化的方式从0到1搭建页面,配置页面动态数据,后期页面运维等一系列服务。不管你是爱好前端技术,还是热衷于 Nodejs 后台开发,搭建服务团队都期待你的加入!欢迎 2020 年毕业的祖国花朵投递简历至:<a href="mailto:butian.wth@taobao.com" target="_blank" rel="noopener">butian.wth@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1fithL4naK1RjSZFBXXcW7VXa-750-150.jpg" alt="步天"></p>
<p>从手淘里的普通页面再到支撑万千页面生成的搭建平台,我们实现了从零到一的效率飞跃。从前端的性能平台再到前端的稳定性治理,我们定义了前端的新数据指标。从 PWA 的全面实践再到 Serverless 理念的引导落地,我们始终站在技术潮流的最前沿。我们是创新业务团队,在淘宝的大平台与前端的风口上尽情挥洒。2020 年毕业的同学,创新业务的未来,有你会更精彩。欢迎投递简历至:<a href="mailto:suhe.ly@taobao.com" target="_blank" rel="noopener">suhe.ly@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1D4gULHPpK1RjSZFFXXa5PpXa-750-150.jpg" alt="苏河"></p>
<h3 id="前端开放体系"><a href="#前端开放体系" class="headerlink" title="前端开放体系"></a>前端开放体系</h3><p>我们尝试 Weex 在跨端应用开发的最佳实践,推进 IDE 在工程链路中的集成,还有面向商家装修的中后台应用,面向 ISV 的开放平台。服务用户广,技术不设限,等你来挑战!欢迎投递简历至:<a href="mailto:zhenghao.zmw@taobao.com" target="_blank" rel="noopener">zhenghao.zmw@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1VMwYLSzqK1RjSZFHXXb3CpXa-750-150.jpg" alt="正豪"></p>
<h3 id="前端工程体系"><a href="#前端工程体系" class="headerlink" title="前端工程体系"></a>前端工程体系</h3><p>如果你想打造整个阿里前端开发的工程化基础底层体系,参与业内领先的前端工程底层系统开发,掌握从项目初始化到线上编译、扫描、发布的全链路体系架构设计与实现。欢迎投递简历至: <a href="mailto:ada@taobao.com" target="_blank" rel="noopener">ada@taobao.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1fhQZLFzqK1RjSZSgXXcpAVXa-750-150.jpg" alt="阿大"></p>
<h3 id="图形渲染"><a href="#图形渲染" class="headerlink" title="图形渲染"></a>图形渲染</h3><p>2020 年毕业热爱图形渲染的你一定不要错过这班车,这里有一群致力于 WebGL/WebGPU 跨端标准化实现的小伙伴,诞生了为极致渲染而生的图形渲染引擎 GCanvas,正在面向 5G/IoT 发力创造新一代渲染框架的路上邀你同行,欢迎投递简历至:<a href="mailto:yuantong.xyt@alibaba-inc.com" target="_blank" rel="noopener">yuantong.xyt@alibaba-inc.com</a></p>
<p><img src="https://img.alicdn.com/tfs/TB1B6.ZLMHqK1RjSZFgXXa7JXXa-750-150.jpg" alt="二同"></p>
<p>了解完将要可能与你一起共事的小伙伴,再来给大家介绍你将会工作的阿里西溪园区。在我看来它拥有国内最好的办公环境。</p>
<p><img src="https://img.alicdn.com/tfs/TB1J9l1LSzqK1RjSZFLXXcn2XXa-1000-664.jpg" alt></p>
<p><img src="https://img.alicdn.com/tfs/TB1VbV9LFzqK1RjSZFoXXbfcXXa-1000-650.jpg" alt></p>
<p><img src="https://img.alicdn.com/tfs/TB1hHJ7LSzqK1RjSZFjXXblCFXa-750-750.jpg" alt></p>
<p><img src="https://img.alicdn.com/tfs/TB1Pd9cLMHqK1RjSZFkXXX.WFXa-3992-2242.jpg" alt></p>
<p><img src="https://img.alicdn.com/tfs/TB1TRFEeDZmx1VjSZFGXXax2XXa-3992-2242.jpg" alt></p>
<p>而至于招聘的要求,千篇一律,不再赘述。相信你已有信心和耐心接受接下来的挑战。</p>
<p>敲黑板,以下是工作地点和内推时间:</p>
<ul>
<li>工作地点:杭州、北京</li>
<li>毕业时间:2019.11.01 ~ 2020.10.31 </li>
<li>内推时间:2019.03.12 ~ 2019.04.23</li>
</ul>
<p>任何疑问,可邮件联系:<a href="mailto:xwt.wuji@alibaba-inc.com" target="_blank" rel="noopener">xwt.wuji@alibaba-inc.com</a> 。亦可通过下面的二维码进入内推流程:</p>
<p><img src="https://img.alicdn.com/tfs/TB1jEi2LNnaK1RjSZFtXXbC2VXa-150-150.png" alt="二维码"></p>
<blockquote>
<p>题图:阿里巴巴五年陈现场,出自 <a href="http://alvinhui.lofter.com/post/1cb252a0_12bf2c4e3" target="_blank" rel="noopener">http://alvinhui.lofter.com/post/1cb252a0_12bf2c4e3</a>。</p>
</blockquote>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://img.alicdn.com/tfs/TB1uzLwLQzoK1RjSZFlXXai4VXa-900-500.jpg" alt="淘宝前端团队 2019 年实习生内部推荐通道已开启"></p>
<p>随着阿里巴巴 2019 年实习生招聘的]]>
</summary>
<category term="实习生" scheme="http://taobaofed.org/tags/%E5%AE%9E%E4%B9%A0%E7%94%9F/"/>
<category term="校招" scheme="http://taobaofed.org/tags/%E6%A0%A1%E6%8B%9B/"/>
<category term="团队生活" scheme="http://taobaofed.org/categories/%E5%9B%A2%E9%98%9F%E7%94%9F%E6%B4%BB/"/>
</entry>
<entry>
<title><![CDATA[imgcook 体验版发布]]></title>
<link href="http://taobaofed.org/blog/2019/01/07/imgcook/"/>
<id>http://taobaofed.org/blog/2019/01/07/imgcook/</id>
<published>2019-01-07T09:09:12.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://img.alicdn.com/tfs/TB19QQxApYqK1RjSZLeXXbXppXa-900-500.png" alt="imgcook 体验版发布"></p>
<h1 id="imgcook-体验版发布"><a href="#imgcook-体验版发布" class="headerlink" title="imgcook 体验版发布"></a>imgcook 体验版发布</h1><p>经过淘宝前端团队内部近一年半的打磨,imgcook 迎来了社区体验版发布,欢迎试用~。</p>
<h2 id="imgcook-是什么?"><a href="#imgcook-是什么?" class="headerlink" title="imgcook 是什么?"></a>imgcook 是什么?</h2><p>imgcook 是专注以各种图像(Sketch/PSD/静态图片)为原材料烹饪的匠心大厨,通过智能化手段将各种图像一键生成可维护的 UI 视图代码,期望此 imgcook (图像大厨) 未来能够成为一位 P5 级别的重构工程师,能切实提高开发的开发效率,并助力开发、设计师、测试的高效协作,我们期望做到:</p>
<ul>
<li>100% 还原【设计师再也不用还原走查了】</li>
<li>100% 兼容【测试再也不用适配样式了】</li>
<li>一键上线【开发再也不用切图写样式了】</li>
</ul>
<p>愿景:能够高度还原各种图像,释放 UI 开发生产力,让你关注更具挑战性的事情!</p>
<p><img src="https://gw.alicdn.com/tfs/TB1ZGmCAXzqK1RjSZSgXXcpAVXa-1440-900.gif#align=left&display=inline&height=466&linkTarget=_blank&originHeight=900&originWidth=1440&width=746#align=left&display=inline&height=466&linkTarget=_blank&originHeight=900&originWidth=1440&width=746" alt></p>
<h2 id="为什么做-imgcook?"><a href="#为什么做-imgcook?" class="headerlink" title="为什么做 imgcook?"></a>为什么做 imgcook?</h2><p>对于 UI 还原而言,早在很多年前,对于类似的从设计稿中还原生成静态 UI 场景的解决方案也都涌现过,比如一些设计稿标注工具(如Marketch),又或者说目前的一些可视化建站平台也都能够在搭建完后直接产出线上页面,为什么做 imgcook,它的优势体现在哪?</p>
<p>我们回到 UI 还原主题上,当我们说图像生成代码的时候,我们关心的不仅仅是生成 UI 的还原度,也关心生成的代码是否合理、可维护,如果生成的代码属于不可读不可维护的快餐代码,开发使用上再对其进行二次开发就会显得极为艰难,而往往后者的难度相比前者更甚,如果需要在生产环境中使用,这个问题就无法避免。</p>
<p>对此,imgcook 的定位就在于解决 UI 还原以及生成可维护代码的问题。</p>
<h3 id="1-设计稿无约束-高还原"><a href="#1-设计稿无约束-高还原" class="headerlink" title="1. 设计稿无约束+高还原"></a>1. 设计稿无约束+高还原</h3><p>对于 UI 还原,目前比较常见的场景是从设计工具(比如Sketch、PS)入手。但比较不如意的是,设计师交付的设计稿里所带的结构化信息往往是杂乱无章的,如果需要精确得解析一个设计稿里某个模块的结构化数据,往往又需要跟设计师进行合作,规范设计稿中的设计以及制定一些约束来进行使用,这就变相要求设计师用设计的方式来写试图代码,一定程度上增加了设计师的成本。<br>imgcook 为了解决这个问题,目前在对设计稿的解析上做了一些智能化的处理,去除了对设计师图层设计规范这块的依赖,当然 imgcook 也提供了一些官方的设计稿规范建议可以让还原效果更符合预期。</p>
<h3 id="2-生成可维护代码"><a href="#2-生成可维护代码" class="headerlink" title="2. 生成可维护代码"></a>2. 生成可维护代码</h3><p>对于可维护性代码的问题,imgcook 会对还原后的 UI 在代码层面上生成目前使用比较广泛的 Flexbox 布局以及相对定位布局,在一些自定义的命名上(比如样式命名),imgcook 也会根据开发者的习惯生成更加人性化的命名。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1EpqnAXYqK1RjSZLeXXbXppXa-2050-1214.png#align=left&display=inline&height=442&linkTarget=_blank&originHeight=1214&originWidth=2050&width=746#align=left&display=inline&height=442&linkTarget=_blank&originHeight=1214&originWidth=2050&width=746" alt></p>
<h2 id="如何使用-imgcook?"><a href="#如何使用-imgcook?" class="headerlink" title="如何使用 imgcook?"></a>如何使用 imgcook?</h2><p>imgcook 目前对外的体验版里,暂时只开放了针对 Sketch 设计稿的还原插件。</p>
<h3 id="1-插件导出模块"><a href="#1-插件导出模块" class="headerlink" title="1. 插件导出模块"></a>1. 插件导出模块</h3><p>在使用 Sketch 插件进行还原的时候,可选中一个模块外层的容器节点(画板、Group或者Symbol)来进行导出。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1A0N9AhTpK1RjSZR0XXbEwXXa-1482-740.png#align=left&display=inline&height=372&linkTarget=_blank&originHeight=740&originWidth=1482&width=746#align=left&display=inline&height=372&linkTarget=_blank&originHeight=740&originWidth=1482&width=746" alt></p>
<h3 id="2-粘贴还原"><a href="#2-粘贴还原" class="headerlink" title="2. 粘贴还原"></a>2. 粘贴还原</h3><p>模块导出完毕后,可前往 imgcook 平台进行粘贴还原。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1AnKhAmzqK1RjSZFpXXakSXXa-1842-960.png#align=left&display=inline&height=389&linkTarget=_blank&originHeight=960&originWidth=1842&width=746#align=left&display=inline&height=389&linkTarget=_blank&originHeight=960&originWidth=1842&width=746" alt></p>
<h3 id="3-保存查阅代码"><a href="#3-保存查阅代码" class="headerlink" title="3. 保存查阅代码"></a>3. 保存查阅代码</h3><p>检查模块还原 UI 以及左侧的布局结构无误后,可进行保存->查阅代码,目前对外的体验版中 imgcook 提供了几种可选择的 DSL 进行代码生成,对于每个 DSL,可在右侧的 playground 里查看具体运行的效果。<br><br><br><img src="https://gw.alicdn.com/tfs/TB120WjAbrpK1RjSZTEXXcWAVXa-1612-1078.png#align=left&display=inline&height=499&linkTarget=_blank&originHeight=1078&originWidth=1612&width=746#align=left&display=inline&height=499&linkTarget=_blank&originHeight=1078&originWidth=1612&width=746" alt></p>
<h2 id="什么场景下使用-imgcook?"><a href="#什么场景下使用-imgcook?" class="headerlink" title="什么场景下使用 imgcook?"></a>什么场景下使用 imgcook?</h2><p>imgcook 的诞生源于业务,也最终服务于业务。</p>
<ul>
<li>场景使用层面上,imgcook 倾向于以页面中的<strong>模块级别</strong>图文组合的模块。</li>
<li>技术层面上,imgcook 对于支持 Flexbox 布局类型的 DSL 都会有一个比较好的支持,目前 imgcook 提供了四种 DSL,接下来也还会陆续提供更多。<blockquote>
<p>适合使用 imgcook 的场景例子</p>
</blockquote>
</li>
</ul>
<p><img src="https://gw.alicdn.com/tfs/TB16pblAXzqK1RjSZFCXXbbxVXa-1790-1166.png#align=left&display=inline&height=473&linkTarget=_blank&originHeight=1166&originWidth=1790&width=726#align=left&display=inline&height=486&linkTarget=_blank&originHeight=1166&originWidth=1790&width=746" alt><br><br><img src="https://gw.alicdn.com/tfs/TB1qazWApzqK1RjSZFCXXbbxVXa-1526-1000.png#align=left&display=inline&height=489&linkTarget=_blank&originHeight=1000&originWidth=1526&width=746" alt></p>
<h2 id="更多信息"><a href="#更多信息" class="headerlink" title="更多信息"></a>更多信息</h2><ul>
<li><a href="https://imgcook.taobao.org/" target="_blank" rel="noopener">下载 imgcook</a></li>
<li><a href="https://imgcook.taobao.org/docs" target="_blank" rel="noopener">imgcook 使用指南</a><h2 id="建议反馈"><a href="#建议反馈" class="headerlink" title="建议反馈"></a>建议反馈</h2>imgcook 目前还处于体验版,会存在一些不确定性的还原 badcase 以及代码生成不合理的 badcase,请向我们反馈,我们会第一时间进行问题收集、跟进处理。</li>
<li><a href="https://github.com/taobaofed/imgcook/issues" target="_blank" rel="noopener">Issue</a></li>
<li>钉钉交流</li>
</ul>
<p><img src="https://gw.alicdn.com/tfs/TB11YVgAbrpK1RjSZTEXXcWAVXa-842-1114.png#align=left&display=inline&height=314&linkTarget=_blank&originHeight=1114&originWidth=842&width=237#align=left&display=inline&height=987&linkTarget=_blank&originHeight=1114&originWidth=842&width=746" alt></p>
<blockquote>
<p>题图出处:<a href="https://unsplash.com/photos/LrPKL7jOldI" target="_blank" rel="noopener">https://unsplash.com/photos/LrPKL7jOldI</a>,寓意为萌芽阶段的前端智能。</p>
</blockquote>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://img.alicdn.com/tfs/TB19QQxApYqK1RjSZLeXXbXppXa-900-500.png" alt="imgcook 体验版发布"></p>
<h1 id="imgcook-体验版发布"><a href="#i]]>
</summary>
<category term="AI" scheme="http://taobaofed.org/tags/AI/"/>
<category term="imgcook" scheme="http://taobaofed.org/tags/imgcook/"/>
<category term="智能还原" scheme="http://taobaofed.org/tags/%E6%99%BA%E8%83%BD%E8%BF%98%E5%8E%9F/"/>
<category term="Web开发" scheme="http://taobaofed.org/categories/Web%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[WebGL 纹理详解]]></title>
<link href="http://taobaofed.org/blog/2018/12/17/webgl-texture/"/>
<id>http://taobaofed.org/blog/2018/12/17/webgl-texture/</id>
<published>2018-12-17T07:35:29.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://img.alicdn.com/tfs/TB1ng7ewCzqK1RjSZFLXXcn2XXa-900-500.png" alt="WebGL 纹理详解"></p>
<h1 id="WebGL-纹理详解"><a href="#WebGL-纹理详解" class="headerlink" title="WebGL 纹理详解"></a>WebGL 纹理详解</h1><p>Buffer(数据缓冲区)与 Texture(纹理)是 WebGL 程序的两大数据来源。Buffer 可以通过 ArrayBuffer 或更语义化的 TypedArray 来构造;而 Texture 在大多数情况下,是通过 Image 对象来构造的。在构造和使用 Texture 的过程中,需要确定很多<strong>选项</strong>来以不同的方式构造 Texture;这些选项之间有着各种各样的关系,或互相依赖,或互相排斥,或互相影响。最近,我又重新梳理了一遍我所用到的 WebGL 纹理各种参数的影响,稍作整理,以防遗忘。</p>
<p>为此,我专门编写了一个 Demo,如下所示。Demo 页的右上角有一个使用 <a href="https://github.com/dataarts/dat.gui" target="_blank" rel="noopener">dat.GUI</a> 生成的控件,其中列举了影响纹理的一些选项。这篇文章将逐个讨论这些选项的作用和相互关系。</p>
<p><a class="jsbin-embed" href="https://jsbin.com/boxazam/latest/embed?output&height=512px" target="_blank" rel="noopener">JS Bin on jsbin.com</a></p>
<h2 id="Wrap"><a href="#Wrap" class="headerlink" title="Wrap"></a>Wrap</h2><p>在 JavaScript 中,创建纹理的基本流程大约如下所示:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 激活纹理单元</span></span><br><span class="line">gl.activeTexture(...);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建和绑定</span></span><br><span class="line"><span class="keyword">const</span> texture = gl.createTexture();</span><br><span class="line">gl.bindTexture(gl.TEXTURE_2D, texture);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 参数设置</span></span><br><span class="line">gl.texParameteri(...);</span><br><span class="line">gl.texParameteri(...);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 填充纹理的内容</span></span><br><span class="line">gl.texImage2D(..., image);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 通过纹理单元将纹理传送给着色器程序</span></span><br><span class="line">gl.uniform1i(...);</span><br></pre></td></tr></table></figure>
<p>然后,在着色器中,使用一个坐标 (x,y) 从纹理上取色。</p>
<figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">vec4</span> color = <span class="built_in">texture2D</span>(<span class="built_in">texture</span>, <span class="type">vec2</span>(x, y));</span><br></pre></td></tr></table></figure>
<p>通常,从纹理上取色的坐标 x 和 y 的值均在 0~1 之间。Wrap 配置项规定了当取色坐标的坐标取值在 (0, 1) 之外(即试图从纹理图片之外取色)时,应该如何取色。Wrap 有两种:切割(CLAMP)和重复(REPEAT)。我们可以操作上面的 Demo,调整 scale 的值使得纹理图片缩小一些,然后切换 Wrap 配置项为 CLAMP 或 REPEAT,可以发现:当选项置为 CLAMP 时,从纹理外部取色会落到对应的纹理的边缘,比如 <code>texture2D(texture, vec2(2.2, 0.5))</code> 的值会等于 <code>texture2D(texture, vec2(1.0, 0.5))</code>;而选项置为 REPEAT 时,纹理会「平铺」开来,从外部取色会把取色坐标按1取模,映射到纹理内部,比如 <code>texture2D(texture, vec2(2.2, 0.5))</code> 的值会等于 <code>texture2D(texture, vec2(0.2, 0.5))</code>。</p>
<p>切换 Wrap 配置项的代码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// CLAMP</span></span><br><span class="line">gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);</span><br><span class="line">gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);</span><br><span class="line"></span><br><span class="line"><span class="comment">// REPEAT</span></span><br><span class="line">gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);</span><br><span class="line">gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);</span><br></pre></td></tr></table></figure>
<p>下图是 REPEAT 的情况:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1XAx8t9zqK1RjSZPcXXbTepXa-600-380.jpg" alt="repeat"></p>
<p>下图是 CLAMP 的情况:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1dgqbt7voK1RjSZFwXXciCFXa-600-375.jpg" alt="clamp"></p>
<blockquote>
<p>有一点需要注意的是,REPEAT 模式对纹理图片的尺寸有要求,宽度和高度必须为 2 的整数次幂,如 32x32 或 1024x256 的图片都是符合要求的,但 500x512 或 300x300 是不符合的。我们可以将控件中的 size 从 512 改成 300,这时 Demo 将加载这张图片的一个尺寸为 300x300 的替代品作为纹理。如果 Wrap 为 CLAMP,我们会发现稍微纹理模糊了一些,但是如果 Wrap 为 REPEAT,则会报警并黑屏。</p>
</blockquote>
<h2 id="FlipY"><a href="#FlipY" class="headerlink" title="FlipY"></a>FlipY</h2><p>FlipY 是 WebGL 的一个全局配置项(准确地说,它的名称是 UNPACK_FLIP_Y_WEBGL),在本文的范畴中,它主要影响了 <code>texImage2D()</code> 方法的行为。</p>
<p>按照 OpenGL 的惯例,当着色器调用 <code>texture2D(t, st)</code> 方法从纹理中取色时,传入的取色坐标 <code>st</code> 的原点是左下角。换言之,如果传入的坐标是 (0, 0),那么 OpenGL 期望取到的是左下角的那个像素的颜色。但是,由于在 Web 上图片数据的存储是从左上角开始的,传入坐标 (0,0) 时,实际上会取到左上角坐标的值。如果我们设置了 <code>FlipY</code> 配置项,那么在向纹理中加载数据的时候,就会对数据作一次翻转,使纹理坐标原点变为左下角。</p>
<p>开启 FlipY 的代码是:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, <span class="literal">true</span>);</span><br></pre></td></tr></table></figure>
<p>在 Demo 中,默认情况下 FlipY 配置项是勾选的,如果你勾选取消,会发现纹理图片被翻转了。下图是取消 FlipY 配置后的情况。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1dFuJtVzqK1RjSZSgXXcpAVXa-600-379.jpg" alt="cancel-flipY"></p>
<h2 id="MIN-FILTER-和-MAG-FILTER"><a href="#MIN-FILTER-和-MAG-FILTER" class="headerlink" title="MIN_FILTER 和 MAG_FILTER"></a>MIN_FILTER 和 MAG_FILTER</h2><p>一个纹理是由离散的数据组成的,比如一个 2x2 的纹理是由 4 个像素组成的,使用 (0,0)、(0, 1) 等四个坐标去纹理上取样,自然可以取到对应的像素颜色;但是,如果使用非整数坐标到这个纹理上去取色。比如,当这个纹理被「拉近」之后,在屏幕上占据了 4x4 一共 16 个像素,那么就会使用 (0.33,0) 之类的坐标去取值,如何根据离散的 4 个像素颜色去计算 (0.33,0) 处的颜色,就取决于参数 MAG_FILTER。在 WebGL 中设置这两个项的代码如下所示:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);</span><br><span class="line">gl.texParameteri(</span><br><span class="line"> gl.TEXTURE_2D, </span><br><span class="line"> gl.TEXTURE_MIN_FILTER, </span><br><span class="line"> gl.LINEAR_MIPMAP_LINEAR</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>MAG_FILTER 有两个可选项,NEAREST 和 LINEAR。顾名思义,NEAREST 就是去取距离当前坐标最近的那个像素的颜色,而 LINEAR 则会根据距离当前坐标最近的 4 个点去内插计算出一个数值,如图所示。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1hgfJt3HqK1RjSZFEXXcGMXXa-600-481.png" alt="MAG_FILTER"></p>
<p>显然 NEAREST 的运行速度更快,但 LINEAR 的效果更好。使用 NEAREST 时,当纹理被拉得比较近,颗粒感会比较明显,而使用 LINEAR 则会顺滑一些。你可以切换右上角空间上的 MAG_FILTER 选项,然后拖动 scale 滑块将纹理放大,体会一下两者的差别。</p>
<p>下图是 MAG_FILTER 为 NEAREST 的效果:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1l_nZtYvpK1RjSZFqXXcXUVXa-600-421.png" alt="MAG_FILTER_NEAREST"></p>
<p>下图是 MAG_FILTER 为 LINEAR 的效果:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1I2_St4TpK1RjSZR0XXbEwXXa-600-419.png" alt="MAG_FILTER_LINEAR"></p>
<p>MAG_FILTER 作用于将纹理拉近/放大的情形,而当纹理远离/缩小的时候,起作用的是 MIN_FILTER。MIN_FILTER 有以下 6 个可选配置项:</p>
<ul>
<li>NEAREST</li>
<li>LINEAR</li>
<li>NEAREST_MIPMAP_NEAREST</li>
<li>NEAREST_MIPMAP_LINEAR</li>
<li>LINEAR_MIPMAP_NEAREST</li>
<li>LINEAR_MIPMAP_LINEAR</li>
</ul>
<p>前两个配置项和 MAG_FILTER 的含义和作用是完全一样的。但问题是,当纹理被缩小时,原纹理中并不是每一个像素周围都会落上采样点,这就导致了某些像素,完全没有参与纹理的计算,新纹理丢失了一些信息。假设一种极端的情况,就是一个纹理彻底缩小为了一个点,那么这个点的值应当是纹理上所有像素颜色的平均值,这才比较合理。但是 NEAREST 只会从纹理中取一个点,而 LINEAR 也只是从纹理中取了四个点计算了一下而已。这时候,就该用上 MIPMAP 了。</p>
<h2 id="Mipmap-和-MIN-FILTER"><a href="#Mipmap-和-MIN-FILTER" class="headerlink" title="Mipmap 和 MIN_FILTER"></a>Mipmap 和 MIN_FILTER</h2><p>为了在纹理缩小也获得比较好的效果,需要按照采样密度,选择一定数量(通常大于 LINEAR 的 4 个,极端情况下为原纹理上所有像素)的像素进行计算。实时进行计算的开销是很大的,所有有一种称为 MIPMAP(金字塔)的技术。在纹理创建之初,就为纹理创建好 MIPMAP,比如对 512x512 的纹理,依次建立 256x256(称为 1 级 Mipmap)、128x128(称为 2 级 Mipmap) 乃至 2x2、1x1 的纹理。实时渲染时,根据采样密度选择其中的某一级纹理,以此避免运行时的大量计算。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1_0.ot9zqK1RjSZPxXXc4tVXa-501-503.png" alt="Mipmap"></p>
<blockquote>
<p>显而易见的是,使用 MIPMAP 同样需要纹理尺寸为 2 的整数次幂。</p>
</blockquote>
<p>WebGL 中,可以通过调用以下方法来生成 mipmap。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gl.generateMipmap(gl.TEXTURE_2D);</span><br></pre></td></tr></table></figure>
<p>我们将控制面板中的 <code>generateMipmap</code> 打开,并把 scale 调到一个较小的值(比如 0.5),然后切换 MIN_FILTER 的各种设置,就可以观察到 mipmap 的效果了。</p>
<p>下图是 MIN_FILTER 为 LINEAR 的效果(NEAREST 效果是类似的):</p>
<p><img src="https://gw.alicdn.com/tfs/TB1CmY0tVzqK1RjSZFvXXcB7VXa-600-420.png" alt="MIN_FILTER_LINEAR"></p>
<p>下图是 MIN_FILTER 为 LINEAR_MIPMAP_NEAREST 的效果(LINEAR_MIPMAP_LINEAR 效果是类似的):</p>
<p><img src="https://gw.alicdn.com/tfs/TB11zLZt9zqK1RjSZPcXXbTepXa-600-418.png" alt="MIN_FILTER_MIPMAP"></p>
<p>可以看到当采用 MIPMAP 时,纹理平滑了很多(特别是头发部分)。</p>
<blockquote>
<p>MIN_FILTER 的默认值是 LINEAR_MIPMAP_NEAREST。在 XXX_MIPMAP_XXX 的格式中,前一个 XXX 表示在单个 MIPMAP 中取色的方式,与单独的 LINEAR 或 NEAREST 类似,而后一个 XXX 表示,随着采样密度连续变化(通常是因为缩放因子连续变化)时,是否在多层 MIPMAP 之间内插。使用 MIPMAP 时,后一个 LINEAR 比较重要,只要后者是 LINEAR,前者的意义其实并不特别大,所以默认选项 NEAREST_MIPMAP_LINEAR 也是最常用的。</p>
</blockquote>
<h2 id="自定义-Mipmap"><a href="#自定义-Mipmap" class="headerlink" title="自定义 Mipmap"></a>自定义 Mipmap</h2><p>我们可以使用 <code>gl.generateMipmap()</code> 方法来根据原始纹理生成一套 mipmap,这种生成的方式是默认的。但是实际上,我们还有一种更灵活的方式来自定义 Mipmap,那就是直接传入另一张图片。比如,这里我们使用的是一张 512x512 的 lena 图,调用 <code>gl.generateMipmap()</code> 会生成 256x256、128x128 直至 1x1 等一系列图片。但是,如果我们手头正好有一张 128x128 尺寸的图片,我们就可以强制指定这张图片作为原始纹理的 1 级 Mipmap。</p>
<p><img src="https://gw.alicdn.com/tfs/TB17fknt9rqK1RjSZK9XXXyypXa-600-503.png" alt="Custom Mipmap"></p>
<p>自定义 Mipmap 纹理的代码如下所示:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 首先要调用此方法</span></span><br><span class="line"><span class="comment">// 如果在 texImage2D 后调用,则定制的 mipmap 会被覆盖</span></span><br><span class="line">gl.generateTexture(gl.TEXTURE_2D);</span><br><span class="line"></span><br><span class="line">gl.texImage2D(</span><br><span class="line"> gl.TEXTURE_2D,</span><br><span class="line"> <span class="number">1</span>, <span class="comment">// 就是这个参数指定几级 Mipmap</span></span><br><span class="line"> gl.RGBA,</span><br><span class="line"> gl.RGBA,</span><br><span class="line"> gl.UNSIGNED_BYTE,</span><br><span class="line"> image <span class="comment">// 尺寸必须严格符合指定级别 Mipmap 应有的尺寸</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>当我们指定好自定义的 Mipmap 纹理后(即勾选 customMipmap),同时勾选 generateMipmap,并且保证 MIN_FILTER 在 XXX_MIPMAP_XXX 的配置上,此时拖动 scale 滑块,会发现在缩小过程中的某个过程,纹理会变成自定义图片。当 MIN_FILTER 在 XXX_MIPMAP_NEAREST 时,纹理是「突变」的;而当 MIN_FILTER 位于 XXX_MINMAP_LINEAR 时,纹理是「渐变」的,这也印证了前面关于 XXX_MIPMAP_XXX 的解释。</p>
<p>下图是 MIN_FILTER 为 LINEAR_MIPMAP_NEAREST 时缩放的效果:</p>
<p><img src="https://gw.alicdn.com/tfs/TB11r3xt7voK1RjSZFDXXXY3pXa-500-316.gif" alt="CustomMipmap1"></p>
<p>下图是 MIN_FILTER 为 LINEAR_MIPMAP_LINEAR 时缩放的效果:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1ctgvtYvpK1RjSZFqXXcXUVXa-500-316.gif" alt="CustomMipmap2"></p>
<h2 id="SRGB-扩展"><a href="#SRGB-扩展" class="headerlink" title="SRGB 扩展"></a>SRGB 扩展</h2><p>RGB 颜色空间是为人眼设计的。但是人眼感光强度和真实光线的物理强度并不是线性关系,这是人眼的生理结构所决定的。比如,在人眼看来,屏幕上显示的白色 #FFFFFF 的亮度,是半灰色 #888888 亮度的 2 倍,但是两者真实的光线物理强度(直射下单位面积上受光照的功率)并不是 2 倍关系。所以,当着色器需要基于一些物理规律来计算颜色时,直接取纹理的颜色没有意义,需要作一次转换。</p>
<p>一个经验是,将人眼感知的颜色归一化到 (0,1) 之间,然后取 2.2 次方幂,得到的结果与光线物理强度是呈线性关系的。如下所示,横轴是 RGB 色彩空间,蓝色直线是人眼的感光曲线,红色曲线是光线物理强度曲线。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1QD7Xt3HqK1RjSZFEXXcGMXXa-297-286.png" alt="SRGB"></p>
<p>SRGB 扩展所做的就是这件事,在用户调用 <code>texImage2D</code> 向纹理中传输数据时,将纹理的每个像素颜色取了自己的 2.2 次方幂。开启 SRGB 扩展的代码如下所示:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> SRGBExtension = gl.getExtension(<span class="string">"EXT_SRGB"</span>);</span><br><span class="line"></span><br><span class="line">gl.texImage2D(</span><br><span class="line"> gl.TEXTURE_2D, </span><br><span class="line"> <span class="number">0</span>, </span><br><span class="line"> <span class="comment">// 接下来两个参数原本是 gl.RGBA</span></span><br><span class="line"> SRGBExtension.SRGB_ALPHA_EXT, </span><br><span class="line"> SRGBExtension.SRGB_ALPHA_EXT, </span><br><span class="line"> gl.UNSIGNED_BYTE, </span><br><span class="line"> image</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>在控件中勾选 SRGB,会发现纹理变暗了很多,因为颜色作为 [0,1] 之间的数值,取了自己的 2.2 次方幂,自然变小了。其实,如果不使用 SRGB 扩展,在着色器中也可以模拟。我们可以取消勾选 SRGB 选项,然后将 postProcess(后处理)项置为 c^2.2,画面也同样变暗了。postProcess 是这个 Demo 埋在 Shader 中的一个普通 uniform 变量,为最后颜色输出增加一个可选的环节(取自己的 2.2 次方幂,或者取自己的 1/2.2 次方幂)。</p>
<p>着色器中有关 postProcess 的代码如下:</p>
<figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(uPostProcess == <span class="number">1</span>){</span><br><span class="line"> <span class="built_in">gl_FragColor</span> = <span class="type">vec4</span>(<span class="built_in">pow</span>(<span class="built_in">gl_FragColor</span>.rgb, <span class="type">vec3</span>(<span class="number">1.0</span>/<span class="number">2.2</span>)), <span class="number">1.0</span>);</span><br><span class="line">}<span class="keyword">else</span> <span class="keyword">if</span>(uPostProcess == <span class="number">2</span>){</span><br><span class="line"> <span class="built_in">gl_FragColor</span> = <span class="type">vec4</span>(<span class="built_in">pow</span>(<span class="built_in">gl_FragColor</span>.rgb, <span class="type">vec3</span>(<span class="number">2.2</span>)), <span class="number">1.0</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>下图是 SRGB 模式的效果,postProcess 选择 c^2.2 能达到同样的效果。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1Aqr8tZbpK1RjSZFyXXX_qFXa-600-419.png" alt="SRGB"></p>
<p>当然,我们也可以勾选 SRGB,然后在 postProcess 中选择取 1/2.2 次方幂,这样输出的颜色和最初的几乎一样。SRGB 扩展将纹理颜色值压低,后处理又把输出的颜色值调亮,两者相互抵消。这正是 PBR 渲染的常用做法,先把纹理颜色从人眼颜色空间的纹理压低成物理空间,然后进行一系列物理规律运算,最后输出时再调亮成人眼颜色空间。</p>
<p>下图是 SRGB + postProcess c^1/2.2 的效果,和不使用 SRGB 和 postProcess 的效果基本一致:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1Qr__tYvpK1RjSZFqXXcXUVXa-600-421.png" alt="SRGB_PostProcess"></p>
<h2 id="Tex-Lod-扩展"><a href="#Tex-Lod-扩展" class="headerlink" title="Tex_Lod 扩展"></a>Tex_Lod 扩展</h2><p>之前说过,从 Mipmap 中的哪一级纹理取色,取决于在此纹理上采样的密度。但是有了 Tex_Lod 扩展,可以不受此限制,在着色器中直接编码指定从某一级纹理取色。</p>
<p>开启 Tex_Lod 扩展:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gl.getExtension(<span class="string">"EXT_shader_texture_lod"</span>);</span><br></pre></td></tr></table></figure>
<p>着色器中也需要加入开启 #extension 宏:</p>
<figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#extension GL_EXT_shader_texture_lod: enable</span></span><br></pre></td></tr></table></figure>
<p>在着色器中,可调用 <code>texture2DLodExt</code> 函数来从指定级别的 Mipmap 上取色。</p>
<figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">gl_FragColor</span> = texture2DLodEXT(uTexture, st, uExtLodLevel);</span><br></pre></td></tr></table></figure>
<p>在右上角的控制器中,确保 Mipmap 相关的选项都打开,然后打开 extLod,拖动 extLodLevel 滑块,可见虽然纹理本身没有在缩放,但是纹理似乎变模糊了,此时实际上就是取到级别较高的 Mipmap 中去了,因为 Mipmap 的级别越高,像素数量越少,在尺寸不变的情况下,就会变模糊了。</p>
<p>下图是 extLod 开启后,在不依赖纹理的缩放导致采样密度变化的情况下,直接手动编码来从不同级别的 Mipmap 上取色的情况:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1xK7vtVYqK1RjSZLeXXbXppXa-500-316.gif" alt="extLod"></p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>至此,完成了上述所有配置项的讨论。</p>
<p>(完)</p>
<script async src="https://static.jsbin.com/js/embed.min.js?4.1.7"></script>
<blockquote>
<p>题图出处 <a href="https://unsplash.com/photos/95YRwf6CNw8" target="_blank" rel="noopener">https://unsplash.com/photos/95YRwf6CNw8</a></p>
</blockquote>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://img.alicdn.com/tfs/TB1ng7ewCzqK1RjSZFLXXcn2XXa-900-500.png" alt="WebGL 纹理详解"></p>
<h1 id="WebGL-纹理详解"><a href="#WebGL-纹]]>
</summary>
<category term="WebGl" scheme="http://taobaofed.org/tags/WebGl/"/>
<category term="纹理" scheme="http://taobaofed.org/tags/%E7%BA%B9%E7%90%86/"/>
<category term="Web开发" scheme="http://taobaofed.org/categories/Web%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[前端架构杂思录:议 Function Component 与 Hooks]]></title>
<link href="http://taobaofed.org/blog/2018/11/27/hooks-and-function-component/"/>
<id>http://taobaofed.org/blog/2018/11/27/hooks-and-function-component/</id>
<published>2018-11-27T03:18:33.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://img.alicdn.com/tfs/TB11clIsgHqK1RjSZFkXXX.WFXa-900-500.png" alt="前端架构杂思录:议 Function Component 与 Hooks"></p>
<p>最近团队里 @大果 分享了 React Hooks,也尝试讨论下 Function Component 与 React Hooks,技术的发展路径总是逐步降低其门槛,简单从轻量级角度我们做一个排序:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">createClass Component > Class Component > Function Component</span><br></pre></td></tr></table></figure>
<p>技术上 Class Component 是可以完全代替 createClass Component 方式,所以已经是废弃不推荐使用,那是不是 Function Component 也可以完全替代 Class Component?在没有 Hooks 之前显然是无法做到的。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Hey</span>(<span class="params">props, context</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="xml"><span class="tag"><<span class="name">span</span>></span>Good {props.boy}<span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Function Component 没有内部状态变化机制,只能从外部进行状态的驱动,组件的可测试性也非常高,是一个没有争议的 Good Design。</p>
<p>但这个 Design 并没法替代 Class Component,只是一个可选。所以现实是无论一个组件内部是不是有状态,大部分开发者一定是用思维惯性在编程,或者说 make it work first,都用 Class Component 没毛病。</p>
<p>但当下基于 Class Component 的 React 应用有哪些小问题?</p>
<p>第一,曾经 createClass 不用关心 this 的问题,现在很糟心,比如下面 <code>handleClick</code> 的 this 并不是当前 Hey 类的实例,一不小就异常,这虽然不是 Class Component 的锅,但确实是用户侧存在的多数问题。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Hey</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>{</span><br><span class="line"> state = {</span><br><span class="line"> name: <span class="string">'yiyi'</span>,</span><br><span class="line"> emoji: <span class="string">'😊'</span>;</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> handleClick() {</span><br><span class="line"> <span class="comment">// throw error</span></span><br><span class="line"> <span class="keyword">this</span>.setState({</span><br><span class="line"> emoji: <span class="string">'😢'</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> render() {</span><br><span class="line"> <span class="keyword">return</span> <span class="xml"><span class="tag"><<span class="name">span</span> <span class="attr">onClick</span>=<span class="string">{this.handleClick}</span>></span>Hey {this.state.name}, {this.state.emoji}<span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>第二,当前的 React 应用很容易陷入 <code>标签嵌套地狱</code> 的情形,比如下面用到 Context 的场景就非常典型,看着眼花缭乱。在数据同步场景里,<br>因为需要通知更新所有引用数据的地方,所以通过 render-props 形式定义在 Context.Consumer 的 Children 中,使用到越多的 Context 就会导致嵌套越多的层级,这简直是噩梦。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">render() {</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <FooContext.Provider value={<span class="keyword">this</span>.state.foo}></span><br><span class="line"> <BarContext.Provider value={<span class="keyword">this</span>.state.bar}></span><br><span class="line"> <BarContext.Consumer></span><br><span class="line"> {bar => (</span><br><span class="line"> <FooContext.Consumer></span><br><span class="line"> {foo => (</span><br><span class="line"> <span class="built_in">console</span>.log(bar, foo)</span><br><span class="line"> )}</span><br><span class="line"> <<span class="regexp">/FooContext.Consumer></span></span><br><span class="line"><span class="regexp"> )}</span></span><br><span class="line"><span class="regexp"> </</span>BarContext.Consumer></span><br><span class="line"> <<span class="regexp">/BarContext.Provider></span></span><br><span class="line"><span class="regexp"> </</span>FooContext.Provider></span><br><span class="line"> )</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>第三,一些有状态的逻辑比较难重用。这个其实不算 React 独有的问题,只能说当前主流前端架构体系里都没有很好的解决方案。</p>
<p>因此 React 团队基于 Function Component 提出 Hooks 的概念,Hooks 有几个关键 API: useState、useEffect、useContext。这些 API 让 React 更 Reactive:</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { useState, useContext, useEffect, createContext } <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">const</span> FooContext = createContext(<span class="string">'foo'</span>);</span><br><span class="line"><span class="keyword">const</span> BarContext = createContext(<span class="string">'bar'</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Hey</span>(<span class="params">props, context</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> [name, setName] = useState(<span class="string">'yiyi'</span>);</span><br><span class="line"> <span class="keyword">const</span> [emoji, setEmoji] = useState(<span class="string">'😊'</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> foo = useContext(FooContext);</span><br><span class="line"> <span class="keyword">const</span> bar = useContext(BarContext);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> handleClick = <span class="function"><span class="params">()</span> =></span> setEmoji(<span class="string">'😢'</span>);</span><br><span class="line"></span><br><span class="line"> useEffect(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'componentDidMount or componentDidUpdate'</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'componentWillUnmount'</span>);</span><br><span class="line"> }</span><br><span class="line"> }, [name]);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <></span><br><span class="line"> <span onClick={handleClick}>Hey {name}, {emoji}<<span class="regexp">/span></span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp"> <FooContext.Provider></span></span><br><span class="line"><span class="regexp"> <BarContext.Provider></span></span><br><span class="line"><span class="regexp"> {foo}, {bar}</span></span><br><span class="line"><span class="regexp"> </</span>BarContext.Provider></span><br><span class="line"> <<span class="regexp">/FooContext.Provider></span></span><br><span class="line"><span class="regexp"> </</span>></span><br><span class="line"> )</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>基于 Function Component 与 Hooks 整体代码是比较简洁的,也直接避免了 this 指向的问题,对比上文中 <code>标签嵌套地狱</code> 的代码,尤其使用 useContext 看起来的确舒服太多了,在使用 Context 的地方尽量通过 Function Component 结合 useContext hook 应该是未来的最佳实践。</p>
<p>Hooks 在架构上最值得称赞是提供一种有状态逻辑的复用机制,并且是通过组合的方式。如下使用 hooks 机制对页面是否可见状态的封装:</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> { useState, useEffect } = <span class="built_in">require</span>(<span class="string">'react'</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useDocumentVisibility</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> [documentVisibility, setDocumentVisibility] = useState(<span class="built_in">document</span>.visibilityState);</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">function</span> <span class="title">handleVisibilityChange</span>(<span class="params"></span>) </span>{</span><br><span class="line"> setDocumentVisibility(<span class="built_in">document</span>.visibilityState);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> useEffect(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="built_in">window</span>.addEventListener(<span class="string">'visibilitychange'</span>, handleVisibilityChange);</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="built_in">window</span>.removeEventListener(<span class="string">'visibilitychange'</span>, handleVisibilityChange);</span><br><span class="line"> };</span><br><span class="line"> }, []);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> documentVisibility;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Hey</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> documentVisibility = useDocumentVisibility();</span><br><span class="line"> <span class="keyword">return</span> {documentVisibility === <span class="string">'visible'</span> ? <span class="xml"><span class="tag"><<span class="name">span</span>></span>hi<span class="tag"></<span class="name">span</span>></span></span>: <span class="literal">null</span>}</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>通过 Hooks 可以方便的把状态关注点进行分离,每一个状态分离后可复用,对于一个高复杂逻辑的项目,往往有非常多的业务数据状态,比如A页面与B页面都有一个登录状态需要同步,在原先我们的做法需要主动去关注状态与渲染之间的关系:</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>{</span><br><span class="line"> state = {</span><br><span class="line"> isLogin: getLoginState()</span><br><span class="line"> }</span><br><span class="line"> componenetDidMount() {</span><br><span class="line"> LoginManager.on(<span class="string">'status'</span>, (status) => { <span class="keyword">this</span>.setState({<span class="attr">isLogin</span>: status})})</span><br><span class="line"> }</span><br><span class="line"> render() {</span><br><span class="line"> <span class="keyword">return</span> {<span class="keyword">this</span>.state.isLogin ? <span class="xml"><span class="tag"><<span class="name">span</span>></span>A<span class="tag"></<span class="name">span</span>></span></span> : <span class="literal">null</span> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>{</span><br><span class="line"> state = {</span><br><span class="line"> isLogin: getLoginState()</span><br><span class="line"> }</span><br><span class="line"> componenetDidMount() {</span><br><span class="line"> LoginManager.on(<span class="string">'status'</span>, (status) => { <span class="keyword">this</span>.setState({<span class="attr">isLogin</span>: status})})</span><br><span class="line"> }</span><br><span class="line"> render() {</span><br><span class="line"> <span class="keyword">return</span> {<span class="keyword">this</span>.state.isLogin ? <span class="xml"><span class="tag"><<span class="name">span</span>></span>B<span class="tag"></<span class="name">span</span>></span></span> : <span class="literal">null</span> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以明显的察觉到两个页面为了做登录状态同步的事情,感觉 80% 的代码都是冗余重复的,如果使用 Hooks 就是完全不同的情形:</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useLogin</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">const</span> [isLogin, setLogin] = useState(getLoginState());</span><br><span class="line"> LoginManager.on(<span class="string">'status'</span>, (status) => { setLogin(status)});</span><br><span class="line"> <span class="keyword">return</span> isLogin;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">A</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> isLogin = useLogin();</span><br><span class="line"> <span class="keyword">return</span> {isLogin ? <span class="xml"><span class="tag"><<span class="name">span</span>></span>A<span class="tag"></<span class="name">span</span>></span></span> : <span class="literal">null</span> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">B</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> isLogin = useLogin();</span><br><span class="line"> <span class="keyword">return</span> {isLogin ? <span class="xml"><span class="tag"><<span class="name">span</span>></span>B<span class="tag"></<span class="name">span</span>></span></span> : <span class="literal">null</span> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>细心的同学可能会发现 Function Component 在 re-render 时除了纯粹的 render 代码之外 useState 也是重复被申明与执行了的,这在逻辑上似乎有些不合常理,为什么下面代码重复被执行组件内上一次的 state 依旧还在?</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [name, setName] = useState(<span class="string">'yiyi'</span>);</span><br><span class="line"><span class="keyword">const</span> [emoji, setEmoji] = useState(<span class="string">'😊'</span>);</span><br></pre></td></tr></table></figure>
<p>这里我们了解下 useState 的工作原理,看如下 useState 实现原理的示例代码,引擎通过代码上 useState 的执行顺序在内部维护一个 stateIndex 来识别当前是哪一个 state,并且只在第一次 render 时的才接受 initState, re-render 的时候从内部维护 state 存储器中获取上一次的 state 值。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> stateIndex = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">let</span> currentComponentInstance = <span class="literal">null</span>;</span><br><span class="line"><span class="keyword">let</span> isComponentDidMount = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useState</span>(<span class="params">initState</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> index = ++stateIndex;</span><br><span class="line"> <span class="keyword">const</span> privateStateStore = currentComponentInstance._state;</span><br><span class="line"> <span class="keyword">if</span> (!isComponentDidMount) {</span><br><span class="line"> privateStateStore[index] = initState;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 示例代码只考虑简单的数据类型</span></span><br><span class="line"> <span class="keyword">const</span> stateUpdater = <span class="function">(<span class="params">state</span>) =></span> privateStateStore[index] = state;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> [privateStateStore[index], stateUpdater];</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>从内部原理实现角度,这个方案并不优雅,解决了问题但坑也比较大,比方说:useState 的执行顺序要在每次 render 时必须保持一致,否则 stateIndex 的顺序就会错乱,对于不熟悉个约定的新手来说是一个噩梦,这个问题一旦发生非常难调试。有人提议借助 Lint 来规避这个问题,这是典型的填补一个坑通过挖另一个坑来解决。 </p>
<p>关于生命周期,使用 useEffect 基本解决了在 Fuction Component 无生命周期的问题,但这也是有代价的,显然 useEffect 在语义上抽象的不确定的,最糟糕的是 useEffect 约定了 return 的函数执行时机等价与 componentWillUnmount 执行时机,表达上比较晦涩,给代码的可读性上带来了些许的不愉快。要清楚 useEffect 并没有避免生命周期的概念,只是用一种方式隐藏了他们,这种隐藏方式我理解是基于 Fuction Component 的一种无奈。</p>
<p>此外 Function Component 还有一个特点是外部对组件的操作只能通过 props 进行控制,所以组件暴露方法来控制组件内部状态的方式不存在了,理想上能统一使用 Function Component 在架构上这一个益处,外部接口暴露更一致了,但只是理想。</p>
<p>结尾,复杂应用尽可能使用 Function Component + Hooks 是值得推荐的,这是更美好的明天。</p>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://img.alicdn.com/tfs/TB11clIsgHqK1RjSZFkXXX.WFXa-900-500.png" alt="前端架构杂思录:议 Function Component 与 Hooks"></p>
<p>最近团队里 @大]]>
</summary>
<category term="React" scheme="http://taobaofed.org/tags/React/"/>
<category term="Hooks" scheme="http://taobaofed.org/tags/Hooks/"/>
<category term="函数式组件" scheme="http://taobaofed.org/tags/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6/"/>
<category term="前端架构" scheme="http://taobaofed.org/tags/%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84/"/>
<category term="Web开发" scheme="http://taobaofed.org/categories/Web%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[Atag - Web Components 最佳实践]]></title>
<link href="http://taobaofed.org/blog/2018/10/31/a-tag/"/>
<id>http://taobaofed.org/blog/2018/10/31/a-tag/</id>
<published>2018-10-31T08:09:15.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB1Q3Y4l4TpK1RjSZFGXXcHqFXa-900-500.jpg" alt="Atag - Web Components 最佳实践"></p>
<h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>上一次社区中谈论起 Web Components 已经可以追溯到三四年前了,彼时 Web Components 仍处于不稳定的草案阶段,Polymer 的出世使大家似乎看到了新一代的前端技术,但直到今天,在今年五月 Google I/O 发布 Polymer 3 之后, Web Components 的规模化应用才看似成为了可能。</p>
<p>过去一段时间,我一直在使用 Web Components 构建淘宝小程序的 <a href="https://github.com/alibaba/rax/tree/master/packages/atag" target="_blank" rel="noopener">基础组件 Atag</a>。MDN 上对 Web Components 这个名词的解释是</p>
<blockquote>
<p>Web Components是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。</p>
</blockquote>
<p>我们从中提取几个关键字:<code>可重用</code> <code>定制元素</code> <code>封装</code> </p>
<p>这些特性刚好能满足可复用组件的需求,更重要的是,这是由 W3C 标准提供的,面向标准编程不需要再考虑我使用的技术在未来几年内会不会过时。目前社区中的框架大都具有<strong>传染性</strong>,什么是传染性?如果你希望使用一个 React 组件,大概率你的整个用户界面都会使用 React 来开发。而 Custom Elements 不是这样,因为它替代的是 <code>div</code>,能使用 <code>div</code> 的地方就能使用它,它即插即用:引入一个 <code>js</code> 文件就可以了,直接操作 DOM 使它的性能更高,它并不跟社区主流的框架相克,这样看来它更适合用来开发底层的基础组件。</p>
<p>回到正题,这篇文章的目的,是希望总结在 Atag 开发阶段中使用 Web Components 的经验,避免大家踩坑。</p>
<h2 id="基础设施"><a href="#基础设施" class="headerlink" title="基础设施"></a>基础设施</h2><h3 id="webcomponentsjs"><a href="#webcomponentsjs" class="headerlink" title="webcomponentsjs"></a>webcomponentsjs</h3><p><a href="https://github.com/webcomponents/webcomponentsjs" target="_blank" rel="noopener">Github 地址</a></p>
<p>这是一系列 Web Components 规范的 polyfill 集合,如果你的目标用户不是最新的现代浏览器,强烈建议引入这个库。</p>
<p>引用方式 :</p>
<p>建议在 html 中使用 script 标签引入它,而不是通过 npm 引入,这样浏览器可以使用缓存帮助你减少二次加载的消耗。</p>
<p>(以下引入方式二选一) </p>
<ol>
<li>使用 bundle 整包引入,这样会引入整个库中包含的所有 polyfills。<ul>
<li>如果你需要按需引入 bundle,这个库的 bundles 目录下有一系列预打包好的 bundle 文件,用后缀标明了它包含的功能:ce 表示 custom-elements,sd 表示 shadow-dom,pf 表示环境的 polyfills。</li>
</ul>
</li>
<li>[推荐] 使用 loader 按需引入,引入 <code>webcomponents-loader.js</code> 后,它会根据浏览器中的特征按需加载。</li>
</ol>
<p>这里有必要对一些名词做一些解释:</p>
<ul>
<li>ShadyDOM 这是 Shadow DOM 的 polyfill 的官方名称,它通过劫持 HTMLElement 的原型方法来实现一些 Shadow DOM 节点拥有的功能,实际上它的原理是把节点添加到了真实(light) DOM 节点之上。</li>
<li>ShadyCSS 跟上面一样,这也是 polyfill 的名称,它提供了一些 Shadow DOM 节点内样式的封装,使得可以在真实 DOM 中模拟 scoped style 的效果。它的原理是通过解析和重写 style 节点内部的样式规则来实现的。</li>
</ul>
<p>需要注意的是,在引入 polyfill 的同时,有一些功能是无法被模拟的,需要我们在使用的时候避开,在下文中会介绍到。</p>
<blockquote>
<p>NOTE: 这个库的 2.1.x 版本对 Symbol 的 polyfill 有一些问题,在官方修复之前建议使用 2.0.4 的稳定版本。</p>
<p>这里提供一份 alicdn 的 bundle URL:</p>
<p><code>https://g.alicdn.com/code/npm/@webcomponents/webcomponentsjs/2.0.4/bundles/webcomponents-sd-ce-pf.js</code></p>
</blockquote>
<h3 id="polymer-3"><a href="#polymer-3" class="headerlink" title="polymer 3"></a>polymer 3</h3><p><a href="https://github.com/Polymer/polymer" target="_blank" rel="noopener">Github 地址</a></p>
<p>在 Web Components 的实践中,Polymer 不是必须的,但有了它会让我们轻松很多。强烈建议使用最新版本的 Polymer 3,在这个版本不再使用 html 定义和引入组件,官方推荐使用 JS 模块的方式进行文件组织,同时抛弃了 bower 迎接 npm,这使得很多现代的前端工具能派上用场,比如使用 webpack 和 babel 进行打包,使用 ESLint 对代码进行规则校验,使用 prettier 对代码进行美化等。</p>
<p>安装 polymer 3:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i @polymer/polymer --save</span><br></pre></td></tr></table></figure>
<p>官方推荐的 polymer-cli 工具比较鸡肋,可以不用。</p>
<p>在使用之前强烈建议对 <a href="https://www.polymer-project.org/3.0/start/quick-tour" target="_blank" rel="noopener">polymer 的文档</a> 或者本文进行一番了解,避免踩坑。</p>
<h3 id="构建配置"><a href="#构建配置" class="headerlink" title="构建配置"></a>构建配置</h3><p>为什么要把构建配置单独拿出来讲呢,当然是因为有坑?。开发组件过程中 ES 678 已经是标配,运行在低版本浏览器还得依赖 Babel。Web Components 中每一个 Component,对应一个类 (class)。Babel 的转换逻辑如下:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyTag</span> <span class="keyword">extends</span> <span class="title">HTMLElement</span> </span>{}</span><br><span class="line"></span><br><span class="line">customElements.define(<span class="string">'my-tag'</span>, MyTag);</span><br><span class="line"><span class="built_in">document</span>.createElement(<span class="string">'my-tag'</span>);</span><br></pre></td></tr></table></figure>
<p><strong>—></strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> MyTag = <span class="function"><span class="keyword">function</span> (<span class="params">_HTMLElement</span>) </span>{</span><br><span class="line"> _inherits(MyTag, _HTMLElement);</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">function</span> <span class="title">MyTag</span>(<span class="params"></span>) </span>{</span><br><span class="line"> _classCallCheck(<span class="keyword">this</span>, MyTag);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> _possibleConstructorReturn(<span class="keyword">this</span>, (MyTag.__proto__ || <span class="built_in">Object</span>.getPrototypeOf(MyTag)).apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> MyTag;</span><br><span class="line">}(HTMLElement);</span><br><span class="line"></span><br><span class="line">customElements.define(<span class="string">'my-tag'</span>, MyTag);</span><br><span class="line"><span class="built_in">document</span>.createElement(<span class="string">'my-tag'</span>);</span><br></pre></td></tr></table></figure>
<p>这样执行的话浏览器会显示如下报错 <code>Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function</code>,大意就是被继承的类 HTMLElement 必须使用 new 来初始化,不能使用函数调用 + apply 的使用方式。</p>
<p>针对这个问题 webcomponentsjs 额外提供了一份 <code>custom-elements-es5-adapter-index.js</code> 的 polyfill 来解决,这个文件的具体代码<a href="https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js" target="_blank" rel="noopener">见此</a>。引入这个文件可以通过在组件库的 webpack 配置中添加或者额外在使用的 html 文件中通过 script 标签引入,只要在组件被注册之前执行这段脚本就可以避免报错。</p>
<h2 id="兼容性"><a href="#兼容性" class="headerlink" title="兼容性"></a>兼容性</h2><table>
<thead>
<tr>
<th>特性</th>
<th>Import</th>
<th>ShadowDOM</th>
<th>CustomElement</th>
<th>Template</th>
</tr>
</thead>
<tbody>
<tr>
<td>Chrome 最新版 (66)</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
</tr>
<tr>
<td>Firefox 最新版</td>
<td>N</td>
<td>N</td>
<td>N</td>
<td>Y</td>
</tr>
<tr>
<td>iOS 最新版 (12)</td>
<td>N</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
</tr>
<tr>
<td>Android (UC 11.6.0.960)</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
</tr>
</tbody>
</table>
<p>其中 import 特性在整个体系下比较鸡肋,可以通过 webpack 打包的方式来替代。</p>
<p>结论是在移动端下,99% 以上的用户可以通过 polyfill 的方式来获得比较好的 Web Components 特性支持。</p>
<h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><table>
<thead>
<tr>
<th>(单位 ms)</th>
<th>注册 1W 个组件</th>
<th>渲染 1W 个组件</th>
</tr>
</thead>
<tbody>
<tr>
<td>pure web-components</td>
<td>55.5ms 48.6ms 47.2ms</td>
<td>934.3ms 889.0ms 915.1ms</td>
</tr>
<tr>
<td>polymer</td>
<td>184.9ms 191.9ms 197.8ms</td>
<td>768.0ms 858.4ms 785.0ms</td>
</tr>
<tr>
<td>React 16.2.0</td>
<td>38.9ms 38.0ms 40.60ms</td>
<td>1834.8ms 1754.8ms 1869.5ms</td>
</tr>
<tr>
<td>Rax 使用 View 和 Text 组件</td>
<td>86.4ms 73.9ms 82.4ms</td>
<td>11587.4ms 11238.0ms 11289.6ms</td>
</tr>
<tr>
<td>Rax使用 append={‘tree’} 模式</td>
<td>82.5ms 67.3ms 81.4ms</td>
<td>798.0ms 823.5ms 878.0ms</td>
</tr>
</tbody>
</table>
<p>可以看到,由于 React JSX 的 VDOM 在构建时解析的加持,React 的注册时间是最短的,但是放大到 1W 个组件的渲染时,原生 DOM 的性能就发挥出来了,web-components 获得了比较优秀的表现。对原生 web-components 和 polymer,后者只是在注册的时候由于需要在运行时解析模板字符串,牺牲了一些性能,但是如果组件数量有限,这个性能差距可以忽略不计,加上 polymer 本身提供的一些组件化开发的便利,整体来看使用 polymer 获得的收益还是比较高的。</p>
<blockquote>
<p>基准环境:</p>
<ul>
<li>Chrome 66</li>
<li>macOS 10.13.1</li>
<li>Macbook Pro 15’ late 2015</li>
</ul>
</blockquote>
<h2 id="组件化"><a href="#组件化" class="headerlink" title="组件化"></a>组件化</h2><p>个人认为,Web Components 在整个前端的语境下更偏向于提供符合 DOM 标准的规范,而 Polymer 则是在这种规范之上的一种框架封装,使用 Polymer 可以带来更便利的组件化开发体验。因此这里就不多介绍如何使用标准的 custom element 来创建自定义标签,下文都使用 Polymer 来封装自定义标签。文章中的组件、自定义标签、自定义组件其实描述的是同一个东西。</p>
<p>推荐使用以下模式创建一个自定义组件。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { PolymerElement, html } <span class="keyword">from</span> <span class="string">'@polymer/polymer'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomElement</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> is() {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'custom-element'</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> template() {</span><br><span class="line"> <span class="keyword">return</span> html<span class="string">`</span></span><br><span class="line"><span class="string"> <style>:host { color: red; }</style></span></span><br><span class="line"><span class="string"> <slot></slot></span></span><br><span class="line"><span class="string"> `</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>使用以下方式注册自定义组件。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">customElements.define(CustomElement.is, CustomElement);</span><br></pre></td></tr></table></figure>
<p>使用以下或任意方式使用自定义组件。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> el = <span class="built_in">document</span>.createElement(<span class="string">'custom-element'</span>);</span><br><span class="line">el.innerText = <span class="string">'Hello World'</span>;</span><br><span class="line"><span class="built_in">document</span>.body.appendChild(el);</span><br></pre></td></tr></table></figure>
<h2 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h2><p>PolymerElement 继承了 HTMLElement,所以它拥有和 HTMLElement 一致的生命周期。</p>
<p><strong>constructor</strong>:组件被 create 的时候会被调用,整个生命周期中最早触发也只会触发一次,通常可以在这里做一些初始化私有变量、记录数据的一些操作;但是出于性能和职责分离的考虑,不建议在这里做一些 DOM 相关的事情。</p>
<p><strong>connectedCallback</strong>:组件被 <code>连接</code> 到 DOM Tree 的时候会触发,这个时机包括节点被插入节点树、节点被从节点树中移动,所以它可能会被触发多次。你可以在这里监听 DOM 事件或者对 DOM 节点做一些修改。</p>
<p><strong>disconnectedCallback</strong>:组件被从 DOM Tree 中移除的时候触发,这个生命周期也可能被触发多次。如果你在 connectedCallback 中监听了事件,一定要记得在这里移除,否则事件监听回调可能会一直引用导致内存泄露和一些奇怪的问题。</p>
<p><strong>adoptedCallback</strong>:不常用,不多介绍。</p>
<p><strong>attributeChangedCallback</strong>:当组件的 attribute 发生变化的时候触发,它的三个形参分别是 <code>name, oldValue, newValue</code>,记得别把顺序搞反了。如果你声明了 properties 对象,对 attribute 的相应值变化也会触发这个回调。需要注意的是,如果你覆盖了组件的 <code>observedAttributes</code> 静态方法,properties 对象中声明的值不会触发,它会按照你覆盖的 <code>observedAttributes</code> 静态方法的返回值为准。</p>
<p>除此之外,polymer 还额外添加了一些生命周期</p>
<p><strong>ready</strong>:由于 HTMLElement 的生命周期中没有一个可以操作 DOM,又只触发一次的周期,Polymer 人为地添加了 ready 这个时机,它在整个生命周期中只会触发一次,也就是第一次节点插入到 DOM 树的时刻。</p>
<p>记得调用 <code>super.ready()</code> 来触发 <code>PolymerElement</code> 的初始化阶段。在初始化阶段,Polymer 会做以下几件事情:</p>
<ul>
<li>attache 组件实例的 Shadow DOM,所以在这个阶段之后才可以访问 <code>this.shadowRoot</code></li>
<li>初始化 properties,并赋初始值</li>
<li>如果 properties 有声明 observer 或者 computed,会执行它们</li>
</ul>
<p>通常可以在 ready 函数中给组件实例添加一个 <code>this._isReady = true;</code> 的状态以标明组件已经 ready。</p>
<h4 id="首屏性能优化技巧"><a href="#首屏性能优化技巧" class="headerlink" title="首屏性能优化技巧"></a>首屏性能优化技巧</h4><p>我们知道页面的首屏渲染直接影响了这个页面的性能,组件也是一样。Polymer 提供了 render-status 的 afterNextRender 方法来帮助你在首次渲染之后执行一些不必要的 DOM 操作,比如添加事件绑定。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { afterNextRender } <span class="keyword">from</span> <span class="string">'@polymer/polymer/lib/utils/render-status'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DeferElement</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> ready() {</span><br><span class="line"> <span class="keyword">super</span>.ready();</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// When possible, use afterNextRender to defer non-critical</span></span><br><span class="line"> <span class="comment">// work until after first paint.</span></span><br><span class="line"> afterNextRender(<span class="keyword">this</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">this</span>.addEventListener(<span class="string">'click'</span>, <span class="keyword">this</span>._handleClick);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="属性-property-和属性-attribute"><a href="#属性-property-和属性-attribute" class="headerlink" title="属性 (property)和属性 (attribute)"></a>属性 (property)和属性 (attribute)</h2><p>在 DOM 中,property 和 attribute 这两个概念是严格区分的,虽然有时它们会产生类似双向绑定的联动,例如 id、input 的 checked 属性等。</p>
<p>在 Polymer 中,使用 properties 静态属性来声明组件的 property:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">XCustom</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> properties() {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> user: <span class="built_in">String</span>,</span><br><span class="line"> isHappy: <span class="built_in">Boolean</span>,</span><br><span class="line"> count: {</span><br><span class="line"> type: <span class="built_in">Number</span>,</span><br><span class="line"> readOnly: <span class="literal">true</span>,</span><br><span class="line"> notify: <span class="literal">true</span>,</span><br><span class="line"> },</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">customElements.define(<span class="string">'x-custom'</span>, XCustom);</span><br></pre></td></tr></table></figure>
<p>在大多数情况下,如果你的属性需要暴露为公有 (public) API,你需要把这个属性声明到 properties 对象中。</p>
<p>关于 properties 对象的声明,可以参考<a href="https://www.polymer-project.org/3.0/docs/devguide/properties" target="_blank" rel="noopener">文档</a></p>
<h3 id="属性的默认值"><a href="#属性的默认值" class="headerlink" title="属性的默认值"></a>属性的默认值</h3><p>通过 property 的 value 可以设置属性的默认值,这里<strong>强烈建议</strong>所有的属性都显式声明一个默认值,这样更清晰于阅读,也避免了默认为 undefined 值处理的坑。</p>
<p>如果声明的 value 是一个函数,Polymer 会在初始化这个组件的时候取函数的返回值为默认属性;如果 value 是一个对象或数组,Polymer 会在初始化这个组件的时候对 value 做一次浅拷贝,所以不用担心会在不同组件实例中会共享同一个对象。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomEl</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> properties() {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> mode: {</span><br><span class="line"> type: <span class="built_in">String</span>,</span><br><span class="line"> value: <span class="string">'auto'</span></span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> data: {</span><br><span class="line"> type: <span class="built_in">Object</span>,</span><br><span class="line"> notify: <span class="literal">true</span>,</span><br><span class="line"> value: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ <span class="keyword">return</span> {}; }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="attribute-映射为-property"><a href="#attribute-映射为-property" class="headerlink" title="attribute 映射为 property"></a>attribute 映射为 property</h3><p>在 properties 对象中声明的属性,polymer 会自动创建从 attribute 到 perperty 的映射解析规则,解析的规则是:</p>
<ul>
<li>大写的 attribute 名称会被转换为小写的 property:<code>firstName</code> 映射为 <code>firstname</code></li>
<li>带中划线 <code>-</code>的 attribute 名会被转换为小驼峰 (camelCased) 的 property:<code>first-name</code> 映射为 <code>firstName</code></li>
</ul>
<p>值的解析会使用声明的类型去判断和解析,对于 JS 基本类型的值,会直接转换类型映射;对于 Object 和 Array 类型,会通过 <code>JSON.parse</code> 来解析。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><my-element id=<span class="string">"foo"</span> book=<span class="string">'{ "title": "Persuasion", "author": "Austen" }'</span>></my-element></span><br></pre></td></tr></table></figure>
<p>由于 JSON 的序列化和反序列化会比较消耗性能,而且我们从数据源获取数据后一般已经是 JS Object,这个时候通过 property 赋值的方式来使用 Object/Array 类型的数据可以避免额外的性能消耗。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> el = <span class="built_in">document</span>.querySelector(<span class="string">'#foo'</span>);</span><br><span class="line">el.book = { <span class="attr">title</span>: <span class="string">'Persuasion'</span>, <span class="attr">author</span>: <span class="string">'Austen'</span> };</span><br></pre></td></tr></table></figure>
<p>关于布尔值,我们在 React 中习惯于显式传递布尔值 <code><Checkbox checked={false} /></code> ,但在 DOM 中,attribute 判断布尔类型的方式是判断 attribute value 字符串的长度。<code><a-checkbox checked /> <a-checkbox checked="false"></code> 前面这些都表示 checked 为 true,而只有 <code><a-checkbox /></code> 才表示 checked 为 false。</p>
<p>所以对于布尔值,这里建议在使用时跟对象一样,使用 property 赋值的方式去修改,避免使用 setAttribute 修改布尔类型的属性。如果你希望修改 checked attribute 的语义,在为字符串 “false” 的时候表示真正的 false 概念,你也可以声明一个 getter 来帮助你在内部做判断,但是其实我不建议这么做。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">X</span> <span class="title">extend</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">get</span> _isLoop() {</span><br><span class="line"> <span class="keyword">var</span> loop = <span class="keyword">this</span>.getAttribute(<span class="string">'loop'</span>);</span><br><span class="line"> <span class="keyword">return</span> loop !== <span class="string">'false'</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="property-映射为-attribute"><a href="#property-映射为-attribute" class="headerlink" title="property 映射为 attribute"></a>property 映射为 attribute</h3><p>双向绑定不是必须的。</p>
<p>通过开启 <code>reflectToAttribute: true</code> 选项可以自动把 property 映射为 attribute 值,序列化的方式跟上边正好相反。由于涉及到 setAttribute 这一 DOM 操作,开启这个功能是比较消耗性能的,仅在必要的时候开启。</p>
<p>使用的例子如 <code>radio</code> 、<code>checkbox</code> 组件的 <code>checked</code> 属性,<code>image</code> 组件的 <code>src</code> 属性等。</p>
<h3 id="计算属性"><a href="#计算属性" class="headerlink" title="计算属性"></a>计算属性</h3><p>跟 Vue 的 computed 很像 (其实是 Vue 参考了这里),计算属性允许你用一个方法来计算 property 的值。</p>
<h3 id="观察属性"><a href="#观察属性" class="headerlink" title="观察属性"></a>观察属性</h3><p>跟 Vue 的 watch 很像,当你的属性发生变化时,polymer 会通知相应的函数。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">XCustom</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> properties() {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> active: {</span><br><span class="line"> type: <span class="built_in">Boolean</span>,</span><br><span class="line"> <span class="comment">// Observer method identified by name</span></span><br><span class="line"> observer: <span class="string">'_activeChanged'</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Observer method defined as a class method</span></span><br><span class="line"> _activeChanged(newValue, oldValue) {</span><br><span class="line"> <span class="keyword">this</span>.toggleClass(<span class="string">'highlight'</span>, newValue);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="观察-children-的变化"><a href="#观察-children-的变化" class="headerlink" title="观察 children 的变化"></a>观察 children 的变化</h2><p>有时候我们需要观察 children 节点的添加或者删除,比如在 <a href="https://developers.taobao.com/components/container/swiper.html" target="_blank" rel="noopener"><code>swiper</code> 组件</a>中,它需要动态地监听子节点中 <code>swiper-item</code> 节点的增删改,来变化其内部的 <code>indicator-dots</code> 数量等。</p>
<p>Polymer 提供了 FlattenedNodesObserver 工具集合来追踪子孙节点的变化。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { FlattenedNodesObserver } <span class="keyword">from</span> <span class="string">'@polymer/polymer/lib/utils/flattened-nodes-observer'</span>;</span><br></pre></td></tr></table></figure>
<p>添加监听器,每当子节点发生变化的时候会触发对应的回调函数,回调函数的第一个参数包含了一些增加或者删除节点的信息可以使用:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>._childrenObserver = <span class="keyword">new</span> FlattenedNodesObserver(<span class="keyword">this</span>, <span class="keyword">this</span>._handleChildrenChanged);</span><br></pre></td></tr></table></figure>
<p>当然不要忘记移除监听器:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>._childrenObserver.disconnect();</span><br></pre></td></tr></table></figure>
<p>获取子孙节点的方法:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">FlattenedNodesObserver.getFlattenedNodes(<span class="keyword">this</span>);</span><br></pre></td></tr></table></figure>
<h2 id="模板语法"><a href="#模板语法" class="headerlink" title="模板语法"></a>模板语法</h2><p>Polymer 使用 <code>html</code> 方法来把模板字符串转换成一个 DOM fragment,并可以自由绑定组件实例上下文中的属性。</p>
<h3 id="创建-Shadow-DOM"><a href="#创建-Shadow-DOM" class="headerlink" title="创建 Shadow DOM"></a>创建 Shadow DOM</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { PolymerElement, html } <span class="keyword">from</span> <span class="string">'@polymer/polymer'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomElement</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> is() {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'custom-element'</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> template() {</span><br><span class="line"> <span class="keyword">return</span> html<span class="string">`</span></span><br><span class="line"><span class="string"> <style>:host { color: red; }</style></span></span><br><span class="line"><span class="string"> <slot></slot></span></span><br><span class="line"><span class="string"> `</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><code>slot</code> 表示该组件子节点的引用,跟 Vue 的 slot 师出同门,对应 React 的 <code>this.props.children</code> 的概念。</p>
<p>需要注意的是反引号语法在这里其实等价于 <code>html(templateString)</code>,但是不要试图在模板字符串中使用插值,至于原因你可以参考阮老师的 <a href="http://es6.ruanyifeng.com/#docs/string#%E6%A0%87%E7%AD%BE%E6%A8%A1%E6%9D%BF" target="_blank" rel="noopener">ES6 参考</a>。</p>
<h3 id="创建-Light-DOM"><a href="#创建-Light-DOM" class="headerlink" title="创建 Light DOM"></a>创建 Light DOM</h3><p>有时候由于 Shadow DOM 的一些限制,它并不能满足所有组件的需求,我们可能需要创建一个拥有真实节点的自定义标签组件。</p>
<p>跟普通的 DOM 操作一样,如果你需要创建真实节点(light dom),直接在 constructor 生命周期中使用 DOM 方法创建并添加到节点的 children 中即可。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { PolymerElement, html } <span class="keyword">from</span> <span class="string">'@polymer/polymer'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomElement</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> is() {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'custom-element'</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">constructor</span>() {</span><br><span class="line"> <span class="keyword">super</span>();</span><br><span class="line"> <span class="keyword">const</span> container = <span class="built_in">document</span>.createElement(<span class="string">'div'</span>);</span><br><span class="line"> container.innerHTML = <span class="string">'Hello World'</span>;</span><br><span class="line"> <span class="keyword">this</span>.appendChild(container);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<blockquote>
<p>详细参考 <a href="https://www.polymer-project.org/3.0/docs/devguide/dom-template" target="_blank" rel="noopener">https://www.polymer-project.org/3.0/docs/devguide/dom-template</a></p>
</blockquote>
<h3 id="公私有方法和属性的命名约定"><a href="#公私有方法和属性的命名约定" class="headerlink" title="公私有方法和属性的命名约定"></a>公私有方法和属性的命名约定</h3><p>这是一个建议,但是 W3C 标准中的方法大多遵守这一约定。除生命周期方法外,组件的实例属性和实例方法会被视作外部接口,可被外部直接访问到。所以如果是私有的属性和方法,需要加上 <code>_</code> 作为前缀以作区分。如</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyVideo</span> <span class="keyword">extends</span> <span class="title">PolymerElement</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> properties() {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 作为接口暴露的属性</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> status: {</span><br><span class="line"> type: <span class="built_in">String</span>,</span><br><span class="line"> value: <span class="string">'paused'</span>,</span><br><span class="line"> },</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> connectedCallback() {</span><br><span class="line"> <span class="keyword">this</span>.addEventListener(<span class="string">'click'</span>, <span class="keyword">this</span>._handleClick);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 私有方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> _handleClick(evt) {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 作为接口暴露的方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> play() {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">var</span> video = <span class="built_in">document</span>.createElement(<span class="string">'my-video'</span>);</span><br><span class="line">video.play(); <span class="comment">// 使用暴露的接口方法</span></span><br></pre></td></tr></table></figure>
<blockquote>
<p>Polymer 官方建议使用 <code>_</code> 前缀表示私有变量,<code>__</code> 前缀表示受保护的变量,个人以为这种区分太过复杂,建议用一个 <code>_</code> 来替代就可以了。</p>
</blockquote>
<h2 id="事件"><a href="#事件" class="headerlink" title="事件"></a>事件</h2><p>这里的事件指的都是 DOM 事件,你可以通过模板事件监听或者 DOM 事件监听两种方式来绑定事件。前者是声明式的而且不需要处理解除绑定,所以更加推荐使用模板事件监听器的方式来绑定事件。</p>
<h3 id="模板事件监听器"><a href="#模板事件监听器" class="headerlink" title="模板事件监听器"></a>模板事件监听器</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">get</span> template(){</span><br><span class="line"> <span class="keyword">return</span> html<span class="string">`</span></span><br><span class="line"><span class="string"> <button on-click="handleClick">Click Me</button></span></span><br><span class="line"><span class="string"> `</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这里的 handleClick 指的是实例方法,其它就跟直接绑定 <code>onclick</code> 事件没什么差别。</p>
<h3 id="DOM-事件监听器"><a href="#DOM-事件监听器" class="headerlink" title="DOM 事件监听器"></a>DOM 事件监听器</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">connectedCallback() {</span><br><span class="line"> <span class="keyword">super</span>.connectedCallback()</span><br><span class="line"> <span class="keyword">this</span>.addEventListener(<span class="string">'click'</span>, <span class="keyword">this</span>._handleClick);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>不要忘记在 disconnectedCallback 的时候解除绑定</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">disconnectedCallback() {</span><br><span class="line"> <span class="keyword">super</span>.disconnectedCallback()</span><br><span class="line"> <span class="keyword">this</span>.removeEventListener(<span class="string">'click'</span>, <span class="keyword">this</span>._handleClick);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>有一种情况下你必须使用 DOM 事件监听器:你希望监听的事件节点不在 ShadowDOM 中,例如使用 window 或者 document 对象代理监听事件等。</p>
<h3 id="关于冒泡"><a href="#关于冒泡" class="headerlink" title="关于冒泡"></a>关于冒泡</h3><p>slot 子节点的冒泡,在 ShadowDOM 中,slot 子节点是出现在影子节点树内部的,所以它冒泡事件传递顺序会包含 slot 节点,在 polyfill 的实现中并不是这样,所以不要依赖这一项来实现一些功能。</p>
<h3 id="触发自定义事件"><a href="#触发自定义事件" class="headerlink" title="触发自定义事件"></a>触发自定义事件</h3><p>有时候组件可能希望在事件中带入一些额外的 <code>detail</code> 信息。如果你的事件名称与 DOM 事件不同名,直接使用 Custom Event 的接口声明和派发即可。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> myEvent = <span class="keyword">new</span> CustomEvent(<span class="string">'myevent'</span>, {</span><br><span class="line"> bubbles: <span class="literal">true</span>,</span><br><span class="line"> datail: { <span class="attr">foo</span>: <span class="string">'bar'</span> },</span><br><span class="line">});</span><br><span class="line"><span class="keyword">this</span>.dispatchEvent(myEvent);</span><br></pre></td></tr></table></figure>
<p>如果你的事件名称与 DOM 事件同名,原生 DOM 事件可不支持修改,但是你可以在它冒泡的时候阻止它并派发一个自己的同名事件出来。</p>
<p>在 <code>window</code> 上捕获你需要拦截的事件,并在事件处理回调中停止冒泡原生事件和派发一个自定义的同名事件:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">connectedCallback() {</span><br><span class="line"> <span class="keyword">super</span>.connectedCallback();</span><br><span class="line"> afterNextRender(<span class="keyword">this</span>, () => {</span><br><span class="line"> <span class="built_in">window</span>.addEventListener(<span class="string">'click'</span>, <span class="keyword">this</span>._handleClick, <span class="literal">true</span>);</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">_handleClick(evt) {</span><br><span class="line"> evt.stopPropagation();</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> event = <span class="keyword">new</span> CustomEvent(<span class="string">'click'</span>, {</span><br><span class="line"> bubbles: <span class="literal">true</span>,</span><br><span class="line"> detail: { <span class="attr">foo</span>: <span class="string">'bar'</span> },</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">this</span>.dispatchEvent(event);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="手势"><a href="#手势" class="headerlink" title="手势"></a>手势</h2><p>在移动端中手势的处理可以说是比较常用的功能,我们可以通过监听 <code>touchstart</code> <code>touchmove</code> <code>touchend</code> <code>touchacancel</code> 事件并合成处理几乎所有的复杂交互。Polymer 为组件开发者提供了 <code>Gesture</code> 工具库,你可以按需地引入它:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> Gestures <span class="keyword">from</span> <span class="string">'@polymer/polymer/lib/utils/gestures'</span>;</span><br></pre></td></tr></table></figure>
<p>它提供了很多合成事件的封装,比如在 ATAG 中,swiper 轮播器组件就使用到了 track 合成事件。</p>
<p>在 ready 的时刻添加 track 事件的监听,</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">ready() {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> Gestures.addListener(<span class="keyword">this</span>, <span class="string">'track'</span>, <span class="keyword">this</span>._handleTrack);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">_handleTrack(e) {</span><br><span class="line"> <span class="keyword">const</span> detail = e.detail;</span><br><span class="line"> <span class="keyword">if</span> (detail.state === <span class="string">'start'</span>) {</span><br><span class="line"> <span class="keyword">const</span> dx = detail.dx;</span><br><span class="line"> <span class="keyword">const</span> dy = detail.dy;</span><br><span class="line"> <span class="keyword">const</span> direction = <span class="built_in">Math</span>.abs(dy) - <span class="built_in">Math</span>.abs(dx);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">this</span>.vertical && direction < <span class="number">0</span> || <span class="keyword">this</span>.vertical && direction > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">this</span>._onTouchStart(detail);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>track 使你追踪手指滑动的过程和方向。</p>
<p>有时候你需要考虑横向滚动和纵向滚动之间的关系,比如在横向滚动 swiper 轮播器的时候,通常并不希望同时触发纵向的页面滚动,这个时候就需要在开始内部滚动的时候为组件添加一个拖动的状态,在拖动状态中的 <code>touchmove</code> 事件需要被执行 <code>preventDefault</code></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">_handleSwiperTouchMove = <span class="function"><span class="params">event</span> =></span> {</span><br><span class="line"> <span class="comment">// Prevent scrolling in swipper when user is dragging</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>.dragging) {</span><br><span class="line"> event.preventDefault();</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>另外在 Polymer 监听 track 事件的时候,组件本身的 <code>touch-action</code> CSS 属性会被置为 <code>none</code>,这是一个 Chrome 下已经支持的属性,当为 <code>none</code> 时组件不向上传递触摸事件和阻止默认事件,跟 <code>preventDefault</code> 的效果一致。所以如果你需要响应滚动事件,可以参考以下方法自定义 <code>touch-action</code> 的值:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">NOTE:</span> if swiper direction is different with parent</span></span><br><span class="line"><span class="comment"> * scroll view direction, set touch-action to pan-x if vertically,</span></span><br><span class="line"><span class="comment"> * to pan-y if not.</span></span><br><span class="line"><span class="comment"> * Excepted case: if parent scroll element is the whole page (body)</span></span><br><span class="line"><span class="comment"> * and the swiper's scroll direction is vertical, set touch action</span></span><br><span class="line"><span class="comment"> * to none to avoid double scrolling.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">_observeVertical() {</span><br><span class="line"> afterNextRender(<span class="keyword">this</span>, () => {</span><br><span class="line"> <span class="keyword">const</span> parentScrollView = <span class="keyword">this</span>._getParentScrollView();</span><br><span class="line"> <span class="keyword">const</span> parentScrollVertical = !(</span><br><span class="line"> parentScrollView && parentScrollView.scrollX</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>.vertical && parentScrollView === <span class="literal">null</span>) {</span><br><span class="line"> Gestures.setTouchAction(<span class="keyword">this</span>, <span class="string">'none'</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">this</span>.vertical === parentScrollVertical) {</span><br><span class="line"> Gestures.setTouchAction(<span class="keyword">this</span>, <span class="string">'auto'</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Gestures.setTouchAction(<span class="keyword">this</span>, <span class="keyword">this</span>.vertical ? <span class="string">'pan-x'</span> : <span class="string">'pan-y'</span>);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<blockquote>
<p>NOTE: </p>
<ul>
<li><p>更多详细的手势使用可以<a href="https://www.polymer-project.org/3.0/docs/devguide/gesture-events" target="_blank" rel="noopener">参考这里</a></p>
</li>
<li><p>touch-action 的 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action" target="_blank" rel="noopener">MDN 文档</a></p>
</li>
</ul>
</blockquote>
<h2 id="样式"><a href="#样式" class="headerlink" title="样式"></a>样式</h2><p>使用 Polymer 的 template 方法创建的 customElement 默认是 Shadow DOM,Shadow DOM 内的样式是局部作用域的,也就是说内部的样式不会影响全局。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">get</span> template() {</span><br><span class="line"> <span class="keyword">return</span> html<span class="string">`</span></span><br><span class="line"><span class="string"> <style></span></span><br><span class="line"><span class="string"> :host {</span></span><br><span class="line"><span class="string"> display: inline-block;</span></span><br><span class="line"><span class="string"> background-color: #fff;</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"> #input {</span></span><br><span class="line"><span class="string"> all: unset;</span></span><br><span class="line"><span class="string"> width: 100%;</span></span><br><span class="line"><span class="string"> height: 100%;</span></span><br><span class="line"><span class="string"> -webkit-text-fill-color: initial;</span></span><br><span class="line"><span class="string"> -webkit-user-select: auto;</span></span><br><span class="line"><span class="string"> user-select: auto;</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"> </style></span></span><br><span class="line"><span class="string"> <input id="input" /></span></span><br><span class="line"><span class="string"> `</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="避免动态修改-style-标签来应用样式"><a href="#避免动态修改-style-标签来应用样式" class="headerlink" title="避免动态修改 style 标签来应用样式"></a>避免动态修改 style 标签来应用样式</h3><p>对于 Shadow DOM 内部的样式,ShadyCSS 会解析和重写以正确地作用到真实节点上,所以如果你的组件内部有动态创建或者写 style 标签的 innerHTML 属性,这些都不会被 ShadyCSS 作用到,应当避免。</p>
<p>如果实在无法避免,我在使用的过程中使用了一种 HACK 的手段:</p>
<ul>
<li>给每一个组件添加一个 uid 的标识来区分组件实例,并带到组件的 data-id attribute 上面</li>
<li>在写 CSS 的时候通过 [data-id=xxx] 选择符来区分</li>
<li>这种方式你需要同时写 :host {} 和 my-element[data-id=xxx]{} 两个 CSS 选择符来保证同时非 polyfill 情况下和 polyfill 情况下都能工作</li>
<li>由于对 ShadyCSS 的源码研究还不够,可能还有更好的办法,如果你有的话也请在下边评论指教~~</li>
</ul>
<h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>一个健壮的基础应用必须有响应的测试机制来保障,一般一款软件的稳定性跟它的测试代码/应用代码的比例正相关。DOM 相关的 UI 测试在前几年一直有着不错的发展,比如截图对比测试,Driver 服务化等,但是真正被大规模使用的寥寥。本着实用至上的原则,Atag 的测试分为自动化的冒烟测试和手动的 UI 测试。</p>
<h3 id="冒烟测试"><a href="#冒烟测试" class="headerlink" title="冒烟测试"></a>冒烟测试</h3><p>顾名思义,就是渲染一个组件,看看有没有报错或者渲染异常,进行一些简单的 DOM 渲染判断。这里我们用到了 Karma + puppteer 的搭配,如果你想验证浏览器兼容性的差异也可以加入更多的 karma-driver。</p>
<p>karma 配置文件:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> webpackConfig = <span class="built_in">require</span>(<span class="string">'./tests/webpack.test.js'</span>);</span><br><span class="line">process.env.CHROME_BIN = <span class="built_in">require</span>(<span class="string">'puppeteer'</span>).executablePath();</span><br><span class="line"></span><br><span class="line"><span class="comment">// karma.conf.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="keyword">function</span>(<span class="params">config</span>) </span>{</span><br><span class="line"> config.set({</span><br><span class="line"> browsers: [<span class="string">'ChromeHeadless'</span>],</span><br><span class="line"> frameworks: [<span class="string">'mocha'</span>, <span class="string">'sinon-chai'</span>],</span><br><span class="line"> reporters: [<span class="string">'spec'</span>, <span class="string">'coverage'</span>],</span><br><span class="line"> files: [<span class="string">'vendors/custom-elements-es5-adapter.js'</span>, <span class="string">'tests/index.js'</span>],</span><br><span class="line"></span><br><span class="line"> preprocessors: {</span><br><span class="line"> <span class="string">'./tests/index.js'</span>: [<span class="string">'webpack'</span>, <span class="string">'sourcemap'</span>]</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> webpack: webpackConfig,</span><br><span class="line"></span><br><span class="line"> webpackMiddleware: {</span><br><span class="line"> stats: <span class="string">'errors-only'</span></span><br><span class="line"> },</span><br><span class="line"> coverageReporter: {</span><br><span class="line"> dir: <span class="string">'./coverage'</span>,</span><br><span class="line"> reporters: [{ <span class="attr">type</span>: <span class="string">'html'</span> }, { <span class="attr">type</span>: <span class="string">'text'</span> }]</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>tests/index.js:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// require all src files except main.js for coverage.</span></span><br><span class="line"><span class="comment">// you can also change this to match only the subset of files that</span></span><br><span class="line"><span class="comment">// you want coverage for.</span></span><br><span class="line"><span class="keyword">const</span> srcContext = <span class="built_in">require</span>.context(<span class="string">'../src'</span>, <span class="literal">true</span>, /^\.\/(?!index(\.js)?$)/);</span><br><span class="line">srcContext.keys().forEach(srcContext);</span><br><span class="line"></span><br><span class="line"><span class="comment">// require all test files (files that ends with .spec.js)</span></span><br><span class="line"><span class="keyword">const</span> testsContext = <span class="built_in">require</span>.context(<span class="string">'./specs'</span>, <span class="literal">true</span>, /\.spec$/);</span><br><span class="line">testsContext.keys().forEach(testsContext);</span><br></pre></td></tr></table></figure>
<p>一个简单的测试用例:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> SOURCE =</span><br><span class="line"> <span class="string">''</span>;</span><br><span class="line"></span><br><span class="line">describe(<span class="string">'<a-audio>'</span>, () => {</span><br><span class="line"> it(<span class="string">'should render a-audio'</span>, () => {</span><br><span class="line"> <span class="keyword">const</span> el = <span class="built_in">document</span>.createElement(<span class="string">'a-audio'</span>);</span><br><span class="line"> el.setAttribute(<span class="string">'src'</span>, SOURCE);</span><br><span class="line"> el.setAttribute(<span class="string">'autoplay'</span>, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="built_in">document</span>.body.appendChild(el);</span><br><span class="line"> expect(el).to.not.be.an.instanceOf(HTMLUnknownElement);</span><br><span class="line"> expect(getComputedStyle(el).display).to.equal(<span class="string">'block'</span>);</span><br><span class="line"> });</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<h3 id="UI-测试"><a href="#UI-测试" class="headerlink" title="UI 测试"></a>UI 测试</h3><p>真正功能性的测试依旧是通过组件例子的方式来验证,因为很多富交互的组件用例很难通过单元测试的方式书写。有了 UI 测试用例,很多可视的元素效果能一目了然;当然你也可以使用一些针对 DOM 编程的测试框架。</p>
<blockquote>
<p>这个是 Atag 的 <a href="https://github.com/alibaba/rax/blob/master/packages/atag/demo/index.htm" target="_blank" rel="noopener">UI 测试用例</a>。</p>
</blockquote>
<h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul>
<li><a href="https://github.com/alibaba/rax/tree/master/packages/atag" target="_blank" rel="noopener">ATAG</a></li>
<li><a href="https://developers.taobao.com/components/" target="_blank" rel="noopener">淘宝小程序组件文档</a></li>
<li><a href="http://w3c.github.io/webcomponents/" target="_blank" rel="noopener">W3C Web Components Specs</a></li>
<li><a href="https://polymer-project.org" target="_blank" rel="noopener">Polymer Project</a></li>
<li><a href="https://www.webcomponents.org/" target="_blank" rel="noopener">Web Components.org</a></li>
<li><a href="https://github.com/polymer" target="_blank" rel="noopener">Github@Polymer</a></li>
<li><a href="https://github.com/webcomponents" target="_blank" rel="noopener">Github@WebComponents</a></li>
</ul>
<blockquote>
<p>题图出处 <a href="https://www.webcomponents.org/community/articles/why-web-components" target="_blank" rel="noopener">https://www.webcomponents.org/community/articles/why-web-components</a></p>
</blockquote>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB1Q3Y4l4TpK1RjSZFGXXcHqFXa-900-500.jpg" alt="Atag - Web Components 最佳实践"></p>
<h2 id="引子"><a href="#]]>
</summary>
<category term="小程序" scheme="http://taobaofed.org/tags/%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
<category term="web-components" scheme="http://taobaofed.org/tags/web-components/"/>
<category term="Polymer" scheme="http://taobaofed.org/tags/Polymer/"/>
<category term="Web开发" scheme="http://taobaofed.org/categories/Web%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[Workbox 3:Service Worker 可以如此简单]]></title>
<link href="http://taobaofed.org/blog/2018/08/08/workbox3/"/>
<id>http://taobaofed.org/blog/2018/08/08/workbox3/</id>
<published>2018-08-08T08:35:49.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB19EDcJMmTBuNjy1XbXXaMrVXa-900-500.jpg" alt="Workbox 3:Service Worker 可以如此简单"></p>
<p>如果你追求极致的 Web 体验,你一定在站点中使用过 PWA,也一定面临过在编写 Service Worker 代码时的犹豫不决,因为 Service Worker 太重要了,一旦注册在用户的浏览器,全站的请求都会被 Service Worker 控制,一不留神,小问题也成了大问题了。不过到了现在有了 Workbox 3,一切关于 Service Worker 的担心都不再是问题。</p>
<h2 id="科普-Service-Worker"><a href="#科普-Service-Worker" class="headerlink" title="科普 Service Worker"></a>科普 Service Worker</h2><p>如果你已经熟悉 Service Worker,可以跳过此段。</p>
<p>Service Worker 是 PWA 中重要的一部分,它是一个网站安插在用户浏览器中的大脑。Service Worker 是这样被注册在页面上的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="string">'serviceWorker'</span> <span class="keyword">in</span> navigator) {</span><br><span class="line"> navigator.serviceWorker.register(<span class="string">'/sw.js'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>为什么说 SW(下文将 Service Worker 简称为 SW)是网站的大脑?举个例子,如果在 <a href="http://www.example.com" target="_blank" rel="noopener">www.example.com</a> 的根路径下注册了一个 SW,那么这个 SW 将可以控制所有该浏览器向 <a href="http://www.example.com" target="_blank" rel="noopener">www.example.com</a> 站点发起的请求。只需要监听 fetch 事件,你就可以任意的操纵请求,可以返回从 CacheStorage 中读的数据,也可以通过 Fetch API 发起新的请求,甚至可以 new 一个 Response,返回给页面。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一段糟糕的 SW 代码,在这个 SW 注册好以后,整个 SW 控制站点的所有请求返回的都将是字符串 "bad",包括页面的 HTML</span></span><br><span class="line">self.addEventListener(<span class="string">'fetch'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">event</span>) </span>{</span><br><span class="line"> event.respondWith(</span><br><span class="line"> <span class="keyword">new</span> Response(<span class="string">'bad'</span>)</span><br><span class="line"> );</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>就是因为 SW 权利太大了,写起来才会如履薄冰,一不小心有些页面资源就不能及时正确的更新了。</p>
<h3 id="一个还算完整的-Service-Worker-示例"><a href="#一个还算完整的-Service-Worker-示例" class="headerlink" title="一个还算完整的 Service Worker 示例"></a>一个还算完整的 Service Worker 示例</h3><p>先来看一个直接手写的 SW 文件</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> cacheStorageKey = <span class="string">'cachesName'</span>;</span><br><span class="line"><span class="keyword">var</span> cacheList = [</span><br><span class="line"> <span class="comment">// 注册成功后要立即缓存的资源列表</span></span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="comment">// 当浏览器解析完 SW 文件时触发 install 事件</span></span><br><span class="line">self.addEventListener(<span class="string">'install'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">e</span>) </span>{</span><br><span class="line"> <span class="comment">// install 事件中一般会将 cacheList 中要换存的内容通过 addAll 方法,请求一遍放入 caches 中</span></span><br><span class="line"> e.waitUntil(</span><br><span class="line"> caches.open(cacheStorageKey).then(<span class="function"><span class="keyword">function</span>(<span class="params">cache</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> cache.addAll(cacheList)</span><br><span class="line"> })</span><br><span class="line"> );</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="comment">// 激活时触发 activate 事件</span></span><br><span class="line">self.addEventListener(<span class="string">'activate'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">e</span>) </span>{</span><br><span class="line"> <span class="comment">// active 事件中通常做一些过期资源释放的工作,匹配到就从 caches 中删除</span></span><br><span class="line"> <span class="keyword">var</span> cacheDeletePromises = caches.keys().then(<span class="function"><span class="params">cacheNames</span> =></span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.all(cacheNames.map(<span class="function"><span class="params">name</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (name !== cacheStorageKey) {</span><br><span class="line"> <span class="keyword">return</span> caches.delete(name);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.resolve();</span><br><span class="line"> }</span><br><span class="line"> }));</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> e.waitUntil(</span><br><span class="line"> <span class="built_in">Promise</span>.all([cacheDeletePromises])</span><br><span class="line"> );</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line">self.addEventListener(<span class="string">'fetch'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">e</span>) </span>{</span><br><span class="line"> <span class="comment">// 在此编写缓存策略</span></span><br><span class="line"> e.respondWith(</span><br><span class="line"> <span class="comment">// 可以通过匹配缓存中的资源返回</span></span><br><span class="line"> caches.match(e.request)</span><br><span class="line"> <span class="comment">// 也可以从远端拉取</span></span><br><span class="line"> fetch(e.request.url)</span><br><span class="line"> <span class="comment">// 也可以自己造</span></span><br><span class="line"> <span class="keyword">new</span> Response(<span class="string">'自己造'</span>)</span><br><span class="line"> <span class="comment">// 也可以通过吧 fetch 拿到的响应通过 caches.put 方法放进 caches</span></span><br><span class="line"> );</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>其实所有站点 SW 的 install 和 active 都差不多,无非是做预缓存资源列表,更新后缓存清理的工作,逻辑不太复杂,而重点在于 fetch 事件。上面的代码,我把 fetch 事件的逻辑省略了,因为如果认真写的话,太多了,而且也不利于讲明白缓存策略这件事。想象一下,你需要根据不同文件的扩展名把不同的资源通过不同的策略缓存在 caches 中,各种 CSS,JS,HTML,图片,都需要单独搞一套缓存策略,你就知道 fetch 中需要写多少东西了吧。</p>
<h2 id="Workbox-3"><a href="#Workbox-3" class="headerlink" title="Workbox 3"></a>Workbox 3</h2><p>Workbox 的出现就是为了解决上面的问题的,它被定义为 PWA 相关的工具集合,其实围绕它的还有一些列工具,如 workbox-cli、gulp-workbox、webpack-workbox-plagin 等等,不过他们都不是今天的重点,今天想聊的就是 Workbox 本身。</p>
<p>其实可以把 Workbox 理解为 Google 官方的 PWA 框架,它解决的就是用底层 API 写 PWA 太过复杂的问题。这里说的底层 API,指的就是去监听 SW 的 install、active、 fetch 事件做相应逻辑处理等。使用起来是这样的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 首先引入 Workbox 框架</span></span><br><span class="line">importScripts(<span class="string">'https://storage.googleapis.com/workbox-cdn/releases/3.3.0/workbox-sw.js'</span>);</span><br><span class="line">workbox.precaching([</span><br><span class="line"> <span class="comment">// 注册成功后要立即缓存的资源列表</span></span><br><span class="line">]);</span><br><span class="line"></span><br><span class="line"><span class="comment">// html的缓存策略</span></span><br><span class="line">workbox.routing.registerRoute(</span><br><span class="line"> <span class="keyword">new</span> <span class="built_in">RegExp</span>(<span class="string">''</span>.*\.html<span class="string">'),</span></span><br><span class="line"><span class="string"> workbox.strategies.networkFirst()</span></span><br><span class="line"><span class="string">);</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">workbox.routing.registerRoute(</span></span><br><span class="line"><span class="string"> new RegExp('</span>.*\.(?:js|css)<span class="string">'),</span></span><br><span class="line"><span class="string"> workbox.strategies.cacheFirst()</span></span><br><span class="line"><span class="string">);</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">workbox.routing.registerRoute(</span></span><br><span class="line"><span class="string"> new RegExp('</span>https:<span class="comment">//your\.cdn\.com/'),</span></span><br><span class="line"> workbox.strategies.staleWhileRevalidate()</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">workbox.routing.registerRoute(</span><br><span class="line"> <span class="keyword">new</span> <span class="built_in">RegExp</span>(<span class="string">'https://your\.img\.cdn\.com/'</span>),</span><br><span class="line"> workbox.strategies.cacheFirst({</span><br><span class="line"> cacheName: <span class="string">'example:img'</span></span><br><span class="line"> })</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>上面的代码理解起来就容易的多了,通过 workbox.precaching 中的是 install 以后要塞进 caches 中的内容,workbox.routing.registerRoute 中第一个参数是一个正则,匹配经过 fetch 事件的所有请求,如果匹配上了,就走相应的缓存策略 workbox.strategies 对象为我们提供了几种最常用的策略,如下:</p>
<h5 id="Stale-While-Revalidate"><a href="#Stale-While-Revalidate" class="headerlink" title="Stale-While-Revalidate"></a>Stale-While-Revalidate</h5><p><img src="https://gw.alicdn.com/tfs/TB1LNY7JFOWBuNjy0FiXXXFxVXa-1014-492.png" alt></p>
<h5 id="Cache-First"><a href="#Cache-First" class="headerlink" title="Cache First"></a>Cache First</h5><p><img src="https://gw.alicdn.com/tfs/TB1kmv2JKSSBuNjy0FlXXbBpVXa-1004-496.png" alt></p>
<h5 id="Network-First"><a href="#Network-First" class="headerlink" title="Network First"></a>Network First</h5><p><img src="https://gw.alicdn.com/tfs/TB1Il7mJQCWBuNjy0FaXXXUlXXa-1014-492.png" alt></p>
<h5 id="Network-Only"><a href="#Network-Only" class="headerlink" title="Network Only"></a>Network Only</h5><p><img src="https://gw.alicdn.com/tfs/TB1LYbuJKOSBuNjy0FdXXbDnVXa-1014-345.png" alt></p>
<h5 id="Cache-Only"><a href="#Cache-Only" class="headerlink" title="Cache Only"></a>Cache Only</h5><p><img src="https://gw.alicdn.com/tfs/TB1oveWJFGWBuNjy0FbXXb4sXXa-1013-344.png" alt></p>
<p>你可以通过 plugin 扩展这些策略,比如增加个缓存过期时间(官方有提供)什么的。甚至可以继续监听 fetch 事件,然后使用这些策略,官方文档在<a href="https://developers.google.com/web/tools/workbox/modules/workbox-strategies#stale-while-revalidate" target="_blank" rel="noopener">这里</a>.</p>
<h2 id="经验之谈"><a href="#经验之谈" class="headerlink" title="经验之谈"></a>经验之谈</h2><p>在经过一段时间的使用和思考以后,给出我认为最为合理,最为保守的缓存策略。</p>
<p>HTML,如果你想让页面离线可以访问,使用 NetworkFirst,如果不需要离线访问,使用 NetworkOnly,其他策略均不建议对 HTML 使用。</p>
<p>CSS 和 JS,情况比较复杂,因为一般站点的 CSS,JS 都在 CDN 上,SW 并没有办法判断从 CDN 上请求下来的资源是否正确(HTTP 200),如果缓存了失败的结果,问题就大了。这种我建议使用 Stale-While-Revalidate 策略,既保证了页面速度,即便失败,用户刷新一下就更新了。</p>
<p>如果你的 CSS,JS 与站点在同一个域下,并且文件名中带了 Hash 版本号,那可以直接使用 Cache First 策略。</p>
<p>图片建议使用 Cache First,并设置一定的失效事件,请求一次就不会再变动了。</p>
<p>上面这些只是普适性的策略,见仁见智。</p>
<p>还有,要牢记,对于不在同一域下的任何资源,绝对不能使用 Cache only 和 Cache first。</p>
<h2 id="最后打个报告"><a href="#最后打个报告" class="headerlink" title="最后打个报告"></a>最后打个报告</h2><p>淘宝 PC 首页的 Service Worker 上线已经有一段时间了,经过不断地对缓存策略的调整,收益还是比较明显的,页面总下载时间从平均 1.7s,下降到了平均 1.4s,缩短了近 18% 的下载时间。</p>
<p>前面的例子中,我们使用的是 Google 的 CDN 地址引入的 Workbox,我已经将 3.3.0 版本迁移到 alicdn,后续还会继续维护更新,使用方法如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">importScripts(<span class="string">'https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js'</span>);</span><br><span class="line">workbox.setConfig({</span><br><span class="line"> modulePathPrefix: <span class="string">'https://g.alicdn.com/kg/workbox/3.3.0/'</span></span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<h3 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h3><ul>
<li><a href="https://developers.google.com/web/tools/workbox/" target="_blank" rel="noopener">Workbox 官方文档</a></li>
<li><a href="https://zoumiaojiang.com/article/amazing-workbox-3/" target="_blank" rel="noopener">神奇的 Workbox 3.0</a></li>
</ul>
<blockquote>
<p>Photo by Peter Wendt on Unsplash</p>
</blockquote>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB19EDcJMmTBuNjy1XbXXaMrVXa-900-500.jpg" alt="Workbox 3:Service Worker 可以如此简单"></p>
<p>如果你追求极致的 Web 体]]>
</summary>
<category term="PWA" scheme="http://taobaofed.org/tags/PWA/"/>
<category term="Service Worker" scheme="http://taobaofed.org/tags/Service-Worker/"/>
<category term="Web开发" scheme="http://taobaofed.org/categories/Web%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[基于 BindingX 的富交互解决方案]]></title>
<link href="http://taobaofed.org/blog/2018/05/31/a-rich-interaction-solution-based-on-bindingx/"/>
<id>http://taobaofed.org/blog/2018/05/31/a-rich-interaction-solution-based-on-bindingx/</id>
<published>2018-05-31T04:22:18.000Z</published>
<updated>2019-03-19T09:18:13.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB15lFuuGmWBuNjy1XaXXXCbXXa-900-500.jpg" alt="基于 BindingX 的富交互解决方案"></p>
<h1 id="基于-BindingX-的富交互解决方案"><a href="#基于-BindingX-的富交互解决方案" class="headerlink" title="基于 BindingX 的富交互解决方案"></a>基于 BindingX 的富交互解决方案</h1><p>BindingX 官网: <a href="https://alibaba.github.io/bindingx/" target="_blank" rel="noopener">https://alibaba.github.io/bindingx/</a><br>BindingX 项目地址: <a href="https://github.com/alibaba/bindingx" target="_blank" rel="noopener">https://github.com/alibaba/bindingx</a></p>
<h3 id="一-背景"><a href="#一-背景" class="headerlink" title="一. 背景"></a>一. 背景</h3><p>在 Weex 环境下实现一些复杂的手势交互效果可能会产生卡顿,这是因为每次手势交互都会产生两次 JS-native 通信。第一次是 native call JS,将手势事件传递到 JS 层交给前端处理,当 JS 层接收到回调后,会产生第二次通信,JS call native,用来驱动界面变化。与此同时,手势回调事件触发的频率是非常高的,频繁通信带来的时间成本很可能导致界面无法在 16ms 中完成绘制,因而产生卡顿。</p>
<p>我们提出了 <code>Expression Binding</code> 方案用来解决这个问题。方案是在手势开始的时候,将具体的手势控制函数以<code>表达式</code>的形式传递给 Native 层,当手势发生时,Native 根据预置的表达式解析器去解释执行表达式,并根据结果驱动视图变化。这样带来的好处是大大的减少了 native-JS 的通信次数,下面两幅图描述了传统方案与 <code>Expression Binding</code> 方案的差别:</p>
<p>图 1:传统方案</p>
<p><img src="https://gw.alicdn.com/tfs/TB1PAkXrbGYBuNjy0FoXXciBFXa-2318-768.png" alt></p>
<p>图 2:Expression Binding 方案</p>
<p><img src="https://gw.alicdn.com/tfs/TB1N0tgrqmWBuNjy1XaXXXCbXXa-2250-708.png" alt></p>
<p>事实上,<code>Expression Binding</code> 不仅仅可以解决手势交互问题,任何 JS-native 频繁通信 + UI 更新的场景理论上都可以复用这套方案。比如:</p>
<ol>
<li>监听容器的滚动,并基于滚动距离等变量更新UI如最常见的视差动画等;</li>
<li>监听陀螺仪方向变化数据,并更新 UI;</li>
<li>监听时间变化,更新 UI;</li>
<li>……</li>
</ol>
<p><img src="https://gw.alicdn.com/tfs/TB1o8AoreSSBuNjy0FlXXbBpVXa-2172-1090.png" alt></p>
<p>因此,我们将原方案进行了横向的扩展,实现了这些新的特性,并将它命名为 BindingX。2018 年 3 月,BindingX 正式开源,并同时支持了 React Native。</p>
<h3 id="二-特性一览"><a href="#二-特性一览" class="headerlink" title="二. 特性一览"></a>二. 特性一览</h3><h4 id="1-手势能力"><a href="#1-手势能力" class="headerlink" title="1. 手势能力"></a>1. 手势能力</h4><p>BindingX 能够监听元素的 pan 事件,基于此可以实现拖拽、卡片横滑等跟手的交互效果。更令人惊喜的是,类似 Weex <a href="http://weex-project.io/cn/references/components/slider.html" target="_blank" rel="noopener">Slider</a> 这样的组件现在也可以使用 BindingX 来实现!</p>
<p><img src="https://gw.alicdn.com/tfs/TB1qzW3iER1BeNjy0FmXXb0wVXa-281-500.gif" alt="demo1"><br><img src="https://gw.alicdn.com/tfs/TB1EoOuiuuSBuNjSsziXXbq8pXa-281-500.gif" alt="demo2"><br><img src="https://gw.alicdn.com/tfs/TB18oHrrkyWBuNjy0FpXXassXXa-281-500.gif" alt="demo3"></p>
<h4 id="2-动画"><a href="#2-动画" class="headerlink" title="2. 动画"></a>2. 动画</h4><p>在 Weex 上实现动画通常的做法是使用 <code>animation module</code>,现在有了新的选择。使用 BindingX 可以实现所有 animation module 能实现的效果,另外,BindingX 内置了 30 多组常见的<a href="https://alibaba.github.io/bindingx/guide/cn_api_interpolator" target="_blank" rel="noopener">插值器</a>,可以自由选择,当然也可以使用 cubicBezier 贝塞尔曲线定制插值器。</p>
<p><img src="https://gw.alicdn.com/tfs/TB1Fn_srgmTBuNjy1XbXXaMrVXa-281-499.gif" alt="demo4"><br><img src="https://gw.alicdn.com/tfs/TB1Csuwiv1TBuNjy0FjXXajyXXa-281-500.gif" alt="demo6"></p>
<h4 id="3-陀螺仪"><a href="#3-陀螺仪" class="headerlink" title="3. 陀螺仪"></a>3. 陀螺仪</h4><p>BindingX 内置了陀螺仪监听器,可以监听设备方向变化。这在很多富交互场景中非常实用,比如在手机淘宝里,你可以看到很多基于陀螺仪的视差效果:</p>
<p><img src="https://gw.alicdn.com/tfs/TB1UjMUrhGYBuNjy0FnXXX5lpXa-281-500.gif" alt="demo7"><br><img src="https://gw.alicdn.com/tfs/TB18ej1rbGYBuNjy0FoXXciBFXa-281-500.gif" alt="demo8"></p>
<h4 id="4-列表滚动监听"><a href="#4-列表滚动监听" class="headerlink" title="4. 列表滚动监听"></a>4. 列表滚动监听</h4><p>BindingX 能够监听列表等滚动容器的 <code>onScroll</code> 事件,通过它可以实现酷炫的视差动画:</p>
<p><img src="https://gw.alicdn.com/tfs/TB18v3VrhGYBuNjy0FnXXX5lpXa-281-499.gif" alt="demo9"><br><img src="https://gw.alicdn.com/tfs/TB1NCGhipOWBuNjy0FiXXXFxVXa-281-500.gif" alt="demo10"></p>
<h3 id="三-使用方式"><a href="#三-使用方式" class="headerlink" title="三. 使用方式"></a>三. 使用方式</h3><p>BiningX 同时支持 ReactNative 和 Weex,对于 Weex 来说不管你是使用 Rax 还是 Vue DSL,都没有关系。下面以 Weex 举例来说明如何使用 BindingX。</p>
<h5 id="第一步:安装依赖"><a href="#第一步:安装依赖" class="headerlink" title="第一步:安装依赖"></a>第一步:安装依赖</h5><ul>
<li>安装 npm 依赖</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install weex-bindingx --save</span><br></pre></td></tr></table></figure>
<ul>
<li>在 JS 代码中引入 BindingX 模块</li>
</ul>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> BindingX <span class="keyword">from</span> weex-bindingx;</span><br></pre></td></tr></table></figure>
<h5 id="第二步:编写表达式"><a href="#第二步:编写表达式" class="headerlink" title="第二步:编写表达式"></a>第二步:编写表达式</h5><ul>
<li>根据业务场景,选择您需要的 eventType。 比如,要监听手势,eventType 值为 pan,监听滚动容器 scrollOffset 变化,eventType 值为 scroll。</li>
<li>根据交互行为,选择要改变的属性,并编写相应的表达式。比如,交互行为是“用户横滑 100 单位,透明度从 1 变化到 0”。则属性为 “opacity”,表达式为 “1 - x / 100”。</li>
</ul>
<h5 id="第三步:绑定表达式"><a href="#第三步:绑定表达式" class="headerlink" title="第三步:绑定表达式"></a>第三步:绑定表达式</h5><p>根据第二步得到的 eventType、Expression 以及 Property,调用 BindingX 模块的 bind 方法,完成绑定。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> result = BindingX.bind({</span><br><span class="line"> eventType: <span class="string">'pan'</span>, ==> 事件类型</span><br><span class="line"> anchor: <span class="string">'foo'</span>, ==> anchor 指的是事件的触发者,如果是 eventType 是 “orientation” 或 “timing”,则不用填</span><br><span class="line"> props: [</span><br><span class="line"> {</span><br><span class="line"> element: view.ref, ==> 要改变的视图的引用或者 id</span><br><span class="line"> expression: <span class="string">"1 - x / 100"</span>, ==> 表达式</span><br><span class="line"> property: <span class="string">"opacity"</span> ==> 要改变的属性</span><br><span class="line"> }</span><br><span class="line"> ] </span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>当调用 bind 方法之后,native 会启动监听,当目标事件(比如手指滑动、设备方向变化等)发生的时候,便会执行您先前绑定的一组或者多组表达式。 bind 方法会返回一个 JS 对象,其中包含了一个 token 属性,可以使用这个 token 取消绑定。</p>
<p>更多细节,请参考我们的<a href="https://alibaba.github.io/bindingx/guide/cn_introduce" target="_blank" rel="noopener">文档</a>。</p>
<h5 id="第四步:取消绑定"><a href="#第四步:取消绑定" class="headerlink" title="第四步:取消绑定"></a>第四步:取消绑定</h5><p>在合适的时机调用 BindingX 的 unbind 方法取消绑定。比如,页面不可见或者即将销毁的时候。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">BindingX.unbind({</span><br><span class="line"> token: result.token,</span><br><span class="line"> eventType: <span class="string">'pan'</span></span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<h3 id="四-内部细节"><a href="#四-内部细节" class="headerlink" title="四. 内部细节"></a>四. 内部细节</h3><p>下面以 Android 为例从 native 的视角介绍下 BindingX 的具体实现,首先我们来梳理整个流程:</p>
<ol>
<li><p>前端通过声明的方式定义具体的视图变化,每个视图变化过程都用一个三元组描述:</p>
<ul>
<li>element:目标元素。</li>
<li>property:要改变的属性。</li>
<li>expression:表达式。通过工具生成抽象语法树。</li>
</ul>
</li>
<li><p>native 根据 eventType 注册对应的事件监听器,并将映射关系保存起来;</p>
</li>
<li>当指定的事件发生的时候,native 自行消费先前绑定的所有表达式,计算结果,并根据结果对视图进行更新。</li>
</ol>
<p>这个过程可以用下面这张图描述:</p>
<p><img src="https://img.alicdn.com/tfs/TB101ahSVXXXXXuXVXXXXXXXXXX-986-432.jpg" alt></p>
<p>在这个模型里,输入可以是手势事件、滚动事件、陀螺仪方向变化事件,而输出则是经过视图变换的view,视图变换的过程在 native 完成。而视图变换的规则是通过<code>表达式</code>来描述的,一个表达式在前端声明之后,会先通过 <a href="https://github.com/alibaba/bindingx/blob/master/parser/src/index.js" target="_blank" rel="noopener">parser</a> 转成 <code>Abstract syntax tree</code>,native 会通过预置的解析器来解析表达式树,并计算出结果,根据结果去驱动视图变化。</p>
<h3 id="五-更多想象力"><a href="#五-更多想象力" class="headerlink" title="五. 更多想象力"></a>五. 更多想象力</h3><p>事实上,<code>BindingX</code> 比我们想象的更加强大,在上面那张架构图中,输出部分画的是 <code>transformed view</code>,但是事实上除了 view,我们还在探索更多有趣的玩法,比如:</p>
<ul>
<li>BindingX 和 Lottie 结合。用 BindingX 驱动 <code>lottie</code> 实现动画;</li>
<li>BindingX 和 <code>Weex SVG</code> 结合,实现好玩的轨迹动画、路径跟随动画,甚至是 morph 变形动画;</li>
<li>BindingX 和 <code>Shader</code> 结合,用 BindingX 来控制着色器!</li>
<li>……</li>
</ul>
<h3 id="六-下一步"><a href="#六-下一步" class="headerlink" title="六. 下一步?"></a>六. 下一步?</h3><p>BindingX 在内部经过很长时间的孵化,在上层衍生出了很多通用的业务组件,它们涵盖了大部分的交互场景,诸如下拉刷新、转场、联动、视差动画,<a href="https://github.com/alibaba/rax/blob/master/packages/rax-tab-panel/src/index.js" target="_blank" rel="noopener">tab-panel</a>、<a href="https://github.com/alibaba/rax/blob/master/packages/rax-parallax/src/index.js" target="_blank" rel="noopener">parallax</a> 就是很好的例子。一个基于 <code>BindingX</code> 的前端交互体系正在成型,下一步我们会将它们逐渐开源到社区,敬请期待!</p>
]]></content>
<summary type="html">
<![CDATA[<p><img src="https://gw.alicdn.com/tfs/TB15lFuuGmWBuNjy1XaXXXCbXXa-900-500.jpg" alt="基于 BindingX 的富交互解决方案"></p>
<h1 id="基于-BindingX-的富交互解决方案]]>
</summary>
<category term="BindingX" scheme="http://taobaofed.org/tags/BindingX/"/>
<category term="交互" scheme="http://taobaofed.org/tags/%E4%BA%A4%E4%BA%92/"/>
<category term="Weex" scheme="http://taobaofed.org/tags/Weex/"/>