-
Notifications
You must be signed in to change notification settings - Fork 1
/
atom.xml
1243 lines (685 loc) · 378 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>Code for fun</title>
<link href="/atom.xml" rel="self"/>
<link href="https://frezc.github.io/"/>
<updated>2022-12-20T05:12:24.484Z</updated>
<id>https://frezc.github.io/</id>
<author>
<name>Freeze Crow</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>撤销恢复 VS 异步操作</title>
<link href="https://frezc.github.io/2022/12/11/undo-redo-with-async-actions/"/>
<id>https://frezc.github.io/2022/12/11/undo-redo-with-async-actions/</id>
<published>2022-12-11T11:09:57.000Z</published>
<updated>2022-12-20T05:12:24.484Z</updated>
<content type="html"><![CDATA[<p>过去一段时间遇到了一些异步操作导致的撤销恢复问题,想了一些解决方案,但考虑到任务排期并没有使用比较好的解决方案。不过这是个很有意思的问题,就想着写篇文章来记录一下。</p><h2 id="撤销恢复"><a href="#撤销恢复" class="headerlink" title="撤销恢复"></a>撤销恢复</h2><p>撤销恢复简单来讲就是:假设有一个状态<code>A</code>,经过一个操作<code>action</code>变为了状态<code>B</code>,此时我们可以通过<code>撤销(undo)</code>操作退回状态<code>A</code>,然后可以通过<code>恢复(redo)</code>操作变为状态<code>B</code>。</p><pre class="mermaid">graph LR A--action-->B--undo-->A--redo-->B</pre><p>最简单粗暴的实现方法就是将每个节点状态都给保存下来,撤销恢复时全量覆盖即可。实际使用时由于要求保存的数据得是Immutable的,不一定适合所有应用,如果每次保存会有比较多的数据复制开销以及内存增长风险的话建议使用后面的方法。</p><p>还有种方法是记录每次的变更,比如哪些字段进行了变更,变更前的值等,这样更适合Mutable的数据,开销更小。下文中的一些方法会基于该实现。</p><h2 id="异步操作会带来的问题"><a href="#异步操作会带来的问题" class="headerlink" title="异步操作会带来的问题"></a>异步操作会带来的问题</h2><p>以一个比较实际的例子来说明,现在我们有一个描述模型的状态,上面有一些position、rotation、type等用户输入的数据,还有类似mesh的后端生成的数据。</p><pre><code class="ts">interface Model { type: string; position: Vector3; mesh: Mesh;}</code></pre><p>对于这个模型会有一些常见操作,比如用户修改<code>type</code>后向后端请求,返回了新的<code>mesh</code>;还有就是用户拖拽模型导致位置变更,直接更新<code>position</code>。</p><pre class="mermaid">graph LR async((Waiting)) model2--change position-->model3 model1--change type & request-->async async-.change mesh.->model2</pre><p>这里请求是个异步操作,如果用户在请求过程中修改了状态会怎么样呢?</p><pre class="mermaid">graph LR async((Waiting)) model1--change type & request-->async async-.change mesh.->model3 model2-->model3 async--change position-->model2</pre><p>一种情况是用户修改了<code>position</code>后记录了节点model2,当状态从model3撤销到model2时可以发现,这个状态是处于<code>type</code>与<code>mesh</code>无法匹配的一个<strong>错误状态</strong>——由于请求还未结束就执行其他操作而保存下来的状态。</p><p>还有种情况是确保异步操作能被单次撤销,那么不记录model2会导致丢失很多中间状态,不是个好方法。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h3 id="剥离异步状态"><a href="#剥离异步状态" class="headerlink" title="剥离异步状态"></a>剥离异步状态</h3><p>如果异步结果干扰了撤销恢复那把它拿出来不就行了? 这是个可行又不可行的方案。</p><p>以上面的<code>Model</code>为例,可以建立<code>Model => Mesh</code>的缓存体系,每次<code>type</code>变更获取时从缓存或者接口中获取。</p><pre class="mermaid">graph LR subgraph data async((Waiting)) model1--change type & request-->async async-.change mesh.->model3 model2-->model3 async--change position-->model2 end subgraph view model1--type fetch-->View model2--type cached-->View model3--type fetch-->View MeshCacher--getOrFetch-->View end</pre><p>另外对于<code>MeshCacher</code>需要做好内存管理,及时回收没用的<code>Mesh</code>。</p><p>这种方法的限制就是只能处理这种不会阻塞后续操作的异步生成状态,上面例子中的<code>mesh</code>就是如此。如果有那么些会影响后续交互的字段由后端(建模服务)控制,比如哪些某些<code>type</code>无法修改尺寸等,就不太合适了。</p><h3 id="回溯"><a href="#回溯" class="headerlink" title="回溯"></a>回溯</h3><p>这个想法来源于在线实时游戏里为了保证客户端实时性的预测与回溯技术,这个技术简单讲就是遇到网络延迟时,客户端可以根据当前人物行为预测未来几帧的结果,然后等网络实际返回后修正为正确的结果。</p><p>我们这里的话就是在请求返回前,允许用户的修改,等请求结束后将状态退回请求开始时的样子并应用结果,然后再应用之前退回的修改。</p><pre class="mermaid">graph LR async((Waiting)) async1((Waiting)) model1--change type & request-->async async--change position-->model2 model2-.请求结束退回position修改.->async1 async1--change mesh-->model3 model3--应用退回的修改-->model4</pre><p>最终的撤销恢复栈为</p><pre class="mermaid">graph LR model1-->model3-->model4</pre><p>要实现这种方法撤销恢复最好是记录变更的实现,不然实现会比较复杂。</p><p>除了复杂了一点外,还有一些问题需要优化:</p><ol><li>异步结束后当前的状态在请求时之前怎么办?</li><li>异步时间长的话大量状态的变更可能会触发视图大面积更新,如何去避免?</li></ol><p>回溯同样无法处理那种影响后续交互的状态。</p><h3 id="等待"><a href="#等待" class="headerlink" title="等待"></a>等待</h3><p>那么只能祭出最终解决方案,等你完成不就行了。这块就需要在交互上做一番设计了,要做到尽量避免中断用户的行为。<br>比如减少全局的loading、短时间的等待不显示loading等。</p><p>对于上面的例子来说,对于每个模型单独设置loading状态来禁止交互也是比较常用的方法,结合上面的方法也能做到正确撤销恢复</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>对于不同的应用会有不同的最优解,实际还是需要根据业务形态来选择最适合方案。</p><p><em>发烧了一天有时候忘记接下来要写啥了,头大 /(ㄒoㄒ)/~~</em></p>]]></content>
<summary type="html">
<p>过去一段时间遇到了一些异步操作导致的撤销恢复问题,想了一些解决方案,但考虑到任务排期并没有使用比较好的解决方案。不过这是个很有意思的问题,就想着写篇文章来记录一下。</p>
<h2 id="撤销恢复"><a href="#撤销恢复" class="headerlink" t
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="undo" scheme="https://frezc.github.io/tags/undo/"/>
<category term="asynchronous" scheme="https://frezc.github.io/tags/asynchronous/"/>
</entry>
<entry>
<title>如何用TS的类型系统进行编程(TS类型体操入门)</title>
<link href="https://frezc.github.io/2022/05/29/ts-type-system-programming/"/>
<id>https://frezc.github.io/2022/05/29/ts-type-system-programming/</id>
<published>2022-05-29T05:51:52.000Z</published>
<updated>2022-12-11T11:10:34.250Z</updated>
<content type="html"><![CDATA[<h2 id="关于TS的类型系统"><a href="#关于TS的类型系统" class="headerlink" title="关于TS的类型系统"></a>关于TS的类型系统</h2><p>写了这么久的ts才了解到它的类型系统是<a href="https://github.com/microsoft/TypeScript/issues/14833" target="_blank" rel="noopener">图灵完备</a>的,可以被拿来编程,网上称其为“类型体操(type gymnastics)”。这个词很有意思,虽然没有考据过来源,但是猜想这种编程和体操的性质很像——能锻炼技能但不实用。下文呢也是我接触学习后的一点总结,尽量从浅入深带大家了解一下。</p><p>TS由enhanced js + type system(类型系统)组成,其中编译后在.d.ts文件内的就是类型系统的代码了,所以对于类型系统编程完全可以在.d.ts内实现,只是需要给每条语句前加上<code>declare</code>关键字。为了简洁一点,本文的代码都是在.ts文件内的代码。</p><p>代码可以直接在typescript的<a href="https://www.typescriptlang.org/play" target="_blank" rel="noopener">playground</a>里写,由于没有系统的IO接口,只能通过hover到类型名上看输出,如下图。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="get-output.png" alt="get output" title> </div> <div class="image-caption">get output</div> </figure><h2 id="将会用到的TS类型语法"><a href="#将会用到的TS类型语法" class="headerlink" title="将会用到的TS类型语法"></a>将会用到的TS类型语法</h2><p>本节会聊一些重要的TS特性,对于之后的编程都是必须掌握的。知道的可以复习一下,不了解的可以学习一下,对于日常编程也是有帮助的。(一些太基础的不做解释,对应的特性都会<strong>标识</strong>出来)</p><p><em>本节的代码可以在<a href="https://www.typescriptlang.org/play?#code/FAej-ACAXBnRvH0NHqg0f0NBekKZMUW-4MKMJy0kCztQSTlAN5UHnEjTUkSQdW1AjdNuGgE8AHAU0gBBAIwjIAXkgiA3Fz6DRAJkmQA5AEMARgGM1cngOEiAzKoDaIgDSQlNzWoC6BhcYAsqgN6QNALnUaapAAvi5GogCsqtAATgCu-HK4kIAqAYAQKkjy4UriUuIAPraQhQ5hikoqUkI5xcYmSWCQgJZKgNBygCFugBx6gBSugHFygCvWgMHxgCRKgLWmWYomuZAAdnEAtlr8MTIMkIACRoCQ5oAOpixtgGNpaePCJpWQsLEAltMA5iuNmyyAZN6ATHL7h4YTZlIa09wrkAD7htADwKbyOQhMHik034ADclv9GoAvLz2TEggAEdQAXCXQbIBGVx6gFg5QC-AYBaE0AsolIQDUSoBYFUA-vJdQAw-2tAKdygCvlQBY8oAKdUAnk7kQCjBoBWxRa9PBbhEAB4ACoAPlUksg-AAHtB+NMACawSBXABmS0gABkAPY6DQAG3lAH5IAAKYAAgFGk3m4AASkg-hh8OWTAgFEogF-FLYMCBowDbNogqdTAPfKzMAyvqAHXlAyKPpAAHLzSWGgDKlxu4oAktNeHFoArlaqNTN5osYjYAAoxfjai6Kssq9Wa84xK7XVRqK7F6D+NSyqQAAwAJJ4G02W8Ep4XB8Ex3JGgCAHqbrfb9eQHf2g8b-eHk+nw87i8Hi-Xy-JAHTDRzQSn3gaGKPs+QV-vuYwVyfgC1SbDQ4lNUtYTNBIAW1OJph0aALkNaZIC0Q01W4I4cxiVR0zmTMsJ7cURCUExpQaZhAE34wAZxLaQMCUAejNAAA5QBJb0AG0VAEQbI4ADENAAa34ABZDReHFIQYmwpV20rWYFiWcxHHrRs1QueC2wrTVrV4D1qyWN0JFlH5uFHHxfjKSAACVRFUHj+KEkTLBsOxIBMBS0wzbNc2uMifSgQAwJUAelNADg5UZg3ANF2FoQKIraQYjgAcX4aABP4WBYA0a5+ClVxJPUqtZJiUc7XtSVsvLDtpEgK01G4FKgn8RoSqMCRmukIqARUHLysawQqumQ06sBZhKha7q2sgbq1PKswqt4GJDS0bRTW4AaGtcZqpBMMbPQRI45lgXspASpKUrSjLxSUbzkgiwKDlilMju61NH0y7rCoPCbOuknTsN676VuYD5DW1ca1patQZJrNQxo+srKy7HtKvUeGbn+yAADoMeh0qpM1LjYPgxDkKqmC4IQpC6qxoxPs1eaACt+FUqq6YZ6AKYPbbvRTaBS0OxLHue8VNF0EdyNSDJEHZXzAAB9QAQTUAMBcGUANz1ADAdQBlv0AHPM0kVhlADdFQBaOQ59lAGqIwBu5UAEzSjlgwnYAE-bVCO5LUvSzKCmc2o3DI1ZAA+3WMegqwozEKDxfMpQBgGK6SAHZO53IEAFfjAD21SAACIatgJPaiT2b5sW7h08KDnLema2Jt56B+afcVA8geTLrIQAq-UAeudAsAJATACV9UKwDRRvAC-1Ho28AU3NADztQAG5wZPpACDNFo2DaZtTVNMT324QNuIuOeF40bhRPEybK0M+SbAANUg-g3vtBfzDUU1VWuaAAAsnB3zUAAZKrGg95PdN+z+36nq8MmwMZox1HqcyKVoCOFfgBAE5gj6mgSAAjGXFV7z3EhvcUoDziH2PtKCBvgv4nnkmZN82EpBILXqgzeDlbA2BcvYQItdmBkJQYvNocBrS01gC6QAEk7ABJgTJCkBZ7MI3pKLM1piEb38HvVyEE4H8Ckb8F0CjuAf08GNRs0A4gxGQhI7gaMr43DvpIFqL8LT4Orrg8x5hZHwPRhjIR69uCiPERQtGsBTQqX4NaEQLobA2P4C6ZwwBgjAB0Ehc4PhxJTAcRQ5xVCnK0ICGoF0cgwnTFgIaK++jDTXGtJoKJagbDEJ8aLQAAd6ADRlJhjiV7kMXhKBej8TIqNcrAhIp8AQNN-uYf+digHTF1NhDB4DIGnhgcfBBaMqkUPQWArBcicGfzPIQtEgBYxX7oAKjlADR8oABW0cApgAFKGiuAAdXzHWLeElYadk8vvSAWZ+Dfg0NAQ0lycZnE8u04QP8rnV2AdhAAEvwDQaoJl-IsmAiBZizyTk8IC4F85PDn0vtfO+D9f7iCqgNe5jznkxARYcq4MzMF3IeW+J5LzpTLkWaeNQ+g9muAAKLKnfPBAiNxHFSkacja4Nc5RmSZbEDQrLPKct-ty4ykozIEumBcrlNzXLYrJbiuV3YbifM6T88wAqWXQDZdccUYK4VqmlBM7VQrdWeQ5WCoZCyrSNGovSQAYXJmuFaq64gAYlRdRat1jj+6ADsPcggAK4xGIAL8V+5tDoJkaFU4jUIqRQYm+98IHosRrS90JKcUvPxUcmVQybCKvfLiylY5qUnlpWZLsUxpXigvrffgc9+r2AAO4vNNGqJw9grAizRG3QAj7aADK9DYgAFNMAGxKyRADYSoAL71GLrMAP6p-bABccvqNoWxAAjfkcIS-FHEACFuD6lVOKfUjSIZLBsBg0CpYxXyosI4Yy56wIXwTai5NPyj1WnvaWfwm7+A7r3Qe-UNgL5P0Kb0j9t6K2xFONW79v790yrcAAThNeoLtDCYCwDcGjKIgAG00AHkajrACf2oAPh025tEABjy5TIC325rwWAvgwBNsY2jD4sAdDdl4NAU0PxrhoxedcEAapjSwBALfH4apUKGl4iARsV8NCwH4AAWj6iqYTLG2MXA4wptwCmIho2o3MU0ABiaAGhV4KcbDoLRsBCYKfrRcOYVwnk2aQgptJykyYPlNApljyQCPSpHjOkjrcKM8IOTm+p3y3nctuQW8lrzcrcrPSlC9KqEZSFpeqyLuUtXMvNXqg1-S9RGuQ4Ar1eqrWFcGWA215jpUSjzRmpVLybAwo-Qi2NU540oqTY0jF6gsWksLVmscso8Fng-auZgA6Yz+UAIDGgAGJQQ0to4XYviQDq+KGDFDd1wfFEtpDnbu24F9N3QAAh5bEAGbagAF+MAOneHdsDJDgFRTYK7rs3ceO6wYjxAC-CYAQpsNgMkAADpgBAyMAE9mgB+v0AIqmgBftTRKMQAxtaAHxYhkMZABYCYAcfjAA28Qup4zxABsjoAOBVACEVoAeH1ACcymSQANN4zsR4Ad+V+6jCOPhEV8pf4nuwoUcVqgYWSmXGZfMsB92pUlKJ6YAAtJYhpxSpmPd9CVHk3XS9lL-McCmpxgtwlSq0sQoL+G1GaeTZkZMnRflIAXQvYAi5+BLuaRFvIpmN6lJ+UxzcnSt+LyX4oFMiDQ4AAqVACrNgyYHI9ADfcoAeEMdhtAD+yQYgBYTUAELmDJqKAHQlENjFACznr3TRvAr6QCfbfEebR4eABdTLYAfwRqjVKJWX+UbDbprzWYy5hAFbcXjtg9QgSsY1bxvdvMrt04Mfd1pwRvktgWBWqVQQhK9ESfjYYi3lGj4j7f20drIiRzafgAP321H-3RxGywAvRPqY0+q-7ZsPttDgxADkmoAeB1ACADIAecV-cP8ABpyr+zb++YsxQAQAy-5wCAA" target="_blank" rel="noopener">这里</a>查看。</em></p><h3 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h3><p>泛型是可以自定义的局部类型变量</p><h4 id="定义了泛型的函数"><a href="#定义了泛型的函数" class="headerlink" title="定义了泛型的函数"></a>定义了泛型的函数</h4><pre><code class="ts">function foo<T>(input: T) {}foo('a');</code></pre><h4 id="定义了泛型的类型别名-Type-alias"><a href="#定义了泛型的类型别名-Type-alias" class="headerlink" title="定义了泛型的类型别名(Type alias)"></a>定义了泛型的<strong>类型别名(Type alias)</strong></h4><pre><code class="ts">type Wrapper<AType> = { a: AType }; const obj: Wrapper<Date> = { a: new Date() };</code></pre><h3 id="元组与字符串模板类型"><a href="#元组与字符串模板类型" class="headerlink" title="元组与字符串模板类型"></a>元组与字符串模板类型</h3><p>ts的<strong>字面量类型</strong>除了常见的数字、字符串、对象、布尔值外还有比较特殊的元组和字符串模板类型。</p><h4 id="元组-Tuple"><a href="#元组-Tuple" class="headerlink" title="元组(Tuple)"></a><strong>元组(Tuple)</strong></h4><p>ts的元组本质就是js里数组的子集,可以理解为指定长度以及每项类型的列表。下面<code>Tuple</code>类型限制了长度为3,每项为确定类型的元组。</p><pre><code class="ts">type Tuple = [number, string, 123];const tuple: Tuple = [1, 's', 123];</code></pre><p>元组可以使用类似js解构的语法组合,下面<code>Tuple1</code>通过组合的方式复用了Tuple类型。</p><pre><code class="ts">type Tuple1 = ['a', ...Tuple];const tuple1: Tuple1 = ['a', 1, 's', 123];</code></pre><p>PS: 如果想指定数组内某项类型也可以用该语法,下面<code>StrLeadingArr</code>类型的数组长度至少为1,且首项必须为字符串。</p><pre><code class="ts">type StrLeadingArr = [string, ...number[]];const slarr: StrLeadingArr = ['1', 1];</code></pre><h4 id="字符串模板-Template-Literal-Types"><a href="#字符串模板-Template-Literal-Types" class="headerlink" title="字符串模板(Template Literal Types)"></a><strong>字符串模板(Template Literal Types)</strong></h4><p>字符串与js里有差不多的能力,作为类型的字符串模板可以使用泛型替代其中部分内容。<br>下面<code>Hello<string></code>类型可以匹配<code>hello</code>开头的字符串,而<code>Hello<'world'></code>类型只能匹配<code>hello world</code>。</p><pre><code class="ts">type Hello<Name extends string> = `hello ${Name}`;const greeting: Hello<string> = 'hello type';const greetingToWorld: Hello<'world'> = 'hello world';</code></pre><h3 id="类型推导与模式匹配"><a href="#类型推导与模式匹配" class="headerlink" title="类型推导与模式匹配"></a>类型推导与模式匹配</h3><h4 id="extends关键字与条件类型-Conditional-Types"><a href="#extends关键字与条件类型-Conditional-Types" class="headerlink" title="extends关键字与条件类型(Conditional Types)"></a>extends关键字与条件类型(Conditional Types)</h4><p>extends关键字除了实现继承以及限制泛型下界外还会用于<a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html" target="_blank" rel="noopener">Conditional Types</a>特性。该特性类似js里的三元表达式,通过<code>extends</code>关键字进行前后类型的匹配。</p><p>匹配是看<code>extends</code>前的类型是否是<code>extends</code>后类型的<strong>子集</strong>,比如下面类型<code>'猫'</code>不是类型<code>'狗'</code>的子集。</p><pre><code class="ts">type 叫声<动物 extends '狗' | '猫'> = 动物 extends '狗' ? '汪' : '喵';type 叫一声 = 叫声<'猫'>; // '喵'</code></pre><p>下面类型<code>123</code>是类型<code>number</code>的子集</p><pre><code class="ts">type 类型<值 extends number | string> = 值 extends number ? '数字' : '字符串';type 值类型 = 类型<123>; // '数字'</code></pre><h4 id="使用infer搭配条件类型的extends"><a href="#使用infer搭配条件类型的extends" class="headerlink" title="使用infer搭配条件类型的extends"></a>使用infer搭配条件类型的extends</h4><p>先说说类型推断(Type Inference),TS可以在很多地方自动判断类型而不需要用户主动声明,不如下面的变量声明和泛型函数</p><pre><code class="ts">// 类型123const typeinfer = 123;// 类型numberlet typeinfer1 = 123;declare function typeinferfn<T>(input: T): T;// 自动推断T为类型'hello'typeinferfn('hello');</code></pre><p>TS也提供了在条件类型里推断类型的能力,条件类型没有设计定义泛型的地方,而是新增了一个<code>infer</code>关键字。下面拿到函数类型返回类型的类型里用<code>infer R</code>替代了原来的类型约束,使TS去推断出R的类型使其能在<code>?</code>后的第一个分支内使用。</p><pre><code class="ts">type MyReturnType<F extends Function> = F extends (...params: any) => infer R ? R : never;// 如果条件类型支持泛型的话可能就会变成这样,如果对泛型函数(<T>() => T)比较熟可能这样会比较好理解// type MyReturnType<F extends Function> = <R> F extends (...params: any) => R ? R : never;type RType = MyReturnType<(input: string, input2: number) => { combined: string }>; // { combined: string } </code></pre><p>infer也能推导interface和type alias里的泛型类型</p><pre><code class="ts">type GetPromiseResultType<P extends Promise<any>> = P extends Promise<infer R> ? R : never;type RType1 = GetPromiseResultType<Promise<string | number>>; // string | number</code></pre><p>infer也能推导元组内某个元素的类型</p><pre><code class="ts">type GetTupleFirstType<Arr extends any[]> = Arr extends [infer First, ...any] ? First : never;type TupleFirst = GetTupleFirstType<Tuple>; // numbertype GetTupleSecondType<Arr extends any[]> = Arr extends [any, infer Second, ...any] ? Second : never;type TupleSecond = GetTupleSecondType<Tuple>; // stringtype GetTupleLastType<Arr extends any[]> = Arr extends [...any, infer Last] ? Last : never;type TupleLast = GetTupleLastType<Tuple>; // 123</code></pre><p>infer也能推导字符串模板内的泛型,这里有点像正则匹配了</p><pre><code class="ts">type GetGreetingTo<Str extends string> = Str extends `hello ${infer Name}` ? Name : never;type Target = GetGreetingTo<'hello world'>; // 'world'</code></pre><h2 id="使用类型系统的编程基础"><a href="#使用类型系统的编程基础" class="headerlink" title="使用类型系统的编程基础"></a>使用类型系统的编程基础</h2><p>OK,讲完了TS的一些基础语法,下面就过一遍编程语言教程都会讲的桥段。</p><p><em>本节的代码可以在<a href="https://www.typescriptlang.org/play?ssl=65&ssc=43&pln=64&pc=30#code/FAej-ACAXBnRvH0NHqg0f0NBekKZMUW-4MKMJy0kCztQSTlAN5UHnEjTUkSQdW1AjdNuGgE8AHAU0gBBAIwjIAXkgiA3Fz6DRAJkmQA5AEMARgGM1cngOEiAzKoDaIgDSQlNzWoC6BhcYAsqgN6QNALnUaapAAvi5GogCsqtAATgCu-HK4kIAqAYAQKkjy4UriUuIAPraQhQ5hikoqUkI5xcYmSWCQgJZKgNBygCFugBx6gBSugHFygCvWgMHxgCRKgLWmWYomuZAAdnEAtlr8MbWwsQCW0wDmtRrT3LXT-ABuSw3MgAI6gBcJdDaAjK49gLBygL8BgLQmgLKJSIDUSoCwKoD+8l2AGH-AAJGgFO5QBXyoAseUAFOqATydyIBRg0ArYotf7jYRuEQAHgAKgA+VTYyD8AAe0H40wAJrBIBsAGZLSAAGQA9joNAAbQkAfkgAApgJBBUzWRzscAAJSQfyHE4xM4QCiUQC-ioAHUwYECYkEA2zaIL7fQD3yiDAMr6gB15VWowyCABy82xzIAyustpiAJLTXhxaBE0nkqkzeaLGI2AAKMX4tLWxO9ZMp1NWMQ22ykag2Hug-jU+KkAAMACSeUPhyPBfNutPBbNyRqCgB6dfrDZrkEbQtbtZbbc7Xbbjd7rd7A77yUF0w0c0EXd4GhiY+7kCnM7mMFcc9XFPDGji7K9Rw5CUFtLi0x00DWzOmkC0zIp3DRjuWUhtczt98TmJEShMuLOkEAm-GAGcS2lVB5AHozQAAOUASW9ABtFQBEGzRAAxDQAGt+AAWQ0XhMSEGJlhJGM-VmBYlnMRwQzDCk1hPaNfWpXleGlAMlklCR8V2bgsx8PYykgAAlURVEQlD0MwywbDsSATFIyAnxfJ1Nm-JgFUAMCVAHpTQA4OVGdVwE1dhaDU3TRjRABxfhoFQ-hYFgDRNn4HFXDwmj-SImIswFIVsXsn1Y2kSAeTUbgLKCfxGg8owJHC6Q3MFFQHO80LBD86ZmSCwVGkqCL4qiyB4uo7yzD83gYmZLRtHZbgUpC1xwqkEwsplU40TmWAk0gEyzIsqybMxJQFOSXS1MAMbS0kMy1WtM+KrTHWz4tc1sctigjGOWRKloq5hLWZWlsqqiK1EIwM1Cy+avL9eNE189Qzq2NbIAAOnuo7PPw6l4KPE8zwvPzD2PU9zyCx6jAW6lioAK34Ki-NB8HoH+1t6rlNFoC9KQ2omqbMU0XRMx-dIkAhJTAAB9QAQTUAMBcAUANz1ADAdQBlv0AHPM0nJgFADdFQBaOXhiFAGqIwBu5UAEzS0SPD7YFQ5rVDa8zLOs2yCgk2o3G-BhIEAD7cjR6HzCjMQoPCUz5AGAYroxvaiWbMgQAV+MAPbVIAAIgC2BLdqS3CuK0ruDtwp4f56ZBZylHxoUSbx0xDXIBI3qyEAKv1AHrnNTACQEwAlfS0sBNUjwAv9R6OPAFNzQA87UABucAT6QAgzRaNg2gjdl2WwmduFVBC1jLiuNG4LCcNyv02JImwADU934WahQr8w1HZclNmgAALJwW+pAAGXystbEipTnvvm6B4O2Jse7brpBkeIs6BHFn1dBXMLv2QSDf7vg2vy5whvMV31ZO+73ED98JfOxI7jpwfSAr7r2-G6iVsDYSS9hAih2YH-G+lc2hwF5CDWA4pAASTsAb671zyQFLtAhu2J7S8m-g3fwbcpK7jPvwIhexxQUO4AvTwWUwzQDiDEC8BDuC3SHlsMekgIozy5O-YOr9+HmFIefO690sH124Lg-BADbqwHZJRfgvIRDihsCI-g4pnDAGCMAHQ55Vg+BwlMCRADpFAPEqAgIahxRyD0dMWAzIh7sOZJsXkmgjFqBsN-FRP5ACxiunQAVHKAGj5QACtpogAKKkhnCeV8WxJE4knpAK6mwQ4Em4pE2IGgYlyQSavZJHFsTcQAFLMg2E3XCJ04xyXbpAe0-AFwaGgMyCpz0klyV7oKCuiTzAZOidAWJmxMTb2WAACX4BoCkuIL63V6Vk-pcl4nDN4nvF+F1GgAX+IAMLlZnZITFsQAMSo7PmXszYkj06ADsPcggAK4xGIAL8V05tDoJkbseZPBjImSWTw-dB7DzHhPVe4g-IpTqQ0ppMRPklLKQ-aANgQXTkac03EFZF7djUPoNE8YpiQumJiAeo9+Bl2SvYAA7s09kFInD2CsJmTUcdACPtoAMr1ACQ5oABTTABsSmidCKFJEACFuCMnJJiRkiT9pLBsA-LcXo8nVIPlIVJUgJXbgHhwke48D6r2FTyRVXp-Bcv4Ly-lgrGQ2AHlPTxYjbrascApUa8ZKiQGxZiPVBqBU4oiFPKZ6hqUQJgLANwt0oiAAbTQAeRqbMAJ-agA+HTjm0QAGPKADRlSAo8ka8FgL4MAxKM23UtLAHQCZeDQHZLsTYt1mmbBABSVksAQCj12BSK8zIkIgDDEPDQsB+AAFokpkirdm3Nax83trcO2iIt0k1zHZAAYmgBoWu7aww6CYbAD67aCVrDmBsRpy7zztrsRRX6o52TtuzckMN2Kc5gQCVG2O8aUGjWxViLp0qTk1LhTOMFiTkniospKj9clVBoo6cIFelTg5HIGUM6Y9JRnjMmdMsDCyAEQag8s1Yqy+HdnvffPesL6nwrBTYV52rPnvIpJ875Kq-nqpA4C9QwLcNvuaRWfEb9uzaqrMwRlhoVKAEBjQADEoAE5BMYtiGYKQmHnUAL5a6zEgn+Oes8djRSUBk6AAEPFUgAzbUAAvxgB07wTtgZIcB-xAiZW0DTOnABk3vswY5nAC-CYAQpsmUAkAADpgBAyMAE9mgB+v0AIqmgBftU1KMQAxtaAHxYgEhpABYCYAcfjAA28YALjkWDmcAExygA2R0AHAqgBCK0APD6gBOZTeIAGm8L1BcAO-K6cRquFkic3JIHRXLEKPk1QrzsQVm4i6WAArLLYhrdMAAWksZkmIrQiqWgUh0OSrT4lXtmdt+YllPmRTyWI+5-C0g5G27izaOozykC1trsAOu7B60Vd8NrXDrcslPKY22Op7e671zE7aRA+sAAVKgBVmwBC5nOgBvuUAPCGKoWBtBexCQYgBYTUAELmAIAKAHQlW5YFACznqnRhvAh6QAo6PHObQAuABdTFUL20RCApBSLCg3nI2B5UTwMHFzCbwk5XKTgqhCeqp8hfVknDU4p5S-ZVvy1Vre-duCZFJVB44JyIKeNgPwKUaIAGBVADsFgCe49KGXsrBE8HjU8AB+sn-vPbRGGWAkr+dTCFzJwTNhZM+sGIAck1ADwOoAQAZADzis9m3gANOUdzzZ7EEIKACAGT3OAgA" target="_blank" rel="noopener">这里</a>查看。</em></p><h3 id="定义变量"><a href="#定义变量" class="headerlink" title="定义变量"></a>定义变量</h3><p>下面的代码都是基于TS类型系统,这节虽然定义的都是<code>"类型"</code>但都会称为<code>"变量"</code>,在后文里同样会优先使用编程语言的术语。</p><h4 id="字面量"><a href="#字面量" class="headerlink" title="字面量"></a>字面量</h4><p>支持的字面量有下面几种</p><pre><code class="ts">type A11 = 1;type A12 = 'abc';type A13 = [1, 2, 'a'];type A14 = { a: 'a' };type A15 = true;</code></pre><h4 id="联合类型"><a href="#联合类型" class="headerlink" title="联合类型"></a>联合类型</h4><p>联合类型组合成的变量值也会偶尔用到,这里提一下</p><pre><code class="ts">type A21 = 1 | 2 | 'a';type A22 = A21 | A13;</code></pre><h4 id="剩下的常用于比较判断"><a href="#剩下的常用于比较判断" class="headerlink" title="剩下的常用于比较判断"></a>剩下的常用于比较判断</h4><pre><code class="ts">type A31 = number; // 所有数字的集合type A32 = string; // 所有字符串的集合type A33 = any; // 所有值的集合type A34 = never; // 空集</code></pre><h4 id="局部变量"><a href="#局部变量" class="headerlink" title="局部变量"></a>局部变量</h4><p>每个泛型都可以看作是局部变量,infer推断的类型也可以看作是局部变量,它们的作用域有所不同</p><pre><code class="ts">type A41<T> = T extends infer LocalT ? ( LocalT) : never;</code></pre><h3 id="定义函数"><a href="#定义函数" class="headerlink" title="定义函数"></a>定义函数</h3><p>泛型类型别名可以当作函数使用</p><pre><code class="ts">type NumToString<Input extends number, Prefix extends string = 'input: '> = `${Prefix}${Input}`;// ^^^^^^^^^^^ ^^^^^ ^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ // name param param type default value function bodytype Str = NumToString<123>; // "input: 123"</code></pre><p>由于TS语法限制,泛型在使用时必须指定确定类型,使得这里的函数不是一等公民</p><pre><code class="ts">type FakeMap<Arr extends number[], Predict extends (p: number) => any> = any;type RA1 = FakeMap<[1, 2, 3], NumToString>;// ^^^^^^^^^^^// Generic type 'NumToString' requires between 1 and 2 type arguments.(2707)</code></pre><h3 id="分支与判断"><a href="#分支与判断" class="headerlink" title="分支与判断"></a>分支与判断</h3><h4 id="字面量与字面量的比较"><a href="#字面量与字面量的比较" class="headerlink" title="字面量与字面量的比较"></a>字面量与字面量的比较</h4><p>在字面量之间使用extends相当于<code>===</code>,所以前后可以互换</p><pre><code class="ts">type GetMessage<Type extends number> = Type extends 1 ? 'yes' : // Type === 1 2 extends Type ? 'no' : // 2 === Type Type extends 3 ? 'probably' : // Type === 3 never;type msg = GetMessage<2>; // "no"</code></pre><h4 id="字面量与集合比较"><a href="#字面量与集合比较" class="headerlink" title="字面量与集合比较"></a>字面量与集合比较</h4><p>判断字面量是否属于某个集合,这里前后不能互换</p><pre><code class="ts">type GetTypeName<Type> = Type extends number ? 'number' : // typeof Type === 'number' Type extends string ? 'string' : // ... Type extends Function ? 'function' : Type extends object ? 'object' : never;type tt = GetTypeName<'abc'>; // "string"</code></pre><h4 id="传入联合类型"><a href="#传入联合类型" class="headerlink" title="传入联合类型"></a>传入联合类型</h4><p>联合类型会分开处理,并将结果合并,其中never会被去除</p><pre><code class="ts">type unionsMsg = GetMessage<1 | 3 | 4>; // 相当于 1 | 3 | 4 分别调用 GetMessage 返回 "yes" | "probably" | nevertype unionsType = GetTypeName<3 | []>; // "number" | "object"</code></pre><h3 id="循环与递归"><a href="#循环与递归" class="headerlink" title="循环与递归"></a>循环与递归</h3><h4 id="使用递归替代循环"><a href="#使用递归替代循环" class="headerlink" title="使用递归替代循环"></a>使用递归替代循环</h4><p>TS的类型系统不支持循环,但是由于类型支持一定程度的递归,所以我们可以像函数式编程语言一样使用递归替代循环。下面的FillArray就是递归填充数组的例子。</p><p>思路是每次生成一个新数组返回,新数组第一个元素为填充元素,后面的元素通过递归传入数组</p><pre><code class="ts">type FillArray<Arr extends any[], Value> = Arr['length'] extends 0 ? // 递归结束条件 [] : // 结束 Arr extends [any, ...infer Rest] ? // 模式匹配拿到除了第一项外的剩余项 [Value, ...FillArray<Rest, Value>] : // 填充值和剩余项递归调用组合成结果 []; // 数组没有一个元素时会走这里type arr = FillArray<[1, 2, 3], 'a'>; // ["a", "a", "a"]</code></pre><p>上面的例子如果不理解可以参照下面的TS版看</p><pre><code class="ts">function fillArrayTS(array: any[], value: any): any[] { return array.length === 0 ? [] : [value, ...fillArrayTS(array.slice(1), value)];}const arr1 = fillArrayTS([1, 2, 3], 'a');console.log('arr1', arr1); // ["a", "a", "a"]</code></pre><p>PS: <code>FillArray</code>函数里<code>Arr extends [any, ...infer Rest]</code>其实已经包含了数组内必须有一个元素的条件,所以<code>Arr['length'] extends 0</code>的判断其实不需要,可以简化一下</p><pre><code class="ts">type FillArray1<Arr extends any[], Value> = Arr extends [any, ...infer Rest] ? [Value, ...FillArray<Rest, Value>] : [];</code></pre><h4 id="再来个例子"><a href="#再来个例子" class="headerlink" title="再来个例子"></a>再来个例子</h4><p>实现一个将字符串数组转为字符串的join函数,可以指定分隔符。</p><p>思路和上面差不多,停止条件不变,生成结果改为字符串模板,每次取第一项字符串加到结果里,然后判断当前数组长度是否等于1,如果不是则在当前项后面加上分隔符,然后剩下部分递归数组剩余项生成。代码如下</p><pre><code class="ts">type JoinWIP<Arr extends string[], Separator extends string> = Arr extends [infer Head, ...infer Rest] ? `${Head}${Arr['length'] extends 1 ? '' : Separator}${Join<Rest, Separator>}` : '';</code></pre><p>看起来没啥问题但是在编译器里提示<code>Head</code>和<code>Rest</code>类型不对,不清楚为什么这里不能推断出正确类型,但是有个解决方法是通过另外定义个带类型约束泛型的类型别名去做类似<code>type guard</code>的工作。如下定义了约束<code>string</code>和<code>string[]</code>的<code>type guard</code>来使<code>Head</code>和<code>Rest</code>成为需要的类型。</p><pre><code class="ts">type ExtractStringArray<T extends string[]> = T;type ExtractString<T extends string> = T;type Join<Arr extends string[], Separator extends string> = Arr extends [ExtractString<infer Head>, ...ExtractStringArray<infer Rest>] ? // 这里使用了ExtractString和ExtractStringArray来确定推断出来的变量类型 `${Head}${Arr['length'] extends 1 ? '' : Separator}${Join<Rest, Separator>}` : '';type str1 = Join<['hello', 'world'], ','>; // "hello,world"</code></pre><h4 id="递归深度限制与尾递归优化"><a href="#递归深度限制与尾递归优化" class="headerlink" title="递归深度限制与尾递归优化"></a>递归深度限制与尾递归优化</h4><p>类型嵌套有深度限制,也就是说只能做有次数限制的循环递归,这点限制了很多复杂逻辑的实现。目前普通递归的次数不超过50次左右,比如对之前的<code>Join</code>函数传入长度超过48的数组就会报错</p><pre><code class="ts">// 创建一个长度为L的数组type MakeArrayByLen<L extends number, Result extends string[] = []> = Result['length'] extends L ? Result : MakeArrayByLen<L, ['0', ...Result]>;type str2 = Join<MakeArrayByLen<49>, ','>;// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^// Type instantiation is excessively deep and possibly infinite.(2589)</code></pre><p>ts4.5 新增了对<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#tail-recursion-elimination-on-conditional-types" target="_blank" rel="noopener">尾递归的优化</a>,如果递归符合<a href="https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8#:~:text=%E4%BC%98%E5%8C%96%E6%9D%A5%E8%A7%A3%E5%86%B3%E3%80%82-,%E5%B0%BE%E9%80%92%E5%BD%92,%E8%B0%83%E7%94%A8%E8%87%AA%E8%BA%AB%E7%9A%84%E9%80%92%E5%BD%92%E5%87%BD%E6%95%B0%E3%80%82" target="_blank" rel="noopener">尾递归</a>的写法递归深度就支持更多的层次。</p><p>这里将上面的<code>Join</code>改成尾递归的写法,由于尾递归优化后程序不会保留函数上下文,需要在递归函数内手动传递,所以改造思路就是将结果字符串传给下一个递归函数,每个函数内将新的结果和传入的字符串拼接然后接着传。</p><pre><code class="ts">type Join1<Arr extends string[], Separator extends string, Result extends string = ''> = Arr extends [ExtractString<infer Head>, ...ExtractStringArray<infer Rest>] ? Join1<Rest, Separator, `${Result}${Head}${Arr['length'] extends 1 ? '' : Separator}`> : Result;</code></pre><p>优化后深度可以达到1000左右了</p><pre><code class="ts">type str3 = Join1<MakeArrayByLen<999>, ','>; // "0,0,0,.....,0,0"</code></pre><h3 id="基础数学运算"><a href="#基础数学运算" class="headerlink" title="基础数学运算"></a>基础数学运算</h3><p>类型系统内没有对数学运算的支持所以这块反而是最难实现的,如果要实现一套完善的浮点数运算就需要从二进制部分实现了,这里就简单提一下在<strong>特定条件</strong>下的数学运算。</p><h4 id="判断正负"><a href="#判断正负" class="headerlink" title="判断正负"></a>判断正负</h4><p>思路就是通过转字符串判断是否以负号开头(用高级特性实现基础特性也是没谁了)</p><pre><code class="ts">type ToString<T extends number | string> = `${T}`;type IsLessThanZero<N extends number> = ToString<N> extends `-${infer Num}` ? true : false;type reless0 = IsLessThanZero<1>; // falsetype reless01 = IsLessThanZero<-1>; // true</code></pre><h4 id="正整数加法"><a href="#正整数加法" class="headerlink" title="正整数加法"></a>正整数加法</h4><p>通过构建各自长度的元组,然后合并拿到结果的<code>length</code>,当然这么做还是会受到递归深度的影响</p><pre><code class="ts">type Add<A extends number, B extends number> = [...MakeArrayByLen<A>, ...MakeArrayByLen<B>]['length'];type resultadd = Add<10, 12>; // 22// 由于递归深度限制只能支持0~999的加法type resultadd1 = Add<999, 999>; // 1998</code></pre><p>另外比较大小、减法、乘法、除法等等略过吧。。</p><h2 id="实战类型体操"><a href="#实战类型体操" class="headerlink" title="实战类型体操"></a>实战类型体操</h2><p>那么如何用上面的语言去大展身手呢?似乎没有什么实用场景,只能拿来写写算法了。目前有一个为这种语言量身定制的<a href="3">题库</a>,挑一道题来练练手吧。</p><h3 id="FlattenDepth"><a href="#FlattenDepth" class="headerlink" title="FlattenDepth"></a><a href="https://www.typescriptlang.org/play?#code/PQKgUABBDMBMAs0IFoIDEA2BDALjgpgHYAi+ADjgBaQrJ300BGAnhAFYCWWhA5gM6VuEABQABTt36DCAW3w4sASggBiOQBMOAVxmqsAJ31ZmNGivMQAilvx8cHAPaFTUAEr4Axlv18OAN3wMVgAzbDwiCAMjVi0yCBwHCHVyKniOOT4AOhd0B30IfAAPLBkyDHwALhyAA1qcZjJbD30OChp6xsiIAF50MIISFMoAHgBtAEYAGghYadHoafgAXTnR0YBWJa2VmYA+CGBgCAnp2ZhFuc2lzIhQ3AJ9Qhm0jPaG-AhGHr77olIKEYnGZzBYQZarDbbJb7Q7HKbA85g1ZXa4Qf6pZLBLBaDA4PjxRKMD7jGi1ao5ACSwXilA+yQBEA4+LI+gcfg4yXU0w4OAA5PieFoDNwCPh1ATPh8yA5fPYAozCAQePh9NkoDR9gA1Dj4ADuECcEAA4jyABJaRgVCCUPBkPgVQ54jyUTJsLJ5HjAOCIMAgYBgAOgCAAfVDYfDYYgAE0HN4IABhBzJCCmlUfCMZ0MQP0BjofTC-QjDACChgKhQG6nx3GYo2h3xopfyRUr+NGHEIwRVKfwWC5EEyg47Xfy7jsSwgAH4aFBRoPMqm++XW5FCLWJ5Oe0uraNF+odvOC+Ei2OcLsJ1aZxAmwBuXPvH7H9EjJvLohV1fr6blAIYN+ED9CB0Il8l6eEPFjRVuxbd98QABjrb4632XorwgrQoP0UZeXKXgqF5CcYIA-Ef0CKcrygV8qigGjHwGEtDH2IiP1fadaNoqiKJoo8BmfYYeKIBj9F2b98F-OZ53QzDpjg6E7zAPN4lsHBvgEwYATGeEzjWaAtkWHZoF2eSg0zDMIAAFWUhMsD4WwQ1M8Ns39dJpX0FTFIAbwgABRABHIUMGmbzCkaDwVIAX1uVldF5UQ82QZ0sAwXDlT4YAtHsDA+F5e9Og8Gy7N6UYaGC0KcGGPyAv4-o-iGMZoTmaERJKkLPHKyqkuqws+KBM5QWWES4VOaZ+qayYWrKir-M6tSevhUZYC2QberGia2qmqrZrq3qQX0iEUR2WBlq0kaLmOK5dmaqBSvWjqMC6p9tpO45Rv2qFjuGxF4GRbZLvG67WrCjaZpq9SqE0uZtNBUZvvOqEDI+hF+suJarp8wH2um+6to0oEFt245YchKFpnGABOWAADY4IADnGAB2RG+rOzY-rAJYA0DEB7IcrM0G8KhuwAZQIO0ed5pywFADUICFwR9A+ZhY3yPgHAwDLHEIe1rVte1HT4Z1XXdfRPW9aBgG4PhdRVGXtT1CBVfV+wnG1m0cDtB1gCdF03UyD0vQQc3HY1l2ZYAWTyD540EZKiFSq03Y9-XDd9j1fX9MAgA" target="_blank" rel="noopener">FlattenDepth</a></h3><p>这道题比较常规也比较实用就拿来写写吧。</p><p><a href="https://www.typescriptlang.org/play?#code/PQKgUABBDMBMAs0IFoIDEA2BDALjgpgHYAi+ADjgBaQrJ300BGAnhAFYCWWhA5gM6VuEABQABTt36DCAW3w4sASggBiOQBMOAVxmqsAJ31ZmNGivMQAilvx8cHAPaFTUAEr4Axlv18OAN3wMVgAzbDwiCAMjVi0yCBwHCHVyKniOOT4AOhd0B30IfAAPLBkyDHwALhyAA1qcZjJbD30OChp6xsiIAF50MIISFMoAHgBtAEYAGghYadHoafgAXTnR0YBWJa2VmYA+CGBgCAnp2ZhFuc2lzIhQ3AJ9Qhm0jPaG-AhGHr77olIKEYnGZzBYQZarDbbJb7Q7HKbA85g1ZXa4Qf6pZLBLBaDA4PjxRKMD7jGi1ao5ACSwXilA+yQBEA4+LI+gcfg4yXU0w4OAA5PieFoDNwCPh1ATPh8yA5fPYAozCAQePh9NkoDR9gA1Dj4ADuECcEAA4jyABJaRgVCCUPBkPgVQ54jyUTJsLJ5HjAOCIMAgYBgAOgCAAfVDYfDYYgAE0HN4IABhBzJCCmlUfCMZ0MQP0BjofTC-QjDACChgKhQG6nx3GYo2h3xopfyRUr+NGHEIwRVKfwWC5EEyg47Xfy7jsSwgAH4aFBRoPMqm++XW5FCLWJ5Oe0uraNF+odvOC+Ei2OcLsJ1aZxAmwBuXPvH7H9EjJvLohV1fr6blAIYN+ED9CB0Il8l6eEPFjRVuxbd98QABjrb4632XorwgrQoP0UZeXKXgqF5CcYIA-Ef0CKcrygV8qigGjHwGEtDH2IiP1fadaNoqiKJoo8BmfYYeKIBj9F2b98F-OZ53QzDpjg6E7zAPN4lsHBvgEwYATGeEzjWaAtkWHZoF2eSg0zDMIAAFWUhMsD4WwQ1M8Ns39dJpX0FTFIAbwgABRABHIUMGmbzCkaDwVIAX1uVldF5UQ82QZ0sAwXDlT4YAtHsDA+F5e9Og8Gy7N6UYaGC0KcGGPyAv4-o-iGMZoTmaERJKkLPHKyqkuqws+KBM5QWWES4VOaZ+qayYWrKir-M6tSevhUZYC2QberGia2qmqrZrq3qQX0iEUR2WBlq0kaLmOK5dmaqBSvWjqMC6p9tpO45Rv2qFjuGxF4GRbZLvG67WrCjaZpq9SqE0uZtNBUZvvOqEDI+hF+suJarp8wH2um+6to0oEFt245YchKFpnGABOWAADY4IADnGAB2RG+rOzY-rAJYA0DEB7IcrM0G8KhuwAZQIO0ed5pywFADUICFwR9A+ZhY3yPgHAwDLHEIe1rVte1HT4Z1XXdfRPW9aBgG4PhdRVGXtT1CBVfV+wnG1m0cDtB1gCdF03UyD0vQQc3HY1l2ZYAWTyD540EZKiFSq03Y9-XDd9j1fX9MAgA" target="_blank" rel="noopener">解题代码</a></p><p>如果是单层的Flatten会比较简单,只需要写一个之前提过的循环去遍历每一项判断是否是数组(<code>item extends any[]</code>),如果是通过组合元组的<code>...</code>语法与剩下项合并即可。</p><pre><code class="ts">type Flatten<Arr extends any[]> = Arr extends [infer Head, ...infer Rest] ? [...Head extends any[] ? Head : [Head], ...Flatten<Rest>] : Arr;</code></pre><p>这题需要支持多层flatten的话就需要不断去调用<code>Flatten</code>函数直到: 1. 达到指定的<code>level</code> 2. 数组中已经没有需要flatten的元素</p><p>其中1比较好判断,可以设置一个计数器(实用数组实现,数组长度即为当前计数)每次递归累加,然后判断是否等于<code>level</code>即可。2的话有多种方法,其中可以判断当前传入的数组是否<code>arr extends number[]</code>,对于只会传入数字数组的这道题来说是ok的,不过如果想要更通用一点可以去比较Flatten后的数组是否和之前一样,这里我们用这种方法去实现题解。</p><pre><code class="ts">type FlattenDepth<Arr extends any[], level extends number = 1, counter extends 0[] = []> = counter['length'] extends level ? Arr : Flatten<Arr> extends Arr ? Arr : FlattenDepth<Flatten<Arr>, level, [...counter, 0]>;</code></pre><p>虽然可以不用考虑性能,但是<code>Flatten</code>虽然调用了两次还是可以优化的,大家可以尝试一下。(提示用之前提过的局部变量)</p><p><a href="https://www.typescriptlang.org/play?#code/PQKgUABBDMBMAs0IFoIDEA2BDALjgpgHYAi+ADjgBaQrJ300BGAnhAFYCWWhA5gM6VuEABQABTt36DCAW3w4sASggBiOQBMOAVxmqsAJ31ZmNGivMQAilvx8cHAPaFTUAEr4Axlv18OAN3wMVgAzbDwiCAMjVi0yCBwHCHVyKniOOT4AOhd0B30IfAAPLBkyDHwALhyAA1qcZjJbD30OChp6xsiIAF50MIISFMoAHgBtAEYAGghYadHoafgAXTnR0YBWJa2VmYA+CGBgCAnp2ZhFuc2lzIhQ3AJ9Qhm0jPaG-AhGHr77olIKEYnGZzBYQZarDbbJb7Q7HKbA85g1ZXa4Qf6pZLBLBaDA4PjxRKMD7jGi1ao5ACSwXilA+yQBEA4+LI+gcfg4yXU0w4OAA5PieFoDNwCPh1ATPh8yA5fPYAozCAQePh9NkoDR9gA1Dj4ADuECcEAA4jyABJaRgVCCUPBkPgVQ54jyUTJsLJ5HjAOCIMAgYBgAOgCAAfVDYfDYYgAE0HN4IABhBzJCCmlUfCMZ0MQP0BjofTC-QjDACChgKhQG6nx3GYo2h3xopfyRUr+NGHEIwRVKfwWC5EEyg47Xfy7jsSwgAH4aFBRoPMqm++XW5FCLWJ5Oe0uraNF+odvOC+Ei2OcLsJ1aZxAmwBuXPvCAAUQrRg8OCbwwAKsuiFXV+v9l6T87zAPMfmPdERibH9CD-Gs62mcoAgwGC-0IHQiXyXp4Q8WNFW7Ftf3xAAGOtvjrQCICvXCtHw-RRl5cpeCoXkJ0I2D8SQwIpyvKBoKqKBBPAgYS0MfZ2L-Z8cFfd9DGGYduyPAYm32achKEpSiGgiT8WgtT1KE-jePUzTBgBYZTKbRD8GQuZ5xoujpmI+sBIMiBCBslUQLAgg7G+UzILGeEzjWaAtkWHZoF2ECg0zDMIE-WwcATLA+FsEM4vDbN-XSaV9GSsCAG8nwARyFDBpmfRo3wgABfW5WV0XlRDzZBnSwDAmOVPhgC0ewMD4Xl706DxUvS3pRhoKrPBwYZHzKjqLP6P4hjGaE5mhXZJimwpqtm+byqWwtAqBM5QWWLa4VOaZzs27aoGmt85oWjAjog1agVGWAtku067p2vbnsOgKPuCkEIohFEdlgX6wcReBLh+raAZmoHFpB8zTvBsEdjWSFtlh674eRAnkYe3bUYO9HlrMqggrmELQVGBHjihyLCYRc7Ef+8nAap16Mbpz7GbmFn8e2aZxgATlgAA2YiAA5xgAdg5s6LggTZdmRpYA0DEAMsyrM0G8KhuwAZQIO1DaN7KwFADUIHNwR9A+ZhY3yPgHAwPrHEIe1rVte1HT4Z1XXdfRPW9aBgG4PhdRVR3tT1CAvZ9+wnADm0cDtB1gCdF03UyD0vQQGO099zPHYAWTyD540ETqiG6q1s9zkOw6Lj1fX9MAgA" target="_blank" rel="noopener">优化后的代码</a></p><h3 id="有更复杂的吗"><a href="#有更复杂的吗" class="headerlink" title="有更复杂的吗"></a>有更复杂的吗</h3><p>有不少大佬实现了比较复杂的算法逻辑,可以参考<a href="https://zhuanlan.zhihu.com/p/426966480" target="_blank" rel="noopener">中国象棋</a>、<a href="https://zhuanlan.zhihu.com/p/427309936" target="_blank" rel="noopener">Lisp解释器</a>、<a href="https://zhuanlan.zhihu.com/p/423175613" target="_blank" rel="noopener">BigInt加法</a>等等,你也可以尝试一下。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>如果看完后对这门语言还是不太理解也不用担心,本质上类型体操也是为了加深对TS的理解,能有所启发就达到目的了。TS还有很多有用的类型特性等你探索,比如官方文档的<a href="https://www.typescriptlang.org/docs/handbook/2/types-from-types.html" target="_blank" rel="noopener">Type Manipulation</a>这章值得去看看。</p><p>最后提醒一下,请不要在生产代码中去写这些代码。</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://juejin.cn/post/7025619077158666270" target="_blank" rel="noopener">https://juejin.cn/post/7025619077158666270</a></li><li><a href="https://juejin.cn/post/7079305963131371550" target="_blank" rel="noopener">https://juejin.cn/post/7079305963131371550</a></li></ul>]]></content>
<summary type="html">
<h2 id="关于TS的类型系统"><a href="#关于TS的类型系统" class="headerlink" title="关于TS的类型系统"></a>关于TS的类型系统</h2><p>写了这么久的ts才了解到它的类型系统是<a href="https://github
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="typescript" scheme="https://frezc.github.io/tags/typescript/"/>
<category term="type system" scheme="https://frezc.github.io/tags/type-system/"/>
</entry>
<entry>
<title>三维旋转没那么复杂</title>
<link href="https://frezc.github.io/2022/01/15/rotation-3d/"/>
<id>https://frezc.github.io/2022/01/15/rotation-3d/</id>
<published>2022-01-15T12:07:05.000Z</published>
<updated>2022-01-15T15:35:21.800Z</updated>
<content type="html"><![CDATA[<p>前段时间看了一些网上关于三维旋转的文章和内容,发现很多人都会从欧拉角讲到四元数,洋洋洒洒讲了很多概念,看完后还是很难建立起清晰的认知以及如何正确使用。所以我就打算来写这么一篇文章,从程序开发的角度(其他领域可能对旋转的认识有所不同)把常见的4种表达方式(<strong>欧拉角</strong>、<strong>矩阵</strong>、<strong>轴角旋转</strong>、<strong>四元数</strong>)理一下。</p><h2 id="表达方式的不同类型"><a href="#表达方式的不同类型" class="headerlink" title="表达方式的不同类型"></a>表达方式的不同类型</h2><p>首先我们这里定义两种表达方式的类型:<strong>几何表达</strong>和<strong>数学表达</strong>(名字瞎起的)。</p><h3 id="几何表达"><a href="#几何表达" class="headerlink" title="几何表达"></a>几何表达</h3><p>几何表达指的是通过这个表达方式可以立马在大脑里想象出来是如何旋转的,很明显上面4种表达方式中只有欧拉角和轴角旋转可以立马想象出旋转。</p><p><code>R(Euler): x=45°, y=45°, z=45°, order=xyz</code> 表达的就是先绕x轴转45度,然后绕y转45度,最后绕z转45°可以得到结果。</p><p><code>R(Axis-angle): axis=(0, 0, 1), angle=45°</code> 表达的就是绕(0, 0, 1)这个轴旋转45度</p><p>几何表达很直观,但是计算多个角度的组合是十分常见的场景,用这种方式计算十分麻烦,你可以尝试用几何知识计算一下一个物体绕 <code>axis=(0,0,1),angle=45°</code> 和 <code>axis(1,0,0),angle=45°</code> 旋转后结果如何使用轴角的方式表示。</p><h3 id="数学表达"><a href="#数学表达" class="headerlink" title="数学表达"></a>数学表达</h3><p>数学表达不像几何表达那么直观,但是这种表达方式在计算多个角度组合时十分方便</p><p>比如上面提到的<code>axis=(0,0,1),angle=45°</code>对应的矩阵<code>M(R1)</code>长这样</p><p>$$\begin{bmatrix}0.9996242168594817 & -0.027412133592044294 & 0 \\ 0.027412133592044294 & 0.9996242168594817 & 0 \\ 0 & 0 & 1 \end{bmatrix}$$</p><p><code>axis(1,0,0),angle=45°</code>对应的矩阵<code>M(R2)</code>长这样</p><p>$$\begin{bmatrix}1 &0&0 \\ 0&0.9996242168594817&-0.027412133592044294 \\ 0&0.027412133592044294&0.9996242168594817 \end{bmatrix}$$</p><p>然后要计算这两个旋转的组合直接通过矩阵乘法即可 <code>M(R2) x M(R1)</code>(矩阵从右到左应用,先进行的旋转在右侧)。</p><p>四元数有点像矩阵,应用以及组合都是通过四元数乘法,左乘右乘的性质和矩阵一样,具体会在下文介绍。</p><h3 id="实际应用"><a href="#实际应用" class="headerlink" title="实际应用"></a>实际应用</h3><p>接下来看看这两种表达方式在实际的应用中是怎么分工的。<br>假设我们需要实现一个三维旋转控件,用户可以通过拖拽来对模型进行旋转,类似下图显示的blender里的旋转控件,拖红色圈使物体绕X轴旋转,绿色Y轴,蓝色Z轴(注意这里的轴都是全局坐标系)。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="blender-rotation.png" alt="blender的旋转组件" title> </div> <div class="image-caption">blender的旋转组件</div> </figure><p>这里的流程本质就是用户输入 <strong>几何表达</strong>(轴角旋转),然后在程序内部将其转换为 <strong>数学表达</strong>,并将其与之前的旋转合并 <code>R(输入旋转) x R(当前旋转)</code>,最后转化为变换矩阵来渲染模型。</p><p>另外在blender右侧会显示当前模型的旋转信息,这里就是将 <strong>数学表达</strong> 转换为 <strong>几何表达</strong> 显示给用户了。(这里直接显示四元数可能是有什么特殊目的,因为Axis Angle和四元数之间是可以直接转换的)</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="blender-rotation-panel.png" alt="blender右侧栏的旋转" title> </div> <div class="image-caption">blender右侧栏的旋转</div> </figure><p>可以说 <strong>几何表达</strong> 是更接近 <strong>交互</strong> 的方式,而 <strong>数学表达</strong> 则是更偏向 <strong>计算</strong> 的方式。</p><h2 id="欧拉角与万向节死锁"><a href="#欧拉角与万向节死锁" class="headerlink" title="欧拉角与万向节死锁"></a>欧拉角与万向节死锁</h2><p>提到欧拉角大家一定会提起的是万向节死锁,然后再提到四元数。(已经成为固定思维了)<br>关于四元数下面一节再提,先谈谈万向节死锁的问题,这个问题的原因是什么?会带来什么影响?</p><h3 id="万向节死锁的原因"><a href="#万向节死锁的原因" class="headerlink" title="万向节死锁的原因"></a>万向节死锁的原因</h3><p>解释万向节死锁时很多人喜欢带上这种图</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="gimballock.gif" alt title> </div> <div class="image-caption"></div> </figure><p>我一直认为这种图很难理解,主要是因为很难把欧拉角与其对应,我更建议看一下<a href="https://krasjet.github.io/quaternion/bonus_gimbal_lock.pdf" target="_blank" rel="noopener">这篇基于公式的解释</a>。</p><p>一句话解释就是,假设欧拉角以XYZ的顺序旋转,当Y的旋转使旋转后的Z与旋转前的X重合时,那么就产生了万向节死锁,此时XZ轴本质是对全局坐标系下同一个轴的旋转。</p><h3 id="万向节死锁的影响"><a href="#万向节死锁的影响" class="headerlink" title="万向节死锁的影响"></a>万向节死锁的影响</h3><p>那么丢失一个自由度会带来什么实际问题呢?<br>这个鲜有文章提及,我这里就提一点我遇到的吧。<br>当用户在输入<code>R(Euler): x=0°, y=90°, z=45°, order=xyz</code>这么一个欧拉角后,在程序内部转换为 <strong>数学表达</strong> 后再转回欧拉角就会遇到问题了。很明显这是存在万向节死锁的一个欧拉角,所有 <code>x + z == 45°</code>的旋转都是相等的,在转回来时是无法还原原来的角度的。</p><h2 id="为什么需要四元数?"><a href="#为什么需要四元数?" class="headerlink" title="为什么需要四元数?"></a>为什么需要四元数?</h2><p>我一直不太理解四元数为什么总会和欧拉角一起被提及,首先他们是不同类型的表达,不应被相提并论。我认为正确的说法应是欧拉角存在万向节死锁的问题,需要使用<strong>轴角旋转</strong>来解决,而四元数则是轴角旋转的数学表达方式。</p><h3 id="四元数基础"><a href="#四元数基础" class="headerlink" title="四元数基础"></a>四元数基础</h3><p>三言两语也解释的不是很清楚,建议有空看下<a href="https://krasjet.github.io/quaternion/quaternion.pdf" target="_blank" rel="noopener">这篇文章</a>。</p><h3 id="与矩阵对比"><a href="#与矩阵对比" class="headerlink" title="与矩阵对比"></a>与矩阵对比</h3><p>轴角旋转能转换为矩阵和四元数,那么如何选择使用哪种方式呢?</p><p>单纯的旋转计算上四元数会有一定的性能优势,但是如果涉及其他变换(偏移、缩放等)还是矩阵会快一点,看实际的使用场景选择吧。</p><p>另外四元数有个独特的特性就是插值计算很方便,目前旋转的插值实现基本都会使用四元数进行插值,所以如果是用于旋转动画的数据建议直接使用四元数,否则还有转换的成本。</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://krasjet.github.io/quaternion/quaternion.pdf" target="_blank" rel="noopener">https://krasjet.github.io/quaternion/quaternion.pdf</a> (非常推荐)</li></ul>]]></content>
<summary type="html">
<p>前段时间看了一些网上关于三维旋转的文章和内容,发现很多人都会从欧拉角讲到四元数,洋洋洒洒讲了很多概念,看完后还是很难建立起清晰的认知以及如何正确使用。所以我就打算来写这么一篇文章,从程序开发的角度(其他领域可能对旋转的认识有所不同)把常见的4种表达方式(<strong>欧拉
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="欧拉角" scheme="https://frezc.github.io/tags/%E6%AC%A7%E6%8B%89%E8%A7%92/"/>
<category term="旋转矩阵" scheme="https://frezc.github.io/tags/%E6%97%8B%E8%BD%AC%E7%9F%A9%E9%98%B5/"/>
<category term="轴角旋转" scheme="https://frezc.github.io/tags/%E8%BD%B4%E8%A7%92%E6%97%8B%E8%BD%AC/"/>
<category term="四元数" scheme="https://frezc.github.io/tags/%E5%9B%9B%E5%85%83%E6%95%B0/"/>
<category term="三维旋转" scheme="https://frezc.github.io/tags/%E4%B8%89%E7%BB%B4%E6%97%8B%E8%BD%AC/"/>
</entry>
<entry>
<title>LeetCode #1383 Maximum Performance of a Team</title>
<link href="https://frezc.github.io/2021/06/06/maximum-performance-of-a-team/"/>
<id>https://frezc.github.io/2021/06/06/maximum-performance-of-a-team/</id>
<published>2021-06-06T11:02:46.000Z</published>
<updated>2021-06-09T17:15:15.691Z</updated>
<content type="html"><![CDATA[<p>在昨天的leetcode challenge上碰到这道题,写完后感觉是道很不错的题目,就想按自己的理解写一下解题思路。另外官方的<a href="https://leetcode.com/problems/maximum-performance-of-a-team/solution/" target="_blank" rel="noopener">Solution</a>写得已经很好了,建议英语不错的小伙伴看看。</p><h2 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h2><p>给长度为 <code>n</code> 的两个数组 <code>speed</code> 和 <code>efficiency</code> ,两个数组里的第 <code>i</code> 个数字分别代表第 <code>i</code> 位工程师的速度和效率,现在要重中挑选最多 <code>k</code> 名工程师,要求有最大<strong>产出</strong>。</p><p><strong>产出</strong>的定义为 <code>所有工程师的速度和 * 所有工程师里的最低效率</code>。</p><p>限制:<code>1 <= k <= n <= 10^5</code></p><blockquote><p>Input: n = 6, speed = [2,10,3,1,5,8], efficiency = [5,4,3,9,7,2], k = 2 \<br>Output: 60 \<br>Explanation: 这里选择第2位和第5位工程师,所以结果为 <code>(10 + 5) * min(4, 7) = 60</code></p></blockquote><h2 id="暴力解法?"><a href="#暴力解法?" class="headerlink" title="暴力解法?"></a>暴力解法?</h2><p>先考虑下暴力解法,我们需要挑选 <code>1 ~ k</code> 名工程师,然后计算每种情况下的工程师产出和,单纯选 <code>k</code> 名工程师的情况下就有 <code>C(n, k)</code> 种组合方式,按题目给的数据范围来说必定会超时。</p><h2 id="解法思路"><a href="#解法思路" class="headerlink" title="解法思路"></a>解法思路</h2><p>因为计算效率是取的是选中的工程师中最低的效率,那么假设在上面的例子里选3人的情况下,很明显效率<strong>最多</strong>就是<code>5</code>,也就是说在效率为<code>5</code>的情况下,只能选择第 <code>1, 4, 5</code> 位工程师,提供的产出也就是 <code>(2 + 1 + 5) * 5 = 40</code>,这明显不是最终结果(因为选2个人都比这个高)。</p><p>那么我们可以尝试放宽效率的要求,现在只要求<code>4</code>,就多了第 <code>2</code> 位工程师可选,在这种情况下我们就需要从<code>4</code>人中挑选出<code>3</code>个速度最快的即可,因为此时我们可以将效率固定为<code>4</code>(更高效率的情况在上面已经计算过了)。得到的结果就是 <code>(10 + 5 + 2) * 4 = 68</code>。</p><p>接下来我们可以继续计算更低效率的情况,然后找出最大产出即可。</p><p>等等,好像漏了什么?上面的算法我们是从效率 <code>5</code> 往更低去找,那么有没有可能在效率更高时找到最大值呢?</p><p>题目中也提到了k代表了 <strong>最多</strong> 选择的人数,当然有可能在选更少人的时候得到更多产出(可能多了拖后腿的呢)。所以我们在迭代效率时需要从<strong>最大的一个</strong>开始,只是此时不会选满<code>k</code>人。</p><p>总体来说算法流程就是,根据效率列表从大到小迭代,每次选出可选工程师里速度最快的<code>k</code>人,计算产出并保存最大值。</p><h2 id="找到速度最快的k人"><a href="#找到速度最快的k人" class="headerlink" title="找到速度最快的k人"></a>找到速度最快的<code>k</code>人</h2><p>上面算法提到每次迭代要找到速度最快的<code>k</code>人,这要怎么实现呢?</p><p>我的第一想法就是维护一个倒序的数组,然后每次将新增的<strong>速度</strong>插入到合适位置(插入排序算法),然后取前<code>k</code>个数字即可。写得很快,啪,超时了。分析了一下,每次插入的时间复杂度是<code>O(n)</code>,加上要迭代长度为<code>n</code>的效率列表,那不就是<code>O(n^2)</code>了。看来出题者想要我们用更快的方法。</p><p>要么优化前面的部分,比如低效率能不能根据高效率计算出来(动态规划)?要么就是这节讲的,有没有方法可以在更少的时间内找到最大的多个数字?</p><h2 id="贪心算法"><a href="#贪心算法" class="headerlink" title="贪心算法"></a>贪心算法</h2><p>因为前面那个插入算法想的太快了,有点先入为主,使得我认为必须每次都找到最大的几个元素。后来证明我是错的,因为这里只需要知道最大<code>k</code>个<strong>速度和</strong>,完全可以使用<strong>贪心算法</strong>来累加运算。</p><p>在上面的例子里<code>效率 >= 5</code>时没有选择,只需要将所有速度加起来就可以得到<strong>最大速度和</strong>,这时候需要将<strong>最大速度和</strong>和<strong>其中每个速度</strong>保存下来,在之后的计算中我们只需要在新加入工程师的速度<strong>大于</strong>其中的最小速度时,将它们的差值加到<strong>最大速度和</strong>上,然后删除最小速度,将新的速度保存下来。</p><p>说到这里你会发现虽然保存的速度变少了,但是按原来的插入算法还是需要<code>O(k)</code>的时间复杂度,那么有没有更快的方法呢?</p><h2 id="小顶堆"><a href="#小顶堆" class="headerlink" title="小顶堆"></a>小顶堆</h2><p>我们可以回过头来看看贪心算法带来了什么好处,其实就是我第一句所提到的,原来的方法是需要维护一个有序数组的,但是贪心不需要,它只需要每次能找到组成速度中的<strong>最小值</strong>即可。这不禁让人联想到某种排序算法,它也是需要在每次迭代找到数组中最小/最大的元素,没错,那就是选择排序…………的某个变种 —— 堆排序。</p><p>我们知道堆排序的时间复杂度是<code>O(nlogn)</code>,很明显堆排序单次找到最大/最小值的时间复杂度就是<code>O(logn)</code>,目的和时间都符合我们的需求,何不直接拿来用呢?</p><p>因为我们这里的目的是找到最小值,所以会采用小顶堆的方式,而且堆都是从头开始构建,所以也不需要实现从现成数组构建的逻辑,只需要实现单次插入(上浮)和拿走顶部元素(下沉)的逻辑即可。这个是很基础的问题了,可以自行查阅。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这道题比较考验对算法的理解程度,有一定难度的,不过不失为一道不错的题,所以就想写下来分享了。<a href="https://github.com/Frezc/leetcode-solutions/blob/master/leetcode-challenge/2021-06/Maximum-Performance-of-a-Team.ts" target="_blank" rel="noopener">我的AC代码</a></p>]]></content>
<summary type="html">
<p>在昨天的leetcode challenge上碰到这道题,写完后感觉是道很不错的题目,就想按自己的理解写一下解题思路。另外官方的<a href="https://leetcode.com/problems/maximum-performance-of-a-team/solu
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="algorithm" scheme="https://frezc.github.io/tags/algorithm/"/>
</entry>
<entry>
<title>讲讲如何实现自动转换ts的import和type-only import</title>
<link href="https://frezc.github.io/2021/01/24/fix-to-ts-type-only-import/"/>
<id>https://frezc.github.io/2021/01/24/fix-to-ts-type-only-import/</id>
<published>2021-01-24T14:32:16.000Z</published>
<updated>2021-02-06T14:37:53.310Z</updated>
<content type="html"><![CDATA[<p>又有半年没写文章了,趁着最近不那么忙先来水一篇。恰巧最近给typescript-eslint实现的<a href="https://github.com/typescript-eslint/typescript-eslint/pull/2751" target="_blank" rel="noopener">支持decorator metadata的PR</a>合掉了,那就拿这个写写吧。标题因为不知道怎么取,就拿最终实现的目标写上去了。</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>先说说做这件事的背景吧。我们公司一小伙苦于项目里的循环依赖,写了个脚本来检测,但是检测归检测并没有给出好的解决方案。我看了下如果要从根本上解决(比如抽离公共依赖之类的方法)解决那肯定少不了一番重构,可是老代码谁也不敢轻举妄动啊。</p><p>刚好上半年把整个组的typescript升级到3.8以上了,我就想3.8新增的<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export" target="_blank" rel="noopener">Type-Only Imports</a>或许能解决一部分问题。纵观循环依赖的代码,大概有一半以上确实只依赖了类型,比如下面这种很常见的情况</p><pre><code class="ts">// Container.tsimport Element from './Element';export default class Container { elements: Element[];}// Element.tsimport Container from './Container';export default class Element { context: Container;}</code></pre><p>虽然有从代码上优化的解决方法(比如抽离接口等方法,本文就不说明了),最简单的方法就是改成<code>import type</code>,另外明显后者是可以用工具自动完成的。</p><h2 id="consistent-type-imports规则"><a href="#consistent-type-imports规则" class="headerlink" title="consistent-type-imports规则"></a>consistent-type-imports规则</h2><p>因为这个优化明显可以通过单文件的代码分析来完成,现成的工具里第一个想到做这个的就是eslint了,于是找了找发现果然有现成的规则[<a href="https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-imports.md" target="_blank" rel="noopener">consistent-type-imports</a>]。这个规则可以通过配置将文件里只有类型引用的import变量改为import type。</p><p>但是试了下这个规则后发现有个问题,会导致项目里的decorator拿不到metadata,一些依赖类型信息实现的功能就会失效。看了下原因是因为当tsconfig里<code>emitDecoratorMetadata === true</code>时,ts会将带有decorator的类里的成员变量类型、方法参数和返回类型输出到js里。比如下面将成员变量类型输出的情况,会在js里引用该类型,而当前规则无法判断这个情况,将原代码改为<code>import type</code>,这时实际输出的js里就拿不到这个<code>Type</code>了。</p><pre><code class="ts">// input ts with emitDecoratorMetadata = trueimport Type from 'type';class A { @deco a?: Type;}// output js ignore some helper functionimport Type from 'type';class A {}__decorate([ deco, __metadata("design:type", typeof (_a = typeof Type !== "undefined" && Type) === "function" ? _a : Object)], A.prototype, "a", void 0);</code></pre><h2 id="什么时候会输出decorator-metadata"><a href="#什么时候会输出decorator-metadata" class="headerlink" title="什么时候会输出decorator metadata"></a>什么时候会输出decorator metadata</h2><p>为了修复这个问题首先要看看ts在什么时候会输出<strong>decorator metadata</strong>。<br>首先能添加到metadata的类型一定只能是个存在对应类的直接类型,任何加工过的类型和type alias都会给你个Object,下面列了一些不能输出或者输出异常的情况。</p><pre><code class="ts">type Type = T1;class A { // 下面decorator全都省略... a: T1 | T2; // 直接输出Object b: Pick<T1, 'key'>; // 同上 c: Type; // 会将 Type 输出,但是type alias会被抹去,所以这里并不能引用到T1 d: React['Component']; // 直接输出Object,使用 React.Component 可以正常输出 e: (a: string) => string; // 直接输出Object}</code></pre><p>decorator通常会出现在4个地方:1. 类的定义上;2. 类的成员变量上;3. 类的方法上;4. 类的方法的参数上;这里以这种方式分开讨论。</p><h3 id="类的定义上"><a href="#类的定义上" class="headerlink" title="类的定义上"></a>类的定义上</h3><p>当类的定义上存在decorator时,构造函数的所有参数类型都会输出metadata。</p><pre><code class="ts">@decoclass A { // Type会被输出到metadata constructor(t: Type) {}}</code></pre><h3 id="类的成员变量上"><a href="#类的成员变量上" class="headerlink" title="类的成员变量上"></a>类的成员变量上</h3><p>当类的成员变量上存在decorator时,该变量的类型会输出metadata</p><pre><code class="ts">class A { @deco field: Type; // Type会被输出到metadata}</code></pre><h3 id="类的方法上"><a href="#类的方法上" class="headerlink" title="类的方法上"></a>类的方法上</h3><p>当类的方法上存在decorator时,该方法<strong>显式声明</strong>的参数类型和返回类型会输出到metadata。另外要注意的是这里指的方法不包括赋值给成员变量的函数。</p><pre><code class="ts">class A { @deco foo(input: Type1): Type2 {} // Type1, Type2会被输出到metadata @deco foo = (input: Type1): Type2 => {}; // 这样子只会被当作普通的成员变量处理}</code></pre><p>这里有一种特例就是 <strong>getter</strong> 和 <strong>setter</strong>,对于<strong>相同名称</strong>的accessor,只要给其中一个添加decorator就会输出相关类型的metadata,这个类型<strong>以setter参数类型优先</strong>。比如下面虽然只给getter加了decorator,却只使用了setter的参数类型Type2作为metadata。</p><pre><code class="ts">// sourceclass A { @deco get foo(): Type1 {} set foo(v: Type2) {}}// output metadata__decorate([ deco, __metadata("design:type", typeof (_b = typeof Type2 !== "undefined" && Type2) === "function" ? _b : Object), __metadata("design:paramtypes", [typeof (_c = typeof Type2 !== "undefined" && Type2) === "function" ? _c : Object])], A.prototype, "foo", null);</code></pre><p>这里 <strong>getter</strong> 和 <strong>setter</strong> 的metadata会按照一定规则合并,一般标识符类型和非复杂的计算值都是支持的。</p><pre><code class="ts">class A { // 相同的普通key、字符串、数字字面量和变量可以合并 get a() {} set ['a'](v: Type) {} get [1]() {} set [1](v: Type) {} // const key = 'k'; get [key]() {} set [key](v: Type) {} // 其他类型和表达式不可以合并 get [true]() {} set [true](v: Type) {} get ['a' + 'b']() {} set ['a' + 'b'](v: Type) {}}</code></pre><h3 id="类的方法的参数上"><a href="#类的方法的参数上" class="headerlink" title="类的方法的参数上"></a>类的方法的参数上</h3><p>当类的方法的参数上存在decorator时结果和类方法上有decorator差不多,参数类型和返回类型都会添加到metadata。比如下面两种情况结果是一样的</p><pre><code class="ts">class A { @deco foo(input: Type1): Type2 {} foo(@deco input: Type1): Type2 {}}</code></pre><p>同样 <strong>setter</strong> 上的参数decorator也是个特例,结果是啥也不会输出。这时不仅metadata不会输出,连decorator的功能也不会生效。<a href="https://github.com/microsoft/TypeScript/issues/41354" target="_blank" rel="noopener">这大概是个TS的BUG</a>。所以下面这种情况需要特殊判断排除掉</p><pre><code class="ts">class A { set foo(@deco input: Type1): Type2 {}}</code></pre><h2 id="如何给typescript-eslint修复"><a href="#如何给typescript-eslint修复" class="headerlink" title="如何给typescript-eslint修复"></a>如何给typescript-eslint修复</h2><p>整理完规则后就是看看如何修复了,具体的代码比较多就不详细说明了,就讲讲涉及的一些模块吧。这里可以直接从<a href="https://github.com/typescript-eslint/typescript-eslint/blob/3cb3aade2864bab15ed1ff8d7cd32766aa57152f/packages/eslint-plugin/src/rules/consistent-type-imports.ts#L1" target="_blank" rel="noopener">consistent-type-imports</a>这个规则的代码入手。</p><p>在代码里可以看到创建规则是通过给<code>createRule</code> 传入几个用来描述规则的参数和承载主要逻辑的 <code>create</code> 函数来实现,<code>create</code>函数返回的是ast visitor,在这里可以实现当前规则所需要的visitor,最后在<code>Program:exit</code> 通过<code>context.report</code> 报告错误信息和修复函数。</p><p>一开始我的想法就是直接在规则代码里通过AST分析判断类型是否会被decorator metadata引用到,拿到最终变量的引用是否全是类型和当前的import类型来判断错误以及修复。后来经过维护者提醒需要将部分判断逻辑实现在scope-manager里,这样对所有规则都是有用的,我也觉得很有道理。</p><p>scope manager是eslint就有的一个概念,它会根据AST生成作用域以及内部的所有变量和引用,eslint-typescript则是重写以支持了TS的语法特性,比如可以通过变量上的<code>references</code><a href="https://github.com/typescript-eslint/typescript-eslint/blob/3cb3aade2864bab15ed1ff8d7cd32766aa57152f/packages/eslint-plugin/src/rules/consistent-type-imports.ts#L145-L149" target="_blank" rel="noopener">拿到所有的引用</a>然后通过<code>Reference</code>上的<code>isValueReference</code>和<code>isTypeReference</code>判断引用类型,在[consistent-type-import]里就可以通过这两点在访问<code>ImportDeclaration</code>时就能判断是否是正确的import类型。原先的BUG也主要就是scope manager在生成引用类型时没有考虑decorator metadata的影响,那么如何修复就很明显了,在scope manager里生成变量引用时将decorator metadata考虑进来。</p><p>scope manager这块也是写各种ast visitor,每次进入新的scope时要 <code>scopeManager.nestForScope(node)</code> 一下,定义了变量时 <code>currentScope.defineIdentifier(...)</code>,引用了变量时 <code>currentScope.referenceValue</code> 或 <code>currentScope.referenceType</code>。因为这个需求只和类有关,同时为了方便隔离嵌套类的情况,就独立了一个<a href="https://github.com/typescript-eslint/typescript-eslint/blob/3cb3aade28/packages/scope-manager/src/referencer/ClassVisitor.ts#L1" target="_blank" rel="noopener"><code>ClassVisitor</code></a>,专门处理class scope的生成,不过好像因为独立的这个visitor没有漏了一些ast node的处理后面还导致了一些bug。。。</p><p>总之虽然目前还有<a href="https://github.com/typescript-eslint/typescript-eslint/pull/2751#discussion_r559297950" target="_blank" rel="noopener">一个小问题</a>,但不妨碍使用,可以升到<a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v4.14.2" target="_blank" rel="noopener">4.14.2</a>愉快使用了。</p>]]></content>
<summary type="html">
<p>又有半年没写文章了,趁着最近不那么忙先来水一篇。恰巧最近给typescript-eslint实现的<a href="https://github.com/typescript-eslint/typescript-eslint/pull/2751" target="_blan
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="typescript" scheme="https://frezc.github.io/tags/typescript/"/>
<category term="eslint" scheme="https://frezc.github.io/tags/eslint/"/>
</entry>
<entry>
<title>一文带你搞懂Javascript里的原型和继承</title>
<link href="https://frezc.github.io/2020/08/08/js-prototype/"/>
<id>https://frezc.github.io/2020/08/08/js-prototype/</id>
<published>2020-08-08T07:20:17.000Z</published>
<updated>2020-08-08T14:31:25.010Z</updated>
<content type="html"><![CDATA[<p>最近两周面试了将近30个人,其中绝大部分都是有3年以上的前端工作经验,但是很多人对js的原型和继承的概念都搞不懂,更不用说更深层次的问题了。这也是我写这篇文章的原因吧,同时也是有必要自己做一个知识总结。</p><h1 id="Javascript里的类"><a href="#Javascript里的类" class="headerlink" title="Javascript里的类"></a>Javascript里的类</h1><p>ES6为我们带来了定义类的keyword <code>class</code>,但这只不过是给程序员少些几行代码的语法糖而已,原型还是必须要掌握的知识。</p><p>类从理论上来说就是 <strong>数据 + 方法</strong> 的集合,那么可以很简单地写出下面的代码</p><pre><code class="js">function Person(name) { return { name: name, say() { console.log(`Hello, I'm ${this.name}`) } }}let person = Person('jolyne');</code></pre><p>写完后就会发现这种“类”很难实现继承,这时就可以借助<strong>原型链</strong>来实现。</p><h1 id="原型链"><a href="#原型链" class="headerlink" title="原型链"></a>原型链</h1><p>原型链特性主要有两点:</p><p>一是可以通过对象上的<strong>原型</strong>不断往上查找属性,数组、字符串等对象携带的方法就是通过这种方式实现的。</p><pre><code class="js">let a = {};// 操作对象的原型也可以通过Object.getPrototypeOf和Object.setPrototypeOfa.__proto__ = { __proto__: { __proto__: { value: 1 } } }// 这里会一直往上查找__proto__a.value === 1;</code></pre><p>还有一点就是 <code>new</code> 这个keyword会创建 <code>__proto__</code>。</p><pre><code class="js">function A() {}let a = new A();// __proto__会引用构造函数的prototypea.__proto__ === A.prototype;// A.prototype === Object.prototypea.__proto__.__proto__ === Object.prototype;</code></pre><p>顺带一提<code>instanceof</code>就是通过比较原型来判断的</p><pre><code class="js">[] instanceof Object// ==>[].__proto__.__proto__ === Object.prototype</code></pre><p>另外还有一个比较有意思的点</p><pre><code class="js">function foo() {}// 函数的__proto__指向了一个内部实现foo.__proto__ // => native code// 不过依旧是继承自Objectfoo.__proto__.__proto__ === Object.prototype</code></pre><h1 id="原型的继承"><a href="#原型的继承" class="headerlink" title="原型的继承"></a>原型的继承</h1><p>通过原型链的特性就能得到实现继承的基本思路了</p><pre><code class="js">// basefunction A() {}A.prototype.foo = function() {}// childfunction B() {}// B.prototype = { __proto__: A.prototype };B.prototype = Object.create(A.prototype);// B.prototype因为会被修改,所以需要是一个新的对象B.prototype.bar = function() {}let b = new B();b.foo === A.prototype.foo;</code></pre><p>这样就实现了基于原型的继承,通常我们会将<strong>方法</strong>挂在原型上,因为它的定义是同个类下所有对象共享的而且<em>通常</em>不受外部影响。那么对于每个对象上特有的数据要怎么办呢?</p><p>最简单的方式就是在子类的构造函数里调用父类</p><pre><code class="js">// basefunction A(value) { this.a = value;}// childfunction B(value) { A.call(this, value); this.b = value;}</code></pre><h1 id="静态变量继承"><a href="#静态变量继承" class="headerlink" title="静态变量继承"></a>静态变量继承</h1><p>静态变量同样可以通过原型链的方式继承,因为静态变量是绑定在构造函数上的,只需将构造函数加入到原型链中就能查找</p><pre><code class="js">function A() {}A.staticField = function() {}function B() {}B.__proto__ = A;B.staticField === A.staticField;</code></pre><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>基本原理就是这么些了,看完这篇你也就能看懂Typescript和Babel如何对类做转换了,有些细节我没讲到,比如容错检查、constructor设置。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf" target="_blank" rel="noopener">https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new" target="_blank" rel="noopener">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new</a></li></ul>]]></content>
<summary type="html">
<p>最近两周面试了将近30个人,其中绝大部分都是有3年以上的前端工作经验,但是很多人对js的原型和继承的概念都搞不懂,更不用说更深层次的问题了。这也是我写这篇文章的原因吧,同时也是有必要自己做一个知识总结。</p>
<h1 id="Javascript里的类"><a href=
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="javascript" scheme="https://frezc.github.io/tags/javascript/"/>
</entry>
<entry>
<title>升级typescript以及如何跨组升级</title>
<link href="https://frezc.github.io/2020/08/02/upgrade-typescript/"/>
<id>https://frezc.github.io/2020/08/02/upgrade-typescript/</id>
<published>2020-08-02T06:09:44.000Z</published>
<updated>2021-02-06T14:31:05.530Z</updated>
<content type="html"><![CDATA[<p>升级TS,听起来应该是件很简单的事,如果你的项目相对独立那确实稍微花点时间就做完了。但通常一个大型项目会将各个模块分给每个小的业务组,还有负责基础库的基架组,这时候想要单独升级业务模块的TS版本,就要考虑对上游和下游的影响了。</p><p>麻烦的问题就来了,TS并<strong>不遵守</strong> <code>Semantic Version</code>,它除了bugfix以外的版本更新几乎都会带来<code>Break Changes</code>。这里的<code>Break Changes</code>可能是两个方面的:</p><ol><li>低版本TS代码在高版本TS下编译会报错,也就是说升级业务模块时要考虑底层依赖的影响。</li><li>高版本TS编译出来的<code>d.ts</code>类型定义在低版本TS下编译会报错,这时就得考虑怎么样升级TS后模块的编译生成的文件给上层应用使用时不会报错。</li></ol><p>这篇文章会对上面两个问题给出我的解决方案,另外由于这篇文章是我去年年底写的,所以只考虑到<code>TS3.0~3.7</code>遇到的问题。</p><p>另外本文只提到了二方包情况,对于社区维护三方包通常会跟随TS版本升级和做兼容的,出现问题先升一下版本。</p><h1 id="低版本TS代码在高版本TS下的错误"><a href="#低版本TS代码在高版本TS下的错误" class="headerlink" title="低版本TS代码在高版本TS下的错误"></a>低版本TS代码在高版本TS下的错误</h1><p>这个问题很好解决,只要让代码在两个版本的TS下同时编译通过即可。当然这要求你去修复所有报错的依赖库,我建议有权限的话就自己做,因为这里的错误不会太多相比跨组协同的成本不值一提。</p><p>下面我总结了一些我遇到的问题以及解决方法。</p><h2 id="可能会遇到的问题"><a href="#可能会遇到的问题" class="headerlink" title="可能会遇到的问题"></a>可能会遇到的问题</h2><h3 id="error-TS2300-Duplicate-identifier-‘IteratorResult’"><a href="#error-TS2300-Duplicate-identifier-‘IteratorResult’" class="headerlink" title="error TS2300: Duplicate identifier ‘IteratorResult’."></a>error TS2300: Duplicate identifier ‘IteratorResult’.</h3><p>升级<code>@types/node</code>即可。</p><h3 id="范型不再默认为-类型,改为了unknown"><a href="#范型不再默认为-类型,改为了unknown" class="headerlink" title="范型不再默认为{}类型,改为了unknown"></a>范型不再默认为<code>{}</code>类型,改为了<code>unknown</code></h3><p>在<strong>3.5</strong>以下的版本中范型变量的类型默认为<code>{}</code>,在<strong>3.5</strong>之后你必须显式地指定范型上界<code>T extends {}</code>或者通过<strong>type guard</strong>才能使参数为对象类型。<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/#breaking-changes" target="_blank" rel="noopener">see detail</a></p><pre><code class="ts">// errorfunction foo<T>(a: T) { return a.toString();}// passfunction foo<T extends {}>(a: T) { return a.toString();}// pass: use type guardfunction isObject(data: any): data is object { return typeof data === 'object' && data != null;}function foo<T>(a: T) { if (isObject(a)) { return a.toString(); }}</code></pre><h3 id="Property-‘wheelDelta’-does-not-exist-on-type-‘WheelEvent’"><a href="#Property-‘wheelDelta’-does-not-exist-on-type-‘WheelEvent’" class="headerlink" title="Property ‘wheelDelta’ does not exist on type ‘WheelEvent’."></a>Property ‘wheelDelta’ does not exist on type ‘WheelEvent’.</h3><p><a href="https://stackoverflow.com/questions/54258968/how-to-fix-property-wheeldelta-does-not-exist-on-type-wheelevent-while-u" target="_blank" rel="noopener">使用deltaY</a></p><h3 id="无法推断的类型从any变为unknown"><a href="#无法推断的类型从any变为unknown" class="headerlink" title="无法推断的类型从any变为unknown"></a>无法推断的类型从any变为unknown</h3><p>之前在<code>new Set()</code>如果没有指定范型,则会默认给<code>any</code>,现在改为了<code>unknown</code>。在自定义的范型函数中也很容易出现这个问题,原来infer不出来的类型会给个<code>any</code>,现在都改为了<code>unknown</code>。</p><pre><code class="ts">// set is Set<unknown> nowconst set = new Set();// errorset.add(1);const set = new Set<number>();// passset.add(1);</code></pre><h3 id="TS2744-Type-parameter-defaults-can-only-reference-previously-declared-type-parameters"><a href="#TS2744-Type-parameter-defaults-can-only-reference-previously-declared-type-parameters" class="headerlink" title="TS2744: Type parameter defaults can only reference previously declared type parameters."></a>TS2744: Type parameter defaults can only reference previously declared type parameters.</h3><p>在ts3.3之后在范型的默认值里就只能引用该项之前的类型。</p><pre><code class="ts">// errorclass A<T = D, D = string> {}// workclass A<D = string, T = D> {}</code></pre><h3 id="Property-‘children’-does-not-exist-on-type-‘xxx’"><a href="#Property-‘children’-does-not-exist-on-type-‘xxx’" class="headerlink" title="Property ‘children’ does not exist on type ‘xxx’"></a>Property ‘children’ does not exist on type ‘xxx’</h3><p>出现这个错误很可能这个React组件使用里函数的方式来声明(如下)</p><pre><code class="ts">function MyCom(props: { href: string }) { return <a {...props} />;}</code></pre><p>这样子声明的组件是不会给Props自动加上<code>{ children?: ReactNode }</code>的。在旧版本的ts里对children的检查不会很严格所以不会报错,但这个问题在新版本里就会显现。</p><p>解决方法:</p><ol><li>使用<code>React.FC</code>或<code>React.SFC</code>来指定类型</li><li>手动加上<code>children</code>类型。</li></ol><h3 id="在3-3之后类继承时方法会应用逆变逻辑"><a href="#在3-3之后类继承时方法会应用逆变逻辑" class="headerlink" title="在3.3之后类继承时方法会应用逆变逻辑"></a>在3.3之后类继承时方法会应用<strong>逆变</strong>逻辑</h3><p>比如下面的代码在3.3以上时会报错</p><pre><code class="ts">interface A { a: string;}interface B extends A { b: number;}class CA { static a: (a: A) => void;}class CB extends CA { // 由于函数参数是逆变的这里会导致报错 static a: (a: B) => void;}</code></pre><p>一般来说只要类型没写错是不会遇到什么问题,但是我们项目中使用了React组件继承的写法,加上<code>contextType</code>时就会暴露问题了。它的类型<code>React.Context</code>是个逆变的类型,但是<code>context</code>又是协变的,这样就很矛盾了。所以不建议继承组件的<code>Context</code>,当然组件继承和<code>Context</code>继承都没啥意义。要写还是得hack一下类型。</p><pre><code class="ts">// class B extends A {}// a = new A()// b = new B()class A extends React.Component<any> { // contextType交给react使用的,不会被我们用到,所以这里指定类型为any static contextType: React.Context<any> = React.createContext(a); context: A;}class B extends A { static contextType = React.createContext(b); context: B;}</code></pre><h1 id="高版本TS编译出来的d-ts类型定义在低版本TS下的报错"><a href="#高版本TS编译出来的d-ts类型定义在低版本TS下的报错" class="headerlink" title="高版本TS编译出来的d.ts类型定义在低版本TS下的报错"></a>高版本TS编译出来的<code>d.ts</code>类型定义在低版本TS下的报错</h1><p>这部分错误不太多,就一个个来解决</p><h2 id="一些错误"><a href="#一些错误" class="headerlink" title="一些错误"></a>一些错误</h2><h3 id="An-accessor-cannot-be-declared-in-an-ambient-context-ts-1086"><a href="#An-accessor-cannot-be-declared-in-an-ambient-context-ts-1086" class="headerlink" title="An accessor cannot be declared in an ambient context.ts(1086)"></a>An accessor cannot be declared in an ambient context.ts(1086)</h3><p><a href="mailto:ts@3.7" target="_blank" rel="noopener">ts@3.7</a>会在<code>.d.ts</code>里生成 <code>getter</code> 和 <code>setter</code>,ts@<3.6会不能识别。</p><p><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#class-field-mitigations" target="_blank" rel="noopener">release note</a></p><h3 id="Cannot-find-name-‘readonly’"><a href="#Cannot-find-name-‘readonly’" class="headerlink" title="Cannot find name ‘readonly’"></a>Cannot find name ‘readonly’</h3><p>3.4后会默认将函数参数类型的Readonly转为readonly关键字</p><p><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#improvements-for-readonlyarray-and-readonly-tuples" target="_blank" rel="noopener">release note</a></p><h3 id="TS2315-Type-‘Generator’-is-not-generic"><a href="#TS2315-Type-‘Generator’-is-not-generic" class="headerlink" title="TS2315: Type ‘Generator’ is not generic"></a>TS2315: Type ‘Generator’ is not generic</h3><p><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-6.html#stricter-generators" target="_blank" rel="noopener">3.6修改了Generator类型</a>,导致编译generator函数生成的类型在旧版本TS里会报错。</p><h2 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h2><p>为了避免引用方大规模更新ts,需要考虑兼容方案。上面第二个问题可以通过<a href="https://github.com/microsoft/TypeScript/issues/30662#issuecomment-478461078" target="_blank" rel="noopener">指定类型来解决</a>,第三个问题也一样,不过考虑到3.6以下<code>Generator</code>不能指定元素类型,可以使用<code>IterableIterator</code>作为返回类型。</p><p>第一个问题的话目前没有兼容性方案。不过在<a href="mailto:ts@3.1" target="_blank" rel="noopener">ts@3.1</a>里支持了<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#version-selection-with-typesversions" target="_blank" rel="noopener">根据ts版本来选择文件的功能</a>,这样我们只要通过<a href="https://github.com/sandersn/downlevel-dts" target="_blank" rel="noopener">这个工具</a>,就能打包出一个兼容低版本的<code>.d.ts</code>文件。(PS: downlevel-dts只支持兼容到3.4版本,3.4下对很常用的readonly array&tuple是不支持的,我在自己的fork里加了这个支持,代码见<a href="https://github.com/sandersn/downlevel-dts/pull/28/files" target="_blank" rel="noopener">PR</a>)。</p><h1 id="跨组升级"><a href="#跨组升级" class="headerlink" title="跨组升级"></a>跨组升级</h1><p>虽然写了很多兼容方法,但我在实际执行时还是向着<strong>推动整个组的项目升级TS</strong>做的,第二个问题其实可以不用考虑,所以我也不能保证像 downlevel-dts 不会有什么其他问题。我其实也非常建议推动整个组升级,虽然跨组沟通成本很高,但是能将收益与风险向着好的方向走。</p><p>这里我简要讲一下如何跨组推动升级TS,这里很重要的一点是要换位思考,可以想想当别的组给你们提了一个需求的时候,你希望了解什么?这样我们就能自己归纳出需要做的准备了,因为每个公司流程不太一样,我就拿自己的做法来举例。</p><p>我首先思考的是就是经典的三大问题,这个需求是做啥?为什么要做?怎么做?第一个问题对于前端来说不需要解释,关于第二个问题的话,我建议将TS升级后<strong>更严格的类型检查</strong>作为主要优势来讲,这些可以在升级自己项目时或者尝试升级其他组项目时,把遇到的问题记录下来作为例子(问题数和升级成本是成反比的,如果实在没啥可以举例的,那么直接说升级成本很低就行了)。新语法新特性附上链接即可。怎么做的话我上面已经讲了很多了,你可以根据自己公司的情况来添加修改。</p><p>回到推进方来思考,我们面临的第一个问题是升级的范围和顺序。如果不能一次性全升完并且项目之间有依赖的话,由于之前提到的不兼容,就得考虑项目的升级顺序。我们公司的项目分为APP、插件和二方包3层,从左往右升就ok了。这里各个业务方主要开发的是插件,所以我的顺序是:</p><blockquote><p>修复二方包里TS在新版本里的问题 -> 通知所有业务方修复TS问题或者直接升级TS(因为APP里基本没有代码所以一般可以不考虑兼容) -> 升级APP的TS版本 -> 通知业务方升级TS -> 升级二方包的TS。</p></blockquote><p>这里插件间也会有依赖还比较复杂,如果要一级级升会比较耗时。恰逢底层框架的升级,我就先挑了依赖该框架的插件来一起升级,这里我用到了<code>yarn list --json</code>来生成依赖树并<a href="https://gist.github.com/Frezc/135542be7c79ccc05d941dc8a168297e" target="_blank" rel="noopener">找到所有的依赖</a>。</p><p>上面这些计划一定要写好<strong>时间表</strong>,并给deadline预留一个月左右的时间,这样能减少因为某些组延期导致后续也要推迟的概率。</p>]]></content>
<summary type="html">
<p>升级TS,听起来应该是件很简单的事,如果你的项目相对独立那确实稍微花点时间就做完了。但通常一个大型项目会将各个模块分给每个小的业务组,还有负责基础库的基架组,这时候想要单独升级业务模块的TS版本,就要考虑对上游和下游的影响了。</p>
<p>麻烦的问题就来了,TS并<str
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="typescript" scheme="https://frezc.github.io/tags/typescript/"/>
</entry>
<entry>
<title>UI性能优化的两大原则</title>
<link href="https://frezc.github.io/2019/11/01/principles-of-UI-performance-improvement/"/>
<id>https://frezc.github.io/2019/11/01/principles-of-UI-performance-improvement/</id>
<published>2019-11-01T07:44:08.000Z</published>
<updated>2020-08-01T06:26:18.335Z</updated>
<content type="html"><![CDATA[<h1 id="优化原则"><a href="#优化原则" class="headerlink" title="优化原则"></a>优化原则</h1><p>之前在内部转岗面试时被问了一个问题:能不能对react的性能优化做一个总结?当时我的回答是:用shouldComponentUpdate来减少更新、用key来标志列表内的元素、注意第三方库的优化(直接调用chart api来更新)等等。现在回想起来这些作为总结来说都过于细节,而且如果react后面改了没有这些api,如果是换了一套UI框架,那这些不就没用了?在做总结时还是应该有更高抽象的“原则”来进行引导,就像设计模式有这么多,但是用熟之后就会发现六大原则才是最重要的,应用原则来设计架构而不是死套模式。</p><p>就我的经验来说,对于react或者说所有UI程序的性能优化也有一个原则,那就是从<strong>空间</strong>和<strong>时间</strong>上进行优化。听起来是不是有点像算法里常提的时间复杂度和空间复杂度,不过呢和它们完全没关系。这里只是一个好记的说法,真正指的是<strong>更新范围</strong>和<strong>更新频率</strong>。</p><h1 id="更新范围的优化"><a href="#更新范围的优化" class="headerlink" title="更新范围的优化"></a>更新范围的优化</h1><p>还记得好几年前还没接触前端前学过一点怎么写windows窗口程序,里面提到的标记矩形来更新的机制让我映像深刻。简单来讲就是当你更新UI时,需要计算出包裹你更新部分的最小矩形,然后更新重绘这部分的内容。这就是很经典的<strong>更新范围</strong>的优化了。</p><p>现在前端的开发虽然不能控制这么多,但也能利用前端的抽象来实现最小范围的更新。这里我就拿React举例:</p><ol><li>有个巨无霸组件传入的props改变了,但是修改真正只影响其中很小的一个子组件,这时候想优化性能就需要给子组件写<code>shouldComponentUpdate</code>或者使用<code>PureComponent</code>(or <code>React.memo</code>),必要时还得使用<code>Immutable data</code>。</li><li>有个很长的列表,我想确保更新数据只会影响对应项时就要给每项一个唯一的<strong>key</strong>以及上面的一些操作。</li></ol><p>上面是React里大家耳熟能详的优化了,这里的核心就是减少<strong>组件范围</strong>的更新。当然标题里的<strong>范围</strong>肯定不止如此,我们看看另一种场景:</p><p>某个图表库需要将我们传入的数据根据配置计算处理成坐标系上的坐标,然后在绘制出响应的图形,并且在绘制好后允许我们将图表镜像、旋转和缩放,那么在进行这些操作时还需要重新计算坐标位置吗?出于性能考虑,当然是不需要了,这些操作只需要知道转化后的坐标点就行(甚至可以直接操作像素点)。这里也是一种更新范围的优化,如果需要什么命名的话可以称为<strong>逻辑范围</strong>吧。</p><p>就这点来说应该还有很多不同的优化方案,和设计模式一样,不用一个个去记,心中自有原则在,解决方法不就手到擒来。</p><h1 id="更新频率的优化"><a href="#更新频率的优化" class="headerlink" title="更新频率的优化"></a>更新频率的优化</h1><p>更新频率的优化可以分为亮点两点面分开来说。</p><ol><li><p>过于频繁的更新<br>在前端开发时,在<code>input</code>、<code>scroll</code>、<code>resize</code>等触发频繁的事件里更新UI是很常见的,但是如果直接监听这些事件来更新复杂UI肯定会有性能问题。通常的优化方案就是降低更新频率到用户能接受的水准,比如使用<code>throttle</code>或者<code>debounce</code>。</p></li><li><p>无用的更新<br>通常编码上的失误或者对边界值的考虑不足等原因会导致出现一些无用的更新,这种大多属于BUG,所以就不详谈了。</p></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这篇文章里讲的优化方案其实也不算新鲜了,我希望的是通过这些例子来理解UI性能优化的一个原则,通过原则我们才能更有条理地解决性能这一棘手问题。</p>]]></content>
<summary type="html">
<h1 id="优化原则"><a href="#优化原则" class="headerlink" title="优化原则"></a>优化原则</h1><p>之前在内部转岗面试时被问了一个问题:能不能对react的性能优化做一个总结?当时我的回答是:用shouldComponent
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="performance" scheme="https://frezc.github.io/tags/performance/"/>
</entry>
<entry>
<title>使用Prometheus来监控前端指标</title>
<link href="https://frezc.github.io/2019/08/31/frontend-metrics-to-prometheus/"/>
<id>https://frezc.github.io/2019/08/31/frontend-metrics-to-prometheus/</id>
<published>2019-08-31T13:16:13.000Z</published>
<updated>2019-09-01T12:44:57.558Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>网上关于前端监控的文章其实已经层出不穷,这篇稍微写些不一样的。近年来<a href="https://prometheus.io" target="_blank" rel="noopener">prometheus</a>打开的<strong>实时指标监控</strong>的新路子越来越被大家接受,都纷纷开始基于prometheus来搞自己的监控警报体系。我们公司也是大规模使用prometheus来搞指标监控,最近打算把前端的指标也接进来,我也趁这个机会写个简单的demo以及一些思考。</p><h1 id="使用prometheus的优势与弊端"><a href="#使用prometheus的优势与弊端" class="headerlink" title="使用prometheus的优势与弊端"></a>使用prometheus的优势与弊端</h1><p>在过去我们公司是使用<a href="https://www.elastic.co" target="_blank" rel="noopener">elastic search</a>来存储前端发送的事件以及处理后的数据,使用ES有个好处,可以直接满足我们两种需求:查原文和查聚合指标。本文要讨论的prometheus并不能取代ES,因为在前端监控的产品中查原文是必不可少的(比如重现某个用户的资源请求情况、查错误日志等),但是prometheus在查聚合指标时会比ES快很多,尤其是算<strong>百分位数</strong>时,相比ES的龟速,prometheus接近实时的速度可以说是最大的亮点。</p><p>当然考虑到prometheus会带来额外的开发成本,如果使用ES可以满足需求那只用ES就可以了。</p><h1 id="整体架构"><a href="#整体架构" class="headerlink" title="整体架构"></a>整体架构</h1><p>由于Prometheus是主动拉取指标的模式,所以我们需要一个类似<a href="https://github.com/prometheus/pushgateway" target="_blank" rel="noopener">pushgateway</a>的服务来接收前端发来的事件并将指标聚合暴露给prometheus。整体如下</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="frontendmetricinfra.png" alt="infra" title> </div> <div class="image-caption">infra</div> </figure><p><em>为了方便起见,下面实现的demo不会实现虚线部分。</em></p><h1 id="前端采集"><a href="#前端采集" class="headerlink" title="前端采集"></a>前端采集</h1><p>第一步当然就是实现前端采集指标的部分,我们通常关心下面几类指标:</p><h2 id="资源和页面性能指标"><a href="#资源和页面性能指标" class="headerlink" title="资源和页面性能指标"></a>资源和页面性能指标</h2><p>这类指标可以通过<code>performance.getEntries()</code>直接拿到,其中<a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming" target="_blank" rel="noopener"><code>PerformanceResourceTiming</code></a>和<a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming" target="_blank" rel="noopener"><code>PerformanceNavigationTiming</code></a>两个类分别包含普通资源和当前页面的性能指标,同时也可以得到<code>paint</code>和自定义的timing。</p><p>其中要注意的是<code>PerformanceResourceTiming</code>会同时包含所有ajax请求,如果你不想在这里采集到ajax指标的话可以通过<code>initiatorType !== 'fetch' && initiatorType !== 'xmlhttprequest' && initiatorType !== 'beacon'</code>过滤掉。</p><p>可以参考下图创建对应的指标</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="resource_loading.png" alt title> </div> <div class="image-caption"></div> </figure><h3 id="监听PerformanceEntry变化"><a href="#监听PerformanceEntry变化" class="headerlink" title="监听PerformanceEntry变化"></a>监听PerformanceEntry变化</h3><p>通常我们会在页面加载完时获取资源指标,但是如何取监听后续新的资源呢?</p><p>最直观的方法就是使用<a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver" target="_blank" rel="noopener"><code>PerformanceObserver</code></a>来监听,如果要考虑兼容性的话可以使用<a href="https://github.com/fastly/performance-observer-polyfill" target="_blank" rel="noopener"><code>polyfill</code></a>或者用<code>setInterval</code>定时获取entries并筛选出新的。</p><h2 id="AJAX指标"><a href="#AJAX指标" class="headerlink" title="AJAX指标"></a>AJAX指标</h2><p>如果你需要通过一些详细数据(比如header、request body等)作为labels的话,就需要额外监听ajax请求了,方法是覆盖默认的<code>fetch</code>以及<code>XMLHttpRequest</code>。</p><h2 id="错误指标"><a href="#错误指标" class="headerlink" title="错误指标"></a>错误指标</h2><p>建议走自定义指标或者ES的聚合</p><h2 id="自定义指标"><a href="#自定义指标" class="headerlink" title="自定义指标"></a>自定义指标</h2><p>约定好数据结构发送给聚合服务即可</p><h2 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h2><p>对于资源指标,我基于<code>PerformanceObserver</code>写了一个<a href="https://github.com/Frezc/metrics-collector" target="_blank" rel="noopener">采集器</a>。</p><p>对于自定义指标,由于我这里不会实现指标配置服务,所以使用了<a href="https://github.com/weaveworks/promjs" target="_blank" rel="noopener">promjs</a>这个库,但是这个库只有文本格式的输出,所以我又写了个<a href="https://github.com/Frezc/promjs-export" target="_blank" rel="noopener">promjs-export</a>来导出结构化的数据,并以此作为前后端通讯的结构。</p><p><a href="https://github.com/Frezc/metrics-collector/blob/master/example/index.ts" target="_blank" rel="noopener">使用示例</a>,代码如下</p><pre><code class="js">import { initCollector } from 'performance-resource-collector';import prom from 'promjs';import { exportMetrics } from 'promjs-export';const registry = prom();const unsupportedCounter = registry.create('counter', 'collector_unsupported_total', 'A counter for browser support for collector');const setupCounter = registry.create('counter', 'collector_setup_total', 'A counter for collector setup');function sendMetrics() { navigator.sendBeacon("http://localhost:8080/custom_metrics", JSON.stringify(exportMetrics(registry))); registry.reset();}initCollector({ // 将2s内新的资源batch调用 callback: (entries) => { navigator.sendBeacon("http://localhost:8080/resources", JSON.stringify(entries)) }, // 当前浏览器不支持PerformanceObserver onUnsupported: () => { unsupportedCounter.inc(); sendMetrics(); }, // 监听成功 onSetUp: () => { setupCounter.inc(); sendMetrics(); }, throttle: 2000,});</code></pre><h1 id="指标聚合服务"><a href="#指标聚合服务" class="headerlink" title="指标聚合服务"></a>指标聚合服务</h1><p>我们需要实现3个接口:接收<code>PerformanceEntry</code>的接口、接收自定义指标的接口和返回规定指标格式给prometheus的接口。</p><h2 id="资源聚合接口"><a href="#资源聚合接口" class="headerlink" title="资源聚合接口"></a>资源聚合接口</h2><p>资源聚合接口比较简单,因为是固定的指标,可以预先定好需要哪些指标、哪些维度,之后在代码里直接写死就好。</p><h2 id="自定义指标聚合接口"><a href="#自定义指标聚合接口" class="headerlink" title="自定义指标聚合接口"></a>自定义指标聚合接口</h2><p>自定义指标的聚合会比较复杂,主要是因为官方实现的client是不能修改label个数的,你可能需要自己实现一个能merge的版本。</p><p>或者采用预定义指标的形式,也就是需要一个额外的指标配置服务。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="metricdef.png" alt="指标配置" title> </div> <div class="image-caption">指标配置</div> </figure><h2 id="Example-1"><a href="#Example-1" class="headerlink" title="Example"></a>Example</h2><p>因为是个demo,于是就用rust来实现了,<a href="https://github.com/Frezc/frontend-metrics-aggregation" target="_blank" rel="noopener">项目地址</a>。</p><p>这里我只聚合了资源中的<code>duration</code>指标以及自定义的<code>Counter</code>类型的指标。</p><p>另外有点需要注意的是如果是用sendBeacon来发送事件的话,chrome目前是不允许我们修改<code>Content-Type</code>的,所以后端的web框架可能就不会自动解析json body了,需要手动parse一下。</p><h1 id="一个完整的demo"><a href="#一个完整的demo" class="headerlink" title="一个完整的demo"></a>一个完整的demo</h1><p>接下来让我们搭一个完整的demo来看看效果如何。</p><p>先把<a href="https://github.com/Frezc/frontend-metrics-aggregation" target="_blank" rel="noopener">frontend-metrics-aggregation</a>通过<code>cargo run</code>跑起来。</p><p>然后到<a href="https://github.com/Frezc/metrics-collector" target="_blank" rel="noopener">metrics-collector</a>里执行<code>npm run pkg</code>将<code>example</code>打包生成单个js。接着通过<a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo" target="_blank" rel="noopener">tampermonkey</a>创建一个脚本,把前面生成的js替换进去。修改<code>@match</code>为想监控的网站,比如<code>http*://*/*</code>来监控所有站点。</p><p>最后下载<a href="https://prometheus.io" target="_blank" rel="noopener">prometheus</a>,修改<code>prometheus.yml</code>,在最后一行的<code>targets</code>里添加一项<code>'localhost:8080'</code>,并执行<code>prometheus</code>即可。</p><p>随便访问些网站,打开<a href="http://localhost:9090/graph,查询`histogram_quantile" target="_blank" rel="noopener">http://localhost:9090/graph,查询`histogram_quantile</a>(0.95, sum(rate(resource_duration_milliseconds_bucket[1h])) by (le))`就能查看到数据了。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="queryp95.png" alt="查询duration的P95" title> </div> <div class="image-caption">查询duration的P95</div> </figure><h1 id="一些问题"><a href="#一些问题" class="headerlink" title="一些问题"></a>一些问题</h1><p>使用资源的<code>name</code>作为label可能会导致high cardinality问题,导致查询变得很慢,你可以看到上面例子里查询一个小时的P95已经需要5秒了,此时<code>name</code>数量大概是4500。所以建议不要采集一些易变的、带参数的资源,比如下面在B站采集的一些图片。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="highcardinalityname.png" alt title> </div> <div class="image-caption"></div> </figure>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>网上关于前端监控的文章其实已经层出不穷,这篇稍微写些不一样的。近年来<a href="https://prometheus.io" targ
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="前端监控" scheme="https://frezc.github.io/tags/%E5%89%8D%E7%AB%AF%E7%9B%91%E6%8E%A7/"/>
<category term="monitor" scheme="https://frezc.github.io/tags/monitor/"/>
<category term="prometheus" scheme="https://frezc.github.io/tags/prometheus/"/>
</entry>
<entry>
<title>一篇文章带你理解和使用Prometheus的指标</title>
<link href="https://frezc.github.io/2019/08/03/prometheus-metrics/"/>
<id>https://frezc.github.io/2019/08/03/prometheus-metrics/</id>
<published>2019-08-03T02:57:04.000Z</published>
<updated>2019-08-03T16:56:30.336Z</updated>
<content type="html"><![CDATA[<h1 id="Why-prometheus"><a href="#Why-prometheus" class="headerlink" title="Why prometheus"></a>Why prometheus</h1><p>实时指标是监控中最重要的一环,用来尝出实时监控图表以及触发警报。传统的指标收集方式是通过采集的日志流处理写入时序数据库(比如Druid),这样做的问题一是流程比较长,任何一环出问题就会导致监控不可用,而警报对稳定性的要求是非常高得;二是存储成本和查询速度,比如我将每次的请求耗时保存下来,那么每次请求都是一条记录,一天下来可能就有千万级别的记录,查询时对机器的性能会有一定的要求。</p><p>Prometheus直接采集服务暴露的指标,少了中间流程能大大减少出问题的概率。另外通过提供client在采集端做了预聚合,虽然这样损失了精确度,但大大减少数据量以及提升查询速度。</p><p>Prometheus所做的预聚合使得其和传统的时序数据库查询完全不同,所以认识和理解指标是使用Prometheus的第一步。下面我会解释各种指标类型以及如何去使用。</p><h1 id="Metric-types"><a href="#Metric-types" class="headerlink" title="Metric types"></a>Metric types</h1><p>下面我主要会以Prometheus的自监控指标举例,你可以自己<a href="https://prometheus.io/docs/introduction/first_steps/" target="_blank" rel="noopener">安装</a>一个来尝试。</p><h2 id="Counter"><a href="#Counter" class="headerlink" title="Counter"></a>Counter</h2><p>Counter可以简单理解为计数器,是个比较简单但又常用的类型。适用于生成请求次数、错误次数等指标。</p><p>比如prometheus暴露的http请求次数指标如下(打开<code>/metrics</code>查看)</p><pre><code class="shell"># HELP prometheus_http_requests_total Counter of HTTP requests.# TYPE prometheus_http_requests_total counterprometheus_http_requests_total{code="200",handler="/api/v1/label/:name/values"} 7prometheus_http_requests_total{code="200",handler="/api/v1/query"} 19prometheus_http_requests_total{code="200",handler="/api/v1/query_range"} 27prometheus_http_requests_total{code="200",handler="/graph"} 11prometheus_http_requests_total{code="200",handler="/metrics"} 8929prometheus_http_requests_total{code="200",handler="/static/*filepath"} 52prometheus_http_requests_total{code="302",handler="/"} 1prometheus_http_requests_total{code="400",handler="/api/v1/query_range"} 6</code></pre><p>指标由<code>指标名</code>、花括号里的<code>labels</code>以及<code>指标值</code>组成,<code>labels</code>可以理解为<code>维度</code>,上面的数据写成表格就很好理解了。</p><table><thead><tr><th>code</th><th>handler</th><th>prometheus_http_requests_total</th></tr></thead><tbody><tr><td>200</td><td>/api/v1/label/:name/values</td><td>7</td></tr><tr><td>200</td><td>/api/v1/query_range</td><td>27</td></tr><tr><td>200</td><td>/graph</td><td>11</td></tr><tr><td>200</td><td>/metrics</td><td>8929</td></tr><tr><td>200</td><td>/static/*filepath</td><td>52</td></tr><tr><td>302</td><td>/</td><td>1</td></tr><tr><td>400</td><td>/api/v1/query_range</td><td>6</td></tr></tbody></table><p>我们可以通过<code>/graph</code>页面来查询这个指标,直接查询如下的instant query就可以看到过去一段时间每个<code>采集点</code>(默认15s)采集到的counter值。</p><pre><code class="promql">prometheus_http_requests_total</code></pre><h3 id="用例"><a href="#用例" class="headerlink" title="用例"></a>用例</h3><p>Counter类型直接查询没有什么意义,通常我们要看的是过去<strong>一段时间</strong>的统计数据,可以这么查</p><pre><code class="promql">prometheus_http_requests_total - prometheus_http_requests_total offset 1h</code></pre><p>打开<code>Console</code> Tab,就能看到过去一个小时内prometheus http请求的次数统计了。</p><h4 id="时间粒度"><a href="#时间粒度" class="headerlink" title="时间粒度"></a>时间粒度</h4><p>如果熟悉传统的时序数据库查询的话,会更习惯指定<strong>时间粒度</strong>来聚合一个时间窗口内的数据。prometheus的counter指标里倒不需要做这样聚合,但为了使结果显示成每个时间粒度一个点而不是像现在的十几秒一个点,可以使用<code>step</code>来指定精度。</p><p>假设我们要查过去一小时内每5分钟有多少次请求可以这么写。这里使用更常用的<code>increase</code>函数和<code>range query</code></p><pre><code class="promql">increase(prometheus_http_requests_total[5m])</code></pre><p>然后将<code>Res</code>里填上<code>300</code>就能得到想要的结果了。</p><h4 id="按维度聚合"><a href="#按维度聚合" class="headerlink" title="按维度聚合"></a>按维度聚合</h4><p>除了时间粒度外还有个很常用的参数叫<code>group by</code>,用来按某些维度来分组聚合指标。在prometheus里做起来差不多,不过稍有限制。我们先看看怎么计算所有http请求的和。</p><pre><code class="promql">sum(increase(prometheus_http_requests_total[5m]))</code></pre><p>这里将上节的promql加了一层<code>sum</code>函数,可以看到结果只有一条线了,这条线上的每一个点就是当前时间上所有<code>code</code>和<code>handler</code>对应指标的总和。</p><p>那么如何看每个<code>code</code>对应的请求数呢?我们可以通过<code>by</code>来实现,写法很像sql。</p><pre><code class="promql">sum(increase(prometheus_http_requests_total[5m])) by (code)</code></pre><p>要注意的是这里<code>by</code>只能跟在聚合函数后面,见<a href="https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators" target="_blank" rel="noopener">文档</a>。</p><h2 id="Gauge"><a href="#Gauge" class="headerlink" title="Gauge"></a>Gauge</h2><p><code>Gauge</code>是一个用来记录实时值的指标,常用于表示CPU使用率、内存使用率、线程数等指标。</p><p>比如prometheus暴露的go协程数指标</p><pre><code class="shell"># HELP go_goroutines Number of goroutines that currently exist.# TYPE go_goroutines gaugego_goroutines 40</code></pre><h3 id="用例-1"><a href="#用例-1" class="headerlink" title="用例"></a>用例</h3><p><code>gauge</code>类型指标最常见的就是用来标识服务是否存活的<code>up</code>指标了,这个指标在大多的exporter上都会有,属于一个可以建通用警报规则的指标。</p><p>大多数<code>gauge</code>指标用法差不多,我就拿<code>go_goroutines</code>来举例。查询<code>go_goroutines</code>我们可以看到一些自动生成的labels,主要关注<code>instance</code>,这个代表了我们的prometheus实例。假设我们有prometheus实例并且想查询所有实例的平均协程数。</p><pre><code class="promql">avg(go_goroutines)</code></pre><p><em>熟悉Prometheus后你会发现 avg 基本只对 gauge 指标有意义</em></p><h4 id="按时间聚合"><a href="#按时间聚合" class="headerlink" title="按时间聚合"></a>按时间聚合</h4><p>上面的结果看起来是满足要求,但是多查几次就发现好像结果有点不太一样?(如果你看不出来可以将<code>Res</code>调大)</p><p>原因是prometheus的精度问题,毕竟只有15s一个点,那么我要算某个时间点的平均值肯定是无法拿到准确值的。为了减小数据偏差,可以先横向将一段时间内的数据聚合,再纵向算平均值。比如先以1分组为粒度算出每分钟的平均数,再算出所有实例的平均数。</p><pre><code class="promql">avg(avg_over_time(go_goroutines[1m]))</code></pre><h4 id="预测磁盘空间"><a href="#预测磁盘空间" class="headerlink" title="预测磁盘空间"></a>预测磁盘空间</h4><p>这是个很有意思的使用场景,可以通过磁盘使用空间来预测。因为磁盘使用空间不像内存使用量和线程数那样变化频繁,具有一定的<strong>局部单调性</strong>,所以可以通过<strong>线性回归</strong>预测,prometheus提供了<a href="https://prometheus.io/docs/prometheus/latest/querying/functions/#predict_linear" target="_blank" rel="noopener">predict_linear</a>来帮助计算。</p><pre><code class="shell"># 利用过去30分钟的数据预测1小时后的数据predict_linear(disk_used_bytes[30m], 3600)</code></pre><h2 id="Histogram"><a href="#Histogram" class="headerlink" title="Histogram"></a>Histogram</h2><p>顾名思义该指标生成的是<a href="https://antv.alipay.com/zh-cn/vis/chart/histogram.html" target="_blank" rel="noopener">直方图</a>数据。</p><p>prometheus暴露的请求耗时指标如下</p><pre><code class="shell"># HELP prometheus_http_request_duration_seconds Histogram of latencies for HTTP requests.# TYPE prometheus_http_request_duration_seconds histogramprometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="0.1"} 60prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="0.2"} 63prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="0.4"} 64prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="1"} 65prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="3"} 65prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="8"} 65prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="20"} 65prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="60"} 65prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="120"} 65prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="+Inf"} 65prometheus_http_request_duration_seconds_sum{handler="/api/v1/query_range"} 2.5237541999999995prometheus_http_request_duration_seconds_count{handler="/api/v1/query_range"} 65# 其他labels下的都删掉了</code></pre><p><code>prometheus_http_request_duration_seconds</code>使用了<code>[0.1,0.2,0.4,1,3,8,20,60,120,+Inf]</code>这几个分桶来采样数据。</p><h3 id="用例-2"><a href="#用例-2" class="headerlink" title="用例"></a>用例</h3><p>通常我们会使用该数据来计算<a href="https://cloud.tencent.com/developer/article/1122377" target="_blank" rel="noopener">百分位数</a>。</p><p>比如我们可以通过以下的promql查询<code>prometheus_http_request_duration_seconds</code>在过去1小时内的P95(95%的请求耗时都小于等于这个值)。</p><pre><code class="promql">histogram_quantile(0.95, rate(prometheus_http_request_duration_seconds_bucket[1h]))</code></pre><p><em>如果出现了<code>NaN</code>数据代表数据量不够,无法计算。</em></p><p>如果有多个prometheus实例,同样可以聚合计算P95</p><pre><code class="promql"># 注意group by le,防止把分桶信息也聚合了histogram_quantile(0.95, sum(rate(prometheus_http_request_duration_seconds_bucket[1h])) by (le))</code></pre><h2 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h2><p><code>Summary</code>类型是在客户端直接聚合生成的百分位数。</p><p>比如Prometheus抓取间隔的百分位数指标</p><pre><code class="shell"># HELP prometheus_target_interval_length_seconds Actual intervals between scrapes.# TYPE prometheus_target_interval_length_seconds summaryprometheus_target_interval_length_seconds{interval="15s",quantile="0.01"} 14.9986389prometheus_target_interval_length_seconds{interval="15s",quantile="0.05"} 14.9991762prometheus_target_interval_length_seconds{interval="15s",quantile="0.5"} 15.0000115prometheus_target_interval_length_seconds{interval="15s",quantile="0.9"} 15.0006213prometheus_target_interval_length_seconds{interval="15s",quantile="0.99"} 15.0010902prometheus_target_interval_length_seconds_sum{interval="15s"} 178380.0282111002prometheus_target_interval_length_seconds_count{interval="15s"} 11892</code></pre><p>虽然<code>Histogram</code>也能计算百分位数但精度受分桶影响很大,分桶少的话会使百分位数计算很不准确,而分桶多的话会使数据量成倍增加。<code>Summary</code>则是依靠原始数据计算出的百分位数,是很准确的值。</p><p>但是平时一般不用<code>Summary</code>,因为它<strong>无法聚合</strong>。想象一下,prometheus抓取了一个集群下多台机器的百分位数,我们怎么根据这些数据得到整个集群的百分位数呢?如果是P0(最小值)和P100(最大值)是可以计算,分别计算所有机器P0的最小值以及P100的最大值就行,但是其他百分位数就束手无策了。</p><p>所以除非你真的需要精确的百分位数,否则不建议使用<code>Summary</code>。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://prometheus.io/docs/concepts/metric_types/" target="_blank" rel="noopener">https://prometheus.io/docs/concepts/metric_types/</a></li><li><a href="https://prometheus.io/docs/practices/histograms/" target="_blank" rel="noopener">https://prometheus.io/docs/practices/histograms/</a></li><li><a href="http://latencytipoftheday.blogspot.de/2014/06/latencytipoftheday-you-cant-average.html" target="_blank" rel="noopener">http://latencytipoftheday.blogspot.de/2014/06/latencytipoftheday-you-cant-average.html</a></li></ul>]]></content>
<summary type="html">
<h1 id="Why-prometheus"><a href="#Why-prometheus" class="headerlink" title="Why prometheus"></a>Why prometheus</h1><p>实时指标是监控中最重要的一环,用来尝出实时监
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="monitor" scheme="https://frezc.github.io/tags/monitor/"/>
<category term="prometheus" scheme="https://frezc.github.io/tags/prometheus/"/>
<category term="监控" scheme="https://frezc.github.io/tags/%E7%9B%91%E6%8E%A7/"/>
</entry>
<entry>
<title>leetcode #84 直方图内最大矩形解题思路</title>
<link href="https://frezc.github.io/2019/05/25/leetcode-largest-rectangle-in-histogram/"/>
<id>https://frezc.github.io/2019/05/25/leetcode-largest-rectangle-in-histogram/</id>
<published>2019-05-25T13:21:43.000Z</published>
<updated>2019-05-26T16:30:34.827Z</updated>
<content type="html"><![CDATA[<p>题目:<a href="https://leetcode.com/problems/largest-rectangle-in-histogram/" target="_blank" rel="noopener">https://leetcode.com/problems/largest-rectangle-in-histogram/</a></p><p>网上也有很多人写了解法了,但是我认为都没有把思路写清楚,需要花一些时间来理解。我写这篇文章主要也是记录一下我自己的思路,希望给人以启发。</p><p>这道题被打上了 <em>stack</em> 的标签,大概是因为栈的解法十分巧妙,但是解这道题栈不是重点,想清楚了我们也能用其他的解法。</p><h1 id="拆分子问题"><a href="#拆分子问题" class="headerlink" title="拆分子问题"></a>拆分子问题</h1><p>这道题一开始我的想法是用DP来做,想通过0~i的最大值来依次推算,可是想不出递推式。后来发现是我子问题就想错了。</p><p>这道题有点像木桶装水,我们可以发现一个区间内的最大矩形的高度一定是<strong>受限于这个区间内的最小高度</strong>。反过来想,对于每个高度的柱子也一定会有一个区间是以其作为最小高度的,那么我们可以得到需要的子问题,就是找出<strong>某个高度作为最小高度时所对应的最大区间</strong>,通过对每个高度的子问题求解就能推算出所有高度的最大区间,也就是该问题的解了。</p><p><em>下图中我把每个高度对应的最大区间标识了出来</em></p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="lowest-intervals.png" alt="每个最小高度对应的最大区间" title> </div> <div class="image-caption">每个最小高度对应的最大区间</div> </figure><h1 id="找到每个高度对应的最大区间"><a href="#找到每个高度对应的最大区间" class="headerlink" title="找到每个高度对应的最大区间"></a>找到每个高度对应的最大区间</h1><h2 id="遍历"><a href="#遍历" class="headerlink" title="遍历"></a>遍历</h2><p>最直观的方法就是从当前柱子开始向两侧遍历,找到第一个<strong>小于</strong>当前高度的柱子,分别就是两侧的边界了。因为如果这个区间包含了比当前高度还小的柱子的话,当前的高度就不是最小高度了。</p><p>当然这种方法会使时间复杂度为O(n²),对于这道题来说会超时。下面是优化这个子问题解法的方案。</p><h2 id="DP"><a href="#DP" class="headerlink" title="DP"></a>DP</h2><p>DP的解法比较易懂,这里拿<code>heights = [2,4,3,1]</code>来举例说明,下面用<code>i</code>来表示每项索引:</p><ol><li><code>i = 0</code>: 对于第一个高度的左边界设为<code>-1</code>。我们在数组里存下当前索引对应的边界索引,<code>dp = [-1]</code></li><li><code>i = 1</code>: 由于第一个高度<code>2</code>小于第二个高度<code>4</code>,我们就能直接得到,第二个高度的左边界<code>0</code>。<code>dp = [-1,0]</code></li><li><code>i = 2</code>: 当前高度<code>3</code>小于前一个高度<code>4</code>,所以我们继续拿前一个索引对应的左边界(<code>dp[1]</code>也就是<code>i = 0</code>)来判断,<code>3 > heights[0]</code>,所以当前高度的左边界就是<code>0</code>。<code>dp = [-1,0,0]</code></li><li><code>i = 3</code>: 同上,我们直接拿<code>dp[2]</code>也就是<code>i = 0</code>比较,发现<code>1</code>还是比较小。再取<code>dp[0] = -1</code>,该索引已经超出数组边界了,所以得到当前左边界 <code>dp[3] = -1</code>。</li></ol><p>在第4步中,我们通过DP构建的数组跳过了一次判断,在比较庞大的数据中能减少更多的判断,这也是为什么DP能将O(n²)优化至O(n)。</p><p>得到左侧边界数组后我们还需要构建右侧边界数组,方法同上只不过需要倒过来生成。</p><h2 id="Stack"><a href="#Stack" class="headerlink" title="Stack"></a>Stack</h2><p>还有一种通过栈的方式来快速得到左右边界的方法,这种方法需要将索引压入栈内,并使栈内的元素保持单调递增,这样就能保证栈内每个高度的前一个高度就是这个当前高度的左边界。同时如果要压入栈内的高度比栈顶高度小,那么这个高度就是栈顶高度的右边界,这样我们就将栈顶高度的左右边界算好了,将其抛弃即可,继续比较下一个栈顶高度。</p><p>同样拿上面的<code>[2,4,3,1]</code>举例</p><ol><li><code>i = 0</code>,栈为空,压入第一个索引<code>0</code>。<code>stack = [0]</code></li><li><code>i = 1</code>,<code>4 > 2</code>,压入第二个索引。<code>stack = [0,1]</code></li><li><code>i = 2</code>,<code>3 < 4</code>,这个时候我们可以得到栈顶索引<code>1</code>对应的左右边界<code>[0,2]</code>,可以直接算出面积。弹出<code>1</code>,<code>stack = [0]</code></li><li><code>i = 2</code>,继续比较栈顶元素<code>3 > 2</code>,压入<code>2</code>。<code>stack = [0,2]</code></li><li><code>i = 3</code>,<code>1 < 3</code>,同理栈顶索引<code>2</code>的左右边界为<code>[0,3]</code>,弹出继续得到栈顶<code>0</code>的左右边界<code>[-1,3]</code>。<code>stack = []</code></li><li>栈为空压入<code>3</code>。<code>stack = [3]</code></li><li>遍历完后将栈顶元素依次弹出,<code>3</code>的左右边界为<code>[-1,4]</code></li></ol><h1 id="其他解法"><a href="#其他解法" class="headerlink" title="其他解法"></a>其他解法</h1><p>有人也提出了<a href="https://leetcode.com/problems/largest-rectangle-in-histogram/discuss/28910/Simple-Divide-and-Conquer-AC-solution-without-Segment-Tree" target="_blank" rel="noopener">分治的解法</a>,相比O(n)的解法比较直白,更像是直接遍历的优化。</p><h1 id="85-Maximal-Rectangle"><a href="#85-Maximal-Rectangle" class="headerlink" title="#85 Maximal Rectangle"></a>#85 Maximal Rectangle</h1><p>由于85的解法是基于该题,所以顺便提一下。</p><p>这道题的关键点也是如何拆子问题,我们可以遍历所有rows,对于每一行将其作为底边,计算上面<code>'1'</code>组成柱子的最大矩形,这样就可以利用前一道题的解法来解决这个子问题了,最后对所有子问题的解算出最大值即可。</p><p>当然如果没有84直接做这道题应该会很难想到这种拆子问题的方法。。</p>]]></content>
<summary type="html">
<p>题目:<a href="https://leetcode.com/problems/largest-rectangle-in-histogram/" target="_blank" rel="noopener">https://leetcode.com/problems/l
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="algorithm" scheme="https://frezc.github.io/tags/algorithm/"/>
</entry>
<entry>
<title>React & React-Hooks & Typescript 快速入门 (draft)</title>
<link href="https://frezc.github.io/2019/05/16/typescript-react-hooks-quickstart/"/>
<id>https://frezc.github.io/2019/05/16/typescript-react-hooks-quickstart/</id>
<published>2019-05-15T16:24:43.000Z</published>
<updated>2019-08-02T13:14:31.500Z</updated>
<content type="html"><![CDATA[<p>9102年了, 学习React其实完全可以从react-hooks开始, 这也是我写这篇文章的初衷。</p><p>另外我也希望能用比较简洁的语言来解释一些相关的原理, 不过本文还是会以如何使用为主, 所以比较适合想了解个大概后快速上手去写的人。</p><p>本文主要内容是解释如何基于Typescript来写React组件, 需要你有javascript基础, 对html有一定的认识。</p><h1 id="准备环境"><a href="#准备环境" class="headerlink" title="准备环境"></a>准备环境</h1><h2 id="开发环境"><a href="#开发环境" class="headerlink" title="开发环境"></a>开发环境</h2><p>已经装过的可以跳过</p><ol><li>安装<a href="https://nodejs.org" target="_blank" rel="noopener">node</a> 8.10以上的版本, 最好就用最新版</li><li>安装<a href="https://www.yarnpkg.com" target="_blank" rel="noopener">yarn</a></li><li>修改yarn的registry, 执行下列命令即可<pre><code class="shell">yarn config set registry http://registry.npm.taobao.org/</code></pre></li><li>安装<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">vscode</a>, 这是对ts支持最好的editor了, 没必要用其他的</li></ol><h2 id="创建项目"><a href="#创建项目" class="headerlink" title="创建项目"></a>创建项目</h2><p><em>你可以使用已有的项目, 或者parcel, webpack等其他构建工具。</em></p><pre><code class="shell">$ mkdir react-quickstart && cd react-quickstart$ yarn create umi</code></pre><p>为了使项目结构简单一点,这里选择<code>app</code>,并选择使用typescript,最后勾选antd。</p><p>然后将<code>src/layouts</code> 和 <code>src/pages</code> 下的文件都删掉,然后在<code>src/pages</code>下创建<code>index.jsx</code>,加上以下内容。</p><pre><code class="tsx">import * as React from "react";export default function Index() { return <span>hello world</span>;};</code></pre><ul><li>yarn install</li><li>yarn start</li></ul><p>打开 <a href="http://localhost:8000/" target="_blank" rel="noopener">http://localhost:8000/</a>, 就能在屏幕上看到 hello world 了。</p><h1 id="React入门"><a href="#React入门" class="headerlink" title="React入门"></a>React入门</h1><h2 id="认识组件"><a href="#认识组件" class="headerlink" title="认识组件"></a>认识组件</h2><p>使用react你唯一需要做的事情就是写组件, react里的组件和其他UI框架差不多, 就是封装了一系列UI和行为的代码.</p><p>上面我们在<code>src/pages/index.jsx</code>里定义了一个页面, 这个页面本身就是一个组件. 在react里定义一个组件的方法如下</p><pre><code class="tsx">// FC is Function Componentfunction Index() { return <span>hello world</span>;};</code></pre><p>没错, 组件就是一个函数(让我们先忽视类的写法), 对于没有状态的组件我们可以将其当作一个纯函数. 比如下面我们用ts来申明组件的参数:</p><pre><code class="tsx">const Index: React.FC<{ name: string; }> = (props) => { return <span>hello {props.name}</span>;};</code></pre><p>这个组件有一个 <strong>参数</strong> 用户名(输入), 以及对应返回的<strong>Virtual DOM</strong>(输出).</p><h2 id="Inversion-of-Control"><a href="#Inversion-of-Control" class="headerlink" title="Inversion of Control"></a>Inversion of Control</h2><p>React是一个典型的IoC框架, 我们只要写好组件交付给React, 它能帮我们处理好UI(HTML DOM)的创建, 更新和销毁.</p><pre><code> 组件 ReactProperties -------> Virtual DOM --------> HTML DOM</code></pre><h2 id="JSX-TSX"><a href="#JSX-TSX" class="headerlink" title="JSX(TSX)"></a>JSX(TSX)</h2><p>上面例子里每个组件都返回了类似HTML的表达式, 这就是JSX.</p><p>我觉得<a href="https://reactjs.org/docs/introducing-jsx.html" target="_blank" rel="noopener">官方文档</a>讲得挺清楚了,你可以花几分钟看一下。另外<a href="https://reactjs.org/docs/jsx-in-depth.html" target="_blank" rel="noopener">这篇文档</a>讲得比较详细,建议简单看一遍,遇到了问题可以多查阅。</p><p>你只要理解下面两点就能完全掌握JSX:</p><ol><li>JSX本身就是<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Expressions_and_Operators#%E8%A1%A8%E8%BE%BE%E5%BC%8F" target="_blank" rel="noopener"><strong>JS表达式(Expression)</strong></a></li><li>JSX里可以在参数部分(包括children参数)用花括号(<code>{</code>和<code>}</code>)内嵌<strong>JS表达式</strong>。</li></ol><h3 id="JSX范式"><a href="#JSX范式" class="headerlink" title="JSX范式"></a>JSX范式</h3><p>下面列了一些比较常见的JSX写法</p><h4 id="条件渲染"><a href="#条件渲染" class="headerlink" title="条件渲染"></a>条件渲染</h4><ol><li><p>在组件内<br><code>`</code>jsx<br>function MyComponent(props) {<br>if (props.visible) {<br> return ‘Hi, I\’m visible.’;<br>}</p><p>return ‘I\’m invisible now.’;<br>}</p></li></ol><p><mycomponent visible> // Hi, I\’m visible.</mycomponent></p><p><mycomponent visible="{false}"> // I\’m invisible now.</mycomponent></p><p><mycomponent> // I\’m invisible now.</mycomponent></p><pre><code>2. 在JSX内,会用到花括号嵌入JS表达式来实现。由于JS中`if..else..`不是表达式,所以会使用逻辑运算符(`&&`或`||`)和三元表达式(`.. ? .. : ..`)```jsxfunction Greeting(props) { return ( <span style={{ color: 'red' }}> {props.lang === 'janpanese' ? 'こんにちは' : 'Hello'} {props.emphasis && '!'} </span> );}<Greeting /> // <span>Hello</span><Greeting lang="janpanese" /> // <span>こんにちは</span><Greeting lang="janpanese" emphasis /> // <span>こんにちは!</span></code></pre><h4 id="列表渲染"><a href="#列表渲染" class="headerlink" title="列表渲染"></a>列表渲染</h4><p>把一系列数据渲染一个列表或者表格也是相当常见的需求,在JSX里我们可以使用数组上的<code>map</code>方法来实现</p><pre><code class="jsx">function Ranking(props) { return ( <div> {props.value && props.value.map((name, index) => ( // 这里需要给列表每一项加上一个id <div key={index}>{index + 1}. {name}</div> ))} </div> )}<Ranking /> // <div></div><Ranking value={[]} /> // <div></div><Ranking value={['dio', 'kars', 'kirayoshikage', 'Diavolo']} />/* output:<div> <div>1. dio</div> <div>2. kars</div> <div>3. kirayoshikage</div> <div>4. Diavolo</div></div>*/</code></pre><p>这个例子有两个小技巧:</p><ol><li>利用<code>&&</code>来判空,兼容<code>value</code>未传入的情况(此时<code>value === undefined</code>)。因为这里没有定义参数类型,使用者可能会传入任何值,所以需要做一些简单处理。不过在使用typescript后就不存在这种问题了。</li><li><code>map</code>中传入的箭头函数其实是以下函数的缩写,<pre><code class="jsx">(name, index) => {return ( <div>{index + 1}. {name}</div>);}</code></pre></li></ol><h5 id="为什么渲染列表时需要传入key这个参数"><a href="#为什么渲染列表时需要传入key这个参数" class="headerlink" title="为什么渲染列表时需要传入key这个参数"></a>为什么渲染列表时需要传入key这个参数</h5><p>这是由于React的<a href="https://reactjs.org/docs/reconciliation.html#recursing-on-children" target="_blank" rel="noopener">diff算法</a>所决定的,我们需要给列表中每项一个在当前列表内的唯一ID。你可以参考<a href="https://reactjs.org/docs/lists-and-keys.html#keys" target="_blank" rel="noopener">这篇文章</a>来选择key。</p><h4 id="封装复杂的渲染逻辑"><a href="#封装复杂的渲染逻辑" class="headerlink" title="封装复杂的渲染逻辑"></a>封装复杂的渲染逻辑</h4><p>当你在一个组件内的JSX写得越来越复杂时,可以将部分逻辑抽离出来单独写一个组件,如果你认为这些逻辑没什么可复用性,再写一个组件要传很多参数比较麻烦,那么单独抽离成一个函数也可以。</p><pre><code class="jsx">function ComplexComponent() { // some state code... const displayType = ...; function renderChart() { // codes } function renderTable() { // codes } return ( <div className="some-layout-style"> <div> { /* some complex jsx */ } </div> {displayType === 'chart' ? renderChart() : renderTable()} </div> )}</code></pre><p>有时候也可以将组件里一些重复用到的JSX写到一个函数内</p><pre><code class="jsx">function TopOfTheYear(props) { function renderRank(value) { return value && value.map((name, index) => ( <div>{index + 1}. {name}</div> )); } return ( <div> <div>ANIMES:</div> {renderRank(props.animes)} <div>MOVIES:</div> {renderRank(props.movies)} </div> )}</code></pre><p>出现嵌套列表时也可以递归调用,比如下面递归渲染的树</p><pre><code class="jsx">function Tree(props) { function renderTreeNodes(nodes) { return nodes.map((node, i) => ( <div className="children"> {node.name} {nodes.children && ( <div className="children"> {renderTreeNodes(nodes.children)} </div> )} </div> )) } return ( <div className="tree-root"> {props.value && renderTreeNodes(props.value)} </div> );}</code></pre><h3 id="通常我们在刚写jsx时会有疑惑,为什么需要import-react?"><a href="#通常我们在刚写jsx时会有疑惑,为什么需要import-react?" class="headerlink" title="通常我们在刚写jsx时会有疑惑,为什么需要import react?"></a>通常我们在刚写jsx时会有疑惑,为什么需要import react?</h3><p>因为JSX被编译出来的JS表达式会使用<code>React</code>,比如下面的例子</p><pre><code class="jsx">// 编译前const virtualDom = ( <MyButton color="blue" shadowSize={2}> Click Me </MyButton>);// 编译后const virtualDom = React.createElement( MyButton, { color: 'blue', shadowSize: 2 }, 'Click Me');</code></pre><p>这里编译后不会自动帮我们加上import,所以需要我们手动加了。如果你不喜欢手动写import,可以通过配置打包工具来少些几行代码,比如<a href="https://stackoverflow.com/questions/35162343/webpack-automatically-require-a-file-in-all-files" target="_blank" rel="noopener">webpack的配置方法</a>。</p><p>这个其实在上面的<a href="https://reactjs.org/docs/jsx-in-depth.html#react-must-be-in-scope" target="_blank" rel="noopener">文档</a>里就有解答,遇到了问题多看看文档。</p><h2 id="状态"><a href="#状态" class="headerlink" title="状态"></a>状态</h2><p>上面我们写的都是<strong>纯函数组件</strong>,但是大部分情况组件都会有内部状态,下面让我们看看怎么给组件添加状态。</p><p><em>PS: 有状态的组件称为<strong>函数组件</strong></em></p><p>引入状态对应的API是<code>React.useState</code>,看下面的例子</p><pre><code class="jsx">import { useState } from 'react';function Counter() { // 这里声明了我们要使用一个初始值为 0 的状态 const [count, setCount] = useState(0); // count只有在第一次渲染时使用初始值,之后都是内部维持的状态 // setCount用来更新状态 return ( <div> <div>count: {count}</div> <button onClick={() => setCount(count + 1)} >+</button> </div> )}</code></pre><h3 id="复杂状态"><a href="#复杂状态" class="headerlink" title="复杂状态"></a>复杂状态</h3><p>有时候状态可能是个复杂的对象,和上面用法差不多,但要注意的是你只更新对象上的一个变量时,也需要生成一个<strong>新对象</strong>。</p><p>只是简单更新单层对象可以参考以下范式</p><pre><code class="javascript">const obj1 = { a: 1, b: 2 };// 解构const obj2 = { ...obj1, b: 3 }; // { a: 1, b: 3 }// Object.assign方法const obj3 = Object.assign({}, obj1, { a: 2 }); // { a: 2, b: 2 }</code></pre><p>例子</p><pre><code class="jsx">import { useState } from 'react';function Counter() { const [state, setState] = useState({ count: 0, other: 'a' }); return ( <div> <div>count: {state.count}</div> <button onClick={() => setState({ // 这里setState会覆盖掉之前的状态,所以要保留other的话,需要将新旧对象合并到一起 ...state, count: state.count + 1, })} >+</button> </div> )}</code></pre><h3 id="分离逻辑(useReducer)"><a href="#分离逻辑(useReducer)" class="headerlink" title="分离逻辑(useReducer)"></a>分离逻辑(useReducer)</h3><p>todo<br><a href="https://reactjs.org/docs/hooks-reference.html#usereducer" target="_blank" rel="noopener">https://reactjs.org/docs/hooks-reference.html#usereducer</a></p><h3 id="多个状态"><a href="#多个状态" class="headerlink" title="多个状态"></a>多个状态</h3><p><code>useState</code>可以在一个组件中创建任意数量的状态,但你不能在条件、循环和嵌套函数里调用。</p><pre><code class="javascript">import { useState } from 'react';function Counter(props) { // 你只能在函数的顶级调用 const [c, s] = useState(0); // error: 不能放到任何条件block内 if (props.visible) { const [c, s] = useState(0); } // error: 同上 const [c, s] = props.visible ? useState(0) : useState(99); function renderContent() { // error: 不能在嵌套函数中调用 const [c, s] = useState(0); } // ...}</code></pre><p>你不用死记这些,因为有eslint会帮你发现这些错误,你只要遇到了知道是什么原因就行。</p><h3 id="为什么要按固定的顺序调用?"><a href="#为什么要按固定的顺序调用?" class="headerlink" title="为什么要按固定的顺序调用?"></a>为什么要按固定的顺序调用?</h3><p>函数组件的状态是由React帮你维护的,每次调用<code>useState</code>,React就会找到对应的状态返回给你,这是很经典的依赖注入的思想。</p><p>下面的例子相信很容易理解</p><pre><code class="javascript">// react 内部的状态列表 internal_state = ['a', 'b', 'c']const [a] = useState('a'); // 返回 internal_state[0]const [b] = useState('b'); // 返回 internal_state[1]const [c] = useState('c'); // 返回 internal_state[2]// 如果某些原因导致顺序发生了变化,拿到的值就不正确了const [a] = useState('a'); // 返回 internal_state[0]const [c] = useState('c'); // 返回 internal_state[1]const [b] = useState('b'); // 返回 internal_state[2]</code></pre><h2 id="useEffect(组件生命周期)"><a href="#useEffect(组件生命周期)" class="headerlink" title="useEffect(组件生命周期)"></a>useEffect(组件生命周期)</h2><h3 id="React-hook"><a href="#React-hook" class="headerlink" title="React hook"></a>React hook</h3><p>上面提到的<code>useState</code>、<code>useReducer</code>以及接下来介绍的<code>useEffect</code>都是<code>React hook</code>,hook你可以理解为函数组件内可以使用的一系列工具,通常 <em>约定</em> 以<code>use</code>开头。</p><h3 id="如何监听生命周期"><a href="#如何监听生命周期" class="headerlink" title="如何监听生命周期"></a>如何监听生命周期</h3><p>如果你之前接触过React,那你可能知道一些很常用的<strong>生命周期方法</strong>,像<code>componentDidMount</code>(组件第一次渲染后)、<code>componentDidUpdate</code>(组件每次更新后)等等。</p><p>这些生命周期方法对于一个组件是非常重要的,在函数组件里我们可以使用<code>useEffect</code>来实现。</p><p>下面是个如何监听页面滚动的例子,我们需要在组件<strong>挂载到节点</strong>后添加事件监听,并在组件<strong>销毁时</strong>移除事件监听以免内存泄漏。</p><pre><code class="javascript">import { useEffect } from 'react';function MyComponent() { // 滚动事件的监听函数 function scrollHandler() {} // 基本等同于componentDidMount useEffect(() => { document.addEventListener('scroll', scrollHandler); // 这里返回的函数会在组件销毁前被调用 return () => { document.removeEventListener('scroll', scrollHandler); }; }, []); return <div>...</div>}</code></pre><h3 id="useEffect"><a href="#useEffect" class="headerlink" title="useEffect"></a>useEffect</h3><p><code>useEffect</code>会接收两个参数,第一个参数是一个函数A,其会在<strong>适当的时机</strong>被调用,该函数如果返回了一个函数B,函数B被称为<code>clean up</code>函数,B会在<strong>下一次A被调用前</strong>或者<strong>组件销毁前</strong>被调用。</p><p>第二个参数用来提示A的<strong>调用时机</strong>,通常有3种情况:</p><ol><li><p>传入<code>undefined</code>(等同不传):这种情况会在每次渲染时调用A,可以理解为在组件首次渲染或者更新时调用。</p><p> 这种情况使用得不多,通常用来手动跟踪某些值和节点的变化。</p></li><li><p>传入<code>[]</code>:这种情况会在组件首次渲染时调用。</p><p> 通常用于组件内发起一次性的请求。</p><pre><code class="js"> const [result, setResult] = useState(); const [loading, setLoading] = useState(true); useEffect(() => { // 这里你不能直接使用async function,因为它会返回的Promise会被误认为cleanup函数 async function fn() { setResult(await fetchSomething()); setLoading(false); } fn(); }, [])</code></pre></li><li><p>传入非空数组:在2的基础上,如果数组中任意值发生了变化,就会去调用。</p><p> 通常用来监听某些状态的变化并执行一些副作用函数,比如将状态保存到浏览器。</p><pre><code class="js"> const [value, setValue] = useState(() => localStorage.getItem('VALUE_ID')); useEffect(() => { // 每次value变化就会将其持久化到浏览器 localStorage.setItem('VALUE_ID', value); }, [value]);</code></pre></li></ol><h1 id="使用typescript来写React组件"><a href="#使用typescript来写React组件" class="headerlink" title="使用typescript来写React组件"></a>使用typescript来写React组件</h1><h2 id="为什么要用typescript"><a href="#为什么要用typescript" class="headerlink" title="为什么要用typescript?"></a>为什么要用typescript?</h2><p>没有类型辅助写大中型项目(尤其在多人协作方面)是件心智负担很重的事, typescript是目前使用最为广泛的 compile to javascript 语言. 当然你也可以使用 flow, reasonml, elm 等语言, 不过这些不是本文的目标。</p><p>如果只想用js, 那么只需要将文件后缀改为 jsx 并且去掉所有类型即可。</p><p>本节只会解释本文会用到的ts特性, 深入可以参考推荐阅读里的内容。</p><h2 id="你需要知道的Typescript"><a href="#你需要知道的Typescript" class="headerlink" title="你需要知道的Typescript"></a>你需要知道的Typescript</h2><p>ts里大部分时间不需要显式声明类型, 因为它能自动推断。</p><pre><code class="typescript">// 基础类型const a: number = 1;const b: string = 'abc';const c: boolean = true;const d: undefined = undefined;const e: null = null;// 函数类型function parseInt(input: string, radix?: number): number { /* implement code */ }// 箭头函数类型const parseInt: (input: string, radix?: number) => number = (input, radix) => { /* implement code */ };// Union Typelet union: number | string | undefined = 1;union = undefined; // okunion = 'ok'; // ok// 字面量类型const f: 'jotaro' | 'giorno' = 'giorno';const g: 1 | 2 = 3; // ts will throw error// 对象字面量类型const obj: { field: string } = { field: 'abc' };// 接口interface A { message?: string; // '?'将会给类型添加undefined, 这里相当于 string | undefined}const h: A = { message: 'now' };const i: A = {}; // message is undefinedconst j = { message: 'now' }; // 如果我们不加类型定义, 这里j会被推断为 { message: string }</code></pre><h2 id="Type-Compatibility"><a href="#Type-Compatibility" class="headerlink" title="Type Compatibility"></a>Type Compatibility</h2><p>ts里如果两个接口的每项参数的类型是匹配的那么, 这两个接口就是匹配的. 比如下面两个接口在ts里其实没有什么区别</p><pre><code class="typescript">interface Point { x: number; y: number;}interface Rectangle { x: number; y: number;}</code></pre><p>这个特性主要对于经常出现的对象字面量很有帮助</p><pre><code class="typescript">interface A { message?: string;}let obj = { message: 'now' }; // 这个对象的类型是 { message: string }// 虽然这个类型和A不是同一个类型,但是它们是兼容的, 所以这里可以将这个对象赋值给类型为 A 的变量 h.const h: A = obj;</code></pre><p>Union Type和其他字面量类型也是很符合直觉的</p><pre><code class="typescript">const a = 'a';const b: 'a' | 'b' = a; // okconst c: string | number = b; // okconst d: number = c; // errorif (typeof c === 'number') { const e: number = c; // ok, c is number type here}</code></pre><p>还有其他比如函数、枚举、泛型的类型匹配可以看<a href="https://www.typescriptlang.org/docs/handbook/type-compatibility.html" target="_blank" rel="noopener">官方文档</a>。因为这些比较少用到,所以碰到了再看文档就行。</p><h2 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h2><p>和其他语言差不多, 在本文里就不深入解释了</p><h2 id="import"><a href="#import" class="headerlink" title="import"></a>import</h2><p>通常你会看到在很多项目的tsx文件里会这么import react</p><pre><code class="typescript">import * as React from 'react';</code></pre><p>这种写法是因为react本身是没有<code>export default</code>,而且ts不像js会默认去兼容commonjs的写法,所以你需要<a href="https://www.typescriptlang.org/docs/handbook/modules.html#import" target="_blank" rel="noopener">import entire module</a>。如果你不喜欢这种写法,可以修改<code>tsconfig.json</code>来兼容。</p><h1 id="编写可维护的组件"><a href="#编写可维护的组件" class="headerlink" title="编写可维护的组件"></a>编写可维护的组件</h1><p>todo</p><h1 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h1><ul><li><a href="https://reactjs.org/docs/hello-world.html" target="_blank" rel="noopener">React官方文档</a></li><li><a href="https://www.typescriptlang.org/docs/handbook/basic-types.html" target="_blank" rel="noopener">Typescript handbook</a></li><li><a href="https://basarat.gitbooks.io/typescript/docs/getting-started.html" target="_blank" rel="noopener">Typescript Deep Dive</a></li></ul><h1 id="常用组件和库"><a href="#常用组件和库" class="headerlink" title="常用组件和库"></a>常用组件和库</h1><ul><li><a href="https://ant.design/" target="_blank" rel="noopener">ant design</a>: UI组件</li><li><a href="https://lodash.com/" target="_blank" rel="noopener">lodash</a>: 函数库</li><li><a href="https://momentjs.com/docs/" target="_blank" rel="noopener">moment</a>: 时间工具</li></ul>]]></content>
<summary type="html">
<p>9102年了, 学习React其实完全可以从react-hooks开始, 这也是我写这篇文章的初衷。</p>
<p>另外我也希望能用比较简洁的语言来解释一些相关的原理, 不过本文还是会以如何使用为主, 所以比较适合想了解个大概后快速上手去写的人。</p>
<p>本文主要内容
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="typescript" scheme="https://frezc.github.io/tags/typescript/"/>
<category term="react-hooks" scheme="https://frezc.github.io/tags/react-hooks/"/>
<category term="tutorial" scheme="https://frezc.github.io/tags/tutorial/"/>
</entry>
<entry>
<title>相对时间表达式 —— 解决相对时间序列化的问题</title>
<link href="https://frezc.github.io/2019/04/26/relative-time-expression/"/>
<id>https://frezc.github.io/2019/04/26/relative-time-expression/</id>
<published>2019-04-26T15:21:16.000Z</published>
<updated>2019-04-27T16:36:08.742Z</updated>
<content type="html"><![CDATA[<p>平时开发监控系统时免不了与时序数据库的查询打交道,在查时序数据库时 <em>时间范围</em> 是必不可少的条件,所以在查询的UI展示上通常会将时间范围作为一个独立的组件来让用户交互。</p><p>时间范围通常会展示为两种形式:相对时间和绝对时间。对于监控系统来说,日常观察指标、建立看板基本都是使用相对时间,因为使用绝对时间的话一是不能及时更新,二是容易引发慢查询。而绝对时间的使用场景一般是定位具体问题。</p><p>在我们的监控前端里主要使用相对时间的地方有两个,一是adhoc查询,另一个是看板。在这两处需求里都需要对相对时间序列化,前者用来分享查询链接,后者用来保存看板配置。下面就谈谈如何序列化相对时间。</p><h1 id="使用key来映射"><a href="#使用key来映射" class="headerlink" title="使用key来映射"></a>使用key来映射</h1><p>这是一开始监控里使用的方式,就是通过一些预定义的key(<code>yesterday</code>, <code>today</code>, <code>thisweek</code>等)来保存相对时间范围,前端在展示时需要额外写死的 <code>Label Map</code> 和 <code>Duration Map</code>。</p><pre><code class="javascript">const LabelMap = { yesterday: '昨天', today: '今天', thisweek: '这周', // and so on..};const DurationMap = { yesterday: () => [moment().subtract(1, 'day').startOf('day'), moment().subtract(1, 'day').endOf('day')], today: () => [moment().startOf('day'), moment().endOf('day')], thisweek: () => [moment().startOf('week'), moment().endOf('week')], // and so on..}</code></pre><p>这种方式很简单但不灵活,如果需要一个新的时间段就必须改这两个Map才行。而且如果用户有一些特殊的相对时间的话,这种方案就行不通了。</p><h1 id="使用结构化数据"><a href="#使用结构化数据" class="headerlink" title="使用结构化数据"></a>使用结构化数据</h1><p>为了灵活性考虑,我们可以使用对象来保存相对时间,这里我们需要先理解相对时间由什么组成。</p><h2 id="相对时间的抽象"><a href="#相对时间的抽象" class="headerlink" title="相对时间的抽象"></a>相对时间的抽象</h2><p>在项目里我们一般用的时间段都是由一个开始点和一个结束点构成,其中一个相对时间点是由一连串计算产生的,这里的计算我们可以分为两类:偏移和区间首尾。对应的moment方法为</p><pre><code class="javascript">// 偏移moment().add(1, 'hour');moment().subtract(1, 'day');// 区间首尾moment().startOf('hour');moment().endOf('day');</code></pre><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>对应的数据结构如下</p><pre><code class="typescript">type Unit = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y';interface Offset { type: 'Offset'; // 用来表示 add 或者 subtract,一般实际使用都是 subtract 所以可以省略 // op: '+' | '-'; number: number; unit: Unit;}interface Period { type: 'Period'; // 用来表示 startOf 或 endOf,实际使用时可以使用开始和结束点来区分,所以也可以省略 // op: 'start' | 'end'; unit: Unit;}type Calc = Offset | Period;interface TimeRange { start: Array<Calc>; end: Array<Calc>;}</code></pre><p>另外只要根据这个数据结构实现一个展示Label的函数和一个计算Duration的函数就行了。</p><p>结构化数据提供了很好的灵活性但暴露了几个缺点:</p><ol><li>展示Label的函数不好写,尤其是对于两步以上的计算就得写很多特殊判断,比如 <em>上周</em> 我们的数据长这样(对象写起来太长,用moment表示一下)<code>[moment().sutract(1, 'w').startOf('w'), moment().sutract(1, 'w').endOf('w')]</code>,反过来将该对象格式化就得写很多判断代码才行。</li><li>为了方便使用,肯定是需要快速筛选,无论这个列表放在前端还是后端都需要写一大堆代码(快速筛选如下)<br> <img src="quick-ranges.png" alt="Quick ranges"></li><li>对象不太方便放到query里,比如在我们监控看板里有一个功能,可以让用户在query里带上时间参数来覆盖看板里的默认配置,如果这里是对象的话就不太方便了。</li></ol><h1 id="使用相对时间表达式"><a href="#使用相对时间表达式" class="headerlink" title="使用相对时间表达式"></a>使用相对时间表达式</h1><p>如果能用表达式来表示上面的结构化数据的话不就能解决以上几条缺点了吗?</p><h2 id="相对时间表达式"><a href="#相对时间表达式" class="headerlink" title="相对时间表达式"></a>相对时间表达式</h2><p>在这点上<a href="http://play.grafana.org/" target="_blank" rel="noopener">Grafana</a>已经提供了一个可用的雏形,我在其语法基础上重写了逻辑,增加了容错性以及语法特性,独立出来了一个库(<a href="https://github.com/Frezc/relative-time-expression" target="_blank" rel="noopener">主页</a>)。这个表达式是基于上一节<a href="#使用结构化数据">结构化数据</a>实现的,但是能更简单明了。比如(取自<a href="https://github.com/Frezc/relative-time-expression#examples" target="_blank" rel="noopener">examples</a>)</p><ul><li><code>now - 12h</code>: 12 hours ago, same as <code>moment().subtract(12, 'hours')</code></li><li><code>-1d</code>: 1 day ago, same as <code>moment().subtract(1, 'day')</code></li><li><code>now / d</code>: the start of today, same as <code>moment().startOf('day')</code></li><li><code>now \ w</code>: the end of this week, same as <code>moment().endOf('week')</code></li><li><code>now - w / w</code>: the start of last week, same as <code>moment().subtract(1, 'week').startOf('week')</code></li></ul><h2 id="如何解决结构化数据的缺陷"><a href="#如何解决结构化数据的缺陷" class="headerlink" title="如何解决结构化数据的缺陷"></a>如何解决结构化数据的缺陷</h2><h3 id="如何解决格式化问题"><a href="#如何解决格式化问题" class="headerlink" title="如何解决格式化问题"></a>如何解决格式化问题</h3><p>将表达式格式化的话特殊区间就不需要写代码进行判断了,只需像<a href="#使用key来映射">第一种方式</a>里一样将标准格式的表达式映射到相应的文本上就行了。比如</p><pre><code class="javascript">const LabelMap = { 'now-d/d to now-d\\d': '昨天', 'now-w/d to now-w\\d': '上周的同一天', // so on..}import { standardize } from 'relative-time-expression';const start = standardize(' now - 1 d /d'); // return now-d/dconst end = standardize('-d\\d'); // return now-d\dconst label = LabelMap[`${start} to ${end}`] || `${start} to ${end}`;expect(label).toEqual('昨天');</code></pre><p>当然在处理 <em>前x小时</em>, <em>前x天</em> 这种情况还是需要写一些判断,和上节的处理差不多,如下</p><pre><code class="javascript">// const start, end = ...import { parse } from 'relative-time-expression';if (end === 'now') { // omit error catch code const ast = parse(start); if (ast.body.length === 1 && ast.body[0].type === 'Offset') { // 如果start只有一项偏移,那么就可以格式化成 `前{number}{单位}` 了 return `前${ast.body[0].number}${ast.body[0].unit}`; } // ...}</code></pre><h3 id="解决剩下两个问题"><a href="#解决剩下两个问题" class="headerlink" title="解决剩下两个问题"></a>解决剩下两个问题</h3><p>值一旦变成普通字符串的话这两个问题也就迎刃而解了。</p><h1 id="时区问题"><a href="#时区问题" class="headerlink" title="时区问题"></a>时区问题</h1><p>区间首尾的计算是基于时区的,比如<code>now/d</code>, 用户期望的通常是他所在地区一天的开始时间(当然也不排除想通过另外时区的时间查数据的情况)。如果计算相对时间实在客户端的话,浏览器其实已经帮我们设定好了正确的时区,但是服务端就不一样了,它只能拿到服务器系统所在时区的时间。</p><p>所以考虑服务端计算相对时间的需求(监控看板里就有类似需求:通过看板组件id直接调用后端接口拿到数据),客户端在调用这些接口时需要带上时区信息。服务端的处理代码如下</p><pre><code class="typescript">import parse from 'rte-moment';import moment from 'moment-timezone';const m = parse('now/d', { base: moment().tz(clientTimezone || 'Asia/Shanghai') });moment().tz('Asia/Shanghai').startOf('day').isSame(m); // true</code></pre><h1 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h1><p>在监控项目里的时间组件基本参照了Grafana的时间组件,不得不说其在监控方面还有很多值得学习的地方。</p><p>另外该项目除了<a href="https://github.com/Frezc/relative-time-expression" target="_blank" rel="noopener">typescript</a>外还用<a href="https://github.com/Frezc/relative-time-expression-rust" target="_blank" rel="noopener">rust</a>练手写了一遍,rust给我印象最深的一点是整套项目构建、文档生成、依赖管理的工具非常好用,上手就可以专心写代码了。</p>]]></content>
<summary type="html">
<p>平时开发监控系统时免不了与时序数据库的查询打交道,在查时序数据库时 <em>时间范围</em> 是必不可少的条件,所以在查询的UI展示上通常会将时间范围作为一个独立的组件来让用户交互。</p>
<p>时间范围通常会展示为两种形式:相对时间和绝对时间。对于监控系统来说,日常观
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="time" scheme="https://frezc.github.io/tags/time/"/>
<category term="moment" scheme="https://frezc.github.io/tags/moment/"/>
<category term="parser" scheme="https://frezc.github.io/tags/parser/"/>
<category term="rust" scheme="https://frezc.github.io/tags/rust/"/>
</entry>
<entry>
<title>看板性能优化实践</title>
<link href="https://frezc.github.io/2019/04/13/dashboard-performance-optimization/"/>
<id>https://frezc.github.io/2019/04/13/dashboard-performance-optimization/</id>
<published>2019-04-13T06:04:47.000Z</published>
<updated>2019-04-27T16:36:08.362Z</updated>
<content type="html"><![CDATA[<p>最近比较有空就打算把看板的性能优化做一下,打开项目用chrome的performance记录了一下一个组件比较多的看板。这个结果还是比较让人惊讶,原本以为性能瓶颈主要在图表(G2的性能堪忧),没想到看板本身的性能也如此糟糕。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="frame-timeline-before-optimize.png" alt="优化前的性能" title> </div> <div class="image-caption">优化前的性能</div> </figure><p><em>本地开发环境,相比生成环境耗时多了40%,本文主要看同一环境下的对比</em></p><p>接下来看看如何通过这个结果进行优化。下面的章节是按必要性进行的排序。</p><h1 id="首屏不要渲染没必要的组件"><a href="#首屏不要渲染没必要的组件" class="headerlink" title="首屏不要渲染没必要的组件"></a>首屏不要渲染没必要的组件</h1><p>我先看了下耗时接近7s的那一帧里,什么组件占了大头。发现主要是两个:即席查询的编辑组件和CMDB选择器。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="dashboard1-mosaic.png" alt="测试用的看板" title> </div> <div class="image-caption">测试用的看板</div> </figure><p>即席查询的编辑组件<strong>不应该被渲染</strong>,因为图表的需要依赖编辑组件里请求的数据进行渲染,但是因此把这么多组件的编辑组件给渲染出来就非常影响性能了。</p><p>这里我们把组件需要的数据移到更上一层去请求,然后除了编辑模式下不再去渲染编辑组件。</p><p>关于CMDB选择器的优化见下一节</p><h1 id="所有组件都应该在独立的帧内渲染"><a href="#所有组件都应该在独立的帧内渲染" class="headerlink" title="所有组件都应该在独立的帧内渲染"></a>所有组件都应该在独立的帧内渲染</h1><p>看板里的所有组件应该关心自己的性能,因为组件可能会是成倍出现的,渲染耗时会被成倍放大。如果很多组件碰巧在相近的时间(或者在比较长的一帧内)请求结束并去渲染结果的话,就会将一帧拖得很长。在这帧内页面会处于假死状态,用户<strong>无法滚动和交互</strong>。</p><p>在我们的看板里就有这个问题</p><ol><li>看板组件的渲染,尤其是Chart和Table都需要对大量结果进行处理。</li><li>CMDB选择器会将所有CMDB数据组织成树状结构。(虽然这里可以改为每次只请求一级来优化,但是这样需要的改动太多了)</li></ol><p>怎么优化呢?这里我们可以参考react fiber的实现方式,一个简单的方案就是使用<code>requestIdleCallback</code>这个API来将每个组件请求结束后的更新拆散到每一帧内。这样就不会出现多个组件同时更新而“撞车”的情况了。</p><pre><code class="javascript">requestIdleCallback(() => { // update component with response data}, { timeout: 2000 });</code></pre><p>我们可以同时应用到组件和CMDB选择器的上,但是我们又希望组件更新能早于CMDB选择器,因为比较重要。不过目前<code>requestIdleCallback</code><a href="https://github.com/w3c/requestidlecallback/issues/68" target="_blank" rel="noopener">没有优先级</a>的概念,不过可以自己实现一个。</p><h2 id="React-Concurrent-Mode"><a href="#React-Concurrent-Mode" class="headerlink" title="React Concurrent Mode"></a>React Concurrent Mode</h2><p>React其实提供了一个类似的优化方案:<a href="https://reactjs.org/blog/2018/11/27/react-16-roadmap.html#react-16x-q2-2019-the-one-with-concurrent-mode" target="_blank" rel="noopener">concurrent mode</a>,了解更多可以查看<a href="https://philippspiess.com/scheduling-in-react/" target="_blank" rel="noopener">这篇文章</a>。</p><p>但是我在使用<code>concurrent mode</code>时碰到一些问题所以暂时没用:</p><ul><li>默认使用<a href="https://reactjs.org/docs/strict-mode.html" target="_blank" rel="noopener">Strict Mode</a>,用了一些组件库会有一堆警告</li><li>不知道是不是和旧API不兼容,用在项目里时看板组件就抛错<pre><code>app.5793fd45.js:6977 Uncaught TypeError: callback is not a function at flushFirstCallback (app.5793fd45.js:6977) at flushWork (app.5793fd45.js:7075) at MessagePort.channel.port1.onmessage (app.5793fd45.js:6817)</code></pre></li><li>目前没有详细文档,遇到问题也不好解决</li></ul><h2 id="Mobx-batch-update"><a href="#Mobx-batch-update" class="headerlink" title="Mobx batch update"></a>Mobx batch update</h2><p>因为在看板里用到了mobx所以顺便提一下,我在组件更新图表数据时使用了<code>reaction</code>函数,这个函数会自动对第二个参数里调用<code>update</code>进行batch</p><pre><code class="javascript">reaction(() => toJS(this.fetchResult), () => { // auto batch all update here store.updateSource(this.fetchResult); store.updateConfig(this.fetchResult);});</code></pre><p>但是如果将更新放到<code>requestIdleCallback</code>里的话,这里就会触发两次更新了。需要手动加上<code>action</code>才能使其batch</p><pre><code class="javascript">reaction(() => toJS(this.fetchResult), () => { // you must use action to enable batch // ↓↓↓↓↓↓ window.requestIdleCallback(action(() => { store.updateSource(this.fetchResult); store.updateConfig(this.fetchResult); }), { timeout: 3000 });});</code></pre><h1 id="懒加载"><a href="#懒加载" class="headerlink" title="懒加载"></a>懒加载</h1><p>经过前两步的优化后,看板加载起来已经顺畅很多了,但是看板内容(就是测试看板图上的看板组件部分)初始化时还是有1.4s的时间。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="frame-timeline-op1.png" alt title> </div> <div class="image-caption"></div> </figure><p>这里其实没有哪块特别耗时了,所以需要其他方法来减少渲染耗时,那么自然就想到懒加载了。就上面展示的看板在首屏时底下有一堆是看不到的,那么我们就不需要在一开始就渲染这些组件。</p><p>一个懒加载组件的简单实现</p><pre><code class="typescript">function isElementInViewport(el: Element) { const rect = el.getBoundingClientRect(); // 因为看板没有左右滚动所以不需要判断垂直坐标 return rect.top < (window.innerHeight || document.documentElement.clientHeight) && rect.bottom > 0;}const RenderUntilInViewport: React.FC<{ wrapper: React.ReactElement<any>;}> = ({ wrapper, children }) => { const ref = React.useRef<HTMLDivElement>(null); const [display, setDisplay] = React.useState(false); React.useEffect(() => { function displayIfInViewport() { if (!display && ref.current && isElementInViewport(ref.current)) { setDisplay(true); return true; } return false; } if (displayIfInViewport()) { return; } window.addEventListener('scroll', displayIfInViewport, { capture: true }); return () => { window.removeEventListener('scroll', displayIfInViewport, { capture: true }); }; }, []); return React.cloneElement(wrapper, { ref }, display && children);};export default RenderUntilInViewport;</code></pre><p>如果使用的是<a href="https://github.com/STRML/react-grid-layout" target="_blank" rel="noopener">react-grid-layout</a>的话,需要把初始动画关闭,不然组件的初始坐标都是(0, 0),会使懒加载失效。<a href="https://github.com/STRML/react-grid-layout/issues/103#issuecomment-202127519" target="_blank" rel="noopener">关闭方法</a>,不要使用<code>useCSSTransforms={false}</code>,这项会比较影响性能(测试看板+500ms的负收益)。</p><p>不过懒加载在测试看板的layout性能时带来的收益不大,大概能快100ms左右,或许用好折叠条组件(可以将一些组件折叠,默认不会去渲染)的话也没必要做吧。</p><h1 id="组件延迟渲染(optional)"><a href="#组件延迟渲染(optional)" class="headerlink" title="组件延迟渲染(optional)"></a>组件延迟渲染(optional)</h1><p>同样在看板初始化阶段,这段时间里有很大一部分处于<code>react-sizeme</code>计算宽高然后触发组件更新所致,如果将组件真正渲染放到布局完成的话可能可以减少几次组件更新的消耗。</p><p>我们修改一下上面的懒加载组件,将第13行代码放到<code>requestIdleCallback</code>里。</p><pre><code class="typescript">// ...codeswindow.requestIdleCallback(() => { setDisplay(true);}, { timeout: 1000 });// ...codes</code></pre><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="idlerendergadget.png" alt title> </div> <div class="image-caption"></div> </figure><p>可以看到看板布局的时间1300ms+ -> 800ms+,虽然看起来提升了很多,但是看板真正展现信息还是要等组件渲染完,所以看板直到可用的等待时间相比原来可能还会增加。因此这节的优化点看实际情况去做吧。</p><h1 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h1><p>虽然上面写了一大堆,但是基本不需要改多少代码就使页面变得不卡了。</p><p>看一下线上的前后对比,同样是上面的测试看板。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="prod-before-optimization.png" alt="优化前" title> </div> <div class="image-caption">优化前</div> </figure><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="prod-after-optimization.png" alt="优化后(滚动页面加载完所有组件)" title> </div> <div class="image-caption">优化后(滚动页面加载完所有组件)</div> </figure><p><em>这里空闲时间比较多是因为请求完成的时间差导致</em></p>]]></content>
<summary type="html">
<p>最近比较有空就打算把看板的性能优化做一下,打开项目用chrome的performance记录了一下一个组件比较多的看板。这个结果还是比较让人惊讶,原本以为性能瓶颈主要在图表(G2的性能堪忧),没想到看板本身的性能也如此糟糕。</p>
<figure class="image
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="react-grid-layout" scheme="https://frezc.github.io/tags/react-grid-layout/"/>
<category term="dashboard" scheme="https://frezc.github.io/tags/dashboard/"/>
<category term="frontend" scheme="https://frezc.github.io/tags/frontend/"/>
</entry>
<entry>
<title>《Touhou Luna Nights》—— 令人惊喜的东方同人游戏</title>
<link href="https://frezc.github.io/2019/04/09/touhou-luna-nights-review/"/>
<id>https://frezc.github.io/2019/04/09/touhou-luna-nights-review/</id>
<published>2019-04-09T15:13:55.000Z</published>
<updated>2019-04-10T14:24:21.171Z</updated>
<content type="html"><![CDATA[<p>最近感觉和人说话的次数少了很多,总觉得这样下去不行啊,表达能力日渐衰退,有话不说憋着也难受。想了想多写写文章也许能发泄一下,就建了这个<strong>REVIEW</strong>目录。平时玩完游戏、看完动画、看完书之类的,如果有什么X后感的话就会写一下吧。</p><p>这次就讲讲清明放假期间玩通的这款《Touhou Luna Nights(东方月神夜)》,这个中文译名总觉得很中二呢。</p><h1 id="总体评价"><a href="#总体评价" class="headerlink" title="总体评价"></a>总体评价</h1><p>这款游戏我认为是中等偏上的一款独立游戏,就算不了解东方也是值得花时间玩一下的。对于东方爱好者来说应该是近几年最值得玩的同人游戏之一了。</p><h2 id="关于同人游戏"><a href="#关于同人游戏" class="headerlink" title="关于同人游戏"></a>关于同人游戏</h2><p>自从ZUN放开对steam的限制后,已经有部分东方的同人游戏上steam了,虽然上的大多完成度都挺高的,但是和优秀的独立游戏还是有明显差距的。以前我也很喜欢东方的同人游戏,比如高中时很喜欢的《幻想乡之谜》,后来随着独立游戏的崛起,出现了很多非常优秀的独立游戏(比如minecraft、以撒的结合、splunky等等)后,我发现很多同人游戏除去IP加成,本身完成度是非常低的。</p><p>在steam上看到本作时,低像素风格但是很精致的画面让人眼前一亮,看了下标签 <em>类银河战士恶魔城</em>,刚打通空洞骑士,对这类游戏还是无法抵抗的。那时候还没正式发售,所以到现在才玩,不过extra stage还没做完,看了下出场人物,猜boss可能是⑨吧。</p><h1 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h1><p>先讲讲画面,游戏里的人物是偏像素风,这么做可能是为了画不同动作简单一点吧,里面各个角色的动作还是挺丰富的,相比空洞骑士就多很多。场景虽然想说是像素风,但是更像是在低像素下画的素材,然后拉伸了而已。不过这种低像素的图加多层卷轴背景让我回想起了《MapleStory》,刚好戳中我喜欢的点。弹幕和飞刀特效也做得很炫酷。</p><p>另外游戏的一大亮点就是以时间停止为主的战斗和解谜了,时停在很多横板动作游戏里都有,但能让人用得这么频繁的基本没有吧。游戏因为时停使得战斗变得简单粗暴,初期玩起来还算是挺爽的。</p><p>从原作衍生的擦弹系统做得也很不错,这个系统算是隐藏特性(没有在教程中写明)。相比大部分原作(除了地灵殿和赶猪传)单纯分数相关的收益,本作里通过擦弹可以回血回魔对于大部分玩家都是很有用的。每个BOSS一般都会设计一个比较弱的阶段,本作里的BOSS也会有一个比较好擦弹的攻击方式,让玩家可以擦弹回血之类的,可以说是非常友好了。基本上掌握了这点,这个游戏就没有一个难的BOSS,对手残也是非常友好的。</p><h1 id="不足"><a href="#不足" class="headerlink" title="不足"></a>不足</h1><p>整个游戏除了第一个场景和所有BOSS战外都挺。。“单调”的。</p><p>关卡设计只有断桥那处给我留下的映像,其他地方无论是场景还是解谜都一般般吧。整个地图只是简单的分叉式,这个对于后期重来时很不友好,因为只为了原来一个拿不到的物品跑一大段往返是很无聊的,当然这和传送门太少也有关,“近路”对于这类游戏还是很重要的。</p><p>时停这系统导致战斗就是一个套路走到底了,基本就是时停-技能,敌人的攻击方式都不知道就带走了。这样玩到后期就太无趣了,不过游戏的时间很短,这个问题倒不是很严重。</p><p>整个游戏花了5个小时玩通,只算我喜欢的BOSS战可能还不到50%的时间,对于62的定价显得不太值了。</p>]]></content>
<summary type="html">
<p>最近感觉和人说话的次数少了很多,总觉得这样下去不行啊,表达能力日渐衰退,有话不说憋着也难受。想了想多写写文章也许能发泄一下,就建了这个<strong>REVIEW</strong>目录。平时玩完游戏、看完动画、看完书之类的,如果有什么X后感的话就会写一下吧。</p>
<p>
</summary>
<category term="REVIEW" scheme="https://frezc.github.io/categories/REVIEW/"/>
<category term="东方" scheme="https://frezc.github.io/tags/%E4%B8%9C%E6%96%B9/"/>
<category term="同人游戏" scheme="https://frezc.github.io/tags/%E5%90%8C%E4%BA%BA%E6%B8%B8%E6%88%8F/"/>
<category term="独立游戏" scheme="https://frezc.github.io/tags/%E7%8B%AC%E7%AB%8B%E6%B8%B8%E6%88%8F/"/>
<category term="Touhou Luna Nights" scheme="https://frezc.github.io/tags/Touhou-Luna-Nights/"/>
</entry>
<entry>
<title>大阪游记</title>
<link href="https://frezc.github.io/2019/03/30/osaka-travel/"/>
<id>https://frezc.github.io/2019/03/30/osaka-travel/</id>
<published>2019-03-30T15:02:10.000Z</published>
<updated>2019-04-09T15:39:54.105Z</updated>
<content type="html"><![CDATA[<p>这个月中旬去和同事去大阪玩了几天,趁着这个月还没结束写个游记吧,内容应该会比较流水账。</p><h1 id="事前规划很重要"><a href="#事前规划很重要" class="headerlink" title="事前规划很重要"></a>事前规划很重要</h1><p>这次虽然也是自由行但是比年初去东京的那次体验好得多吧,上次虽然有一大目的是参加CM,但是除此之外还是有不少时间可以到处玩的,不过缺乏规划的我们也没去成多少地方,主要是因为没有目标就很难有动力到处跑。</p><p>这次去的时候发现google地图真是好用,里面保存地点的功能是规划神器啊,这里分享一下我们这次<a href="https://www.google.com/maps/placelists/list/9smJRMlD4-4GxkbP8HMcSxe5nOzIcQ?hl=zh" target="_blank" rel="noopener">大阪行程的规划</a>(当然里面有因为时间缘故很多没去的地方)。地点里还有个分享的功能,适合和小伙伴一起编辑和投票,这个功能和自己保存的地方不能互通,也是比较奇怪。</p><p>如果要去圣地巡礼的话,可以利用<a href="https://seichimap.jp/" target="_blank" rel="noopener">这个网站</a>找到感兴趣的地点,它会直接给你google map的地点,然后保存到自己的列表里再做规划。</p><p>另外要注意景点的时间限制,日本的景点很多下午就关门了,要早去。</p><h1 id="飞机、酒店和电话卡"><a href="#飞机、酒店和电话卡" class="headerlink" title="飞机、酒店和电话卡"></a>飞机、酒店和电话卡</h1><p>这次虽然是自由行,机酒还是通过旅行社购买的(公司规定),可能会比单买贵一点吧,不过会方便一些。这次坐的厦航和全日空比,除了没有座位前的小平板外其实没什么差距,体验还不错。酒店住的是HOTEL MYSTAYS Otemae,算是名宿?相比之前秋叶原住的酒店便宜很多,房间也大很多(感觉有2倍大),除了没有每日打扫和厕所有点味外和住一般酒店差不多吧,但是实惠很多。</p><p>前次去东京玩一个很大的问题就是定位总是会失效,这次换了一家买电话卡,结果是要好一些,除了在车站和地铁里外都能好好工作,不知道这方面WIFI是不是会好用一些。</p><h1 id="第一天"><a href="#第一天" class="headerlink" title="第一天"></a>第一天</h1><p>中午坐的飞机再坐电车到酒店都已经傍晚了,这次坐的是普通电车只要1000多一点,大阪这边也有类似东京的skyliner的haruka,需要额外买票,看了下haruka的到达时间没有快多少,价格却贵了不少。</p><p>到了大阪的车站就发现和东京很不同的一点,在东京的车站里是没有“出口”这个称呼的,写的都是“改札口”(检票口),而在大阪的车站里则是清一色的“出口”,可能是地方差异吧。还有一点是在车站地铁里大多是右侧通行,我印象中东京靠左的比较多。</p><p>晚上在道頓堀附近逛了一下,吃了一餐烤肉。</p><h1 id="第二天"><a href="#第二天" class="headerlink" title="第二天"></a>第二天</h1><p>第二天上午先去了日本桥附近,逛了下黑门市场、一些动漫游戏店和信长书店(我也不知道为什么要进去了)。中午吃了王将(饺子就一种,但是没有国内的油腻)后就去大阪城了。</p><p>大阪城公园还是挺大的,要说有什么特别的话就是乌鸦很多吧。<br><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="osaka-castle-park-karasu.jpg" alt="随处可见的乌鸦" title> </div> <div class="image-caption">随处可见的乌鸦</div> </figure></p><p>大阪城作为很多电影游戏里的常客,实际看到比想象里的小一些。里面有很多层,基本都是讲相关历史,展出的一些重制的(也可能是原版)武士刀、火器、服装、屏风等挺有意思的。</p><p>之后去了梅田附近,逛了逛大阪站这里的pokemon center,买了些小东西;去附近的游戏厅玩了一会;坐了次hep five摩天轮,这个我到觉得白天坐比较好,晚上没什么好看的。晚餐是一风堂的拉面,听说这家是以比较辣的拉面出名的,不过当时没点那款,听小伙伴描述并不辣的样子。</p><h1 id="第三天"><a href="#第三天" class="headerlink" title="第三天"></a>第三天</h1><p>这天就是神户一日游,神户这里有很多fate里冬木市的取景地,所有算是一次圣地巡礼了。</p><p>上午先走了一遍神户大桥和附近的公园,很容易会想起fate里的一些名场景。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="kobe-ohashi.jpg" alt="从旁边公园里拍的神户大桥" title> </div> <div class="image-caption">从旁边公园里拍的神户大桥</div> </figure><p>之后去了北野异人馆街,这里的看点就是比较欧式风格的建筑吧,不过对fate fans来说也是一大圣地,有好几处熟悉的取景地。下面是風見鶏の館,fate中远坂凛家的取景地。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="kazamidori.jpg" alt="風見鶏の館" title> </div> <div class="image-caption">風見鶏の館</div> </figure><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="kazamidori-table.jpg" alt="桌子上有一些熟悉的纸人,后面藏着一个pipi美" title> </div> <div class="image-caption">桌子上有一些熟悉的纸人,后面藏着一个pipi美</div> </figure><p>中午去吃了神户牛(还是烤肉),似乎神户这里的牛肉店都是神户牛啊。</p><p>之后本来预想是去六甲山牧场的,但是到六甲山下已经3点多了,被告知牧场4点半就不让进了,坐铁轨上山+公交到牧场是来不及的,所以只能放弃去牧场了。</p><p>上山后大概4点多本想去下摩耶山掬星台,不过巴士司机告诉我们这是最后一班车,不能过去了,至于理由日语太烂了没问清。之后就只能去东面玩了。</p><p>在六甲山天览台俯视风景还是很棒的,能看到云朵就在很近的距离飘过,就是风太大了,这个季节还是有点冷。在一个甜品店坐到晚上看看夜景就回去了。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="rokko-terasu.jpg" alt="白天俯瞰风景" title> </div> <div class="image-caption">白天俯瞰风景</div> </figure><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="rokko-terasu-yoru.jpg" alt="傍晚" title> </div> <div class="image-caption">傍晚</div> </figure><p>晚餐吃了蟹道乐,个人最喜欢的是前菜的生蟹,比较像温州的江蟹生。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="douraku.jpg" alt="放毒" title> </div> <div class="image-caption">放毒</div> </figure><h1 id="第四天"><a href="#第四天" class="headerlink" title="第四天"></a>第四天</h1><p>第四天去的是京都一块。</p><p>京都这边寺庙很多,其中也有不少圣地,光看各种寺庙和神社都能走一天。这天先去看了伏见稻荷大社,这个神社的特点就是一排排鸟居了,上面都刻着捐赠者的名字,走在千本鸟居下爬了一圈山,风景不错但是一早就爬山真的很累。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="senbontorii.jpg" alt="千本鳥居" title> </div> <div class="image-caption">千本鳥居</div> </figure><p>去拉面小路吃了一份黑富山酱油拉面后就坐公交到了清水寺,这次来得不巧,最有特色的建筑刚好在维修,从外面看着怪怪的。内部能看到很多含苞待放的樱花,晚来几天应该会非常好看。清水寺下来是一块很大的墓地,这边也没什么可看的,最好还是原路返回出去。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="kiyomizutemple.jpg" alt="维修中的清水寺" title> </div> <div class="image-caption">维修中的清水寺</div> </figure><p>随后去了花见小路附近,因为还是下午好像店都没开,不过街上人是真的多。</p><p>休息了一下就向宇治出发了,顺路刚好先去看了下京阿尼的两栋办公楼。其中一栋下面是个周边店,卖京都动画的各种周边,这里有卖叫“生動画”的东西,看起来是一些分镜原稿之类的东西。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="kyoani.jpg" alt="京阿尼" title> </div> <div class="image-caption">京阿尼</div> </figure><p>随后电车坐到了宇治站,本来以为很看到京吹的一些人物牌子,好像被撤掉了。宇治这里的话就是京吹的主要取景地了,能看到这么多动画里的原场景还是很让人激动的。过了宇治桥,沿着宇治川往南走,发现这边虽然比较乡下但还是会有旅游团会来的,毕竟还是有“宇治抹茶”这个特产。我们来的这个季节倒也没什么游客(人也很少),又不巧的是宇治川的岸边被封住了,中间的小岛也在建什么东西的样子。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="ujihashi.jpg" alt="桥" title> </div> <div class="image-caption">桥</div> </figure><p>宇治这里有4个附带名场景的桥,上面这个应该是最不知名的一个。沿着岸边走完了几个桥就去大吉山展望台了,算是京吹第8话的圣地巡礼吧。</p><p>路上会遇到宇治神社和宇治上神社,在神社里甚至还能看到京吹剧场版的宣传图。展望台这只有一个小亭子,如果没有京吹里那么甜的一幕,应该也不会有很多参观者吧。这边并不高,但是几乎能把整个宇治收入眼底,待这看了日落就下去了。这里山路没有灯,路上还有凸起来的树枝和石头,晚上还是有一点危险的。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="daikichiyama.jpg" alt="大吉山展望台" title> </div> <div class="image-caption">大吉山展望台</div> </figure><p>宇治这里我还是挺喜欢的,人少很安静,路上也能看到一些和我们相同目的的人。之后要走的时候看到车站附近有一家回转寿司店,就顺路吃了晚餐。去日本玩的话回转寿司还是吃饭的好去处,好吃不贵,点餐也是自助的。</p><h1 id="第五天"><a href="#第五天" class="headerlink" title="第五天"></a>第五天</h1><p>最后一天就是收拾东西回家了,去机场坐的电车还有前半后半目的地不同的操作,这个google map上是不会告诉你的,需要注意一下。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>最后做一遍技术总结,要想玩得开心,有以下要领:</p><ul><li>提前规划,和小伙伴讨论行程</li><li>量力而为,太累会影响后面情绪</li><li>兴趣使然,如果要去圣地巡游,那么有顺路的名景点就去吧,不然就不要强迫自己去了</li></ul>]]></content>
<summary type="html">
<p>这个月中旬去和同事去大阪玩了几天,趁着这个月还没结束写个游记吧,内容应该会比较流水账。</p>
<h1 id="事前规划很重要"><a href="#事前规划很重要" class="headerlink" title="事前规划很重要"></a>事前规划很重要</h1><p
</summary>
<category term="ESSAY" scheme="https://frezc.github.io/categories/ESSAY/"/>
<category term="旅游" scheme="https://frezc.github.io/tags/%E6%97%85%E6%B8%B8/"/>
</entry>
<entry>
<title>react hooks踩坑记录</title>
<link href="https://frezc.github.io/2019/03/23/react-hooks-traps/"/>
<id>https://frezc.github.io/2019/03/23/react-hooks-traps/</id>
<published>2019-03-23T03:02:14.000Z</published>
<updated>2019-06-20T15:48:24.089Z</updated>
<content type="html"><![CDATA[<p>自从 <a href="mailto:react@16.8" target="_blank" rel="noopener">react@16.8</a> 正式发布react hooks已经有一段时间了,这段时间我也一直在项目里使用hook的方式来写组件,其间也遇到了不少问题,下面列一下踩坑记录。</p><p>该篇假设你已经了解react hooks的基本用法,如果对react hooks毫不了解,建议先阅读<a href="https://reactjs.org/docs/hooks-intro.html" target="_blank" rel="noopener">官方文档</a>。</p><h1 id="想在第一次render前执行的代码,可以放在useState里"><a href="#想在第一次render前执行的代码,可以放在useState里" class="headerlink" title="想在第一次render前执行的代码,可以放在useState里"></a>想在第一次render前执行的代码,可以放在useState里</h1><p>类似class component里的<code>constructor</code>和<code>componentWillMount</code>。例如</p><pre><code class="javascript">const instance = useRef(null);useState(() => { instance.current = 'initial value';});</code></pre><h1 id="useState里数据务必为immutable"><a href="#useState里数据务必为immutable" class="headerlink" title="useState里数据务必为immutable"></a>useState里数据务必为immutable</h1><p>虽然class component的state也提倡使用immutable数据,但不是强制的,因为只要调用了<code>setState</code>就会触发更新。但是使用<code>useState</code>时,如果在更新函数里传入同一个对象将<strong>无法</strong>触发更新。</p><p>举个例子,有时可能会写出这种代码</p><pre><code class="javascript">const [list, setList] = useState([2,32,1,534,44]);return ( <> <ol> {list.map(v => <li key={v}>{v}</li>)} </ol> <button onClick={() => { // bad 这样无法触发更新 setList(list.sort((a, b) => a - b)); // good 必须传入一个新的对象 setList(list.slice().sort((a, b) => a - b)); }} >sort</button> </>)</code></pre><h1 id="useMemo-has-no-semantic-guarantee"><a href="#useMemo-has-no-semantic-guarantee" class="headerlink" title="useMemo has no semantic guarantee"></a>useMemo has no semantic guarantee</h1><p>这句话出自useMemo的<a href="https://reactjs.org/docs/hooks-reference.html#usememo" target="_blank" rel="noopener">API Reference</a>。</p><blockquote><p><strong>You may rely on <code>useMemo</code> as a performance optimization, not as a semantic guarantee.</strong> In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without <code>useMemo</code> — and then add it to optimize performance.</p></blockquote><p>也就是说在未来react的版本,<code>useMemo</code>在必要时会清除缓存而重新执行creater function,所以最好不要使用<code>useMemo</code>来执行具有<strong>side effect</strong>的函数。</p><p>举个例子,比如我想实现基于lodash throttle的hook,使用<code>useMemo</code>来实现会比较理想,类似下面的代码</p><pre><code class="javascript">const thRef = useRef(null);useMemo(() => { if (thRef.current) { thRef.current.cancel(); } thRef.current = _.throttle(fn, wait, options);}, [wait, options && options.leading, options &&options.trailing]);// ...other codes</code></pre><p>目前这么写是没问题,但是考虑到未来react实现了上述“forget”特性的话,这样的代码就可能产生预料之外的结果,解决方法就是自己实现一个稳定的useMemo,<a href="https://github.com/Frezc/use-lodash-debounce-throttle/blob/master/src/use-stable-memo.ts" target="_blank" rel="noopener">实现示例</a>。</p><h1 id="useEffect和useLayoutEffect有什么区别?"><a href="#useEffect和useLayoutEffect有什么区别?" class="headerlink" title="useEffect和useLayoutEffect有什么区别?"></a>useEffect和useLayoutEffect有什么区别?</h1><p>简单来说就是调用时机不同,<code>useLayoutEffect</code>和原来<code>componentDidMount</code>&<code>componentDidUpdate</code>一致,在react完成DOM更新后马上<strong>同步</strong>调用的代码,会阻塞页面渲染。而<code>useEffect</code>是会在整个页面渲染完才会调用的代码。</p><p>官方建议优先使用<code>useEffect</code></p><blockquote><p>However, <strong>we recommend starting with useEffect first</strong> and only trying useLayoutEffect if that causes a problem.</p></blockquote><p>在实际使用时如果想避免页面抖动(在<code>useEffect</code>里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在<code>useLayoutEffect</code>里。</p><p>不过<code>useLayoutEffect</code>在服务端渲染时会出现一个warning,要消除的话得用<code>useEffect</code>代替或者推迟渲染时机。见<a href="https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85" target="_blank" rel="noopener">说明</a>和<a href="https://github.com/facebook/react/issues/14927" target="_blank" rel="noopener">讨论</a>。</p><h1 id="在useEffect和useLayoutEffect里使用async函数"><a href="#在useEffect和useLayoutEffect里使用async函数" class="headerlink" title="在useEffect和useLayoutEffect里使用async函数"></a>在useEffect和useLayoutEffect里使用async函数</h1><p>因为async函数肯定会返回一个<code>Promise</code>,会和<code>useEffect</code>返回的cleanup函数混淆所以不要直接将<code>async function</code>传给<code>useEffect</code>,最简单的解决方法是<code>IIFE</code>。</p><pre><code class="javascript">useEffect(() => { (async () => { await fetchSomething(); })();}, []);</code></pre><h1 id="使用useCallback时,要注意闭包问题(或者说是Capture-Value特性)"><a href="#使用useCallback时,要注意闭包问题(或者说是Capture-Value特性)" class="headerlink" title="使用useCallback时,要注意闭包问题(或者说是Capture Value特性)"></a>使用useCallback时,要注意闭包问题(或者说是Capture Value特性)</h1><p>和避免使用inline function一样,有时需要使用<code>useCallback</code>来优化性能,但是<code>useCallback</code>会返回之前的闭包,使用到的局部变量是不会更新的。<br>举个例子</p><pre><code class="javascript">const [count, setCount] = useState(0);const increaseCount = useCallback(() => { setCount(count + 1);}, []);// 因为useCallback总是返回第一次render时传入的闭包,increaseCount内访问到的count永远都是0increaseCount(); // 相当于setCount(1);increaseCount(); // 无论调用几次都是setCount(1);</code></pre><p>解决方法就是避免引用外部的局部变量</p><pre><code class="javascript">const [count, setCount] = useState(0);const vRef = useRef(0);const increaseCount = useCallback(() => { // 传入function的话每次都能拿到最新值 setCount(prevCount => prevCount + 1); // 用一个对象来保存,适用于不需要触发更新的情况 vRef.current += 1;}, []);// 使用useReducer解决const [count, increase] = useReducer((c, increment) => c + increment, 0);const increaseCount = useCallback(() => { increase(1);}, []);</code></pre><p>但是注意不要<a href="https://github.com/ryardley/hooks-perf-issues#whats-the-difference" target="_blank" rel="noopener">这么写</a>。</p><p>关于<strong>Capture Value</strong>可以参考<a href="https://juejin.im/post/5c9827745188250ff85afe50#heading-3" target="_blank" rel="noopener">这篇文章</a>。</p><h1 id="useEffect、useCallback、useMemo等API的第二个参数数组的长度不能变"><a href="#useEffect、useCallback、useMemo等API的第二个参数数组的长度不能变" class="headerlink" title="useEffect、useCallback、useMemo等API的第二个参数数组的长度不能变"></a>useEffect、useCallback、useMemo等API的第二个参数数组的长度不能变</h1><p>有时可能会写出这样的代码</p><pre><code class="javascript">const [selectedStatuses, setSelected] = useState([]);useEffect(() => { fetchListById(selectedStatuses);}, selectedStatuses);</code></pre><p>这里如果将<code>selectedStatuses</code>从<code>[]</code>更新为<code>['active']</code>是不会触发effect的,react也会给你一个warning。<a href="https://github.com/facebook/react/blob/f161ee2eb7e78d6cb3d3878fe1812ac1057fedc6/packages/react-reconciler/src/ReactFiberHooks.js#L290" target="_blank" rel="noopener">相关源码</a>。</p><p>这里最好将整个state作为deps的一项传入,或者使用一个key来控制</p><pre><code class="javascript">useEffect(() => { fetchListById(selectedStatuses);}, [selectedStatuses]);</code></pre><h1 id="泛型参数怎么写?"><a href="#泛型参数怎么写?" class="headerlink" title="泛型参数怎么写?"></a>泛型参数怎么写?</h1><p>通常我们会这么写函数组件</p><pre><code class="tsx">const MyCom: React.FC<MyComProps> = (props) => { return <div>...</div>};</code></pre><p>但是在参数里使用了泛型,就不能这么写了,因为在变量声明里必须要指定确切的类型,所以这里要回到传统<code>function</code>的写法。放心,这么写也是有类型提示的。</p><pre><code class="tsx">// generic propsinterface MyComProps<T> { value?: T; onChange?(value: T): void;}function MyCom<T extends { type: string; }>(props: MyComProps<T>) { return <div>{props.value && props.value.type}</div>;}</code></pre><p>不过如果要用<code>React.forwardRef</code>的话目前没什么什么优雅的方案,还是需要明确类型才行。</p><h1 id="使用React-memo和React-forwardRef包装的组件为什么提示我children类型不对?"><a href="#使用React-memo和React-forwardRef包装的组件为什么提示我children类型不对?" class="headerlink" title="使用React.memo和React.forwardRef包装的组件为什么提示我children类型不对?"></a>使用<code>React.memo</code>和<code>React.forwardRef</code>包装的组件为什么提示我children类型不对?</h1><p>过去使用<code>Component</code>、<code>FC</code>等类型定义组件时一般不需要我们定义props里<code>children</code>的类型,因为在上述类型里已经帮你默认加上了 <code>{ children?: ReactNode }</code> 的定义。但是<code>@types/react</code>的维护者认为这样导致<code>children</code>几乎没有类型约束,组件开发者应该显式地声明<code>children</code>类型。所以对新的API应该都不会自动加上<code>children</code>的定义了,需要开发者手动添加。</p><p>详情见<a href="https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33602" target="_blank" rel="noopener">讨论</a>。</p><h1 id="使用act给react-hooks写单元测试"><a href="#使用act给react-hooks写单元测试" class="headerlink" title="使用act给react hooks写单元测试"></a>使用<a href="https://reactjs.org/docs/test-utils.html#act" target="_blank" rel="noopener"><code>act</code></a>给react hooks写单元测试</h1><p><a href="mailto:react@16.8" target="_blank" rel="noopener">react@16.8</a>给test-utils新加了一个 <code>act</code> API,关于这个API可以看看作者写的这篇<a href="https://github.com/threepointone/react-act-examples" target="_blank" rel="noopener">通俗易懂的解释</a>。</p><p>这里简单总结一下<code>act</code>主要是为了解决<code>useEffect</code>的测试问题出现的,因为<code>useEffect</code>的执行时机会很晚,甚至在断言之后。如果在<code>useEffect</code>里执行了ui变更,就很难写测试了,虽然可以用<code>useLayoutEffect</code>解决,但是不能为了通过测试而修改原代码。这里用<code>act</code>就能很好地解决了,这个API能同步执行所有<code>useEffect</code>以及相应的更新,在断言时就能拿到正确的结果了。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="https://user-images.githubusercontent.com/18808/52914771-3ecb4b00-32c4-11e9-9923-c577f371a4aa.png" alt="before" title> </div> <div class="image-caption">before</div> </figure><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="https://user-images.githubusercontent.com/18808/52914772-3ecb4b00-32c4-11e9-99c4-4915af46c149.png" alt="use act" title> </div> <div class="image-caption">use act</div> </figure><p>总之所有可能触发更新的代码都应该放到<code>act</code>里,不然test-utils会给一个warning。</p><p>另外<code>act</code>会batchUpdate,可能会导致一个隐藏bug,见文章中<a href="https://github.com/threepointone/react-act-examples/blob/master/sync.md#events" target="_blank" rel="noopener">这段内容</a>,需要注意一下。</p><h1 id="Function-Component与ant-design的Form"><a href="#Function-Component与ant-design的Form" class="headerlink" title="Function Component与ant design的Form"></a>Function Component与ant design的Form</h1><p>Form非常适合使用react hooks来实现,<a href="https://github.com/ant-design/ant-design/issues/10640" target="_blank" rel="noopener">官方</a>目前看来没什么进展,不过目前hoc也没什么问题。</p><p>用FC写的自定义表单控件会有ref相关的warning,因为antd form需要拿到组件的<code>ref</code>,而FC默认是没有实例的。这里我们可以通过<code>React.forwardRef</code> + <a href="https://reactjs.org/docs/hooks-reference.html#useimperativehandle" target="_blank" rel="noopener"><code>useImperativeHandle</code></a>解决,<a href="https://codesandbox.io/s/31mv8004rp" target="_blank" rel="noopener">官方示例</a>。不过这么写<code>validateFieldsAndScroll</code>这个API可能就没用了,建议把<code>ref</code>传给底层的表单元素组件。</p><hr><p>我收集了一些常用的hooks,欢迎使用和一起开发。<br><a href="https://frezc.github.io/react-hooks-common/">https://frezc.github.io/react-hooks-common/</a></p><h1 id="References"><a href="#References" class="headerlink" title="References"></a>References</h1><ul><li><a href="https://kentcdodds.com/blog/useeffect-vs-uselayouteffect" target="_blank" rel="noopener">useEffect vs useLayoutEffect</a></li><li><a href="https://reactjs.org/docs/hooks-faq.html" target="_blank" rel="noopener">react官方文档</a></li><li><a href="https://github.com/threepointone/react-act-examples/blob/master/sync.md" target="_blank" rel="noopener">https://github.com/threepointone/react-act-examples/blob/master/sync.md</a></li><li><a href="https://github.com/facebook/react" target="_blank" rel="noopener">https://github.com/facebook/react</a></li><li><a href="https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33602" target="_blank" rel="noopener">https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33602</a></li><li><a href="https://ant-design.gitee.io/components/form-cn/#components-form-demo-customized-form-controls" target="_blank" rel="noopener">https://ant-design.gitee.io/components/form-cn/#components-form-demo-customized-form-controls</a></li><li><a href="https://juejin.im/post/5c9827745188250ff85afe50" target="_blank" rel="noopener">https://juejin.im/post/5c9827745188250ff85afe50</a></li></ul>]]></content>
<summary type="html">
<p>自从 <a href="mailto:react@16.8" target="_blank" rel="noopener">react@16.8</a> 正式发布react hooks已经有一段时间了,这段时间我也一直在项目里使用hook的方式来写组件,其间也遇到了不少问题
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="react-hooks" scheme="https://frezc.github.io/tags/react-hooks/"/>
<category term="test" scheme="https://frezc.github.io/tags/test/"/>
<category term="ant-design" scheme="https://frezc.github.io/tags/ant-design/"/>
</entry>
<entry>
<title>使用typescript实现依赖注入框架</title>
<link href="https://frezc.github.io/2018/08/17/how-to-impletement-denpendency-injection-in-typescript/"/>
<id>https://frezc.github.io/2018/08/17/how-to-impletement-denpendency-injection-in-typescript/</id>
<published>2018-08-17T05:58:15.000Z</published>
<updated>2019-04-09T15:39:43.064Z</updated>
<content type="html"><![CDATA[<p>首先思考一个问题:我们为什么需要依赖注入(Dependency injection下面简称DI)?</p><p>之前用java的spring、php的laravel和angular时发现它们的模式非常相似,框架会把请求处理、线程管理、错误处理等都封装好,你只需要实现对应的横向和纵向切面,然后让框架来管理和调用你的代码,这就是设计模式中有名的<strong>控制反转</strong>(简称IOC)。</p><p>而DI是IOC的一种比较通用的实现方式,举个例子我们的web服务中有controller(接口层)和service(业务逻辑层),我们需要在controller中调用service的代码,但是service一般会有<strong>上下文(context)</strong>(比如使用了当前的请求对象、数据库连接、全局参数等)。如果我们每次在调用service时都要手动给它这么多参数实在太麻烦了,而且代码会很耦合。此时DI就能解决这个问题了,我们只需要声明需要的对象,框架就能自动创建好带有<strong>上下文</strong>对象。那么下面我们来看看怎么用ts实现一个简单的依赖注入框架。</p><hr><p><em>下文写的时候我还没有使用过nodejs写过复杂的后端服务,所以造了个简单的轮子来梳理项目代码,使用的hapijs社区也不太活跃,所以本文仅适合作为参考和学习使用。要使用nodejs开发大型应用的话建议使用<a href="https://github.com/nestjs/nest" target="_blank" rel="noopener">nest.js</a>或者<a href="https://github.com/eggjs/egg/" target="_blank" rel="noopener">eggjs</a>。</em></p><h1 id="核心API"><a href="#核心API" class="headerlink" title="核心API"></a>核心API</h1><p>先看看实现的API长什么样</p><pre><code class="typescript">import * as Knex from 'knex';import { autowired, impl, context } from '../injection';class MyController { @autowired userRepository: IUserRepository; getUsers() { return this.userRepository.getAllExistUsers(); }}// 这里用抽象类来表示接口(下面会通称为“接口”)abstract class IUserRepository { abstract getAllExistUsers(): PromiseLike<IUser[]>;}@impl(IUserRepository)class UserRepositoryImpl extends IUserRepository { @context('knex') knex: Knex; getAllExistUsers() { return this.knex('users').select().where('deleted', false); }}</code></pre><p>这里的API设计稍微参考了下spring,还有一些妥协设计(比如为什么要用<code>abstract class</code>而不用<code>interface</code>、为什么 <code>@impl</code> 需要传入对应接口),这些下面会解释。</p><h2 id="API实现原理"><a href="#API实现原理" class="headerlink" title="API实现原理"></a>API实现原理</h2><p>这里虽然实现了3个<a href="https://www.typescriptlang.org/docs/handbook/decorators.html" target="_blank" rel="noopener"><code>decorator</code></a>,但是这些<code>decorator</code>的作用其实和java里的<code>annotation</code>一样 —— 定义metadata,所以实现上很简单,基本上都是一句话就能讲清楚里面的逻辑:</p><ul><li><code>@autowired</code> (需要自动注入的变量):把当前的property key(<code>'userRepository'</code>)以及对应的type(<code>IUserRepository</code>)存到当前类的<code>metadata</code>中,方便后面注入的时候传入。</li><li><code>@impl</code> (实现某个接口的类):将当前的接口和类保存到一个全局Map<接口, 实现>。</li><li><code>@context</code>(需要注入当前应用上下文的变量):将当前key(<code>'knex'</code>)与需要注入的context key(<code>'knex'</code>)保存到当前类的<code>metadata</code></li></ul><p>下面是autowired的实现</p><pre><code class="typescript">export const metaKey = Symbol('autowiredKeys');interface IAutowiredKey { // 字段名 key: string; // 对应类型,通过metadata返回的类型必定是Object与其子类 type: Function;}export default function autowired(target: any, propertyKey: string) { const autowiredKeys = getAutowiredKeys(target); // 得到当前装饰成员变量的类型 const type = Reflect.getMetadata('design:type', target, propertyKey); autowiredKeys.push({ key: propertyKey, type }); // 将变量保存到当前类的metadata里 Reflect.defineMetadata(metaKey, autowiredKeys, target);}/** * 拿到在当前类上定义的需要自动注入的key和type */export function getAutowiredKeys(target: any): IAutowiredKey[] { return Reflect.getMetadata(metaKey, target) || [];}</code></pre><h2 id="Typescript-metadata"><a href="#Typescript-metadata" class="headerlink" title="Typescript metadata"></a>Typescript metadata</h2><p>typescript可以通过<code>metadata</code>拿到3种类型信息</p><ul><li>对象上的成员变量类型</li><li>函数的参数类型</li><li>函数的返回类型</li></ul><p>但是又有非常大的限制,可以看一下<a href="http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-4#3-basic-type-serialization_1" target="_blank" rel="noopener">这一节文章</a>,简单来说就是拿不到 <code>interface</code> 的类型,而 <code>abstract class</code> 可以,所以使用中需要用 <code>abstract class</code> 来代替 <code>interface</code> 。</p><p>另外关于 <code>@impl</code> 为什么要传入对应接口,主要是因为如果不传入接口的话,在注入<code>@autowired</code>变量时,我必须要遍历被<code>@impl</code>装饰的类来判断其是否是该变量类型的本身或者子类。</p><p>这里可能会出现一个问题,如果<code>@autowired</code>的变量类型是<code>interface</code>啥的话,由于上面提到的限制我只能拿到 <code>Object</code> 这个类型,由于所有类都是其子类,所以就会注入错误的类型了。</p><h2 id="注入"><a href="#注入" class="headerlink" title="注入"></a>注入</h2><p>关于<code>@autowired</code>字段的注入实现非常简单,实现以下几步就行了:</p><ol><li>拿到对象需要注入的字段及其类型</li><li>根据类型判断并创建需要注入的对象</li><li>递归注入上一步生成的对象,并注入上下文</li><li>将生成的对象传给成员变量</li></ol><pre><code class="typescript">const implMap: Map<any, any> = new Map();export default function impl<T, C extends T>(p1: T) { return function (ctor: C) { implMap.set(p1, ctor); }}export function injectAutowired(target: any, context: { [key: string]: any }) { const needAutowiredKeys = getAutowiredKeys(target); needAutowiredKeys.forEach(({ key, type }: { key: string, type: any }) => { const ctor = implMap.get(type); let inst = null; if (ctor && typeof ctor === 'function') { inst = new ctor(context); } else { // type must be Object inst = new type(context); } injectAutowired(inst, context); injectContext(inst, context); target[key] = inst; });}</code></pre><h1 id="路由设计"><a href="#路由设计" class="headerlink" title="路由设计"></a>路由设计</h1><p>路由层参考<a href="https://laravel.com/docs/5.6/routing" target="_blank" rel="noopener">laravel</a>框架,因为我个人认为将路由放在一个地方同一管理比spring那种分散到<code>Controller</code>上定义要方便索引(<code>api -> controller</code>)。</p><p>提供的API如下</p><pre><code class="typescript">import { Route } from '../injection';const route = new Route();// 设置放置controllers的目录,默认是 ${work directory}/controllersroute.setControllersRoot('server/controllers');// 指定Controller的method作为handlerroute.post('/apples/{id}', 'SampleController@updateApple');route.get('/users', 'SampleController@getUsers');// 直接传入函数作为hanlderroute.match(['get', 'post'], '/healthz', () => 'ok');// prefixroute.prefix('admin').group((r) => { r.post('users/{id}/ban', 'AdminController@banUser');})export default route;</code></pre><p>这里除了将 <code>Controller</code> 引入并绑定到对应的 <code>path</code> 上外,还要检测对应的方法是否<strong>存在</strong>,这样就能将错误放在程序<strong>启动</strong>时而不是<strong>运行</strong>时抛出了。</p><h1 id="接口层的IO"><a href="#接口层的IO" class="headerlink" title="接口层的IO"></a>接口层的IO</h1><p>目前设计的API如下</p><pre><code class="typescript">// controller内class MyController { getUsers(@param id: number, @query detail: boolean = false, @payload body: Object) { return { users: [] }; } getUser(@query('name') userName: string, request: Hapi.Request, h: Hapi.ResponseToolkit) { return { users: [] }; }}// 直接传入路由的函数route.get('welcome/{name}', (name: string) => { return { name, message: `welcome ${name}` }});</code></pre><p>这里有3个<code>decorator</code>,分别代表 路径参数(<code>@param</code>)、查询参数(<code>@query</code>)、和请求体(<code>@payload</code>),作用同样是设置<code>metadata</code>。另外有一些框架特定类型的参数(<code>Hapi.Request</code>和<code>Hapi.ResponseToolkit</code>),是为了支持更加特殊的需求。</p><p>对于直接传入路由的函数,我对其的定位是“不需要复杂输入的简单逻辑”,所以只会把路径参数的指根据顺序传进去。</p><p>注入数据时需要考虑参数类型,我这里定了几个规则:</p><ul><li>如果类型是 <code>string</code>、<code>number</code>、<code>boolean</code>,那么需要将数据转为对应的基础类型</li><li>如果类型是一些特定类型,比如<code>Hapi.Request</code>,那么由对应框架的bind来判断注入</li><li>如果类型是 <code>Object</code>(可能是object、interface等),那么将数据原样返回</li><li>如果类型是 <code>Function</code>(class),分为以下的情况<ul><li>先new对应的类,如果注入的数据不是基础类型,并且对应的class的构建函数没有参数,那么将注入数据<code>Object.assign</code>给新建对象</li><li>如果对应的class的构建函数有参数,或注入的数据是基础类型,那么将注入数据传入class的构建函数</li></ul></li></ul><p>返回类型和异常处理都是目前是由<code>Hapi.js</code>自己处理的,还没研究过<code>express</code>这些库的处理方式,不过应当遵循下面的规则:</p><ul><li>返回类型应当支持所有能JSON序列化的值和<code>Promise</code>。</li><li>抛出异常应当可以直接<code>throw</code>,并有一个统一处理方法</li></ul><h1 id="项目结构"><a href="#项目结构" class="headerlink" title="项目结构"></a>项目结构</h1><p>因为对于依赖注入的API来说<code>controllers</code>、<code>services</code>和<code>repositories</code>都是一样的,所以项目结构其实可以由自己的项目情况决定,不过建议分为以下几个层面:</p><ul><li><code>controllers</code>: 负责接口IO处理,表单验证,流程控制</li><li><code>services</code>: 负责业务模块逻辑</li><li><code>repositories</code>: DAO层,负责与数据库打交道</li><li><code>models</code>: 数据模型</li><li><code>routes.ts</code>: 定义路由</li><li><code>app.ts</code>: 项目的启动、配置</li></ul><h1 id="绑定Hapi-js"><a href="#绑定Hapi-js" class="headerlink" title="绑定Hapi.js"></a>绑定Hapi.js</h1><p>目前在项目里用到的service端实现是<code>hapi.js</code>,所以讲讲injection与<code>hapi.js</code>的bind需要实现的功能:</p><ul><li>根据路由配置生成hapi的路由配置</li><li>在handler里注入所有的接口依赖、上下文依赖以及方法的参数依赖</li></ul><pre><code class="typescript">import { injectAutowired, injectContext, callHanlderWithInjection } from '../injection';// 生成Hapi route handler的函数function createControllerHandler<T extends IClassType>(Controller: T, methodName: string, context: { [key: string]: any }) { return (request: Hapi.Request, h: Hapi.ResponseToolkit, err?: Error): Hapi.Lifecycle.ReturnValue => { // 将请求对象绑定到当前上下文 const contextInLifecycle = Object.assign({ request }, context); const c: any = new Controller(contextInLifecycle); injectAutowired(c, contextInLifecycle); injectContext(c, contextInLifecycle); return c[methodName](request, h, err); }; }</code></pre><h1 id="Todo"><a href="#Todo" class="headerlink" title="Todo"></a>Todo</h1><ul><li>路由层的权限控制</li><li>更加通用的参数验证</li><li>更加通用的错误处理</li><li>更加通用的Request与Resposne结构</li><li>DAO层使用ORM</li><li>实现Laravel里的Facades模式?</li><li>利用typescript的compiler解决上面的局限问题</li></ul>]]></content>
<summary type="html">
<p>首先思考一个问题:我们为什么需要依赖注入(Dependency injection下面简称DI)?</p>
<p>之前用java的spring、php的laravel和angular时发现它们的模式非常相似,框架会把请求处理、线程管理、错误处理等都封装好,你只需要实现对应的
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="typescript" scheme="https://frezc.github.io/tags/typescript/"/>
<category term="web service" scheme="https://frezc.github.io/tags/web-service/"/>
</entry>
<entry>
<title>Grafana的auto decimal实现以及与G2结合使用</title>
<link href="https://frezc.github.io/2018/08/02/auto-decimal-of-grafana/"/>
<id>https://frezc.github.io/2018/08/02/auto-decimal-of-grafana/</id>
<published>2018-08-02T07:25:03.000Z</published>
<updated>2019-06-03T14:21:59.369Z</updated>
<content type="html"><![CDATA[<h1 id="Why-auto-decimals"><a href="#Why-auto-decimals" class="headerlink" title="Why auto decimals?"></a>Why auto decimals?</h1><p>现在即席查询里的单位格式化用的是从grafana里copy出来的kbn,kbn里的format函数的参数除了value外通常还有<code>decimals</code>和<code>scaledDecimals</code>。</p><p><code>decimals</code>指的是传统意义上的保留小数位,如1.23对应的就是2。</p><p><code>scaledDecimals</code>指的则是单位转换过后的保留小数位,比如kbn转换时间时会默认将超过1000的时间进位,1234ms -> 1.23s,这里1.23就是scaledDecimals = -1时的结果(单位转换时会增加decimals,这里从ms -> s就增加了3位decimals)。</p><p>decimals和scaledDecimals的使用规则是,如果没有scaledDecimals统一使用decimals,有的话在发生单位转换时使用scaledDecimals,其他情况使用decimals。</p><p>下面要讲的auto decimals就是用来计算这两个值的。</p><h1 id="grafana的auto-decimals算法"><a href="#grafana的auto-decimals算法" class="headerlink" title="grafana的auto decimals算法"></a>grafana的auto decimals算法</h1><p>这里主要讲的是graph panel下的auto decimals,graph下tick的decimals和tooltip稍有不同,后者的decimals多一位,scaledDecimals多两位</p><p><img src="nonescaled-deciamls.png"><img src="scaled-decimals.png"></p><p>主要实现代码在 <code>grafana\public\app\core\utils\ticks.ts</code> 文件的 <code>getFlotTickDecimals(datamin, datamax, axis, height)</code> 函数。</p><pre><code class="javascript">export function getFlotTickDecimals(datamin, datamax, axis, height) { const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax); const noTicks = 0.3 * Math.sqrt(height); const delta = (max - min) / noTicks; const dec = -Math.floor(Math.log(delta) / Math.LN10); const magn = Math.pow(10, -dec); // norm is between 1.0 and 10.0 const norm = delta / magn; let size; if (norm < 1.5) { size = 1; } else if (norm < 3) { size = 2; // special case for 2.5, requires an extra decimal if (norm > 2.25) { size = 2.5; } } else if (norm < 7.5) { size = 5; } else { size = 10; } size *= magn; const tickDecimals = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1); // grafana addition const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); return { tickDecimals, scaledDecimals };}</code></pre><p>从这个函数的第二行可以发现grafana是通过图表的height来动态计算y轴的<code>tick count</code>,然后得到 <code>delta = tick interval</code> 来进行接下来的运算。</p><p>计算decimals比较简单,在代码的27行,基本上就是求以10为底的delta的对数,并且对于超过10的数字decimals都为0,所以在grafana里要显示比较大数字的小数,如10.012,就必须要手动设置decimals。</p><p>计算scaledDecimals的方法比较奇怪,首先算出了delta的位数的最小值 magn (例:9999 -> 1000,424 -> 100),然后算出了在1~10内的标准值 norm (例:9999 -> 9.999,424 -> 4.24)。接下来是一堆 if else 组成的分段函数根据 norm 求得 size * magn,最后用decimals减以10为底的size对数得到scaledDecimals。这个计算方法奇怪之处是根据 norm 求 size 的部分,因为这块size小于10是完全不会影响结果的,不知道为什么要分开这么多段。(可能是代码写错或者我没考虑到一些特殊情况)</p><p>这种算法在一般情况下还是很适合的,比如下面3张图,第一张的interval = 25k,算得的scaledDecimals刚好抵消了单位转换带来的额外小数;第二张的interval = 10000,算得的scaledDecimals刚好是-4,而单位转换的额外decimals是6(0 -> k -> Mil,每次进位多3位小数),所以保留了2位小数;第三张没有转换所以用的是由interval = 0.1得到的decimals = 1。</p><p><img src="decimalsexamples1.png"><img src="decimalsexamples2.png"><img src="decimalsexamples3.png"></p><h1 id="接入即席查询"><a href="#接入即席查询" class="headerlink" title="接入即席查询"></a>接入即席查询</h1><p>肯定不能简单地将grafana里的代码copy过来直接用,因为G2计算ticks interval的算法和grafana里不一样,在尽量少修改的基础上想了2个方案</p><h2 id="因为G2可以自定tick-count和interval,或许可以考虑直接用grafana的利用高度计算ticks-count的算法?"><a href="#因为G2可以自定tick-count和interval,或许可以考虑直接用grafana的利用高度计算ticks-count的算法?" class="headerlink" title="因为G2可以自定tick count和interval,或许可以考虑直接用grafana的利用高度计算ticks count的算法?"></a>因为G2可以自定tick count和interval,或许可以考虑直接用grafana的利用高度计算ticks count的算法?</h2><p>我觉得grafana计算tick count的算法还是挺不错的,不过使用场景有限:只能用于直角坐标系、只能用于连续型数据、每次高度变化都要去更新scale的format函数(会导致G2重新处理数据,性能上可能会有问题)。</p><h2 id="拿到G2计算好的ticks然后传给计算auto-decimals的函数。"><a href="#拿到G2计算好的ticks然后传给计算auto-decimals的函数。" class="headerlink" title="拿到G2计算好的ticks然后传给计算auto decimals的函数。"></a>拿到G2计算好的ticks然后传给计算auto decimals的函数。</h2><p>这里麻烦的是拿到G2计算的ticks,虽然可以通过处理好数据的G2对象拿到ticks,但是这样意味着每次都要渲染两次,不太合理。所以还是要主动调用相关的api来 计算,因为G2没有暴露相关的api,所以会用一些比较hack的方式去调用。</p><p>需要的计算ticks的函数在 <code>@antv\scale\src\auto\number.js</code> 里,这个函数看起来参数比较多,其实只要传入min、max就可以计算了,但是为了兼容之后可能会添加的列定义表单,还是找更为上层的封装更合适。于是利用位于 <code>g2\src\chart\controller\scale.js</code> 的 <code>ScaleController</code> ,这个类只需要传入转换后的Scale,并调用其 <code>createScale</code> 方法来得到所需结果。</p>]]></content>
<summary type="html">
<h1 id="Why-auto-decimals"><a href="#Why-auto-decimals" class="headerlink" title="Why auto decimals?"></a>Why auto decimals?</h1><p>现在即席查询里的
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="grafana" scheme="https://frezc.github.io/tags/grafana/"/>
<category term="g2" scheme="https://frezc.github.io/tags/g2/"/>
<category term="visualization" scheme="https://frezc.github.io/tags/visualization/"/>
</entry>
<entry>
<title>如何修复错误的merge提交</title>
<link href="https://frezc.github.io/2018/04/30/how-to-fix-wrong-merge/"/>
<id>https://frezc.github.io/2018/04/30/how-to-fix-wrong-merge/</id>
<published>2018-04-30T15:39:49.000Z</published>
<updated>2019-04-09T15:39:43.132Z</updated>
<content type="html"><![CDATA[<p>因为最近帮别人解决了一次git merge丢代码的情况,当时处理地简单粗暴,reset到merge前一次commit后重来一遍。后来又设想了一下,如果merge后又commit很多次,这样岂不会很麻烦?如果这个分支不允许force push,那这样做也不行。于是就去查了一些资料,自己尝试并总结了一下几个方法。</p><hr><h1 id="用git-reset重新来过"><a href="#用git-reset重新来过" class="headerlink" title="用git reset重新来过"></a>用git reset重新来过</h1><p>这种方法很简单,就是<code>git reset</code>到merge之前的一次commit,然后重新merge,然后如果之前在merge之后还有commit的话利用<code>git cherry-pick <commit start>^...<commit end></code>apply到当前分支。最后<code>git push --force</code>到远端分支(如果之前已经push了的话)</p><p>这种方法适合你在push之前就发现了错误的情况,因为用了<code>git reset</code>后当前分支就必须要<strong>force push</strong>了。</p><p>force push有什么问题?<br>一般来说不应该在非私人分支上force push,因为这有可能会覆盖掉其他人在该分支上的commit。而且远端的protected branch是不能直接force push的。</p><h1 id="更好的方法?"><a href="#更好的方法?" class="headerlink" title="更好的方法?"></a>更好的方法?</h1><p>首先解决第一个问题,如何不用force push?</p><p>如果我们不修改要push分支的历史的话是不需要force push的,那么很简单了,只要把需要修改历史的reset操作在另外一个不需要push的分支上操作就行了。</p><p>实际操作就是从当前分支拉一条新分支出来,然后进行上面reset的方法,最后再rebase或merge回当前分支。</p><p>既然这里也提到了rebase,其实我们还可以简化一下上面的方法。</p><pre><code class="sh"># 假设我们这里merge feature到master# 使用第一种方法git checkout -b fix-mergegit reset <commit before merge> --hardgit merge feature # fix conflictgit cherry-pick <commit start>^...<commit end> # maybe need fix conflictgit checkout mastergit merge fix-merge # fix conflict# 用rebase简化一下,因为rebase也是一个会修改历史的操作,所以还是需要新开分支git checkout -b fix-merge# 用rebase代替了reset > merge > cherry-pick的操作git rebase -i <commit before merge> # fix conflictgit checkout mastergit merge fix-merge # fix conflict</code></pre><p>这里在rebase时添加了<code>-i</code>的参数,可以清楚地展示和操作每次会应用的commit。如果不懂rebase可以看看<a href="https://git-scm.com/docs/git-rebase" target="_blank" rel="noopener">官方文档</a>。</p><h1 id="用上面的方法要处理的冲突太多了怎么办?"><a href="#用上面的方法要处理的冲突太多了怎么办?" class="headerlink" title="用上面的方法要处理的冲突太多了怎么办?"></a>用上面的方法要处理的冲突太多了怎么办?</h1><p>无论是cherry-pick还是rebase都是将commit一次次给apply,所以merge之后的commit如果有冲突,那也得一次次解决,有时候一直要改同一个文件的话,那解决冲突就是很痛苦的一件事了。</p><p>这里有两个方案可以考虑一下:</p><ol><li><p>其实在第一种方法中完全没必要用cherry-pick,新分支在重新merge后可以直接merge到当前分支的。当然这样还是一样要处理很多冲突,只不过可以一次处理完了。</p><pre><code class="sh">git checkout -b fix-mergegit reset <commit before merge> --hardgit merge feature # fix conflictgit checkout mastergit merge fix-merge # fix large conflict</code></pre></li><li><p>如果是丢了代码并知道丢的是哪次提交的代码,那么可以直接<code>git cherry-pick <commit id></code>。</p></li><li><p>直接改代码。</p></li></ol><h1 id="git-revert可以解决问题吗?"><a href="#git-revert可以解决问题吗?" class="headerlink" title="git revert可以解决问题吗?"></a>git revert可以解决问题吗?</h1><p>我在网上也看到很多用<code>git revert</code>来解决错误merge的方法,但是这样做只能<strong>撤销merge</strong>而无法去<strong>修复merge</strong>,而且就算提交了revert commit,当前分支也不能重新merge,因为revert只是普通的commit而不能改变已经存在的merge commit。</p>]]></content>
<summary type="html">
<p>因为最近帮别人解决了一次git merge丢代码的情况,当时处理地简单粗暴,reset到merge前一次commit后重来一遍。后来又设想了一下,如果merge后又commit很多次,这样岂不会很麻烦?如果这个分支不允许force push,那这样做也不行。于是就去查了一些
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="git" scheme="https://frezc.github.io/tags/git/"/>
</entry>
<entry>
<title>恢复更新</title>
<link href="https://frezc.github.io/2017/12/16/%E8%BF%99%E4%B8%AA%E5%8D%9A%E5%AE%A2%E4%B8%8D%E4%BC%9A%E5%86%8D%E6%9B%B4%E6%96%B0%E4%BA%86/"/>
<id>https://frezc.github.io/2017/12/16/这个博客不会再更新了/</id>
<published>2017-12-16T11:18:38.000Z</published>
<updated>2019-04-27T16:36:08.745Z</updated>
<content type="html"><![CDATA[<p>本来打算就用<a href="https://github.com/Frezc/Blog/issues" target="_blank" rel="noopener">issue</a>写文章了,但是写了一点后发现github上还是太严肃了,不太好瞎扯些非技术性地东西,所以还是换回hexo写了。。</p><p>这次花了点时间把typecho上的文章都迁了,有点心思想把动漫排行捡起来,不过匹配关联这块要自动化确实不太好做,手工的话一个人没什么精力。</p>]]></content>
<summary type="html">
<p>本来打算就用<a href="https://github.com/Frezc/Blog/issues" target="_blank" rel="noopener">issue</a>写文章了,但是写了一点后发现github上还是太严肃了,不太好瞎扯些非技术性地东西,所以
</summary>
</entry>
<entry>
<title>webpack和webpack-dev-server的配置备忘</title>
<link href="https://frezc.github.io/2016/08/06/webpack%E5%92%8Cwebpack-dev-server%E7%9A%84%E9%85%8D%E7%BD%AE%E5%A4%87%E5%BF%98/"/>
<id>https://frezc.github.io/2016/08/06/webpack和webpack-dev-server的配置备忘/</id>
<published>2016-08-06T14:17:00.000Z</published>
<updated>2019-04-09T15:39:43.454Z</updated>
<content type="html"><![CDATA[<p>自从上次用webpack写web应用感觉已经过了一段时间了,这次重新拾起又花了不少时间。</p><p>为了下次能更快地使用webpack开发,决定写篇文章把配置的问题记录下来。</p><a id="more"></a><h3 id="webpack基本配置"><a href="#webpack基本配置" class="headerlink" title="webpack基本配置"></a>webpack基本配置</h3><ul><li><p>devtool: 在开发中可以使用<code>'eval'</code>(打包速度快)、<code>'inline-source-map'</code>(打包速度慢,但在浏览器里能直接查看编译前的代码)等值;生成环境中不要设置或设为<code>false</code></p></li><li><p>entry: 结构可以是</p><pre><code class="javascript"> entry: { app: APP_DIR + '/app.js', vendor: APP_DIR + '/scripts/vendor.js', login: [ APP_DIR + '/login.js' ] }</code></pre></li></ul><ul><li><p>output: 注意下<code>publicPath</code>,要设置为打包后资源的url路径</p></li><li><p>module: 注意下scss的loaders的写法</p><pre><code class="javascript"> // 开发中 loaders: ['style', 'css', 'postcss', 'sass'] // 生成环境 导出到一个css文件 loader: ExtractTextPlugin.extract('style', ['css', 'postcss', 'sass'])</code></pre></li></ul><ul><li><p>resolve: 可以用<code>extensions</code>来指定<code>import</code>时可以省略的后缀名;可以用<code>alias</code>指定从非npm引入的库,如<code>jquery: path.resolve(__dirname, './bower_components/jquery/dist/jquery.js')</code>,这样在<code>import</code>时可以替换引用路径</p></li><li><p>externals: 指定从全局引入的库,如<code>jquery: "jQuery"</code>,这样在<code>require('jquery')</code>时会引入<code>jQuery</code>对象</p></li></ul><h3 id="常用插件"><a href="#常用插件" class="headerlink" title="常用插件"></a>常用插件</h3><ul><li><p>ProvidePlugin: 定义一些在import时能自动引入的变量,如定义了<code>$: 'jquery'</code>后,可以在文件中直接使用$,webpack可以自动帮你加上<code>var $ = require('jquery')</code></p></li><li><p>CommonsChunkPlugin: 将多个entry里的公共模块提取出来放到一个文件里,这个插件可以用来将库和自己代码分离</p></li><li><p>DllPlugin: 将一些模块预编译,类似windows里的dll,可以在项目中直接使用,无需再构建。注意要在output中指定<code>library</code>,并在DllPlugin中指定与其一致的<code>name</code>,在有多个入口时可以使用<code>[name]</code>和<code>[hash]</code>来区分,因为这个参数是要赋值到global上的,所以这里使用<code>[hash]</code>不容易出现变量名冲突的情况</p></li><li><p>DllReferencePlugin: 引用之前打包好的dll文件,注意下context参数,这个应该根据manifest.json文件中的引用情况来赋值,如果引用的都是npm安装的库,这里就填项目根目录就好了</p></li><li><p>NoErrorsPlugin: 在打包时不会因为错误而中断</p></li><li><p>DefinePlugin: 可以定义编译时的全局变量,有很多库(React, Vue等)会根据<code>NODE_ENV</code>这个变量来判断当前环境。为了尽可能减少包大小,在生产环境中要定义其为<code>JSON.stringify("production")</code></p></li><li><p>optimize.UglifyJsPlugin: 配置压缩代码,如</p><pre><code class="javascript"> compress: { unused: true, dead_code: true, warnings: false }</code></pre></li></ul><ul><li><p>optimize.OccurrenceOrderPlugin: 可以减少文件大小</p></li><li><p>optimize.DedupePlugin: 可以减少重复文件数</p></li><li><p>ExtractTextPlugin: 可以将所有css文件打包到一个css文件中,配置见loader</p></li></ul><h3 id="webpack-dev-server和react-hot-loader的相关配置"><a href="#webpack-dev-server和react-hot-loader的相关配置" class="headerlink" title="webpack-dev-server和react-hot-loader的相关配置"></a>webpack-dev-server和react-hot-loader的相关配置</h3><p>如果只进行客户端开发,不适用nodejs渲染的话,推荐安装webpack-dev-server CLI,可以省去很多配置。</p><p>首先看看cli里常用的几项配置:</p><ul><li><p>–port=8080:这项指定了服务器端口,相当于在entry中加上<code>webpack-dev-server/client?http://0.0.0.0:8080</code>,前者是后者的简便用法,注意这两个不要重复。</p></li><li><p>–hot:开启热替换功能。如果要使用react-hot-loader,这项是必须要开启的;如果只是想在更新代码后自动刷新页面,则不需要。这项等同于在<code>plugins</code>中添加<code>new webpack.HotModuleReplacementPlugin()</code>,同样注意不要重复。</p></li><li><p>–inline:这个的功能是为了能在你自己的测试服务器上获取的html页面中获取webpack-dev-server动态生成的js文件,基本上如果要用webpack-dev-server这个是必要的。详情说明见<a href="http://webpack.github.io/docs/webpack-dev-server.html#combining-with-an-existing-server" target="_blank" rel="noopener">文档</a>。这个参数会在entry里加上<code>webpack/hot/dev-server</code>,注意不要重复了。<br> 虽然加了这项就能变更后自动刷新页面了,但是要用hmr还有几点要注意:</p><ol><li>output.publicPath一定要使用完整的url,如<code>"http://localhost:8080/dist/"</code>,端口要和wds相同,不然会出现跨域的错误提醒。</li><li>在html页面中当然也要以完整的url来引入。</li></ol></li></ul><p>如果要写同构应用的话,使用<a href="https://github.com/glenjamin/webpack-hot-middleware" target="_blank" rel="noopener">webpack-hot-middleware</a>并按照文档的写法就可以了。</p>]]></content>
<summary type="html">
<p>自从上次用webpack写web应用感觉已经过了一段时间了,这次重新拾起又花了不少时间。</p>
<p>为了下次能更快地使用webpack开发,决定写篇文章把配置的问题记录下来。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="webpack" scheme="https://frezc.github.io/tags/webpack/"/>
<category term="webpack-dev-server" scheme="https://frezc.github.io/tags/webpack-dev-server/"/>
</entry>
<entry>
<title>给RN的app添加widgets</title>
<link href="https://frezc.github.io/2016/06/26/%E7%BB%99RN%E7%9A%84app%E6%B7%BB%E5%8A%A0widgets/"/>
<id>https://frezc.github.io/2016/06/26/给RN的app添加widgets/</id>
<published>2016-06-25T18:12:00.000Z</published>
<updated>2019-04-09T15:39:43.622Z</updated>
<content type="html"><![CDATA[<p>widgets应该是android平台上最有用的特性之一了吧,将应用的数据利用一个小视图嵌入其他应用(如桌面),可以快速地获得app的状态而不用启动app。</p><p>最近用react-native写了一个<a href="https://github.com/Frezc/TodoLite" target="_blank" rel="noopener">todo app</a>,感觉将计划利用widgets直接在桌面上展现出来会十分的方便,于是就给这个app加了widgets。<br>当然rn没有给我们提供这方面的支持,毕竟是android端的东西,只能直接写原生了。虽然很久没写过原生android了,但是看看文档还是没有问题的。</p><a id="more"></a><h3 id="appwidgets基础"><a href="#appwidgets基础" class="headerlink" title="appwidgets基础"></a>appwidgets基础</h3><p>首先可以看看<a href="https://developer.android.com/guide/topics/appwidgets/index.html" target="_blank" rel="noopener">官方文档</a>吧,虽然它里面讲的东西在Android studio上点击New->Widget->App widget就能帮你生成好,不过看看文档了解下配置参数和AppWidgetProvider的原理还是不错的。</p><p>由于我这里要用到ListView,还是要按照文档的内容进行一下修改,主要就下面的5个文件(还要在AndroidManifest.xml注册接收者和服务):</p><ul><li>TodoWidget: AppWidgetProvider的子类,用来生成和更新根view。</li><li>TodoWidgetService: 用来生成ListView中每项视图的服务</li><li>todo_widget.xml: 每项视图的layout文件</li><li>todos_widget.xml: 根视图的layout文件</li><li>todo_widget_info.xml: 配置文件</li></ul><p>这些按照文档里的写就可以了,不过有点需要注意的是如果你选了最小宽度为4格,minWidth会给你填上250dp,实际在android上运行的时候这个widget的最小宽度是3格。我去github上看到一个开源app里appwidiget的<a href="https://github.com/benniaobuguai/opencdk-appwidget/blob/master/app/src/main/res/xml/widget_xml_news.xml" target="_blank" rel="noopener">配置文件</a>里看到4格写的是294dp,试了下这个数值没什么问题就直接用了。</p><h3 id="appwidgets里得到app的数据"><a href="#appwidgets里得到app的数据" class="headerlink" title="appwidgets里得到app的数据"></a>appwidgets里得到app的数据</h3><p>appwidgets里使用的数据最好是持久化的,如果你想让widgets自动更新的话(widgets默认会有一个更新周期)。RN里提供了持久化api-AsyncStorage,那么原生app中怎么从中得到数据呢?</p><p>看看AsyncStorage的源码里的这条语句</p><pre><code class="javascript">// Use RocksDB if available, then SQLite, then file storage.var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyncFileStorage;</code></pre><p>可见一般情况下在android平台上是通过SQLite进行存取的。<br>另外在源码的<code>/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java</code>中我们可以得到数据库名、表名和表中每列的名称。实际上AsyncStorage只是将k-v值直接保存在一张表里而已。<br>那么从数据库中读取数据应该是很简单了,只要在每次更新时进行读取就行了,也就是在<code>RemoteViewsFactory</code>的<code>onCreate</code>和<code>onDataSetChanged</code>中读取。(ps:你当然可以通过网络请求来获取数据,rn使用了okhttp+fresco,你可以在app中直接使用这两个库)</p><p>由于我们在rn中保存时一般是转成json字符串的,所以读取后还需要解析成对象才能使用,这里用一个你会熟悉的解析库就行了。(RN自带了一个jackson-core库,不过这个用起来不是那么方便,你可以再引入jackson-databind就会好用很多了)</p><p>另外有一点要注意的是,app在没有保存任何东西的情况下,AsyncStorage对应的表是不会创建的,这时候直接读取这张表肯定会报错。<br><strong>解决方案</strong>:捕获一下异常即可</p><pre><code class="java">private String queryFromDB(SQLiteDatabase db, String key) { // try-with-resource need min-api 19 Cursor c = null; try { c = db.rawQuery("select * from " + TABLE_CATALYST + " where " + KEY_COLUMN + " = ?", new String[]{key}); if (c.getCount() > 0) { c.moveToFirst(); String result = c.getString(c.getColumnIndex(VALUE_COLUMN)); c.close(); return result; } } catch (SQLiteException ignored) { } finally { if (c != null) c.close(); } return "";}</code></pre><h3 id="app响应点击事件"><a href="#app响应点击事件" class="headerlink" title="app响应点击事件"></a>app响应点击事件</h3><p>设置点击事件的PendingIntent就按照文档的写法就可以了,如果要在rn中得到点击传递的参数的话,还要写一个原生模块来获取当前Activity的Intent对象并去除数据。</p><p>这个部分可以参考<a href="https://github.com/Neson/react-native-system-notification" target="_blank" rel="noopener">react-native-system-notification</a>这个开源库里的写法,不过在点击时app是不会再前台运行的(目前来说是这样的),所以点击后只要startActivity并在Intent中带上<code>FLAG_ACTIVITY_CLEAR_TOP</code>这个flag就行了。然后在rn中通过原生模块获得Intent中带的数据。</p><p>具体完成的样子就是这样了<br><img src="/images/2016/06/2195390602.png" alt="todolite_widget.png"></p><p>点击后的跳转<br><img src="/images/2016/06/4092440308.jpg" alt="todolite_widget_click.jpg"></p><h3 id="8-1更新-关于RemoteViews的一个BUG"><a href="#8-1更新-关于RemoteViews的一个BUG" class="headerlink" title="[8.1更新] 关于RemoteViews的一个BUG ?"></a>[8.1更新] 关于RemoteViews的一个BUG ?</h3><p>之前发现在appwidgets里如果description没有数据会显示<code>"No description."</code>,第一次滚动下来时会显示正确的结果,不过重新滚回去再滚下来显示就会出现异常。</p><p>Debug半天发现取得和生成的字符串没问题后,仔细观察了下异常数据,发现异常的数据似乎是前面项遗留的数据。我之前只是简单地判断了下有description时更新到视图中,于是我再加了条在没有description把显示值设置为<code>"No description."</code>,发现就没问题了。看起来是由于RemoteViews重用而导致的BUG,还真是个坑啊。</p>]]></content>
<summary type="html">
<p>widgets应该是android平台上最有用的特性之一了吧,将应用的数据利用一个小视图嵌入其他应用(如桌面),可以快速地获得app的状态而不用启动app。</p>
<p>最近用react-native写了一个<a href="https://github.com/Frezc/TodoLite" target="_blank" rel="noopener">todo app</a>,感觉将计划利用widgets直接在桌面上展现出来会十分的方便,于是就给这个app加了widgets。<br>当然rn没有给我们提供这方面的支持,毕竟是android端的东西,只能直接写原生了。虽然很久没写过原生android了,但是看看文档还是没有问题的。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="android" scheme="https://frezc.github.io/tags/android/"/>
<category term="react native" scheme="https://frezc.github.io/tags/react-native/"/>
<category term="widgets" scheme="https://frezc.github.io/tags/widgets/"/>
</entry>
<entry>
<title>React的渲染性能优化</title>
<link href="https://frezc.github.io/2016/04/25/React%E7%9A%84%E6%B8%B2%E6%9F%93%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
<id>https://frezc.github.io/2016/04/25/React的渲染性能优化/</id>
<published>2016-04-24T17:20:00.000Z</published>
<updated>2019-04-09T15:39:43.254Z</updated>
<content type="html"><![CDATA[<p>都说网页的性能瓶颈在DOM操作,所以目前的主流前端框架(React、 Vue、AngularJs等)都在极力地减少DOM操作。</p><p>就React而言,使用Virtual DOM和一个diff算法(实际就是使用一个uid来判断)来尽可能的重用现有的DOM,以此来减少DOM操作(毕竟添加比更新要耗时)。</p><p>但是在一些需要高帧数刷新的组件里,即使是Virtual DOM的re-render也会显得十分耗时,那么我们看看怎么进行优化。</p><a id="more"></a><h2 id="渲染速度问题"><a href="#渲染速度问题" class="headerlink" title="渲染速度问题"></a>渲染速度问题</h2><p>比如需要实现这么一个列表</p><pre><code><div> <Indicator percent={this.state.percent} /> { this.state.items.map(item => <Item item={item} /> ) }</div></code></pre><p>这个组件我们需要监听<code>scroll</code>事件,在处理函数里更新state中的percent属性。<br>默认React不会进行任何优化处理,就这么写的话会发现在滚动页面时会有卡顿感,我用自己项目的一个列表项测试了一下(<a href="https://github.com/Frezc/Share-Favors-App/blob/master/src/components/RepoAbList.js" target="_blank" rel="noopener">RepoAbList.js</a>),打印了scroll开始、结束和render开始的时间,如下。<br><img src="/images/2016/04/4058937871.png" alt="scroll-update-all.png"></p><p>下面是滚动时不进行render的情况<br><img src="/images/2016/04/1059717184.png" alt="scroll-no-update.png"></p><p>可以发现正常情况scroll回调的间隔是16、17ms,如果render的时间超过这个值就会影响到scroll的刷新率。虽然react更新dom是异步进行的,但是对Virtual DOM的渲染时同步的。所以这里scroll也得等render结束才能继续。</p><p>那么就必须去优化一下Virtual DOM的渲染速度了。这里我们可以打印一下列表项中的每个<code>Item</code>的render时间,就能发现虽然每项都只用了1、2毫秒来渲染,但是加起来就不少了。</p><h2 id="shouldComponentUpdate"><a href="#shouldComponentUpdate" class="headerlink" title="shouldComponentUpdate"></a>shouldComponentUpdate</h2><p>React也有给出<a href="https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate" target="_blank" rel="noopener">解决方案</a>,<code>shouldComponentUpdate</code>在默认情况下是返回<code>true</code>的,也就是说只要<code>props</code>或<code>state</code>改变,这个组件就会更新,同样子组件由于<code>props</code>的改变也会更新,具体情况可以参考<a href="https://facebook.github.io/react/docs/advanced-performance.html#shouldcomponentupdate-in-action" target="_blank" rel="noopener">这里</a>。</p><p>由于列表的Virtual DOM在每次render时都是重新生成的,所以就不要指望<code>vDOMEq</code>相同了,最简单的方法就是实现每个<code>Item</code>的<code>shouldComponentUpdate</code>方法(通过比较当前和传入的<code>props</code>来判断是否需要更新,由于属性为<code>object</code>需要判断内部的变化,或许我们需要<a href="https://facebook.github.io/immutable-js/" target="_blank" rel="noopener">immutable-js</a>)。</p><h2 id="PureRenderMixin"><a href="#PureRenderMixin" class="headerlink" title="PureRenderMixin"></a>PureRenderMixin</h2><p>React提供的<code>PureRenderMixin</code>其实已经帮我们做好了上面需要做的事,按<a href="https://facebook.github.io/react/docs/pure-render-mixin.html" target="_blank" rel="noopener">文档</a>中的做法就行了。不过这个插件会比较所有的属性,在某些情况下可能会和预期相违。比如传入一个函数的情况,可以看看我提的一个<a href="https://github.com/facebook/react/issues/6601" target="_blank" rel="noopener">Issue</a>。</p><p>官方要保持这个插件的单一性,所以要忽略函数的话需要我们自己实现一下。<br>我们可以使用<a href="https://facebook.github.io/react/docs/shallow-compare.html" target="_blank" rel="noopener">Shallow Compare</a>这个插件来十分简便地完成,只要筛选掉原来props对象中的类型为<code>'function'</code>属性,再调用<code>Shallow Compare</code>就可以了。</p>]]></content>
<summary type="html">
<p>都说网页的性能瓶颈在DOM操作,所以目前的主流前端框架(React、 Vue、AngularJs等)都在极力地减少DOM操作。</p>
<p>就React而言,使用Virtual DOM和一个diff算法(实际就是使用一个uid来判断)来尽可能的重用现有的DOM,以此来减少DOM操作(毕竟添加比更新要耗时)。</p>
<p>但是在一些需要高帧数刷新的组件里,即使是Virtual DOM的re-render也会显得十分耗时,那么我们看看怎么进行优化。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="performance" scheme="https://frezc.github.io/tags/performance/"/>
</entry>
<entry>
<title>谈谈4月新番</title>
<link href="https://frezc.github.io/2016/04/18/%E8%B0%88%E8%B0%884%E6%9C%88%E6%96%B0%E7%95%AA/"/>
<id>https://frezc.github.io/2016/04/18/谈谈4月新番/</id>
<published>2016-04-17T17:39:00.000Z</published>
<updated>2019-04-09T15:39:54.105Z</updated>
<content type="html"><![CDATA[<p>跳过了整个1月番后,感觉上都已经好久没追番了。说实话近期的新番实在不对我胃口,于是空闲时把逆转无赖开司和混沌武士给补了。</p><p>不过这次的4月番看起来还是不错了,总之又有动力去追了,到目前为止看了一些比较感兴趣的,其中倒是有几部十分有潜力呢。<br>下面就稍微谈谈</p><a id="more"></a><h3 id="迷家"><a href="#迷家" class="headerlink" title="迷家"></a>迷家</h3><p>冈妈的大名使得大家对这部番的走向想的出奇地一致啊,从前两集看来结局或许是那么回事。</p><p>这部番的第一个显而易见的特点就是角色多,为了展现每个人的特点特地花了半集来做自我介绍。在12话的时间内要写出这么多角色的特点还是十分困难的,如果写不出特点那就和普通的路人无二了。<br>不过从前两集来看人物描写的还是十分有趣的,希望别没说出自己的故事就莫名其妙的挂了啊。</p><h3 id="JOJO第四部"><a href="#JOJO第四部" class="headerlink" title="JOJO第四部"></a>JOJO第四部</h3><p>JOJO第三部说实话并没有期望中的那么好看,有几段让我印象深刻(赌徒达比、伊奇那几段),但是最后和屌爷大战时过程就没那么精彩了。<br>这次的不灭钻石当然还是很期待的。</p><h3 id="逆转裁判"><a href="#逆转裁判" class="headerlink" title="逆转裁判"></a>逆转裁判</h3><p>原作游戏本就没有什么引人入胜的剧情,更多的是找证物在法庭上反驳证人的乐趣,看到动画这么还原,只能是fans向了吧。</p><h3 id="当女孩遇到熊"><a href="#当女孩遇到熊" class="headerlink" title="当女孩遇到熊"></a>当女孩遇到熊</h3><p>女主好萌。<br>日常。<br>日常。<br>看第二集还是有点意思的。<br>属于那种可看可不看的番吧。</p><h3 id="从零开始的异世界生活"><a href="#从零开始的异世界生活" class="headerlink" title="从零开始的异世界生活"></a>从零开始的异世界生活</h3><p>听说小说挺受欢迎的,不看轻小说的我看了第一集也被吸引了。<br>这部番可以说是这季除了甲铁城外的最大黑马了吧。看了两集后,发现有漫画就补掉了。至少就漫画的内容来说确实是十分有趣的。</p><p>抛去回溯时间这种东西,这番的亮点不就是男主了吗,说实话男主的表现是最让我想看的,除了这点当然还有挺有特色的角色(比如帕克)。</p><h3 id="Joker-Game"><a href="#Joker-Game" class="headerlink" title="Joker Game"></a>Joker Game</h3><p>目前只看了一集,看起来是写二战谍战的,不过主角不是间谍啊(自己这么说的),所以要表达什么不是很清楚。<br>这种番还是看完再发表意见比较好。</p><h3 id="双星之阴阳师"><a href="#双星之阴阳师" class="headerlink" title="双星之阴阳师"></a>双星之阴阳师</h3><p>前两集看了没什么特点(除了那些挺好看的过场画面),没什么特点是指都能猜到接下来要干什么了。<br>不过听说漫画很有趣,好了不多说了,我先去找个能看这个漫画的app了。</p><h3 id="文豪野犬"><a href="#文豪野犬" class="headerlink" title="文豪野犬"></a>文豪野犬</h3><p>这番对于不了解剧中出场的日本文豪来说会挺无聊吧(除了妹子),只认识太宰治的我看到那个叫太宰治的帅哥喊出[人间失格]来发动和当妈一样能力的时候还真是一口老血啊。<br>我觉得我还是再去看一遍人间失格也比这番好吧。</p><h3 id="线上游戏的老婆不可能是女生(简称-老婆不可能是女生)"><a href="#线上游戏的老婆不可能是女生(简称-老婆不可能是女生)" class="headerlink" title="线上游戏的老婆不可能是女生(简称 老婆不可能是女生)"></a>线上游戏的老婆不可能是女生(简称 老婆不可能是女生)</h3><p>游戏中同工会的好友在现实中都是萌妹子,在现实中我肯定是不信的。<br>虽然是传统的套路,看了前两集还是稍微想看下去的。</p><h3 id="甲铁城的卡巴内瑞"><a href="#甲铁城的卡巴内瑞" class="headerlink" title="甲铁城的卡巴内瑞"></a>甲铁城的卡巴内瑞</h3><p>制作阵容还是十分豪华的,巨人的制作公司wit studio,鲁鲁修的剧本大河内一楼(最近几作是没什么可说的),还有只生产XX神曲的泽野弘之。还没放就已经万众瞩目了吧。<br>第一集的质量是意料之中的高,不过有大河在后面还真不敢保证,不过就前半相信是十分精彩的。<br>ps: 此作中的萝莉好萌,prpr。</p><h3 id="坂本ですが"><a href="#坂本ですが" class="headerlink" title="坂本ですが"></a>坂本ですが</h3><p>漫画很好看,动画做的也很不错,但是这么火还是挺奇怪的。</p><h3 id="飞翔的魔女"><a href="#飞翔的魔女" class="headerlink" title="飞翔的魔女"></a>飞翔的魔女</h3><p>又是一部日常系的吧。第一集看下来真的很平淡,不像女孩遇到熊那样还有些笑点。</p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>女高有个外传来着,不过语速太慢也没什么笑点。<br>还有些泡面就不说了。</p>]]></content>
<summary type="html">
<p>跳过了整个1月番后,感觉上都已经好久没追番了。说实话近期的新番实在不对我胃口,于是空闲时把逆转无赖开司和混沌武士给补了。</p>
<p>不过这次的4月番看起来还是不错了,总之又有动力去追了,到目前为止看了一些比较感兴趣的,其中倒是有几部十分有潜力呢。<br>下面就稍微谈谈</p>
</summary>
<category term="ESSAY" scheme="https://frezc.github.io/categories/ESSAY/"/>
<category term="anime" scheme="https://frezc.github.io/tags/anime/"/>
</entry>
<entry>
<title>React的服务端渲染</title>
<link href="https://frezc.github.io/2016/04/17/React%E7%9A%84%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%B8%B2%E6%9F%93/"/>
<id>https://frezc.github.io/2016/04/17/React的服务端渲染/</id>
<published>2016-04-16T17:19:00.000Z</published>
<updated>2019-04-09T15:39:43.132Z</updated>
<content type="html"><![CDATA[<p>最近在使用React来做一个收藏夹类似的网站,同时也是为了学习一下前端技术,于是就尽可能的使用了流行的框架。</p><p><a href="https://github.com/Frezc/Share-Favors-App" target="_blank" rel="noopener">项目链接</a><br>这个项目中使用了React + Redux + React-router + babel + webpack + scss,来构建了一个同构应用。虽然同构应用并不是十分必要的,不过为了尝试下新技术还是尽可能地去用了。</p><p>后端由于一开始就考虑用Laravel了(那时候还不知道有同构应用这东西),而且直接丢给同学做了,所以并没有做到完全地同构,仅仅只是在渲染页面上同用一套代码。(由于不是用NodeJs,也就没使用GraphQL+Relay了)。</p><a id="more"></a><h2 id="在什么地方使用"><a href="#在什么地方使用" class="headerlink" title="在什么地方使用"></a>在什么地方使用</h2><p>服务端渲染能带的好处就是对搜索引擎友好,所以对于一般的SPA是不必要的。<br>所以在资源页面(用户信息页面、资源列表页面、资源详情页面等)上做好服务端渲染就行了。在一些编辑页面上就没什么必要了。</p><h2 id="Redux"><a href="#Redux" class="headerlink" title="Redux"></a>Redux</h2><p>如果要使用React来做服务端渲染,我觉得像Redux这样的库是有必要的。<br>Redux使用单一的状态树,传回初始状态时会十分方便,将下面的标签放到你的脚本标签前就行了。</p><pre><code class="html"><script> window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}</script></code></pre><p>当然这里不要忘了过滤标签,我这里使用的方法是</p><p><strong>[Server]</strong></p><pre><code class="javascript">let initState = JSON.stringify(store.getState());// 使用encodeURIComponent过滤掉特殊字符let page = renderFullPage(initView, encodeURIComponent(initState));</code></pre><pre><code class="html">// 别忘了引号<script>window.__INITIAL_STATE__ = "${initState}"</script></code></pre><p><strong>[Client]</strong></p><pre><code class="javascript">const initState = window.__INITIAL_STATE__;const store = configureStore(JSON.parse(decodeURIComponent(initState)));</code></pre><h2 id="服务端渲染时的生命周期"><a href="#服务端渲染时的生命周期" class="headerlink" title="服务端渲染时的生命周期"></a>服务端渲染时的生命周期</h2><p>忘了在哪看到服务端渲染时不会执行React组件的生命周期方法了,实际上<code>componentWillMount</code>这个方法还是会执行的,所以在这个方法内还要注意下执行环境。</p><h2 id="配合React-Router和共用Action-Creator"><a href="#配合React-Router和共用Action-Creator" class="headerlink" title="配合React-Router和共用Action Creator"></a>配合React-Router和共用Action Creator</h2><p>可以使用React-Router提供的match来匹配路由表,然后通过回调函数得到路由信息,在路由信息中我们是可以得到对应的组件的,所以我们可以把调用的对应Action Creator(用过API获取数据)放到这个组件的一个静态方法里直接调用,这样我们就可以对所有路由一视同仁了。</p><p>相信大多数人会把网络请求放到Action Creator中来写,那么我们就可能懒得再去对服务器端写API请求了,而是直接dispatch这个Action Creator。作为同构代码这个Action Creator就需要注意下运行环境了,首先当然是使用<code>isomorphic-fetch</code>这个库,当然你可以自己判断当前环境来使用浏览器或node上的<code>fetch</code>。</p><h2 id="服务器端的同步请求"><a href="#服务器端的同步请求" class="headerlink" title="服务器端的同步请求"></a>服务器端的同步请求</h2><p>我这里的<strong>同步</strong>指的是服务端等待API请求结束后再返回页面。<br>在客户端只要在请求结束后进行异步地更新状态就好了,在服务端得等待这次请求(可能会有多个请求)完全结束才行。很多人会想到回调函数,不过给Action Creator加个回调函数的参数显得不太美观,而且似乎不能处理多个请求的情况。这里既然已经使用了<code>fetch</code>,那么干脆就使用es6中的<code>Promise</code>来处理。</p><p>1、首先在需要预渲染数据的页面里实现一个相同名称的静态方法来调用Action Creator。</p><pre><code class="javascript">// server fetchstatic fetchData = (params) => { // 注意要返回 return fetchUserNetwork(params.id)};</code></pre><p>2、然后在这个Action Creator中调用Api,同样要注意返回<code>fetch</code>返回的<code>Promise</code></p><pre><code class="javascript">export function fetchUserNetwork(id) { return (dispatch) => { // 这里调用的相当于 fetch(url) return Api.userInfo(id) .then(response => { if (response.ok) { // 结果中有其他耗时处理的Promise同样要返回 return response.json().then(json => { dispatch(fetchUserSuccess(json)); // 让服务端能判断是否请求成功 return response.status; }); } else { return response.status; } }) .catch(error => { return 'error'; }); }}</code></pre><p>3、在服务端我们将所有的<code>Promise</code>使用<code>Promise.all</code>方法放到一个<code>Promise</code>中</p><pre><code class="javascript">function fetchComponentsData(dispatch, components, params) { let fetchData = components.reduce((pre, cur) => { return Object.keys(cur).reduce((acc, key) => { return cur[key].hasOwnProperty('fetchData') ? acc.concat(cur[key].fetchData) : acc; }, pre) }, []); const promises = fetchData.map(fetch => dispatch(fetch(params))); return Promise.all(promises);}</code></pre><p>4、最后我们在这个<code>Promise</code>的<code>then</code>和<code>catch</code>中将组件渲染啊成string再返回就可以了。</p><h2 id="返回不同的状态码"><a href="#返回不同的状态码" class="headerlink" title="返回不同的状态码"></a>返回不同的状态码</h2><p>如果需要在返回页面时返回相应的状态码的话,只要像上面在<code>fetch().then</code>里<code>return response.status</code>。<br>然后就能通过<code>Promise.all</code>创建的<code>Promise</code>的<code>then</code>中得到一个状态码的数组了。</p><pre><code class="javascript">fetchComponentsData(store.dispatch, renderProps.components, renderProps.params) .then(status => { console.log('statu', status[0]) }</code></pre><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>判断当前环境的函数</p><pre><code class="javascript">export const isBrowser = new Function("try { return this === window; } catch(e) { return false; }");export const isNode = new Function("try { return this === global; } catch(e) { return false; }");</code></pre>]]></content>
<summary type="html">
<p>最近在使用React来做一个收藏夹类似的网站,同时也是为了学习一下前端技术,于是就尽可能的使用了流行的框架。</p>
<p><a href="https://github.com/Frezc/Share-Favors-App" target="_blank" rel="noopener">项目链接</a><br>这个项目中使用了React + Redux + React-router + babel + webpack + scss,来构建了一个同构应用。虽然同构应用并不是十分必要的,不过为了尝试下新技术还是尽可能地去用了。</p>
<p>后端由于一开始就考虑用Laravel了(那时候还不知道有同构应用这东西),而且直接丢给同学做了,所以并没有做到完全地同构,仅仅只是在渲染页面上同用一套代码。(由于不是用NodeJs,也就没使用GraphQL+Relay了)。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="server render" scheme="https://frezc.github.io/tags/server-render/"/>
</entry>
<entry>
<title>标准化React + Redux = 纯函数式编程?</title>
<link href="https://frezc.github.io/2016/03/06/%E6%A0%87%E5%87%86%E5%8C%96React-Redux-%E7%BA%AF%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B%EF%BC%9F/"/>
<id>https://frezc.github.io/2016/03/06/标准化React-Redux-纯函数式编程?/</id>
<published>2016-03-05T18:35:00.000Z</published>
<updated>2019-04-09T15:39:43.688Z</updated>
<content type="html"><![CDATA[<p>最近看到github上一个Airbnb公司的js风格<a href="https://github.com/airbnb/javascript" target="_blank" rel="noopener">指导手册</a>,感觉里面讲的都挺有道理的,里面的风格都是趋向使用es6来取代之前的各个api。<br>其中有关React有一条提到了<em>在没有state和ref时,推荐使用单纯的绘制函数代替类</em>,看到后翻了下官方文档,发现<code>propTypes</code>和<code>defaultProps</code>还是能用的。</p><p>想起之前用的Redux框架,其核心不就是将所有组件无状态化吗,配合它岂不是就能做到完全的函数式编程?<br>于是马上去改写了下之前写的<a href="https://github.com/Frezc/AnimeList-in-React-Redux" target="_blank" rel="noopener">小例子</a>。</p><a id="more"></a><p>改写需要做的也就是把类中的所有函数拿出来,将<code>render</code>名字改为类名,传入参数<code>props</code>,删掉<code>this</code>,将其他函数的参数进行相应的修改,最后别忘了<code>export default XXX;</code>。<br><a href="https://github.com/Frezc/AnimeList-in-React-Redux/blob/3078681dc70e6a5b55fa2739314ac9c2d07eecc3/src/components/AnimeDayList.js" target="_blank" rel="noopener">AnimeDayList.js</a> –改写前 <a href="https://github.com/Frezc/AnimeList-in-React-Redux/blob/master/src/components/AnimeDayList.js" target="_blank" rel="noopener">AnimeDayList.js</a> –改写后</p><p>由于基于flux思想进行的设计,所有组件都可以改为纯函数。由于在根组件<strong>App</strong>里定义了<code>componentWillMount</code>生命周期方法,所以就没改了,当然根组件作为有状态组件在flux思想里也是完全合理的。</p><p>整个例子的6个组件只定义了一个类,其他全部都是纯函数,使得整个项目清爽了很多啊。</p>]]></content>
<summary type="html">
<p>最近看到github上一个Airbnb公司的js风格<a href="https://github.com/airbnb/javascript" target="_blank" rel="noopener">指导手册</a>,感觉里面讲的都挺有道理的,里面的风格都是趋向使用es6来取代之前的各个api。<br>其中有关React有一条提到了<em>在没有state和ref时,推荐使用单纯的绘制函数代替类</em>,看到后翻了下官方文档,发现<code>propTypes</code>和<code>defaultProps</code>还是能用的。</p>
<p>想起之前用的Redux框架,其核心不就是将所有组件无状态化吗,配合它岂不是就能做到完全的函数式编程?<br>于是马上去改写了下之前写的<a href="https://github.com/Frezc/AnimeList-in-React-Redux" target="_blank" rel="noopener">小例子</a>。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="redux" scheme="https://frezc.github.io/tags/redux/"/>
<category term="fp" scheme="https://frezc.github.io/tags/fp/"/>
</entry>
<entry>
<title>开始使用Redux</title>
<link href="https://frezc.github.io/2016/03/02/%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8Redux/"/>
<id>https://frezc.github.io/2016/03/02/开始使用Redux/</id>
<published>2016-03-01T17:29:00.000Z</published>
<updated>2019-04-09T15:39:43.555Z</updated>
<content type="html"><![CDATA[<p>最近开始试着用React来写前端了,虽然感觉在构建Web上React实在是有些大材小用,但是毕竟这个库的可是打着<code>Learn Once, Write Anywhere</code>的口号来着,在写不同平台应用时会用React写得开心就行。</p><p>然后我发现了Redux这个东西,这是flux的一个实现,那么flux是什么玩意呢?<br>很明显flux不是什么实际的框架,而是一种框架思想,特点就是单项数据流吧,它是将所有状态更新都放到一个数据流中进行处理。相比MVC,Flux弱化了Controller的功能吧。</p><p>下面讲讲具体的<a href="https://github.com/Frezc/AnimeList-in-React-Redux" target="_blank" rel="noopener">例子</a>。(请确保你对React已经十分熟悉)</p><a id="more"></a><p>官方的todoApp是十分简单明了的演示了,第一次照着例子打一遍可以有一个大致的理解。</p><p>首先看一下应用的一个流程图<br><img src="/images/2016/03/653251524.png" alt="flux-simple-f8-diagram-with-client-action-1300w.png"></p><p>这是应用内部数据流的演示,实际上Redux已经帮我们把数据流相关的函数封装好了,所以剩下的需要我们写的也就<strong>action creator</strong>和<strong>reducer</strong>了。<br>我写了一个<a href="https://github.com/Frezc/react-redux-starter" target="_blank" rel="noopener">starter</a>,可以直接拿来修改开发。</p><h2 id="View"><a href="#View" class="headerlink" title="View"></a>View</h2><p>我个人习惯还是从View开始开发,View部分和原本的React开发并没有什么不同。只要注意一点,<strong>不要在组件中使用state</strong>,所有需要控制的地方都使用属性,之后你需要从父组件将属性传入。<br>页面我使用全文本表示了,并没有加多少css。</p><h2 id="Store"><a href="#Store" class="headerlink" title="Store"></a>Store</h2><p>因为没有在组件里使用state,所以状态怎么保存呢?<br>Redux里将所有状态都放到了一棵状态树里,而这个状态树保存在Store实例里。那么在View如何获得这个状态树呢。</p><p><img src="/images/2016/03/1929471663.png" alt="create_store.png"></p><p>这里做了3件事,绑定中间件、创建store、通过Provider组件绑定View和store。这么做以后store里的状态树更新时都会传递给View。<br>然后在你需要获得状态树的组件里(官方建议只在顶层组件里获取)<br><img src="/images/2016/03/4278813248.png" alt="view_connect.png"></p><p>通过connect绑定该View后,每次状态树更新时都会调用<em>select</em>函数,参数就是状态树,该函数的返回对象会传递给该View的props对象<br><img src="/images/2016/03/755933590.png" alt="state_props.png"></p><p>这样之前根据之前写的通过属性控制的View就会自动更新了。</p><h2 id="Action-amp-Action-Creator"><a href="#Action-amp-Action-Creator" class="headerlink" title="Action & Action Creator"></a>Action & Action Creator</h2><p>Action指的是更新状态的请求,其格式一般(不是一定)是这样的</p><pre><code>{type: DOWHAT, params: optional}</code></pre><p>type是提醒Redux要做什么,Action中也能跟其他参数。当然你可以自己定义格式,只要在后面Reducer里处理时对应就好。</p><p>Action Creator就是返回Action对象的纯函数,其存在的意义主要是少打代码、减少错误。<br><img src="/images/2016/03/3965136547.png" alt="action_creator.png"></p><p>有了Action后只要调用<em>dispatch</em>方法(见上上图)就能通知Redux要干什么了,接下来Action会传递给Reducer处理,以更新状态树。</p><p>PS: 如果需要请求API的话只要不写在Reducer里就可以了,dispatch action纯粹只是用来更新状态的,和之前setState功能相同。所以只要请求完后异步dispatch一下就可以了。不一定要使用redux-thunk之类的异步库。</p><h2 id="Reducer"><a href="#Reducer" class="headerlink" title="Reducer"></a>Reducer</h2><p>Reducer有点类似于MVC中Controller的功能,其作用就是通过用户传来的Action更新状态树。<br>Reducer都是纯函数,所以这里需要注意下函数式编程(fundamental pattern)的思想。<br>怎么写见<a href="http://redux.js.org/docs/basics/Reducers.html" target="_blank" rel="noopener">官方文档</a>。</p><p>写Reducer做好3点就可以了</p><ol><li>按照状态树的结构拆分,根Reducer最好调用<em>combineReducers</em></li><li>所有Reducer函数里的state<strong>必须</strong>要有初始值,在es6中很简单,直接在参数中赋值就行了,像其他编程语言一样。</li><li>为了符合fp的思想,<strong>不要</strong>在reducer中修改传入的state。如果要修改则应该返回一个全新的state。</li></ol><p>总的来说reducer怎么写取决于状态树的设计,所以建议在写reducer前把状态树设计好,再对应状态树写函数。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>虽然一开始搭建一个Redux的结构比普通项目要麻烦很多。但是之后再进行修改时就会发现项目结构十分的清晰,比起MVC中将管理状态、组件更新、网络请求等全部塞到Controller里,Redux把这些功能都分散到了Reducer、Store、Action Creator里,就显的很整洁了。</p><p>说实话理解Redux还是花了不少时间的,尤其是要解决<em>“这比起MVC有什么优势吗”</em>这个疑问。<br>我这里还是推荐<a href="http://redux.js.org" target="_blank" rel="noopener">官方文档</a>(<a href="http://cn.redux.js.org/" target="_blank" rel="noopener">中文版</a>)。</p>]]></content>
<summary type="html">
<p>最近开始试着用React来写前端了,虽然感觉在构建Web上React实在是有些大材小用,但是毕竟这个库的可是打着<code>Learn Once, Write Anywhere</code>的口号来着,在写不同平台应用时会用React写得开心就行。</p>
<p>然后我发现了Redux这个东西,这是flux的一个实现,那么flux是什么玩意呢?<br>很明显flux不是什么实际的框架,而是一种框架思想,特点就是单项数据流吧,它是将所有状态更新都放到一个数据流中进行处理。相比MVC,Flux弱化了Controller的功能吧。</p>
<p>下面讲讲具体的<a href="https://github.com/Frezc/AnimeList-in-React-Redux" target="_blank" rel="noopener">例子</a>。(请确保你对React已经十分熟悉)</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="react" scheme="https://frezc.github.io/tags/react/"/>
<category term="redux" scheme="https://frezc.github.io/tags/redux/"/>
</entry>
<entry>
<title>react-native对md风格中StatusBar的处理方式</title>
<link href="https://frezc.github.io/2016/02/21/react-native%E5%AF%B9md%E9%A3%8E%E6%A0%BC%E4%B8%ADStatusBar%E7%9A%84%E5%A4%84%E7%90%86%E6%96%B9%E5%BC%8F/"/>
<id>https://frezc.github.io/2016/02/21/react-native对md风格中StatusBar的处理方式/</id>
<published>2016-02-20T20:09:00.000Z</published>
<updated>2019-04-09T15:39:43.221Z</updated>
<content type="html"><![CDATA[<p>自从rn支持android以后,出现了不少的material design的库。<br>用了下一个叫<a href="https://github.com/react-native-material-design/react-native-material-design" target="_blank" rel="noopener">react-native-material-design</a>的库,先不谈性能方面的缺陷,这个库完全忽视了<strong>StatusBar</strong>的处理,使得其Drawer弹出时会很难看。<br>至少目前还没有看到哪个库对<strong>StatusBar</strong>进行了特殊处理,官方倒是有一个StatusBar的组件可以用来控制应用中<strong>StatusBar</strong>的表现,不过这个组件在android里是调用5.0加入api来实现的,也就是说只对5.0以上的系统有效。而且这个组件在实现Drawer占满屏幕的效果时,<strong>StatusBar</strong>要设为translucent,此时和在theme中设置<strong>windowTranslucentStatus</strong>没有什么区别,依然要考虑<strong>StatusBar</strong>空出来的地方。所以这个组件还不如设置theme呢,起码后者能兼容到4.4。</p><p>下面就谈谈比较合适的方法。</p><a id="more"></a><p><img src="/images/2016/02/1657079296.png" alt="bilibili_md.png"></p><p>拿b站的app来看看要实现的效果。</p><h2 id="RN中的实现"><a href="#RN中的实现" class="headerlink" title="RN中的实现"></a>RN中的实现</h2><p>这个效果基本上都是设置translucent后,在Statusbar的位置画一个相同大小的<strong>View</strong>来实现的。rn里虽然没有现成的库,但是实现起来也是非常简单的。</p><p>首先在android项目里的控制app theme的style项里添加</p><pre><code><item name="android:windowTranslucentStatus">true</item></code></pre><p>然后在<strong>Navigator</strong>上添加View<br><img src="/images/2016/02/2531076439.png" alt="statusbar_add.png"></p><p>这里的style为<br><code>`</code>javascript<br>statusbar: {<br> backgroundColor: COLOR.googleBlue700.color, // Statusbar的颜色<br> height: 24 // Statusbar的高度,android上为24dp<br>}</p><p>实现的效果<br><img src="/images/2016/02/1361499160.png" alt="my_statusbar.png"></p><p><img src="/images/2016/02/925583762.png" alt="my_statusbar_drawer.png"></p><p>这样做还是有些不足的,比如<strong>Statusbar</strong>在未来的android版本中出现变动的话就得修改代码了,当然为了避免这点我们可以写个原生模块动态获取<strong>Statusbar</strong>的高度。</p><p>还有个办法就是在原生的android的项目中自定义View,不过这种方法麻烦而且扩展性不好,就不去实现了。</p>]]></content>
<summary type="html">
<p>自从rn支持android以后,出现了不少的material design的库。<br>用了下一个叫<a href="https://github.com/react-native-material-design/react-native-material-design" target="_blank" rel="noopener">react-native-material-design</a>的库,先不谈性能方面的缺陷,这个库完全忽视了<strong>StatusBar</strong>的处理,使得其Drawer弹出时会很难看。<br>至少目前还没有看到哪个库对<strong>StatusBar</strong>进行了特殊处理,官方倒是有一个StatusBar的组件可以用来控制应用中<strong>StatusBar</strong>的表现,不过这个组件在android里是调用5.0加入api来实现的,也就是说只对5.0以上的系统有效。而且这个组件在实现Drawer占满屏幕的效果时,<strong>StatusBar</strong>要设为translucent,此时和在theme中设置<strong>windowTranslucentStatus</strong>没有什么区别,依然要考虑<strong>StatusBar</strong>空出来的地方。所以这个组件还不如设置theme呢,起码后者能兼容到4.4。</p>
<p>下面就谈谈比较合适的方法。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="android" scheme="https://frezc.github.io/tags/android/"/>
<category term="react native" scheme="https://frezc.github.io/tags/react-native/"/>
</entry>
<entry>
<title>在Android的原生模块中得到RN组件的引用</title>
<link href="https://frezc.github.io/2016/02/19/%E5%9C%A8Android%E7%9A%84%E5%8E%9F%E7%94%9F%E6%A8%A1%E5%9D%97%E4%B8%AD%E5%BE%97%E5%88%B0RN%E7%BB%84%E4%BB%B6%E7%9A%84%E5%BC%95%E7%94%A8/"/>
<id>https://frezc.github.io/2016/02/19/在Android的原生模块中得到RN组件的引用/</id>
<published>2016-02-19T15:23:00.000Z</published>
<updated>2019-04-09T15:39:43.520Z</updated>
<content type="html"><![CDATA[<p>最近学习了下使用react-native(以下简称rn)来做App,这是目前唯一一个编写<strong>跨平台</strong>的<strong>本地应用</strong>的<strong>开源</strong>库,所以人气也是非常高啊。<br>因为是新东西,所以相关的讨论、开源库之类的都很少,目前有很多东西还是得依赖原生模块来处理。好在的是rn提供了一些挺简单的交互方法,所以不至于会有无法解决的严重后果。</p><p>不过在调用原生模块时,一开始我不太清楚怎么去获得rn里组件的实例引用,在网上也没找到相关问题,后来在看源代码后发现该怎么做了。</p><a id="more"></a><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>一开始我是想看看<strong>DrawerLayoutAndroid</strong>组件是如何实现调用<em>openDrawer</em>和<em>closeDrawer</em>方法的,发现它是通过<strong>UIManager</strong>这个模块来分发命令,然后去看了下这个模块的源代码,发现了创建、删除、查找View等方法的装饰方法,具体是用<strong>UIImplementation</strong>这个类实现的。<br>于是找到<strong>UIImplementation</strong>中的<em>createView</em>方法</p><p><img src="/images/2016/02/2765341748.png" alt="uiimplementation_createview"></p><p>可见这个方法并没有创建真正的本地组件,之后通过<strong>NativeViewHierarchyOptimizer</strong>(优化UI层次)和<strong>UIViewOperationQueue</strong>(渲染的缓冲队列)两个类的处理后调用了<strong>NativeViewHierarchyManager</strong>的<em>createView</em>方法。</p><p><img src="/images/2016/02/2125075922.png" alt="nativeviewhierarchymanager_createview.png"></p><p>上图是这个方法中创建本地组件的关键语句。这里有三行注释,主要讲的就是把react的tag存到组件的id中,这样更容易复用组件。调用<em>setId</em>将tag保存到组件的属性当中。<br>这里我们就发现了原生组件的id其实就是对于rn组件的tag。那么最后一个问题就是怎么获得tag呢?</p><p><img src="/images/2016/02/422551472.png" alt="drawerlayoutandroid_gettag.png"></p><p>在<strong>DrawerLayoutAndroid.android.js</strong>文件中我们找到<em>closeDrawer</em>方法,它调用了<strong>UIManager</strong>的<em>dispatchViewManagerCommand</em>方法,这个方法是讲指令传递给目标组件管理器调用的(下篇博客会讲),在源码中就能知道第一个传入参数其实就是rn组件的tag。那么如何获取就和<em>_getDrawerLayoutHandle</em>方法写的一样了。</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>自己实现原生模块时需要找到rn组件实例的话只要相应的传入tag,然后调用<em>findViewById</em>即可。</p><p><img src="/images/2016/02/1683079715.png" alt="toastmodule_settext.png"></p><p>这里我偷了个懒,直接把MainActivity的实例暴露了出来,最好的方法还是利用消息传递给Activity处理。(<strong>更正</strong>:最好的方法应该是注册<strong>Package</strong>时传入activity实例,然后在<em>createNativeModules</em>方法中将activity传给module对象。)<br>这里要注意的是,模块的方法是异步执行的,如果你要更新ui的话,要切到主线程执行。</p><p>上面的代码还是存在挺严重的bug的,第二次调用<em>setText</em>就会时程序fc,目前还不清楚原因。<br>目前来说并不推荐利用这样的方法来更新rn的组件。</p>]]></content>
<summary type="html">
<p>最近学习了下使用react-native(以下简称rn)来做App,这是目前唯一一个编写<strong>跨平台</strong>的<strong>本地应用</strong>的<strong>开源</strong>库,所以人气也是非常高啊。<br>因为是新东西,所以相关的讨论、开源库之类的都很少,目前有很多东西还是得依赖原生模块来处理。好在的是rn提供了一些挺简单的交互方法,所以不至于会有无法解决的严重后果。</p>
<p>不过在调用原生模块时,一开始我不太清楚怎么去获得rn里组件的实例引用,在网上也没找到相关问题,后来在看源代码后发现该怎么做了。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="android" scheme="https://frezc.github.io/tags/android/"/>
<category term="react native" scheme="https://frezc.github.io/tags/react-native/"/>
</entry>
<entry>
<title>用SVG画一个笑脸男图标</title>
<link href="https://frezc.github.io/2015/12/31/%E7%94%A8SVG%E7%94%BB%E4%B8%80%E4%B8%AA%E7%AC%91%E8%84%B8%E7%94%B7%E5%9B%BE%E6%A0%87/"/>
<id>https://frezc.github.io/2015/12/31/用SVG画一个笑脸男图标/</id>
<published>2015-12-30T16:23:00.000Z</published>
<updated>2019-04-09T15:39:43.654Z</updated>
<content type="html"><![CDATA[<p>自从看完攻壳的第一季后就想自己做个会动的笑脸男的矢量图图标了,最近花了点时间终于搞定了。<br>下面我会谈谈网页上矢量图该怎么写。<br><a href="http://frezc.github.io/NetResources/waraiotoko/">预览</a>(在ie和edge上文字无法滚动,在chrome上是正常的,其他浏览器不清楚)。<a href="https://github.com/Frezc/NetResources/blob/master/waraiotoko/main.js" target="_blank" rel="noopener">源代码</a>。</p><a id="more"></a><h2 id="制作矢量图的工具"><a href="#制作矢量图的工具" class="headerlink" title="制作矢量图的工具"></a>制作矢量图的工具</h2><p>虽然在网页上是用代码实现的,但是设计还是需要一个能画矢量图的工具的。<br>可以用Illustrator(Ai)或者一个在线的工具就可以了。我这里直接用了别人用ai设计的矢量图,所以对详情设计也不太了解。</p><p>然后使用一个越方便越好的画svg的js库就行了。原本看网上大家都推荐使用raphael.js,但是这个库没有画路径文字的方法,于是果断找了另外一个库svg.js,这个库能解决这个问题,还比raphael.js要小。</p><h2 id="用代码画矢量图"><a href="#用代码画矢量图" class="headerlink" title="用代码画矢量图"></a>用代码画矢量图</h2><p>笑脸男的图像里基本上都是使用路径画好的,所以只要使用画路径的方法就行了。关于路径,w3有一套<a href="http://www.w3.org/TR/SVG/paths.html" target="_blank" rel="noopener">规范</a>,只要能看懂这里的写法就好,这是所有库都通用的标准写法。<br>其中可能让人困惑的一点就是arc画法了(其实规范里的图已经说明的挺好了)。它是提供当前点(起始点)、x、y上的半径、旋转角度、large-arc-flag、sweep-flag和终点来确定一个弧的。其中large-arc-flag指是否画大一点的弧,sweep-flag指从起点到终点是否为顺时针画线。</p><p>对路径熟悉后就可以把你设计图给搬进代码里了,如果你的编辑器能直接生成w3标准的代码的话就再好不过了。</p><h2 id="转动的文字"><a href="#转动的文字" class="headerlink" title="转动的文字"></a>转动的文字</h2><p>文字的部分其实有点麻烦,麻烦在做动画上。</p><pre><code class="javascript"> var t1 = paper.text(s); t1.fill('#23498C').font({size: 50, 'letter-spacing': 2.3, weight: 700}) t1.path('M 605,365 A 300,300 0 1,1 605,260')</code></pre><p>先将路径确定好,并调整文字的一些属性,使其和原图一样。</p><pre><code class="javascript"> t1.textPath().attr('startOffset', '0').animate({ ease: '-', duration: 10000}).attr('startOffset', '100%').loop()</code></pre><p>然后调用动画方法就能动了。但是这样无法将文字循环,文字播放到终点就消失了。<br>一开始我想再用一个文字矢量图接着这个画的,但是svg里的<code>startOffset</code>不能使用负值(负值当然也是有效的,但是不能像0~100%这样设置)。于是想用逆时针路径然后倒过来播放,可惜这样文字就倒过来了,也不知道是否有办法设置。</p><p>最后终于想到一个完美的方案</p><pre><code class="javascript"> t1.path('M 605,365 A 300,300 0 1,1 605,260 M 605,365 A 300,300 0 1,1 605,260') t1.textPath().attr('startOffset', '0').animate({ ease: '-', duration: 10000}).attr('startOffset', '50%').loop()</code></pre><p>把路径画两遍,然后播放时<code>startOffset</code>从0~50%进行循环就行了。<br>如果你也想实现这种无限循环地旋转的话,可以试试这个方法。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>之所以做这个还是由于对笑脸男这个人物的喜爱吧,有能力的同时愿意来为真相做出贡献。有意思的是,攻壳第二季第二集的主人公和他完全就是相反的人。空想但没有勇气去做,想想自己和大概也就是这样的人了。</p>]]></content>
<summary type="html">
<p>自从看完攻壳的第一季后就想自己做个会动的笑脸男的矢量图图标了,最近花了点时间终于搞定了。<br>下面我会谈谈网页上矢量图该怎么写。<br><a href="http://frezc.github.io/NetResources/waraiotoko/">预览</a>(在ie和edge上文字无法滚动,在chrome上是正常的,其他浏览器不清楚)。<a href="https://github.com/Frezc/NetResources/blob/master/waraiotoko/main.js" target="_blank" rel="noopener">源代码</a>。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="动漫" scheme="https://frezc.github.io/tags/%E5%8A%A8%E6%BC%AB/"/>
</entry>
<entry>
<title>动画的排行榜 - 页面实现</title>
<link href="https://frezc.github.io/2015/12/25/%E5%8A%A8%E7%94%BB%E7%9A%84%E6%8E%92%E8%A1%8C%E6%A6%9C-%E9%A1%B5%E9%9D%A2%E5%AE%9E%E7%8E%B0/"/>
<id>https://frezc.github.io/2015/12/25/动画的排行榜-页面实现/</id>
<published>2015-12-25T04:18:00.000Z</published>
<updated>2019-04-09T15:39:43.587Z</updated>
<content type="html"><![CDATA[<p>离上篇文章都已经一个多月了啊,最近都在玩mho,也没什么时间来做这个。不过目前还是把基本的功能都给实现了,做了一个展示页面。</p><p>和以前说的一样,后端是使用<a href="https://laravel.com/" target="_blank" rel="noopener">Laravel</a>框架实现的api服务器,前端是用<a href="http://cn.vuejs.org/" target="_blank" rel="noopener">vue.js</a>和<a href="http://semantic-ui.com/" target="_blank" rel="noopener">semantic-ui</a>实现的页面。<br><a href="http://statistics.frezc.com/anime-rank.html" target="_blank" rel="noopener">点此访问</a></p><a id="more"></a><h2 id="后端"><a href="#后端" class="headerlink" title="后端"></a>后端</h2><p>Laravel也不是第一次用了,还是挺熟悉的,不过也碰到过一些问题,这里记录一下。</p><ul><li>数据库配置:api返回错误信息<code>The Response content must be a string or object implementing __toString()</code>时,首先还是要检查一下数据库的配置是否正确。我这里将动漫排行的数据放在了一个独立的数据库中管理,所以要在laravel里额外配置不同的数据库连接,这里会十分容易出错。</li><li>跨域请求:使用ajax请求不同域名的api时需要注意下跨域请求的问题,为了能进行跨域请求,返回的header里需要有<code>Access-Control-Allow-Origin</code>这一项来确定允许跨域请求的域名。我这里为了方便,让所有api能让所有域名访问(方便本地测试),直接修改nginx的配置文件,在<code>location</code>中添加一条<code>add_header Access-Control-Allow-Origin *;</code>就ok了。</li></ul><h2 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h2><p>前端的css库使用了某人推荐的semantic-ui,我也没用过bootstrap,所以用什么都无所谓,不过semantic比起bootstrap的资料真是少。<br>然后是vue.js,数据的双向绑定和定义组件都是非常好用的,我没有用过其他类似的库,所以也不好比较。</p><p>虽然没有必要,但还是试着用了webpack管理项目,这个库简单来说就是将一堆js和css文件打包的一个js文件中,在发布时你的页面中只要引用一个打包后的js文件就行了,对于非常复杂的单页应用来说,这是个能减少项目复杂度的工具。</p><p>然后讲讲问题</p><ul><li>渲染速度:原本我想把1000多项数据直接展示的,不过渲染速度还是有限,vue.js渲染1000项的列表也得要接近2秒的时间,于是想想还是分页了。</li><li>提示框:在页面中使用了semantic的提示框组件,不过使用的时候仅仅在html中添加<code>data-title</code>和<code>data-content</code>并没有什么用,还要调用这些节点的<code>popup()</code>函数。但是节点都是通过vue.js之后动态生成的,那要怎么在渲染完成后调用呢?<br>这里可以使用vue.js提供的自定义指令实现,注册一个指令,在<code>bind</code>接口中调用就ok了。</li></ul><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>本来想借用下Bangumi的图片的,不过没法跨域请求,那就没办法了,之后有空就去爬取一遍吧。</p><p>剩下还有个留言评论的模块,没时间就用多说来做了。</p><p>PS: 最近一直在玩mho 华东一 隐士之森 id: 心悦会员 欢迎找我玩<br>PS2: steam也开始打折了,不过启示录2怎么就第一章打折啊,卡表还是坑。</p>]]></content>
<summary type="html">
<p>离上篇文章都已经一个多月了啊,最近都在玩mho,也没什么时间来做这个。不过目前还是把基本的功能都给实现了,做了一个展示页面。</p>
<p>和以前说的一样,后端是使用<a href="https://laravel.com/" target="_blank" rel="noopener">Laravel</a>框架实现的api服务器,前端是用<a href="http://cn.vuejs.org/" target="_blank" rel="noopener">vue.js</a>和<a href="http://semantic-ui.com/" target="_blank" rel="noopener">semantic-ui</a>实现的页面。<br><a href="http://statistics.frezc.com/anime-rank.html" target="_blank" rel="noopener">点此访问</a></p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="动漫" scheme="https://frezc.github.io/tags/%E5%8A%A8%E6%BC%AB/"/>
<category term="排行" scheme="https://frezc.github.io/tags/%E6%8E%92%E8%A1%8C/"/>
<category term="anime" scheme="https://frezc.github.io/tags/anime/"/>
<category term="rank" scheme="https://frezc.github.io/tags/rank/"/>
</entry>
<entry>
<title>做个动画排行榜 - 标题匹配</title>
<link href="https://frezc.github.io/2015/11/22/%E5%81%9A%E4%B8%AA%E5%8A%A8%E7%94%BB%E6%8E%92%E8%A1%8C%E6%A6%9C%20-%20%E6%A0%87%E9%A2%98%E5%8C%B9%E9%85%8D/"/>
<id>https://frezc.github.io/2015/11/22/做个动画排行榜 - 标题匹配/</id>
<published>2015-11-22T13:07:00.000Z</published>
<updated>2019-04-09T15:39:43.422Z</updated>
<content type="html"><![CDATA[<p>上次写了篇做排行的<a href="/2015/11/19/做一个多网站的动画排行榜吧">文章</a>,然后做到现在,匹配不同网站的信息还是一大难点啊,做到现在,剩下的数据还是手工去处理会准确且快一些。</p><p>这次在将Ann与Sati信息匹配时使用了与Bgm和Sati匹配时不同的方法。</p><a id="more"></a><h2 id="Ann的信息匹配"><a href="#Ann的信息匹配" class="headerlink" title="Ann的信息匹配"></a>Ann的信息匹配</h2><p>这次匹配的核心是ratio,jaro和jaro_winkler这3个字符串相似度算法,利用这些算法很明显可以解决之前存在的标题中有单个字符不匹配的情况。这次使用的<a href="https://github.com/Frezc/Anime-Statistics-Crawl/blob/master/name_compare_rule.md" target="_blank" rel="noopener">一些规则</a>。<br>这里原先Ann的日期获取缺失了很多,后来看了几个页面后才发现是日期会有不同的结构,重新爬取了一遍(<a href="https://github.com/Frezc/Anime-Statistics-Crawl/blob/master/anime_statistics/spiders/ANNConsSpider.py" target="_blank" rel="noopener">代码</a>),顺便做了次筛选,分辨出评价人数过少的番剧。<br>这次爬取后,日期基本就完整了,使用了日期比对后匹配率一下子就上来了,最后只剩85项未匹配。这些就直接手工解决了。</p><p>最后一个问题就是有些番剧在一个网站上是合并的,而在其他两个网站上是分开的。这里我原本是直接留下第一季的信息,但是后来发现还挺多的,还是将多个url用逗号隔开保存了,到时候取分时直接计算多个url的平均分了。</p><h2 id="网站计划"><a href="#网站计划" class="headerlink" title="网站计划"></a>网站计划</h2><p>网站我目前的想法是,后端api服务器Laravel,前端Semantic UI + Vue.js。<br>不过目前时间太少,这个计划会挺慢的。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>因为python没有和c那样的for循环,所以在项目中一直使用while循环进行迭代,这次加了个continue,发现就出bug了。于是又找了下资料,才知道python中写索引迭代要这么写</p><pre><code class="python"> for i in range(start, end): your code</code></pre><p>相当于</p><pre><code class="python"> for(i=start; i < end; i++){ your code }</code></pre>]]></content>
<summary type="html">
<p>上次写了篇做排行的<a href="/2015/11/19/做一个多网站的动画排行榜吧">文章</a>,然后做到现在,匹配不同网站的信息还是一大难点啊,做到现在,剩下的数据还是手工去处理会准确且快一些。</p>
<p>这次在将Ann与Sati信息匹配时使用了与Bgm和Sati匹配时不同的方法。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="anime" scheme="https://frezc.github.io/tags/anime/"/>
<category term="rank" scheme="https://frezc.github.io/tags/rank/"/>
</entry>
<entry>
<title>做一个多网站的动画排行榜吧</title>
<link href="https://frezc.github.io/2015/11/19/%E5%81%9A%E4%B8%80%E4%B8%AA%E5%A4%9A%E7%BD%91%E7%AB%99%E7%9A%84%E5%8A%A8%E7%94%BB%E6%8E%92%E8%A1%8C%E6%A6%9C%E5%90%A7/"/>
<id>https://frezc.github.io/2015/11/19/做一个多网站的动画排行榜吧/</id>
<published>2015-11-19T15:55:00.000Z</published>
<updated>2019-04-09T15:39:43.488Z</updated>
<content type="html"><![CDATA[<p>起源是以前看过的一个<a href="http://www.bilibili.com/video/av1184434/" target="_blank" rel="noopener">B站视频</a>。<br>视频里统计了中国的<a href="http://bgm.tv" target="_blank" rel="noopener">Bangumi</a>,日本的<a href="http://www.animesachi.com/" target="_blank" rel="noopener">Sati</a>和欧美的<a href="http://www.animenewsnetwork.com/" target="_blank" rel="noopener">Ann</a>的评分,并取三者平均分进行排名。</p><p>总的来说,这个排名还是挺靠谱的,所以我也想做一个这样的排名,利用网页的形式展示并能够定时地更新。</p><a id="more"></a><h2 id="利用爬虫抓信息"><a href="#利用爬虫抓信息" class="headerlink" title="利用爬虫抓信息"></a>利用爬虫抓信息</h2><p>经某人推荐用了<a href="http://www.scrapy.org" target="_blank" rel="noopener">Scrapy</a>这个库来写爬虫。<br>首先很简单通过每个网站的列表可以得到动画名和url, 由于ann上默认是英文名,所以必须去爬取每个动画的详情页来获得日文名,sati和bgm上都是能直接得到日文名的,所以并没有去爬取详情页。<br>这些存入数据库后就开始下一步</p><h2 id="统一信息"><a href="#统一信息" class="headerlink" title="统一信息"></a>统一信息</h2><p>这是最麻烦的一步了,因为各个站点的命名并不统一,就算你使用了很多的规则去匹配也不可能做到100%准确的,还是需要人工检查。虽然如此,当然能用程序匹配得越多越好了。</p><p>匹配当然需要通过原日文名,于是我观察了一下sati和bgm上的日文命名区别,写了一些规则:</p><ul><li>“ミス・モノクローム -The Animation-“ 在SATI中没有 ‘-‘ 符号</li><li>‘!’ 在SATI上是 ‘!’, 在ann和bgm上不确定</li><li>bgm可能会比sati多个副标题 如 ‘攻殻機動隊ARISE’系列</li><li>标题间的空格可能会不相同</li><li>sati会在剧场版动画名前加上 ‘劇場版’ , 而bgm不会</li><li>sati可能会在标题尾加上括号并有补充内容</li></ul><p>当然这些并不是很全,实际在匹配时用了精确到模糊的查找。</p><ul><li>将原标题中的’劇場版’和前后空格去掉,将一些全角符号替换成半角的</li><li>精确查找,考虑到有括号补充内容的问题,也同样将括号去掉的内容和括号内的内容进行精确的查找</li><li>考虑到标题中的特殊字符存在或有其他替代的不确定性,先将它们使用’_’通配符进行匹配,匹配的符号<br><code>re.compile(ur'[-+・\'!-:.\(\)\s]')</code> , 这里还少了个 ‘~’,因为使用正则表达式替换会出现替换两次的问题,我就使用replace函数进行替换了。</li><li>考虑到空格的不确定性,直接使用’%’通配符替换了标题中的所有空格</li><li>考虑到副标题的问题,在标题首尾加上’%’通配符</li><li>最后再找不到就来一次将特殊字符全部替换成’%’的匹配</li></ul><p>前后最多匹配10次,如果在匹配中出现了多个结果就直接手工填充了,<a href="https://github.com/Frezc/Anime-Statistics-Crawl/blob/master/anime_statistics/db_filter/BGMNameFilter.py" target="_blank" rel="noopener">代码</a>。(注意下,最好将查找的字段添加上索引,不然可能要搜索个几分钟)</p><p>除了标题其实还有一个特征值:放送时间。但是通过bgm网页上获取的放送时间是中文形式的,需要去format才能和其他网站匹配。后来一想bgm不还有api吗,看了下api的属性里果然有air_date的标准形式,也可以拿来筛选。</p><h2 id="目前的进度"><a href="#目前的进度" class="headerlink" title="目前的进度"></a>目前的进度</h2><p>第一次用程序匹配完,4000多项数据填了3000多项,结果还是不够理想,毕竟剩下1000项数据也没法手工填。<br>于是我干脆就先删除无用数据吧,sati上评论数少于20的就不去考虑了,删了一圈,并用放送时间进行了一圈匹配(将时间相同的输出,手工填),剩下1000多项数据,其中85项未填,看了下未填项,还真是有很多坑啊。</p><p><img src="/images/2015/11/3870579292.png" alt="bad_name.png"></p><p>(第一条全角空格没考虑,第二条两个网站的字竟然不一样,第三条sati上使用了汉字而bgm上的日文名使用了平假名)</p><p>目前还需要手工把剩下的数据填上了。</p>]]></content>
<summary type="html">
<p>起源是以前看过的一个<a href="http://www.bilibili.com/video/av1184434/" target="_blank" rel="noopener">B站视频</a>。<br>视频里统计了中国的<a href="http://bgm.tv" target="_blank" rel="noopener">Bangumi</a>,日本的<a href="http://www.animesachi.com/" target="_blank" rel="noopener">Sati</a>和欧美的<a href="http://www.animenewsnetwork.com/" target="_blank" rel="noopener">Ann</a>的评分,并取三者平均分进行排名。</p>
<p>总的来说,这个排名还是挺靠谱的,所以我也想做一个这样的排名,利用网页的形式展示并能够定时地更新。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="动漫" scheme="https://frezc.github.io/tags/%E5%8A%A8%E6%BC%AB/"/>
<category term="排行" scheme="https://frezc.github.io/tags/%E6%8E%92%E8%A1%8C/"/>
<category term="anime" scheme="https://frezc.github.io/tags/anime/"/>
<category term="rank" scheme="https://frezc.github.io/tags/rank/"/>
</entry>
<entry>
<title>自己在Android开发中碰到的问题和解决办法</title>
<link href="https://frezc.github.io/2015/11/10/%E8%87%AA%E5%B7%B1%E5%9C%A8Android%E5%BC%80%E5%8F%91%E4%B8%AD%E7%A2%B0%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98%E5%92%8C%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/"/>
<id>https://frezc.github.io/2015/11/10/自己在Android开发中碰到的问题和解决办法/</id>
<published>2015-11-09T16:51:00.000Z</published>
<updated>2019-04-09T15:39:43.720Z</updated>
<content type="html"><![CDATA[<p>说起来android我已经半年多没碰过了,这次为了让小组成员会写网络请求和用design库,我还是自己动手写了网络请求的封装类和一个design库的demo。当然,还是不免遇到了一些问题。<br>ps: 因为是前几天的事,也没截图,就随便记录一下啦。</p><a id="more"></a><h2 id="项目升级"><a href="#项目升级" class="headerlink" title="项目升级"></a>项目升级</h2><p>看idea总提示我项目所用的支持库需要升级,于是第一件事当然是把<code>build.gradle</code>里引用库的版本号都改到最新了,还添加了design库的引用,然后就报错了。</p><p>一共有两个错误</p><ol><li>资源重定义:搜了下发现是库里的命名冲突了,我这个项目中是<code>com.zzt.inbox</code>这个库和<code>appcompat-v7</code>间的冲突,前者并不会使用了,于是就直接移除了。</li><li>资源未找到:这个还是因为我只将支持库升级到了<code>23.1.0</code>,而sdk版本还停留在22的原因,将其改为23就ok了。</li></ol><h2 id="Intellij-Idea-15"><a href="#Intellij-Idea-15" class="headerlink" title="Intellij Idea 15"></a>Intellij Idea 15</h2><p>看到idea升级到15了就马上去装了15,打开后直接是一个错误:jre未找到。我顺着错误中给出的路径:<code>\jre\bin\java.exe</code>浏览了一下,发现这个路径就是错误的,实际上是<code>\jre\jre\bin\java.exe</code>。不知道我是不是个例。</p><p>当然解决方法很简单,在系统中安装一个自己的java就ok了,我由于没有安装32位的java所以idea自动使用了自带的java。最后我直接使用64位的idea就完全没问题了。</p><p>idea15的新特性我了解的不多,不过最直观的感受就是打开时稍微快了一点吧。</p><h2 id="Context的空指针问题"><a href="#Context的空指针问题" class="headerlink" title="Context的空指针问题"></a>Context的空指针问题</h2><p>我在一个<code>Activity</code>中在某个变量初始化时调用了<code>this.getApplicationContext()</code>结果出现了空指针问题,当时我把这个函数放在<code>onCreate()</code>里后就好了。</p><p>后来我看到一篇关于<code>Context</code>的<a href="http://blog.csdn.net/guolin_blog/article/details/47028975" target="_blank" rel="noopener">博客</a>,才明白是怎么回事。</p><p><img src="/images/2015/11/74545713.png" alt="context.png"></p><p>从上面这张图可以看出<code>Activity</code>只是一个装饰者,通过源码可以知道<code>getApplicationContext()</code>调用的还是其内部的一个<code>ContextImpl</code>对象。而这个对象是通过<code>attachBaseContext(Context)</code>这个函数传递给<code>Activity</code>的,所以在这个函数调用之前去调用<code>getApplicationContext()</code>自然会出现空指针异常了。</p>]]></content>
<summary type="html">
<p>说起来android我已经半年多没碰过了,这次为了让小组成员会写网络请求和用design库,我还是自己动手写了网络请求的封装类和一个design库的demo。当然,还是不免遇到了一些问题。<br>ps: 因为是前几天的事,也没截图,就随便记录一下啦。</p>
</summary>
<category term="DEVELOPING" scheme="https://frezc.github.io/categories/DEVELOPING/"/>
<category term="android" scheme="https://frezc.github.io/tags/android/"/>
</entry>
<entry>
<title>近期的一些琐事</title>
<link href="https://frezc.github.io/2015/11/09/%E8%BF%91%E6%9C%9F%E7%9A%84%E4%B8%80%E4%BA%9B%E7%90%90%E4%BA%8B/"/>
<id>https://frezc.github.io/2015/11/09/近期的一些琐事/</id>
<published>2015-11-08T17:01:00.000Z</published>
<updated>2019-04-09T15:39:54.154Z</updated>
<content type="html"><![CDATA[<p>近几个星期真是莫名的忙啊,积累了不少事可以写写了,这篇文章就当做一个记录了。</p><a id="more"></a><h2 id="创新实践课的安卓项目"><a href="#创新实践课的安卓项目" class="headerlink" title="创新实践课的安卓项目"></a>创新实践课的安卓项目</h2><p>这些项目对于个人来说并不是十分积极去参与的,不过既然都当了组长还是要做好的。带着其他几个完全不懂的还是要干很多事啊。于是呢,我就干干设计app结构,设计api,写文档,然后写服务端(另一组不会写api,我就自己做了),现在还要操心app这边的网络请求管理。。</p><p>服务器端被某人推荐用Laravel+jwt+dingo去写了。laravel框架还是非常方便的,对于我这种没写过php但知道服务器端如何处理请求的人来说看完文档也就会做了;dingo这个插件并没有某人说的那么方便,虽然能直接返回数据自动生成json,但会像下面这样:</p><pre><code>{Entity: {"xx": "xx", ...}}</code></pre><p>在外面多嵌套了一层,这样在app上解析时就不太方便了,于是最终还是使用<code>response()->json()</code>的方式。</p><h2 id="机器视觉课的小组项目"><a href="#机器视觉课的小组项目" class="headerlink" title="机器视觉课的小组项目"></a>机器视觉课的小组项目</h2><p>对于这种课都要做个项目我表示很吃惊,说实话我宁愿考试呢。其中翻译一篇长的要死的论文+做ppt就算了,还要小组做项目,我还是挺后悔选这门课的。不过既然不能换了,那还是要好好做的。</p><p>项目的题目是通过摄像头来监视交通系统,我选这个主要还是因为没有现成的加上挺有意思这两点吧。不过这样我的组员表示完全不懂了,想想到最后应该还是要我来设计算法吧。</p><p>这个项目我觉得还是挺有意思的,在十字路口通过双摄像头图像识别车辆的三维空间信息,然后就能十分精确的判断是否出现事故了,识别误差主要在图像到三维信息上。这个项目其实就一个问题,如何通过摄像机图像判断车辆中心点和矩形区域。这个我觉得可以使用模板对比的方法实现,但是对于一个尺寸的车辆还好,车辆模型一多工程量就会非常大了,总之还是有取巧的方法的,能给老师看看就够了。</p><h2 id="软考"><a href="#软考" class="headerlink" title="软考"></a>软考</h2><p>吐槽下周六的软考吧。<br>基础知识考的真是泛,计组、操作系统、数据库、软件工程、编译原理、设计模式、数据结构、算法分析、计网,我感觉以上全都有考到,对了还有门英语。虽然多,但都是选择题,应该能及格吧。</p><p>应用技术的考试我觉得就有意思多了,考的是系统设计(数据流图)、数据库设计(ER图)、软件设计(用例图、类图)、算法(很简单的求最长公共子串),还有道拿策略模式说话,实际就是简单的面向对象的多态编程。总的来说题还是出的不怎么样,有明显错误。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>能把薄膜键盘敲出机器键盘的声音的室友早上8点起来敲代码这事已经在督促我早睡早起了!<br>不然早上10点前我肯定睡不着了。(わらい)</p>]]></content>
<summary type="html">
<p>近几个星期真是莫名的忙啊,积累了不少事可以写写了,这篇文章就当做一个记录了。</p>
</summary>
<category term="ESSAY" scheme="https://frezc.github.io/categories/ESSAY/"/>
</entry>
<entry>
<title>typecho上的第一篇博客</title>
<link href="https://frezc.github.io/2015/11/02/my-first-blog/"/>
<id>https://frezc.github.io/2015/11/02/my-first-blog/</id>
<published>2015-11-02T02:20:00.000Z</published>
<updated>2019-04-09T15:39:54.154Z</updated>
<content type="html"><![CDATA[<p>自己撸了个域名后就决定换个地方写博客了,以前的博客<a href="https://frezc.github.io">点这访问</a>。<br>关于为什么要换地写呢,主要还是因为电脑装win10的时候把d盘格式化掉了,原来配置了挺长时间的博客配置(改了文件能用https访问,还用七牛做了国内的cdn)都没了,我也没劲再来一遍了,于是就干脆试试typecho吧。</p><p>从上学期Hackthon比赛后我就没怎么写过Android了,一直在学cocos2d-js和unity,毕竟我对游戏开发还是比较感兴趣的。<br>现在只用Unity了,毕竟比起cocos包含了很多方便的工具,自己做做游戏的话会省事很多。<br>近期为了创新实践的项目还写了个api服务器,不得不说Laravel框架在熟悉了后还是十分简单的,对于我这种没写过php的人都能写的行云流水啊。</p>]]></content>
<summary type="html">
<p>自己撸了个域名后就决定换个地方写博客了,以前的博客<a href="https://frezc.github.io">点这访问</a>。<br>关于为什么要换地写呢,主要还是因为电脑装win10的时候把d盘格式化掉了,原来配置了挺长时间的博客配置(改了文件能用https访问