-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
2418 lines (2418 loc) · 702 KB
/
search.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"?>
<search>
<entry>
<title><![CDATA[使用htm-webpack-plugin构建最终的html]]></title>
<url>%2F2021%2F08%2F08%2Fhtm-webpack-plugin%2F</url>
<content type="text"><![CDATA[确定使用html-webpack-plugin发现项目中有多个entry html,重复性极高,想去统一处理一下,只留两个entry: 一个pc端,一个mobile端。又看了一下项目的代码发现用的html-res-webpack-plugin,这个包去npm上查了一下发现更新已经是两年前了,所以这个是要干掉的。之前一直都用html-webpack-plugin,查了一下它的功能,发现还是可以满足我的需求的,于是我决定使用它来重构。另外项目中还有一些pug、 ejs的loader,用上的话感觉有点重,所以还是只留一个plugin吧 要满足的功能:首先整理一下构造一个最终的html,需要哪些点? 支持html partial:这样html回看起来比较清爽 支持css js文件inline插入:项目中有一些inline需要插入,这个也似于partial 支持图片链接的转换:html中会引用一些文件,希望文件可以通过webpack的打包 支持变量的替换: html中有动态的变量 支持打包后文件的注入 如何解决partial引入4和5是plugin支持的功能,其余的html-loader能满足,于是我加入了这样的配置 test: /\.html$/, use: [ { loader: 'html-loader' }, ] 但是实际用起来发现我的plugin不起作用啊,为什么会这样呢?在issuse中找到一个答案:html文件经过webpack的打包会变成module.export的形式,这样根本就走不到plugin,解决的办法可以将html变成ejs。ejs写起来太麻烦了,不想变成ejs,于是继续搜啊搜,终于找到了另一个办法<%= require('html!./partial.html').export %>,哈哈,想回复中说的,<%= %>是js代码,你可以任意写你想要的,那么2如法炮制,也采用这样的做法 <style> <%= require('css-loader!./index.css').default %> </style> <script> <%= require('babel-loader!./test.js') %> </script> 如果js不想进行打包也可以使用raw-loader,它直接将文件内容处理成一个string变量 图片的引入3的话,可以将引入图片的html做成一个partial,使用html来处理 <%= require('html-loader!./partial.html').default %> 或者也可以这样 <img src="<%= require('./logo.png') %>"> 感觉第一种方法会简单一些 另外迁移到项目中遇到的问题:js已经被loader打包了,单独指定不会有作用, 需要使用exclude排除掉。在解决的过程中,有一句话也很有意义: 最好不使用copy-webpack-plugin,会跳过webpack编译,失去webpack的一些功能 参考链接: is-there-a-way-to-include-partial-using-html-webpack-plugin https://github.com/jantimon/html-webpack-plugin/issues/1400 html-loader-overwrite-htmlwebpackplugin-expression how-does-html-webpack-plugin-work-with-html-loader html-loader-overwrite-htmlwebpackplugin-expression]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[单元测试我写的好烂]]></title>
<url>%2F2021%2F06%2F06%2Fbad-jest%2F</url>
<content type="text"><![CDATA[之前的积累:可以说很少在之前的公司,是最后才开始写测试的,那个时候写起来就感觉有点难受。我记得那个case是我尝试去点击Link,然后判断页面的url是否有改变,我刚开始尝试用侦测window.location去感知url的改变,但是history的改变,不会去触发location的事件,最后的最后,我退而其次去判断了函数有没有被调用,现在来看也不是一个好办法,判断的条件是这样才更稳妥: const spy = jest.spyOn(router, 'push') expect(spy).toBeCalledWith('new url') 使用jest暴露的问题这次刚进去的task也是写单测,相对之前,每个小业务都被封装在包中,react的部分也少,jest写单测的话,真的是捉襟见肘啊,而且我好像没有掌握jest的技巧,感觉写单测都是以写集成测试的思想在写,所以写出来的质量不怎么样。我新增的一个文件的测试,觉得还可以,但一看覆盖率才百分之四十多,于是我开始疯狂的补case想要把每个函数的每个分支都给覆盖了,所以一眼看上去我的测试写的特别多,特别长,但是对照其他同学写的,感觉我写的好像是特别笨重,效果不是那么大。我写了好几天的case,只提升了五个点💔,我总结一下这次遇到问题: 1. 异步函数:callback形式代码可能长这样: dynamicInsertScript('scriptUrl', (data) => { window.data = data // data.xxx }) data上的属性和方法特别多,mock起来特别费劲,要是callback里面代码巨多,那针对这一个函数要写很多case。首先刚开始写的时候,我又陷入了误区,一个很朴素的想法,我在写的时候,sleep上很长时间,等待这个函数load完,window.data不就有值了吗,然后我再去判断或者触发data上的属性,于是我写下了: test('case', () => { setTimeout(() => { expect(window.data).no.toBe(undefined) }, 1000) }) 写完之后,跑过了,完美,以后这样的例子就这么写就行了,开心的提了mr,reviewer给了comment:setTimeout在这里面是无效的,需要done一下。然后我又在setTimeout的最后一行加了done,但是jest一直报错 async callback was not invoked within the 5000ms timeout specified by jest.settimeout.,这个时候我想到setTimeout用错地方了,我还是老老实实的mock所有函数吧。于是改成了下面这样: jest.mock('@utils', () => ({ dynamicInsertScript: (_, cb) => { const data = { // xxx: } cb(data) } })); test('case', () => { expect(window.data).no.toBe(undefined) }) 这样的话就没有异步了,但是如果data上有很多很多属性,如果当前测试的文件里面有用到,必须把这些属性全部mock,如果有复杂的属性,就写起来比较艰难。 2. 异步函数:promise有了第一个的经验,可不能再用setTimeout了,这个mock的想法和上面的类似,对于代码 conts getList = () => { const data = await fetchData('xxx') // 下面是处理data的部分 } 可以这样写case: test('case', () => { const spy = jest.spyOn(api, 'fetchData') // 每次调用返回不同的值 spy.mockImplementationOnce(() => Promise.resolve({ // xxx })) // 一定要记得加await!!! await getList() spy.mockImplementationOnce(() => Promise.resolve({ // xxx })) await getList() }) 剩下的问题和上一个一行,你要mock的数据要完整。 3. setTimeoue如何快速执行针对下面的代码: const change = () => { setTimeout(() => { // xxx }, 0) } 可以这样来: jest.useFakeTimers(); test('case', () => { change() jest.runAllTimers() // 然后check结果 }) 4. 检查函数调用的参数针对下面的代码: const tip = () => { message.error('error') } 可以这样来: test('case', () => { message.error = jest.fn() tip() expect(message.error.mock.calls[0][0]).toBe('error'); }) 5. window.addEventListentest('case', () => { const map = {} window.addEventListen = (type, func) => { map[type] = func } window.removeEventListen = (type) => { delete map[type] } map.error({preventDefault: () => {}}) // 参数为event对象 }) 6. localStoragetest('case', () => { const spyGetItem = jest.spyOn(window.localStorage.__proto__, 'getItem') }) 7. new Date()test('case', () => { // jest < version 26 const mockDate = new Date(1466424490000) const spy = jest .spyOn(global, 'Date') .mockImplementation(() => mockDate) spy.mockRestore() }) 8. mock window伤的对象Object.defineProperty(window.document, 'cookie', { get: jest.fn(), });]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[史蒂夫·乔布斯传: 星星的陨落]]></title>
<url>%2F2021%2F03%2F26%2Fsteve-jobs%2F</url>
<content type="text"><![CDATA[怎么发现这本书的呢?买了kindle之后,在亚马逊上闲逛,看到了雪球出的段永平问答实录,段永平老师是一个很棒的企业家,早期的小霸王到现在的oppo和vivo。他现在重仓了苹果的股票,有人来问他原因,我大约记得的回复是:苹果是一个很专注的公司,并且按照目前的商业模式,十年后甚至更长的时间,苹果也会发展的很好。但看苹果的价格相比同类产品价格有点高,但是使用年限较长,算起来性价比还是很高的。 我是工作之后才开始用苹果的产品,mac、ipad、iphone,怎么说呢,首先是不卡而且扛用,使用体验比之前的电子产品都较好。大学甚至工作后,除了使用,对苹果没有更多的认识。不知道哪一天,突然去B站上找了iphone4的发布会看,非常震撼,要是还是在07年的话,一定会比这个时候更震撼,那时候还用着键盘手机,哒哒哒的打字呢。看视频的时候能感受到乔布斯的魅力所在。我一直觉得没有做不到的,没有想不到的,一切的发展靠想象,进而可行。乔布斯的一系列创新,都为行业带来了颠覆性的发展。 那段时间,是马斯克星链刚出来,吃饭的时候一起谈论,忽然发现对名人的认识好像有些欠缺,就开始找传记来看,第一本是马斯克,第二本就看到了乔布斯,不得不感叹,乔布斯找的作家也非常的棒,看书的时候,甭感受到乔布斯的热情,看得我也很热血澎湃。 产品timeline看完书,回想一下乔布斯的产品,麦金塔电脑 -> NeXT -> 皮克斯 -> iMac -> iPod -> iTunes -> iPhone -> iPad,每一个都是那么优秀。一方面是互联网的萌芽,再带上个人的美学,一切都是那么不可思议。 乔布斯的创新是设计,然后用现实扭曲立场,鞭策工程师们,最后完成一个又一个的产品。 皮克斯是创作动画响亮亮的工作室,从未想过和乔布斯很有渊源。出于美,在资金困难的情况下,乔布斯自掏腰包每年创造一部小短片,经历了五年的烧钱,终于制作出玩具总动画,五年磨一剑啊。NeXT和皮克斯都让乔布斯放弃了端到端一体化,放弃了硬件,那现在的M1芯片也算一直在践行延续他的之前的想法,毕竟系统已经这么出神入化了。 在乔布斯被驱逐出苹果,我还记得一个印象深刻的事,甲骨文的董事长埃里森想恶意收购苹果,然后让乔布斯回去当CEO,乔布斯拒绝了,还想以让他们请乔布斯回去,最后事情如乔布斯所想。不得不感叹,乔布斯大方向把我的还是很精准的,他能清楚的认识到当时的连续几个CEO都不会让苹果变得更好。 当乔布斯重回苹果后,比较震惊的是把几千条产品线砍到只剩四个领域,这里我理解了段永平说的专注。将更多的精力集中在有目标更清晰的方向上,把一个产品做好做精。大刀阔斧的改革之后,出现了iMac,现在看早造型可能有点土,找了当时的采访,乔布斯形容像果冻。苹果的设计师艾弗也非常酷! iPhone真的是鬼斧神工,那么时候还是键盘手机,键盘占手机长度的二分之一,它的出现是对键盘的颠覆性冲击,尽管那个时候已经有诺基亚,摩托罗拉那样的手机生产商,但是第一次涉足手机领域,就有如此的产品,简直是太神了!多点触控、滑动解锁、键盘只在需要时弹起、输入纠错、旋转屏幕,还有手机的包边,它们都可以集中在一个手机上!一切都难以想象! 热爱乔布斯在斯坦福大学的毕业演讲说“I love it”。当被自己创立的公司驱逐,备受打击,如何继续自己的事业,因为热爱,所以能继续下去。以我的角度来看,热爱是生命之源,热爱生活,热爱工作,让一切多姿多彩。总而言之,看完这本书,我收获的是“热爱和专注”。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[高亮文件中识别出的文字结果最佳实践 - canvas]]></title>
<url>%2F2021%2F02%2F28%2Fdraw-canvas%2F</url>
<content type="text"><![CDATA[对于一个文件,ocr识别后,会吐出文件中所有文字的坐标,结合图片标示出来,目前就是这样的效果: step1: 只标示图片这个好办啊,只有图片,一个image搞定,标示高亮用div绝对定位展示 .container .inner img src="xxxx" .highlight1 style="xxxx" .highlight1 style="xxxx" 遇到需要旋转的图片也能搞定,给img增加transform的style就可以搞定,参照我之前写的基于大旋转角度摆正图片的血泪汗 step2: 增加pdf的展示,但是可以只展示第一页哦在原来的基础上增加了一个新的类型,那么可以延续之前的方式,使用pdf-dist将pdf的第一页渲染到canvas上,再用canvas转化成图片的base64 const viewport = page.getViewport({ scale: 1 }) const context = $canvas.getContext('2d') $canvas.height = viewport.height $canvas.width = viewport.width const task = page.render({ canvasContext: context, viewport }) const base64 = $canvas.toDataURL('image/png') 这样就可以支持pdf,canvas在里面只充当了一个中间商,觉得有些可惜,也没有再扩展别的方法。 总之是决定img一条路走下去,里面的路已经走的差不多了 step3: 支持整个pdf的支持从step2走下去,按照原来的方式继续做,实现的非常ok。但是我详细的了解一下,pdf现在是10页的规格,以后说不准有多少…顿时傻眼了 目前的highlight都是使用的div表示的,一页保守计算是20个,10页的就是200个div,按照现在的方式,太多的dom元素会有性能问题。所以去看看还有没有更好的方式。 canvas真是及时雨,它什么都可以render。那我直接画上highlight岂不是一个canvas元素就搞定了,使用canvas代替一个img+n个highlight。在step2的基础上加上: const ctx = canvas.getContext('2d'); // 1. 清除canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // 2. render pdf // 参照step2的code // 3. render highlight ctx.fillStyle = 'rgba(87, 87, 244, .3)'; highlightList.forEach(item => { const { top, left, width, height } = item; ctx.fillRect(left, top, width, height); }); 另外对于图片的展示,canvas也是手到擒来,旋转的图片也可以搞定。这样比使用style摆正图片也简单一些。 const image = new Image(); image.onload = () => { const { naturalWidth, naturalHeight } = image; const scale = width / naturalWidth; $canvas.width = width; $canvas.height = naturalHeight * scale; ctx.drawImage(image, 0, 0); }; image.src = imgBase64; 另外在canvas里面画带框的矩形 ctx.strokeStyle = borderColor; ctx.strokeRect(left, top, width, height); 这里要注意的问题是, 对canvas设置width: 100%; height: 100%;的效果是在原来300*150基础上拉伸,所以一定要等canvas设置好宽高再进行draw 本来使用一个canvas来表示一个pdf,但是canvas有最大的宽高32767*32767。我图省事,将canvas的高设置称为200000,结果发现chrome上canvas只剩一个哭脸图标。参见详情 总结step3中决定使用canvas全部代替image是一个好的决定。其实2中就应该拥抱canvas,可谁知道当时怎么想的。使用每页渲染的方式,以后pdf页数多了,也可以使用按需加载进行优化。 所以遇到巨多的dom节点时候,看是否可以使用cavas去代替!!!]]></content>
<categories>
<category>ocr</category>
</categories>
<tags>
<tag>ocr</tag>
</tags>
</entry>
<entry>
<title><![CDATA[咦,图片到浏览器上怎么会自动旋转]]></title>
<url>%2F2021%2F01%2F23%2Fexif-data%2F</url>
<content type="text"><![CDATA[不知道你有没有遇见过图片在电脑上打开是正的,但是使用img标签显示的时候,却被旋转了。旋转会在chrome firefox上发生,safari上没有发生旋转。同样你将图片发送给安卓和苹果手机的用户,让他们分别打开,发现,咦,两个手机的表现竟然也不一样,这是为什么呢? 首先先定位到问题的原因:戳这里chrome会读取图片的EXIF data信息,并对图片做旋转。 关于EXIF,可以查看维基百科的介绍: 可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的文件格式,可以记录数码照片的属性信息和拍摄数据。Exif可以附加于JPEG、TIFF、RIFF等文件之中,为其增加有关数码相机拍摄信息的内容和索引图或图像处理软件的版本信息。 因为chrome是不支持tiff的展示,我做了“tiff -> png”的转换,所以在处理上我只需要处理jpg展示的格式就可以了。我怎么把它去掉呢? 直接能抹掉吗第一反应就是这个,我读取图片的base64之后,能过固定的格式拿到exif data的图片,然后移除掉。抱着这个想法,去查找有没有对应的方案,得到的答案就否。移除信息之后造成对文件的破坏,答案链接 使用canvas当中介在canvas上绘制图片的时候,canvas是不读取exif信息的,所以你可以先draw,然后再导出来。 在原来旋转的基础上再进行旋转对jpg的文件的旋转,是读取exif中的orientation确定的,orientation有8种情况: 数字分别对应:1 = Horizontal (normal)2 = Mirror horizontal3 = Rotate 1804 = Mirror vertical5 = Mirror horizontal and rotate 270 CW6 = Rotate 90 CW7 = Mirror horizontal and rotate 90 CW8 = Rotate 270 CW 图片左边的 1 3 6 8比较常见,2 4 5 7涉及到图片镜像的反转,一般比较少见。例如当orientation的值为6的时候,表示浏览器已经对图片做了90度的旋转,那么你再进行360 - 90的旋转,就可以得到原来的样子。 如何得到orientation值呢?有一个exif-js这个库做这个: import EXIF from 'exif-js' import { decode } from 'base64-arraybuffer' // 这里的imgSrc是图片的base64 if (imageType.indexOf('jpeg') > -1 || imageType.indexOf('jpg') > -1) { const arraybuffer = decode(imgSrc.split(',')[1]) const exif = EXIF.readFromBinaryFile(arraybuffer) console.log(exif) } 另外,mozilla的文档上也有base64转换成array buffer的方法 参考资料 https://stackoverflow.com/questions/42401203/chrome-image-exif-orientation-issue https://stackoverflow.com/questions/19791494/how-to-remove-exif-data-from-image-using-javascript https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side https://developer.mozilla.org/zh-CN/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_1_%E2%80%93_JavaScript's_UTF-16_%3E_base64 https://www.impulseadventure.com/photo/exif-orientation.html https://www.impulseadventure.com/photo/lossless-rotation.html]]></content>
<categories>
<category>ocr</category>
</categories>
<tags>
<tag>ocr</tag>
</tags>
</entry>
<entry>
<title><![CDATA[(译)深入了解React Fiber的内部]]></title>
<url>%2F2021%2F01%2F15%2Ftranslate-deep-dive-into-react-fiber-internals%2F</url>
<content type="text"><![CDATA[原文链接:A deep dive into React Fiber internals - LogRocket Blog 有没有想过当你调用ReactDOM.render(<App />, document.getElementById('root'))发生了什么? 我们知道ReactDom在后台构建DOM树,并将应用渲染到屏幕上。但是ReactDom是怎么构建DOm树的?当app的state改变时,它是如何更新树? 在这篇文章中,我开始从React在15.0.0之前如何构建DOM树,这个模型的陷阱以及在16.0.0的新模型中如何解决这些问题。这篇文章会涵盖很多内部实现细节的概念,它对于使用React进行实际的开发不是必要的。 Stack reconciler让我们从熟悉的ReactDOM.render(<App />, document.getElementById('root'))开始。 ReactDOM模块将<App />传给reconciler,这里有两个问题: <App />指的什么? reconciler是什么 让我们解开这两个问题:<App />是一个react元素,也是元素描述树(lements describe the tree)。 元素是描述组件的实例或DOM节点及其所需属性的普通对象。 –React Blog 换句话来说,元素不是真正的DOM节点或者组件实例。它们只是用来向React描述它们是什么元素类型,有什么属性,子节点是什么的一种方法。 这就是React的真正力量所在。React消除了如何构建、渲染、管理真正DOM树的生命周期的复杂部分,简化开发人员的工作。要了解他们的真正含义,让我们看一下使用面向对象的概念。 在典型的面向对象世界中,开发者需要实例化和管理每个DOM元素的生命周期。例如,你想要创建一个简单的表单和一个提交按钮,那么状态管理甚至是简单的操作都需要开发人员的努力。 假设Button组件有一个状态变量isSubmitted,Button的生命周期类似于以下的流程图,其中的每个状态都需要app处理: 流程图的大小和代码的行数随着状态变量的增加而呈指数级上升。 React有精确解决此问题的元素,在React中,有两种类型的元素: DOM元素:当元素的类型是一个字符串的时候,例如:<button class="okButton"> OK </button> Component元素:当类型是一个类或者函数的时候,例如:<Button className="okButton"> OK </Button>,这里Button是一个类组件或者函数组件,这是我们用的典型的React组件。 理解两种类型(class or function)是一个简单的对象是非常重要的。它们仅仅描述在屏幕上需要渲染什么,在你创建或者实例化的时候不会导致任何渲染的发生。这对于React解析和遍历它们来构建DOM树是非常容易的。当遍历完成后才会发生实际的渲染。 当React遇到一个class或者函数组件时,会根据组件的props去确定它需要渲染哪个元素。例如,如果<App />如下: <Form> <Button> Submit </Button> </Form> React会根据相应的props确定<Form>和<Button>渲染内容。例如,Form组件是一个函数组件,看起来是这样: const Form = (props) => { return( <div className="form"> {props.form} </div> ) } React会调用render()来确定它渲染的元素,并最终看到它渲染一个带有child的<div>。React将重复这个过程,直到确定页面上的每个组件的基础DOM标签元素。 递归遍历一棵树来确定React应用程序组件树的底层DOM标签元素的过程称为reconciliation。在reconciliation结束后,React知道了DOM树的结果和像react-dom或react-native这样的渲染器更新DOM节点所需的最小变更集。 这意味着当你调用ReactDOM.render()或者setState的时候,React将执行一个reconciliation。在setState的情况下,它执行遍历,并将新树和老树进行比较后找出变化。接着,它在现在的树上应用这些变化,从而更新成调用setState相应的状态。 现在我们理解reconciliation是什么了,现在让我们看一下这个模型的陷阱。 顺便说一句,为什么叫做the “stack” reconciler? 这个名字来源于stack数据结构,它是后进先出的机制。stack和我们刚才看的有什么关系吗?好吧,事实证明,由于我们实际上做了递归,因此它和stack有关系。 Recursion(递归)要了解为什么会发生这个情况,让我们举一个简单的例子来看一下调用栈(call stack)发生了什么 function fib(n) { if (n < 2){ return n } return fib(n - 1) + fib (n - 2) } fib(10) 正如我们看到的这样,调用栈会将每次fib()的调用push进栈中,直到弹出要返回的第一个函数调用fib(1)。然后它继续push递归调用,并在到达return语句的时候弹出。这样它有效的使用堆栈,直到fib(3)返回成为最后一个从栈中pop的最后一个元素。 我们刚刚就看到的reconciliation算法是纯递归算法。子树的更新会立即重新渲染。虽然这很好用,但是有一些限制。正如Andrew Clark所说: 在一个UI中,每次的更新立即应用是不必要的,事实上,这样做很浪费会导致帧下降,降低用户的体验。 不同类型的更新有不同的优先级 - 一个动画的更新需要比数据更新更快的完成 现在,我们说的掉帧是什么意思?为什么递归方法有问题?为了掌握这一点,让我从用户的体验角度简要说明什么是帧频(frame rate)以及它为什么这么重要。 帧频是指连续图像显示在屏幕上的频率,我们在电脑屏幕上看到的一切是由在屏幕上以瞬时出现的速率播放的图像或帧组成。 为了理解它,可以将电脑显示器想像成翻书,当你翻书时,将每一页视为一定速率播放的帧。换句话来说,电脑屏幕只是一本自动翻页的书,会一直播放屏幕的变化。如果还不能理解,请看下面的视频(这是xx的一个视频,下面用图片代替)。 通常情况下,人眼看视频会感觉流畅的话,视频需要以每秒30帧(FPS)的速率播放。更高的速率会有更好的体验。这是为什么游戏玩家更喜欢第一人称精确射击游戏中更高帧频主要的原因。 话说回来,现在的大多数设备刷新屏幕在60帧,或者换句话说,1/60=16.67ms,这意味着每16ms显示一个新的帧。这个数字是非常重要的,因为React在屏幕上渲染超过16ms的话,浏览器就会掉帧。 事实上,浏览器还有一些基础工作要做,所以你的操作必须在10ms内完成。如果你没有办法满足这个要求,帧就会下降,屏幕上的内容会显得特别乱。这会对用户的体验产生了负面的影响。 当然,这不是静态和文本内容引起注意的重要原因。但是在显示动画的情况下,这个数字至关重要。所以如果React的reconciliation算法每次遍历整个App树后,有更新就重新render。如果遍历超过16ms,就会导致掉帧。 这是为什么最好按优先级对更新进行分类,而不是盲目地将传递给reconciliation的每个更新都应用的重要原因。还有一个好的特性是在下一帧中暂停和恢复。这样,React能使用16ms的预算更好的控制渲染。 以上使React团队重写reconciliation算法,新算法叫做Fiber。我希望现在对于Fiber的为什么存在以及具有的意义有了进一步的认识。出现在让我们看一下Fiber是如何解决这个问题的。 How Fiber works现在我们知道了开发Fiber的动机,让我们总结一下它需要实现的功能。 同样,我引用Andrew Clark的笔记: 为不同类型分配不用的优先级 暂停并稍后恢复它 如果不需要的话,能终止 复用之前完成的工作 实现这样的事情的挑战之一是JavaScript引擎的工作方式,并且在js中某种程度上缺乏这样的线程。为了理解这个,让我们探索一下JS引擎是如何处理执行上下文。 JavaScript execution stack每当你在js中写一个函数,当我们调用函数的时候,js引擎创建了执行上下文。每次js引擎开始,它会创建一个全局上下文,它包含全局对象,例如浏览器中的window对象、nodejs的global对象。在js中,这些上下文使用stack数据结构来处理。 所以,当你写以下代码时: function a() { console.log("i am a") b() } function b() { console.log("i am b") } a() Js引擎首先创建一个全局上下文,并push进执行栈(the execution stack)中。接着它创建函数a()的函数执行上下文,因为a()中调用了b(),所以它将创建b()的函数执行上下文,并push进栈中。 当函数b()返回时,引擎会销毁b()的上下文,当我们从a()中退出,a()的上下文被销毁。执行期间的栈看起来像这样: 但是当浏览器有了一个异步事件例如http request,会发生什么?js引擎是否会存进栈中来处理异步事件,等待事件的完成? JS引擎在这里做了不同的处理,在执行栈的最上面,js引擎还有一个queue的数据结构,也称为事件队列。事件队列处理异步事件的调用,例如浏览器中的http或者网络事件。 Js引擎处理队列的方式是等待执行栈为空。所以每次执行栈变空的时候,js引擎会查看事件队列,pop出里面的item,然后执行它。重要的是要注意:JS引擎仅仅当执行栈为空或者执行栈中只有全局执行上下文是去检查事件队列。 尽管我们将它们称为异步事件,但这里有一个细微的区别:事件相对于何时进入队列是异步的,但相对于它们何时真正得到处理则并不是真正异步的。 回到stack reconciler,React遍历树是在执行栈中做的。所以当计算更新完成后,它们会放到事件队列中。只有当执行栈为空的时候,更新才会被执行。这正是Fiber解决的问题,几乎要实现栈的暂停、恢复、停止的智能功能。 再次引用 Andrew Clark的笔记: Fiber is reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame. The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them however (and whenever) you want. This is crucial for accomplishing the goals we have for scheduling. Aside from scheduling, manually dealing with stack frames unlocks the potential for features such as concurrency and error boundaries. We will cover these topics in future sections. 简单来说,Fiber是具有自己虚拟栈的工作单元。在之前实现的reconciliation短发中,React创造了一个不可变的树对象,然后递归的遍历它。 在现在的实现中,React创建一个可修改的fiber节点树,fiber节点描述了组件的state、props以及需要渲染的底层DOM元素。 而且由于fiber节点是能改变的,React不需要为更新再创建每一个节点,当有更新时,它可以简单的克隆和更新节点。另外,如果是fiber树,React不会做递归遍历,而是,它创建一个单链表做父亲优先、深度优先的遍历。 Singly linked list of fiber nodesfiber节点代表栈中的帧(stack frame),也代表React组件的一个示例,一个fiber节点包含以下属性: Typediv,span等,例如基础组件(host components)或者class、funciton的组合组件 Key与传递给React组件的key一样 Child当我们调用组件的render返回的元素, 例如: const Name = (props) => { return( <div className="name"> {props.name} </div> ) } 这里Name的child是div,因为它返回的是一个div元素 Sibling表示render返回的元素列表情况 const Name = (props) => { return([<Customdiv1 />, <Customdiv2 />]) } 在上面的例子中,<Customdiv1 />和<Customdiv2 />是<Name>的child。<Customdiv1 />的Subling是<Customdiv2 />,它们形成单链表。 Return表示返回的栈中的帧,从逻辑上讲,返回的是父fiber节点,因此它代表的是父亲。例如上面例子中Customdiv1的return指向Name pendingProps and memoizedProps备份(Memoization)意味着存储函数执行的结果以便后续的使用,从而避免重复计算。pendingProps代表传递给组件的props,memoizedProps在执行栈的末尾初始化,并存储该节点的props。 当传入的pendingProps和memoizedProps相等时,代表fiber之前的输出可以被重用,避免不必要的工作。 pendingWorkPriority表示Fiber代表的工作优先级的数字,ReactPriorityLevel模块列出了不同优先级以及它的含义。除NoWork为零外,数字越大表示优先级越低。 例如,你可以使用以下函数来检查fiber的优先级是否至少与给定级别一样高。调度程序使用优先级字段来搜索要执行的下一个工作单元。 function matchesPriority(fiber, priority) { return fiber.pendingWorkPriority !== 0 && fiber.pendingWorkPriority <= priority } Alternate在任何时候,一个组件的实例最多对应两个fiber:当前的fiber和进行中(in-progress)的fiber。当前节点的alternate是进行中的fiber,进行中的fiber的alternate对应的是当前的fiber。当前的fiber表示已经渲染的,进行的fiber表示还没有返回的栈中的帧。 OutputReact的叶子节点。他们是渲染环境中普通的标签(例如浏览中,它们是div、span等),在jsx中,它们用小写的标签名字表示。 从概念上讲,fiber的output就是函数的返回值。每个fiber最终都有输出,但是output仅通过基础组件创建叶子节点。然后将output挂载到树上。 output的值提供给渲染器,以便可以刷新更改。例如,让我们来看一下面代码的fiber树: const Parent1 = (props) => { return([<Child11 />, <Child12 />]) } const Parent2 = (props) => { return(<Child21 />) } class App extends Component { constructor(props) { super(props) } render() { <div> <Parent1 /> <Parent2 /> </div> } } ReactDOM.render(<App />, document.getElementById('root')) 我们可以看到fiber树是由相互链接的子节点单链表和父子关系的链表组成。这种树可以使用深度优先搜索遍历。 Render phase为了理解React是如何构建这棵树,并在其上执行reconciliation算法,我决定在React源码中写一个单元测试,来debugger这个过程。 如果你对这个过程很感兴趣,克隆React的源码,走到这个目录,增加jest测试来debbuger。我写的测试很简单,是很基本的渲染:一个带有文字的按钮。当你点击按钮的时候,app会销毁这个按钮渲染一个带有不同文字的div,此处的text是一个状态变量。 'use strict'; let React; let ReactDOM; describe('ReactUnderstanding', () => { beforeEach(() => { React = require('react'); ReactDOM = require('react-dom'); }); it('works', () => { let instance; class App extends React.Component { constructor(props) { super(props) this.state = { text: "hello" } } handleClick = () => { this.props.logger('before-setState', this.state.text); this.setState({ text: "hi" }) this.props.logger('after-setState', this.state.text); } render() { instance = this; this.props.logger('render', this.state.text); if(this.state.text === "hello") { return ( <div> <div> <button onClick={this.handleClick.bind(this)}> {this.state.text} </button> </div> </div> )} else { return ( <div> hello </div> ) } } } const container = document.createElement('div'); const logger = jest.fn(); ReactDOM.render(<App logger={logger}/>, container); console.log("clicking"); instance.handleClick(); console.log("clicked"); expect(container.innerHTML).toBe( '<div>hello</div>' ) expect(logger.mock.calls).toEqual( [["render", "hello"], ["before-setState", "hello"], ["render", "hi"], ["after-setState", "hi"]] ); }) }); 在初始渲染中,React创建了一个当前树,这是最开始要渲染的树。createFiberFromTypeAndProps()是一个根据React元素的数据创建每一个React fiber的函数。当我们跑测试时,在这个函数中加一个断点,就能看到调用栈,它看起来像这样: 正如我们看到的,调用栈从render()开始调用,最后到createFiberFromTypeAndProps()。这里还有一些我们非常感兴趣的函数:workLoopSync(), performUnitOfWork(), beginWork()。 function workLoopSync() { // Already timed out, so perform work without checking if we need to yield. while (workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress); } } 在workLoopSync()中,React从<App>节点开始构造树,并递归的移动到子节点<div>,<div>, <button>上。workInProgress保留接下来需要更新的fiber节点引用。 performUnitOfWork()将fiber节点当作输入参数,来得到节点的alternate,并调用beginWork(),这相当于在执行栈中开始执行一个函数上下文。 当React构建树时,beginWork()只是简单的使用createFiberFromTypeAndProps()来创建fiber节点。React递归执行,最终performUnitOfWork()返回null,表示已经到了树的末尾。 但我们做instance.handleClick()(点击button触发状态的更新)会发生什么?在这种情况下,React遍历fiber树,克隆每一个节点,并检查时是否需要更新每个节点,当我们看这种场景下的调用栈,它看起来像这样: 尽管我们没有在第一个调用栈中看到completeUnitOfWork() 和completeWork(),但是我们在这个图里能看到。就像performUnitOfWork()和beginWork(),这两个函数执行当前运行的完成部分,这实际上意味着已经返回到堆栈。( these two functions perform the completion part of the current execution which effectively means returning back to the stack.) 正如我们看到的这样,这四个函数配合执行,并且还控制当前正在完成的工作,这正是stack reconciler缺少的。从下图可以看出,每个fiber节点都由四个阶段组成: 记住每个节点直到子节点和兄弟节点执行completeWork()并返回之后才会执行completeUnitOfWork()是非常重要的。例如,对于<App>,它开始执行performUnitOfWork()和beginWork(),然后继续执行Parant1的performUnitOfWork()和beginWork()等等…一旦<App/>所有的子节点执行完completeWork()后,才会回到<App/>上执行。 以上是React的渲染阶段。基于click()更新的构建的树叫做workInProgress树,它是等待渲染的草稿树。 Commit phase一旦渲染阶段完成,React就到了提交阶段。在此阶段,它交换当前树和workInProgress树的根指针,从而完成当前树和基于click()更新的草稿树的替换。 不仅如此,React还会重用之前的节点。这个优化会使从先前状态到下一个状态的的变化是一个平滑的过渡。 那16帧的时间呢?React对每个运行的工作单元有一个内部计数器,并在执行的时候不断监听时间的限制,时间一到,React就会暂停当前正在执行的工作单元,并将控制权交给主线程,让浏览器渲染。 接着,在下一帧,React回从暂停的地方继续构建树。当有足够的时间,它就会提交workInProgress树来完成渲染。 总结为此,我强烈推荐你看 Lin Clark 的这个视频,这里她用很好的动画来解释这个算法,看起来更容易理解。(这是xx的一个视频,下面用图片代替) 希望你喜欢阅读这篇文章。 推荐: react-custom-renderer-1,这里面有好多概念,可以看一下 inside fiber in depth overview of the new reconciliation algorithm in react React Components, Elements, and Instances]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>react</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一次乌龙的排查-webapck重复打印log]]></title>
<url>%2F2021%2F01%2F10%2Fwebpack-print-log-repeat%2F</url>
<content type="text"><![CDATA[之前写了一个webpack的plugin,用来上传静态资源到oss上,它经过三个项目的考验,还是很实用的。这次在另一个项目中,使用了它,我发现jk的log中,上传的log竟然出现了三次!!!刚开始我怀疑是jk的问题,但是我在本地进行了一次打包,凉凉,发现本地上传的log也是发生了三次,于是我就开始找到底是哪的问题 难道是webpack用的hook有问题上传的plugin用的是afterEmit这个hook,它的解释是: Called after emitting assets to output directory. 项目也用了别的plugin,例如copyPlugin、htmlPlugin,这两个也都会释放文件到ouputPath中,难道它们会多次触发afterEmit,于是拿着“webpack afterEmit call multi times”去搜,但是没有找到相关的问题,于是我开始模拟这种webpack的配置 // webpack.config.js // ... plugins: [ new MyPlugin(), new CopyWebpackPlugin( [ path.resolve(__dirname, "source") ], ) ], // my_plugin.js class MyPlugin { constructor() { } apply(compiler) { this.doWith(compiler) } doWith(compiler) { compiler.hooks.afterEmit.tapPromise('MyPlugin', () => { console.log('afterEmit run run!!!') return Promise.resolve() }) } } module.exports = MyPlugin 测试下来发现afterEmit只打印了一次。我开始想是不是想错了,顺着两个另外的plugin的代码,像htmlPlugin它用的是Compilation Hooks中的processAssets来生成html,确定了我的猜想是错误的,afetrEmit只会执行一次,除非你recomplie了。 难道是umi的问题现在的项目是使用umi2生成中,其中的配置完全是黑盒,难道它里面同时开了两个compile,抱着这样的猜想,去找了umi中的webpack配置,发现和平时没有什么不同,我看了package.json中的依赖,有一个webpackbar,它是来显示webpack打包的进度,我尝试把它禁用,然后再执行一次buid,发现log上只打印了一次!!!我尝试用“webpack print log duplicate”搜索没有收获,找webpackbar的issue也没什么收获。 最后的最后看了一下webpack的hooks,反正都是上传文件,done之后传也行,为了安心,于是增加了一个配置,使用done这个hook来执行上传动作。 又一次验证了一个道理:你的问题如果在issue和stackoverflow中找不到答案,那一定是你错了。]]></content>
<categories>
<category>webpack</category>
</categories>
<tags>
<tag>webpack</tag>
</tags>
</entry>
<entry>
<title><![CDATA[css解决不了的,就用js解决]]></title>
<url>%2F2020%2F12%2F20%2Fuse-js-relove-css-unresolve%2F</url>
<content type="text"><![CDATA[对于展示上的问题,我一向是css能解决的就不要动用js。这个迭代中加了一个放大缩小的功能,放大的话势必要出现横向滚动条,但是在实际上还会有超宽的absolute元素,我写了一个简单的demo <style> .container { width: 400px; height: 600px; border: 1px solid rgb(198, 198, 199); margin: 50px auto; overflow-y: scroll; overflow-x: auto; } .inner { position: relative; border: 1px solid blue; } .mark { position: absolute; left: 120px; top: 120px; width: 600px; height: 600px; border: 2px solid red; } img { width: 120%; } </style> <div class="container"> <div class="inner"> <img src="https://aibici-test.oss-cn-beijing.aliyuncs.com/document-mining-backend/c9325f71bc7df09b10de105835314c2e.jpg" /> <div class="mark"></div> </div> </div> mask超级宽,一旦它超出父元素的宽度是,势必会让父元素变宽。我想要的效果是mask不会影响父元素的宽度。于是我开始在这个基础上各种魔改,以达到我的效果。结果折腾了大半天也不可以。 最后是使用js会动态计算inner的宽高。同时我反思了一下这个css方案的失败。img的宽度使用百分比的时候,它依赖于inner的宽度,如果我需要hidden absolute元素的宽度,必须在inner上加overflow: hidden。这样一来,图片超出100%的时候,也是无法滚动的。hidde和scroll互相矛盾;如果给mark加一个父元素testtest来包裹,有一个问题是testtest使用absolute定位式,宽度是等于inner的宽度的,即是inneryou了scroll。 所以最后的方案是使用js去计算inner的宽度,img永远保持width为100%]]></content>
<categories>
<category>前端</category>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安全篇-介绍xss以及如何应用]]></title>
<url>%2F2020%2F12%2F11%2Fxss-introduction-md%2F</url>
<content type="text"><![CDATA[xss的类型 反射型例如:从url中读取再渲染,在项目中,读取出来的值会当作string去渲染,不会使用innerHTML 存储型例如:知识点的答案,富文本中输入script标签,这个会在node过滤掉,但是对于,这个前端在渲染的时候会使用xssFilter过滤掉,即使不过滤,在增加csp的情况下,也执行不了。 import { filterXSS } from 'xss' export function xssFilter(html: string) { return filterXSS(html) } DOM型例如:一般在搜索列表的时候,返回的数据会在原来的string的基础上,加上em标签,这个时候你不得不使用innerHTML来使em标签起作用,假如title是<img src=1 onerror='xxx'>,这个就中枪了 策略 [high]重要对外的项目一定增加csp策略,例如吾来、mage、官网 [high]第三方组件/资源 禁止引入非可信来源的第三方js,应检查页面内引入的第三方js资源是否可控 [high]禁止使用eval [high]开启xss保护模式:”X-XSS-Protection”: “1; mode=block” X-XSS-Protection:浏览器监测到xss的攻击会立即阻止页面的加载。浏览器支持性不太好app.use(helmet.xssFilter()); [middle]定期检查依赖关系 使用npm/yarn audit得到易受攻击的列表,升级他们来避免安全问题,github中有专门的bot来检测repo的依赖,有风险的会自动提pr [middle]减少没有必要的dangerouslySetInnerHTML使用 [middle]设置Referrer-Policy: 用来控制在request中可以包换多少refer信息设置方式: 在header中设置:”Referrer-Policy”: “no-referrer” app.use( helmet.referrerPolicy({ policy: ["origin", "unsafe-url"], }) ); 或者a标签增加rel = noopener or noreferrer meta中设置 [low]使用script标签引入资源时,增加integrity来验证资源没有被篡改过 <script src= "https://example.com/example-framework.js" integrity= "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..." crossorigin= "anonymous" ></script> [low]增加Strict-Transport-Security、X-Frame-Options、X-Content-Type-Options相关头部设置 Strict-Transport-Security: 允许网站告诉浏览器应该使用https访问,而不是http X-Frame-Options: HTTP响应标头可用于指示是否应允许浏览器在,,或中呈现页面 X-Content-Type-Options: 相当于提示标志,被服务器用来提示客户端一定要遵循在content-type中对MIME类型的设定 [low]Feature-Policy: 限制浏览器特性和api的访问(该功能还在实验状态)"Feature-Policy": "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none';" // 可以在这个网站securityheaders得到网站的分数 参考链接: 面向DevSecOps的编码安全指南|JavaScript篇 13 Security Tips for Front-End Apps Prevent DOM-based cross-site scripting vulnerabilities with Trusted Types Protect the frontend Does Security Matter to Front End Developers and Tips To Not Get Hacked 10 security tips for frontend developers Cross-site_scripting mdn 前端安全系列(一):如何防止XSS攻击? Cross-site scripting]]></content>
<categories>
<category>安全</category>
</categories>
<tags>
<tag>安全</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图解网络-总结]]></title>
<url>%2F2020%2F11%2F23%2Fnetwork-knowledge-summary%2F</url>
<content type="text"><![CDATA[五类问题: HTTP 基本概念 Get 与 Post HTTP 特性 HTTPS 与 HTTP HTTP/1.1、HTTP/2、HTTP/3 演变 HTTPHTTP 基本概念基本概念首先先回顾一下网络的五层协议:物理层(光纤)、数据链路层(以太网、ppp不太熟)、网络层(IP,实现主机与主机之前的通信,也叫点对点通信)、传输层(tcp)、应用层(http) 参考: https://zh.wikipedia.org/wiki/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F http是超文本传输协议,可以这样理解:http是一个用在计算机世界里的协议,它使用计算机能够理解的语言确定了一种计算机世界中两点之前传输文字图片音频视频等等超文本数据的规范,以及相关的各种控制和错误处理方式(行为约定和规范)。 状态码 我经常用到的是100(跨域情况下post请求会先发一个options侦测一下是否可以进行跨域),200(成功),204(成功了但是没数据返回),206(断点续传,没用过),304(使用本地的缓存资源),403(没有权限),404, 500(服务器端错误),502(Bad Gateway) 常见字段 host Content-Length Connection:常见值keep alive,用与请求复用 Content-Type: json、html、form Content-Encoding: 数据压缩的办法,常见gzip Get 与 Post区别 get用于读的请求,post用于修改数据的请求,这里引申出俩概念:安全和幂等,安全是不破坏服务器上的资源,幂等是多次执行相同的操作,结果都是相同的 get能被缓存,post不能被缓存 get发送一个tcp包,post发两个tcp包(https://www.zhihu.com/question/28586791) HTTP 特性优缺点http1.1的优点:这个之前不清楚 简单 灵活和易于扩展 应用广泛和跨平台 http1.1的缺点: 无状态cookie、jwt 明文传输 上面两个导致了不安全: 窃听、篡改、冒充 HTTPS 与 HTTP区别 http + 在tcp和http之间增加ssl/tls安全协议,使报文能够加密传输 https在tcp三次握手后,还需要进行安全协议的握手过程 http 80, https 443 https协议需要想CA(证书权威及后申请数字证书),来保证服务器的身份是可信的 解决了http的不安全 信息加密:交互信息无法被窃取 校验机制:无法篡改,篡改了就不能正常显示 身份证书:可以证明网站的真实性 如何解决的 混合加密的方式实现信息的机密性,解决了窃听的风险 摘要算法的方式来实现完整性,它能够为数据生成独一无二的指纹,指纹用于校验数据的完整性,解决了篡改的风险 将服务器公钥放入到数字证书中,解决了冒充的风险 混合加密:在通信建立前,采用非对称加密的方式交换会话密钥,后续拿着会话密钥进行对称加密的交换数据摘要算法(https://www.cnblogs.com/shichangming/p/10906040.html)用来实现完整性能够为数据生成独一无二的指纹,用于校验数据的完整性,解决了篡改问题 SSL/TLS协议基本流程 client向服务器索要并验证服务器的公钥 双方协商生产会话密钥 双方采用会话密钥进行加密通信前两步是建立过程,也就是握手阶段,涉及到四次通信;前三次的通信会各自生成一个随机数,client 随机数 + server 随机数 + premaster(公钥加密的随机数) 算出会话密钥配图原图看不清楚 HTTP/1.1、HTTP/2、HTTP/3 演变http 1.1http1.1提升的性能: 为了提升1.0中每个请求都会新建一个TCP连接,引入长连接,还记得上面的keep-alive吗 采用长连接,使管道传输成为可能。第一个请求发出去,不必等它回来,可以继续发第二个请求出去,减少整体相应时间。但还是要保持顺序 仍然存在的性能瓶颈: 头部没有压缩就发送,header信息越多,延迟越大 发送冗长的首部。每次互相发送相同的首部造成浪费 服务器是按照请求顺序响应的,如果服务器响应慢,会招致客户端请求不到数据,也就是队头阻塞 没有请求优先级控制 请求只能从客户端开发,服务器只能被动响应 http 2http 2的优化: 头部压缩:HPACK算法:客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就发索引号 帧:数据是二进制格式 一个请求/回应的所有数据包成为数据流:不按照顺序、可指定优先级,客户端发出的编号是奇数,服务端发的是偶数 多路复用:一个连接中并发多个请求,不用按照顺序一一对应 server push 总之,尽管到了http2,丢包仍是命门。多个http请求复用tcp连接,下层的tcp协议是不知道有多少http请求的。一旦发生丢包,就会出发tcp的重传机制,这样在一个tcp连接中的所有http请求都必须等待这个丢了的包重传回来。总而言之,这是基于tcp传输的问题。 http 3直接将http的下层协议改为基于UDP的QUIC协议实现类似tcp的可靠性传输 QUIC(https://www.cnblogs.com/zhoulujun/p/13060875.html)有自己的机制来保证传输的可靠性,当某个流发生丢包时,只会阻塞这个流,不会影响其他流 TLS3升级成最新的1.3(https://zhuanlan.zhihu.com/p/44980381)版本,头部压缩算法也升级成Qpack https建立一个连接要6次交互,QUIC合并成了3次QUIC 是一个在 UDP 之上的伪 TCP + TLS + HTTP/2 的多路复用的协议。 参考: 陈皓.HTTP的前世今.酷壳CoolShell.https://coolshell.cn/articles/19840.html QA 我看文中 TLS 和 SSL 没有做区分,这两个需要区分吗?SSL 是洋文 “Secure Sockets Layer 的缩写,中文叫做「安全套接层」,将它标准化后名称改为TLS(Transport Layer Security),中文叫传输层安全协议 为啥 ssl 的握手是 4 次 IP 基本认识IP的作用是在复杂的网络环境将数据包发送给最终目的主机和数据链路层的区别:mac实现直连的两个设备之前的通信,而ip则负责在没有之恋的两个网络之间进行通信传输 NAT: https://zhuanlan.zhihu.com/p/26992935 IP地址的基础知识五类ipv4是由32位的整数组成,每8位一组,共四组,五种类型的ip地址主机号全1的指定某个网络下所有的主机,用于广播;全0指定某个网络 D类和E类地址是没有主机号的,所以不可用于主机ip,d类常被用于多播(将包发送给特定组内的所有主机),e类地址是预留的分类,暂时未使用 优缺点IP分类的优点是:简单明了、选路(基于网络地址)简单缺点是:同一网络下没有地址层次,缺少地址的灵活性;不能很好的与现实网络匹配;可以使用 CIDR 无分类地址解决 无分类地址 CIDR没有分类地址的改变,32位被划分为两部分,网络号+主机号:a.b.c.d/x;或者子网掩码,它的另一个作用是划分子网 公有ip和私有ip 环回地址是同一台计算机上的程序之间进行网络通信时所使用的一个默认地址,127.0.0.1 IP分片与重组数据链路的最大传输单元MTU是不相同的,之所以不相同是因为每个不同类型的数据链路的使用目的不同。 在分片传输中,一旦某个分片丢失,则会造成整个 IP 数据报作废,所以 TCP 引入了 MSS (最大分段大小)也就是在 TCP 层进行分片不由 IP 层分片,那么对于 UDP 我们尽量不要发送一个大于 MTU 的数据报文。 IPv6v4 32位不够用了,v6是128位 亮点: IPv6 可自动配置,即使没有 DHCP 服务器也可以实现自动分配IP地址,真是便捷到即插即用啊。 IPv6 包头包首部长度采用固定的值40字节,去掉了包头校验和,简化了首部结构,减轻了路 由器负荷,大大提高了传输的性能。 IPv6 有应对伪造 IP 地址的网络安全功能以及防止线路窃听的功能,大大提升了安全性。 改进: 取消了首部校验和字段 取消了分片/重组相关字段,不允许路由器进行分片与分组,只能在源与目标主机上操作 取消选项字段 IP协议相关技术DNS域名解析:将域名网址自动转换成IPARP:路由表确定IP数据包的下一跳,网络层的下一层是数据链路层,所以还要知道下一跳的mac地址;主机通过广播ARP请求,统一个链路中的所有设备收到ARP请求时,会拆开请求包的内容,如果ARP请求包中的目标ip地址和自己的意志,那么设备就将自己的mac地址塞入ARP的响应包返回给主机。对应关系会缓存起来。 与ARP相关的是RAPP协议。需要架设RARP服务器,需要ip的机器先发出询问,根据应答信息设置自己的ip地址。 DHCP用来动态获得ip地址,这不是发现注册吗???在DHCP交互过程中,全部用的UDP广播;当DHCP服务器和客户端不在同一个局域网内,路由器不会转发广播包,为了解决这个问题,就出现了DHCP中继代理,有了中继代理,对不同网段的IP地址也可以由同一个DBHCP服务器进行管理。 NAT主要进行网络地址转换,为了缓解IPv4地址耗尽的问题,NAT会讲同一区域的主机对外通信时,将私有IP换成公有IP地址。NAPT的作用时将网络地址和端口互换。NA(P)T依赖自己的转换表,有一下问题: 外部无法主动和NAT内部服务器进行连接,因为没有转换记录 转换表的生成和转换操作都会产生性能开销 通信过程中,如果NAT路由器重启了,所有的TCP连接都将被重置 解决办法:IPv6 + NAT穿透技术 ICMP(互联网控制报文协议)的主要功能包括:确认IP包是否成功送达目标地址、报告发动过程中IP包被废弃的原因、改善网络设置等。大致可以分为两大类: 用于诊断的查询信息,也就是查询报文类型 通知出错原因的错误信息,也就是差错报文类型 IGMP 是因特网组管理协议,工作在主机(组播成员)和最后一跳路由之间。 QAping是如何工作的?ping基于ICMP协议工作,其工作过程为: 源主机首先会构建一个ICMP回送请求消息的数据包:类型为8+序号为1+发送时间 ICMP协议将这个数据包连同目标地址一起交给IP层,本级IP地址作为源地址,协议字段设置为1表示ICMP地址,再加上一些其他的控制信息,构建一个IP数据包,再加入MAC头 目标主机收到数据帧时候,先检查MAC地址,如符合就接收,否则则丢弃;接受后将IP数据包从帧中提取出来,ip层检查后,将有用的信息提取后交给ICMP 目标主机会否件一个ICMP回送响应信息数据包,回送响应数据包的类型字段为0,序号是接收到的请求数据包中的序号,然后发送给源主机 在规定的时间内,没收到应答包,则说明目标主机不可达;接收到了说明可达。此时,源主机会检查,用当前时刻减去数据包最初的源主机上的时刻,就是ICMP数据包的延迟。 traceroute - 差错报文类型的使用作用是: 故意设置特殊的 TTL,来追踪去往目的地时沿途经过的路由器。 故意设置不分片,从而确定路径的 MTU。 经典问题:输入网址后,期间会发生什么 URL解析,生产http信息:请求报文 URL对应的ip地址查询:域名服务器 协议栈+tcp:我理解是五层协议从上到下的过程来构成一个报文。tcp报文、三次握手、ip报文、mac地址获得 硬件设施:网卡(数字信号转换成电信号)、交换机(根据mac地址表查找mac地址,然后将信号发送给响应的端口)、路由器(去掉包开头的mac头部,解析ip地址,包上自己的mac地址在此进行传输 解析:五层协议由下向上 tcp相关问题tcp连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括socket、序列号和窗口大小成为连接 tcp和udp差别 TCP 和 UDP 区别: 连接TCP 是面向连接的传输层协议,传输数据前先要建立连接。 UDP 是不需要连接,即刻传输数据。 服务对象TCP 是一对一的两点服务,即一条连接只有两个端点。 UDP 支持一对一、一对多、多对多的交互通信 可靠性TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。 UDP 是尽最大努力交付,不保证可靠交付数据。 拥塞控制、流量控制TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。 首部开销TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。 传输方式TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。 分片不同TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输 层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完 数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输 效率非常差,所以通常 UDP 的报文应该小于 MTU。 应用场景:由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于: FTP 文件传输、 HTTP / HTTPS由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于: 包总量较少的通信,如 DNS 、 SNMP 等;视频、音频等多媒体通信;广播通信 保活机制定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作用,每个一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前tcp连接已经死亡,系统内核将错误信息通知给上层应用程序。 在linux中,tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接 相关的活动,则会启动保活机制tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。所以最少需要经过2小时11分15秒才可以发现一个死亡连接 为什么是三次握手比较常见的回答是因为三次握手才能保证双方具有接收和发送的能力 阻止重复历史连接的初始化(例如旧的syn比新的syn先到达) 同步双方的初始序列号 避免资源浪费(例如客户段的syn阻塞了) SYN攻击攻击者短时间伪造不同的IP的SYN报文,server每接收到一个就进入SYN_RECV状态,但是server发出去的报文无法得到client的应答,时间一长就会沾满server的SYN的接收队列,使server不能为正常的用户服务 解决方式:修改Linux参数(p129) 重传机制超时重传:设置一个定时器,超出指定时间没有收到对方的ACK报文,就重发该数据;(RTO的值应略大于RTT的值)快速重传:当收到三个相同的ACK报文时,会在定时器过期之前,重传丢失的报文段;缺点是不清楚重传哪些报文SACK:在tcp的头部选项字段中加一个sack的东西,将缓存的地图发送给发送方,这样发送方就知道哪些数据收到了,哪些数据没收到,就可以只重传丢失的数据D-SACK:使用sack来告诉发送方有哪些数据被重复接收了 滑动窗口一来一往效率低下,于是创建一个窗口,可以一下子发出去好多包,根据收到的响应的报文,滑动窗口 流量控制发送方不能无脑的发数据给接收方,要考虑接收方的处理能力。 拥塞控制在网络出现拥堵时,如果继续发送大量的数据包,可能会导致数据包时延、丢失等,这是tcp就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这种情况就会进入恶性循环被不断放大。于是又了拥塞控制,避免发送方的数据填满整个网络。发送方没有在规定的时间内接收到ack应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。这时候发送窗口大小=min(拥塞窗口,接收窗口)。拥塞控制的四个算法 慢启动:刚完成tcp连接的时候,有一个慢启动的过程(173)。规则:当发送方每收到一个ACK,拥塞窗口大小就会加1,呈指数式增长。但是有一个慢启动门限的状态变量ssthresh,cwnd=时,就会使用拥塞避免算法 拥塞避免:规则:每当收到一个ACK时,cwnd增加1/cwnd 拥塞发生:超时重传(ssthresh = cwmd/2,cwnd = 1)、快速重传(ssthresh = cwnd,cwnd = cwnd/2) 快速恢复:cwnd = sshresh+3,重传丢失的数据包,如果再收到重复的ACK,那么cwnd增加1;如果收到洗数据的ACK后,把cwnd设置为拥塞发生时的ssthresh 实战篇(p179)这篇只写结论,具体推理过程请查看原文档。 分析网络的两大利器:tcpdump 和 Wireshark TCP 第一次握手的 SYN 丢包了,会发生了什么?客户端在一直没收到服务短的ACK时,会一直超时重传5(这个值可以设置)次。每次的RTO超时时间是不同的,都是指数(翻倍)上涨,超过最大重传次数之后,客户端不会再发送SYN包。 TCP 第二次握手的 SYN、ACK 丢包了,会发生什么?客户端会超时重传SYN包,服务端也会超时重传SYN+ACK包,重传次数根据系统设置,默认是5 TCP 第三次握手的 ACK 包丢了,会发生什么?服务端没收到第三次握手的ACK,会重传SYN+ACK,超过最大重试次数后,服务端会断开tcp连接客户端则有两种情况: 如果客户端没发送数据包,一直处于ESTABLISHED状态,然后经过2小时11分钟15秒才发现死亡连接,于是客户端就会断开连接。 如果客户端发送了数据包,一直没收到服务端对该数据包的确认报文,则一直重传该数据包,直到重传次数超过tcp_retries2值后,客户端会断开连接 TCP快速建立连接一般建立tcp连接需要三次握手,在快速建立连接的情况下,只需要两次握手即可。这个是TCP Fast Open。过程是:在第二次的握手时,服务器会在SYN+ACK的基础上再加一个cookie,客户端收到后,在下次请求的时候,在SYN包中带上cookie发送给服务端,就提前可以跳过三次握手的过程 tcp半连接队列和全连接队列满了会发生什么?半连接队列(SYN队列):收到SYN的连接会存储到这里全连接队列(accept队列):收到客户端发来的SYN确认报文后,将连接从半连接队列移除,然后创建新的完全连接,然后添加到accept队列 当全连接队列满了之后,后续的请求就会被丢弃。在linux中可以指定用什么策略回应客户端。丢弃连接只是linux的默认行为,我们还可以像客户端发送RST复位报文,告诉客户端连接已经建立失败。那么如何增大全连接队列的呢?TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是min(somaxconn, backlog) 在前面我们在分析 TCP 第一次握手(收到 SYN 包)时会被丢弃的三种条件: 如果半连接队列满了,并且没有开启 tcp_syncookies,则会丢弃; 若全连接队列满了,且没有重传 SYN+ACK 包的连接请求多于 1 个,则会丢弃; 如果没有开启 tcp_syncookies,并且 max_syn_backlog 减去 当前半连接队列长度小于 (max_syn_backlog >> 2),则会丢弃; tcp_syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功。]]></content>
<categories>
<category>网络</category>
</categories>
<tags>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安全篇-介绍CSP以及如何应用]]></title>
<url>%2F2020%2F11%2F23%2Fcsp-introduction%2F</url>
<content type="text"><![CDATA[内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。 你可以配置你的服务器返回 Content-Security-Policy HTTP头部,也可以使用meta元素配置该策略。 配置内容安全策略涉及到添加 Content-Security-Policy HTTP头部到一个页面,并配置相应的值,以控制用户代理(浏览器等)可以为该页面获取哪些资源。比如一个可以上传文件和显示图片页面,应该允许图片来自任何地方,但限制表单的action属性只可以赋值为指定的端点。一个经过恰当设计的内容安全策略应该可以有效的保护页面免受跨站脚本攻击。 CSP directivesCSP directives很多,重点只看下面几个(带删除线的都是一般情况下用不着的): 1. default-src为其他的CSP指令提供备选项,对于以下列出的指令,假如不存在的话,那么用户代理会查找并应用default-src指令的值 child-src connect-src font-src frame-src img-src manifest-src media-src object-src script-src style-src worker-src 2. script-src指定javascript的有效源,不仅包括加载到<script>元素中的url,还包括哪联脚本事件处理程序(onclick等) 3. style-src指定stylesheets的有效源 4. img-src指定images和favicons的有效源 5. connect-src用于控制允许通过脚本接口加载的链接地址,受影响的api: a标签 fetch xhr websoket EventSource(https://developer.mozilla.org/en-US/docs/Web/API/EventSource) 6. font-src指定使用@font-face加载字体的有效源 7. frame-ancestors指定可以使用<frame>,<iframe>,<object>,<embed>或<applet>嵌入页面的有效父项。 8. object-src指定了 <object>, <embed>, and <applet> 元素的源 9. media-src指定使用audio和video加载的media的有效源 10. child-src它定义了Web Worker的有效源,例如iframe 11. navigate-to会限制文档通过任何方式发起的导航URL,包括form-action、a、window.location、window.open等 CSP directives的常用值1. <host-source>域名或者ip地址,网站的地址也可以包含前导通配符,也可以使用*作为端口号,表示对所有的合法端口都有效。例如:http://*.example.com,mail.example.com:443,https://store.example.com,*.example.com, 2. <scheme-source>像http:,https:,data:,mediastream,blob:,filesytem, 3. ‘self’指和现在的来源一样,包括相同的url和端口号。注意你一定要用单引号包起来 4. ‘unsafe-eval’允许使用eval()和类似方法从字符串创建代码。注意你一定要用单引号包起来2020-12-20补充:$('body').html('<script>alert(1)</script>')这种也属于eval,点这里查看,所以你的项目如果是spa一定要加上 5. ‘unsafe-hashes’允许启动特定的内联事件处理程序。如果你只需要允许内联事件处理程序,而不允许内联script元素或者javascript: URL,与unsafe-inline表达式相比,这是一种更安全的方法。 6. ‘unsafe-inline’允许内联资源的使用,例如<script>元素、javascript: URL、内联的事件处理、内联的<style>元素 7. ‘none’空集,也就说说没有匹配的url 8. 'nonce-<base64-value>'使用加密随机数的特定内联脚本的允许列表。服务器应在在每次请求产生唯一的nonce。 <script nonce="xxxxx"> </script> 9. '<hash-algorithm>-<base64-value>'脚本或样式的sha256,sha384或sha512哈希 nodejs的实现 在express中可以使用helmet来实现 const csp = { directives = { defaultSrc: ["'self'", '*.example.com'], scriptSrc: [ "'self'", '*.example.com', "'unsafe-inline'", "'unsafe-eval'", (req, res) => `'nonce-${res.locals.cspNonce}'`, ], styleSrc: ["'self'", 'blob:', '*.example.com', "'unsafe-inline'"], imgSrc: ["'self'", 'data:', '*'], connectSrc: ['*', 'data:'], fontSrc: ["'self'", '*.example.com', 'data:',], mediaSrc: ["'self'", 'data:'], childSrc: ["'self'"], } } app .use((req, res, next) => { res.locals.cspNonce = uuidV4() next() }) .use(helmet.contentSecurityPolicy(csp)) 在koa中,可以使用koa-helmet,代码形式同上 参考资料:内容安全策略( CSP )default-src]]></content>
<categories>
<category>安全</category>
</categories>
<tags>
<tag>安全</tag>
</tags>
</entry>
<entry>
<title><![CDATA[2020.11.14本周总结]]></title>
<url>%2F2020%2F11%2F14%2F2020-q4-summary-1%2F</url>
<content type="text"><![CDATA[本周遇到了五个问题,三个react的,剩下的是业务上的 1. input change问题修复文件上传部分是手写的,对于选择文件的变化一般是监听input(type=”file”)的change事件,但是当两次选择的文件的一样的时候是不会触发change事件的。如何修复它呢?借用react中的key,change事件中改变input的key,会使react重新创建input,保证每次的input的都是不一样的,就不会有上述的问题了 const Rc = () => { const inputKey = useRef(v4()) const changeFile = () => { inputKey.current = v4() } return ( <input type="file" key={inputKey.current} onChange={changeFile} /> ) } 2. for中循环改变state现在所有的开发已经全部拥抱hook了,除非hook搞不定。目前的场景是在一个函数中循环改变state的值,在class中使用前一定要先取值,但是hook的话在一个函数中,直接不取就直接用了,导致值只会改变一次,因为在函数运行的时候,state的值就行了。想清楚这个,我需要定义一个局部变量作为每次循环中的累积。 const Rc = () => { const [tab, setTab] = useState(0) const handle = () => { let newTab = tab for (...) { newTab = ... setTab(newTab) } } } 3. useEffect对待多个effect不同的操作首先需求上显示这并不是一个复杂的组件,所以还是选择hook而不是class来组织。但是遇到的问题是,两个fetch方法会被三个值影响,两个props,一个state,有的值改变会导致两个fetch,有的值会导致一个fetch。在class中你可以通过prev和current进一步区别到底是哪个值改变,但是hook是没有这个记忆功能的,一个组件都写完了我也不想改成class了,最后的解决是定义一个ref来保存之前的prev const Rc = () => { const prevTab = useRef(default) useEffect(() => { if (prevTab !== tab) { fetch1() } fetch2() }, [tab, value2, value3]) } 4. 业务中二级关系变成了三级这个在需求中应该是breakchange,按照正常的迭代节奏,必须数据库的表结构必须要兼容之前的老数据。如果当作新需求进行表结构设计非常简单。和后端同学交流后,考虑到灰度到上线两天内的数据,就必须要兼容,所以技术方案和之前比较纯粹的比显得复杂。 5. 在包含不重复的数字数组中随机取出n个的数字对待这个问题,想到一个最简单粗暴的方法,使用random + set,使用set的不存在重复值的特性 const getNnumber = (list, n) => { const { length } = list const nums = new Set() while(nums.size < n) { const index = Math.ceil(Math.random() * length) nums.add(nums[index]) } return nums } 同事提供的方法是这个 不过在stackoverflow上又找到了另一个方法:可以先混洗数组,然后取前n个how-to-get-random-elements-from-an-array,这个方法看起来挺好👍 numbers.sort( function() { return 0.5 - Math.random() } );]]></content>
<categories>
<category>总结</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从微前端实践总结]]></title>
<url>%2F2020%2F10%2F24%2Fmicro-service%2F</url>
<content type="text"><![CDATA[目前公司主要是维护一个大的toB平台,整个项目很是庞大,目前是由两个SPA项目组成,使用nginx转发进行两个项目之间跳转,这就有一个体验不统一的问题,跳转菜单时,有些页面是部分刷新,有些页面是整体刷新。另外目前还在进行迭代开发,尽管是分成了两个项目,但是每一个项目也很大,导致build时间很长。 概念由来同事推荐了一个single-spa的package,真是眼前一亮。它的描述中有一个微前端的概念,微前端首先是由tw的一个工程师提出,Micro Frontends,文中也说了microfront的目的: In this article we’ll describe a recent trend of breaking up frontend monoliths into many smaller, more manageable pieces, and how this architecture can increase the effectiveness and efficiency of teams working on frontend code. single-spa但是single-spa作为jsEntry其实有很多限制: 增加一个配置中心:经过webpack打包的静态资源一般都是带有hash串的,每次更新,entry file的文件名是不一样的。所以使用它,你必须增加一套机制来更新app的入口文件名 需要hack css的顺序: 有些css的引入会优先在entry file前引入,如果要在single-spa中实现,你需要多写点代码了== 在我的眼中,single-spa是一个引路人,它奠定一个大基础:微前端一定是和技术栈无关的。但是如果要使用它,需要在它的基础上增加很多配置,但是,这些配套的配置很难找到任何指导材料,对于新手来说,很不友好。 qiankunqiankun是由阿里出品,在single-spa的基础上进行封装的一些框架。相比于single-spa的js entry来说,它采用的是html entry,所以你在原有项目的基础上进行很少的改造,就可以完成介入。 采用html entry之后,它为每个activeApp创建沙盒,来保证不同app的数据的单纯性。但这样带来的问题就是:同一个url上只能对应一个app。我想保证我的portal(基座)项目纯净来保持项目的稳定,对于左菜单右内容的布局来说,想把菜单和内容拆成两个app是不可以的,我不得不放弃一些portal的的稳定性,将菜单放入portal。这是我使用qiankun的最大痛点。 另外还有一些别的微前端框架: https://github.com/ice-lab/icestark https://github.com/worktile/ngx-planet(这个是angular的) 其他问题1.common resource:每个项目是单独打包,那么无法知道里面的一些公共资源,如何处理common resoures呢?目前我的做法就忽略这个了,没有做这方面的优化 联调困难:将项目分离后,开发的时候,就不如之前顺手了。将菜单移出去之后,跳转页面变得困难了== 整合项目的复杂度和现有项目有关:如果你的项目不是从脚手架开始的,所有的webpack都是自己配置的,那么你可能在项目build兼容umd上会花费很多时间。 另外,我在想微前端是否可以和微服务进行类比,我可以向它学习一些经验。在spring微服务实战中,里面有介绍微服务的构建要素: 微服务不仅仅是业务逻辑,你需要考虑服务将要运行的环境以及服务将如何扩展并具有弹性 合适的大小:如何确保你的微服务具有合适的大小,这样就不会让一个微服务承担太多的职责? 位置透明:在一个微服务应用,多个服务实例可以快速启动和关闭,你如何管理服务调用的物理细节 有弹性的:你如何保护你的微服务消费者和路由失败的服务应用程序的完整性,并确保你有一种快速失败的方法? 可服用的:你如何确保生成的服务的每个新实例都具有与生产中所有的其他服务实例相同的配置和基础代码? 可扩展的:你如何受用异步处理和事件最小化服务之间的直接依赖并确保你能优雅的扩展你的微服务? 上面的要素放在微前端的话,可以不用关注这个,k8s完全可以胜任,另外还有两个概念是:服务注册和服务发现,它可以类比成上文提到的single-spa中的配置中心,在go中有现成的envoy可以做这个事情,里面的细节暂不清楚是如何实现的。另外由于前端的代码是最后运行在一个容器中,那么会比微服务会多一个命名隔离的概念,有了css modules你可以解决css的重名问题。 微服务对比微服务来说,微服务中的重点又是什么呢? 在Spring微服务实战中这样介绍微服务: 微服务是分布的、松散耦合的、单一职责的软件服务2014 年左史,微服务概念被引入到软件开収社区。它得到了许多试图在技术上和觃模 上挑戓单体应用的大型组织的直接响应。微服务是一个小的,松散耦吅的分布式服务。微服务允许你将一个大型的应用程序分解成易亍管理和职责明确的组件。微服务通迆将大型仒码 分解成小的、定义明确的块,帮劣应对复杂性的传统问题。你需要接受一个关键概念:微服务可仔将应用程序的功能分解和分拆,它们是完全独立的。 看到这我想插一句:之前用ror开发的时候,它就是一个单体应用,所有的代码都在一个项目中,ror现在有微服务相关的指导方案了吗?[TODO] 参考资料: https://zhuanlan.zhihu.com/p/34862889]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[你是否按下enter键]]></title>
<url>%2F2020%2F10%2F10%2Fwhether_to_press_enter%2F</url>
<content type="text"><![CDATA[今天在查看代码的时候,判断按下的键是否是enter,有两个不同的方式,有监听keypress的,也有监听keydown的,判断键是否是enter有使用event.which的,也有使用event.keyCode的,它们之前又有什么区别呢? <Input onKeyPress={keyPress} onKeyDown={keyDown} /> keypress event的定义 The keypress event is fired when a key that produces a character value is pressed down. Examples of keys that produce a character value are alphabetic, numeric, and punctuation keys. Examples of keys that don’t produce a character value are modifier keys such as Alt, Shift, Ctrl, or Meta. keydown event的定义 The keydown event is fired when a key is pressed.Unlike the keypress event, the keydown event is fired for all keys, regardless of whether they produce a character value. 所以两者的区别是按下修饰键不回出发keypress事件。所以对于enter键的判断使用两个事件进行捕捉都是可以的,但是keypress已经不推荐使用。 另外,两者的第一个参数都是keyEvent,在mdn的解释中,keyCode和which作用一样,定义都是 Returns a Number representing a system and implementation dependent numeric code identifying the unmodified value of the pressed key 不过这两个方法也是不推荐使用了,MDN推荐使用KeyboardEvent.key 来代替,不过这个属性会返回string类型,而keyCode和which都返回number类型。怎么说,有Don't break the web这个理念在这里,只能说你还可以用下去。 我拿着keyDown去react里面搜了一下,发现了一个domEvents.js的文件,里面全都是创建一些常见类型的事件。关于react中的合成事件可以点这里]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[v8字节码]]></title>
<url>%2F2020%2F09%2F21%2Fjs-byte-code%2F</url>
<content type="text"><![CDATA[对于字节码,之前有过听过,但是没有深入的了解。 高级语言中,cpp是直接转换成二进制,java作为一个跨平台的语言,其中就有字节码的概念(点这里),对于js,都知道的V8引擎,它是怎么工作的呢? 请先阅读参考文章: understanding-v8s-bytecode 上篇文章的翻译 文章中最重要的是这张图片了,v8会先将javascript翻译成字节码,再将字节码翻译成机器语言,如何查看生成的字节码呢?使用node --print-bytecode test.js,只要再原来的基础上加上–print-bytecode就可以了。 让我们写一个for循环 function for_loop(){ for (let i = 0; i < 10; i++) { console.log(i) } } for_loop() 使用上面的命令,会打印出来很多很多东西,v8不仅仅会翻译你的代码,其中也会有一些自己包含的东西在里面,例如console,可以使用--print-bytecode-filter来过滤一下,你会得到 [generated bytecode for function: for_loop] Parameter count 1 Frame size 24 17 E> 0x1897b5c16f02 @ 0 : a0 StackCheck 36 S> 0x1897b5c16f03 @ 1 : 0b LdaZero 0x1897b5c16f04 @ 2 : 26 fb Star r0 41 S> 0x1897b5c16f06 @ 4 : 0c 0a LdaSmi [10] 41 E> 0x1897b5c16f08 @ 6 : 66 fb 00 TestLessThan r0, [0] 0x1897b5c16f0b @ 9 : 94 1c JumpIfFalse [28] (0x1897b5c16f27 @ 37) 23 E> 0x1897b5c16f0d @ 11 : a0 StackCheck 58 S> 0x1897b5c16f0e @ 12 : 13 00 01 LdaGlobal [0], [1] 0x1897b5c16f11 @ 15 : 26 f9 Star r2 66 E> 0x1897b5c16f13 @ 17 : 28 f9 01 03 LdaNamedProperty r2, [1], [3] 0x1897b5c16f17 @ 21 : 26 fa Star r1 66 E> 0x1897b5c16f19 @ 23 : 57 fa f9 fb 05 CallProperty1 r1, r2, r0, [5] 48 S> 0x1897b5c16f1e @ 28 : 25 fb Ldar r0 0x1897b5c16f20 @ 30 : 4a 07 Inc [7] 0x1897b5c16f22 @ 32 : 26 fb Star r0 0x1897b5c16f24 @ 34 : 85 1e 00 JumpLoop [30], [0] (0x1897b5c16f06 @ 4) 0x1897b5c16f27 @ 37 : 0d LdaUndefined 77 S> 0x1897b5c16f28 @ 38 : a4 Return 根据参考文章中的带a的单词,一般是用来检查累加器,那么可以尝试理解一下: StackCheck // 检查堆栈 LdaZero // 累加器置0,acc = 0 Star r0 // 将累加器的值赋给r0,r0 = 0 LdaSmi [10] // 累加器置为常数10,acc = 10 TestLessThan r0, [0] // 比较r0和acc JumpIfFalse [28] (0x1897b5c16f27 @ 37) // 为false,根据后面的地址会跳到LdaUndefined这一行 StackCheck // 检查堆栈 LdaGlobal [0], [1] // 累计器置为全局变量console Star r2 // 将累加器的值赋给r2, r2 = console LdaNamedProperty r2, [1], [3] // 读取r2的属性赋值acc,本来理解的是后面的1、3指的是.log,log,但是我试了其他的字节码,这个指针会变,所以这一块我也不是奔清楚 Star r1 // r1 = acc, 也就是console.log CallProperty1 r1, r2, r0, [5] // 这是调用输出,5不清楚代表的什么 Ldar r0 // acc = r0, Inc [7] // acc++ Star r0 // r0 = acc JumpLoop [30], [0] (0x1897b5c16f06 @ 4) // 循环,跳到LdaSmi [10]这一行 LdaUndefined // 累加器置为undefined Return // 返回累加器中的值 在理解产生的字节码的时候,比较迷惑的是[数字], 不知道代表什么意思,LdaSmi [10]这个是参考文章中有,LdaNamedProperty r2, [1], [3]这个文章中也有,1和3代表索引,但是再尝试理解另一个函数的字节码时,也是console.log,但是索引就变了== 刚开始有一个错误理解,每种语言的字节码应该会大同小异,为此,我还找了一些java的字节码看,但是对比下来,用处不大。还是看不懂,一顿搜索,终于找到了一篇好文(下面参考文章的第一个),那个文章是为了对比let和var的效能问题,总算有了眉目。 借用维基中的一句话:「理解字节码以及理解Java编译器如何生成Java字节码与学习汇编知识对于C/C++程序员有一样的意义。」 参考文章: 從 V8 bytecode 探討 let 與 var 的效能問題 understanding-v8s-bytecode]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[javascript设计模式 - 总结]]></title>
<url>%2F2020%2F09%2F05%2Fjs-design-pattern%2F</url>
<content type="text"><![CDATA[反模式(anti pattern)针对一个问题的不良解决方案可能会导致糟糕的情况。在react blog中有一篇使用中的反模式,可以看一下You Probably Don’t Need Derived State Constructor pattern在oop中,Constructor一种在内存已经分配该对象的情况下,用于初始化对象的特殊方法。但是js中,几乎所有东西都是对象,我们通常最感兴趣的是object constructors。 function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles; this.toString = function () { return this.model + " has done " + this.miles + " miles"; }; } // We can create new instances of the car var civic = new Car( "Honda Civic", 2009, 20000 ); var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); The Constructor Pattern Difference between Constructor pattern and Prototype pattern Singleton pattern它限制了类的实例化次数。Singleton模式,在实例不存在的情况下,可以通过一个方法创建;如果试过实例存在,它就会返回该对象的引用。 最近遇到的场景是,项目有一套自定义的配置模板,你需要使用js去读取它。但是在umi创建的项目中,发现这个文件会反复执行好几遍,于是可以使用单例,在config存在的情况下直接返回,不需要再进行读取文件的等等的操作 const FOO_KEY = Symbol("foo") global[FOO_KEY] = { foo: "bar" } // define the singleton API // ------------------------ var singleton = {} Object.defineProperty(singleton, "instance", { get: function(){ return global[FOO_KEY] } }) // ensure the API is never changed // ------------------------------- Object.freeze(singleton) // export the singleton API only // ----------------------------- module.exports = singleton The Singleton Pattern Creating A True Singleton In Node.js, With ES6 Symbols Observer pattern一个对象维持一系列依赖它的对象,将有关任何变更自动通知给它们。想想经常用的库,Rxjs中是不是有observer这个概念。 The Observer Pattern Observer vs Pub-Sub Pattern Mediator pattern中介者是一种行为设计模式,它允许我们公开一个统一的接口,系统的不同部分可以通过该接口通信。 对于DOM事件冒泡和事件委托,如果所有的事件处理在document上,不是在单个的node节点上,当前document就充当了一个中介者的角色。 Is the use of the mediator pattern recommend? The Mediator Pattern Facade pattern外观模式为更大的代码体提供了一个方便的更高层次的接口,能够隐藏其底层的真实复杂性。 最经常用jquery即是外观模式。 The Facade Pattern]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:ComponentDidMount和useEffect是不一样的,为什么]]></title>
<url>%2F2020%2F08%2F28%2Ftranslate-componentdidmount-and-useeffect-are-not-the-same-heres-why%2F</url>
<content type="text"><![CDATA[原文链接: https://medium.com/javascript-in-plain-english/componentdidmount-and-useeffect-are-not-the-same-heres-why-cea02f474c82 当组件挂载的时候进行一些设置是很常见的,例如网络请求。在hooks没有出现的时候,我们可以使用componentDidMount方法。在过渡到函数组件的时候,自然会寻求等效的hooks。 hooks和声明周期基于不同的原则。诸如compomentDidMount之类的方法是围绕声明周期和渲染时间展开,而hooks则是围绕state和与DOM的同步设计的。 许多程序员认为使用useEffect(fn, [])可以代替componentDidMount,虽然这不会导致大的错误产生,但是它仍会导致一些破坏程序的错误。两种方法在根本上是不一样的,你可能会得到预料之外的错误。程序员不应该认为hooks是组件挂载执行的方法。假如hooks以这样的方式工作,那么会影响你理解hooks。 state和props的捕获不一样最明显的不用在使用异步方式的时候 class App extends Component { state = { name: '' } componentDidMount() { someResolve().thene(() => { console.log(this.state.name) }) } onChange = (event) => { this.setState() } render() { return ( <input value={this.state.name} onChange={this.onChange} /> ) } } 这个组件看起来很简单,一旦它挂载了,它会调用一个异步函数,一旦异步函数成功执行,会将当前state的name值打印出来。让我们尝试将代码移植到函数组件上。 const App = () => { const [name, setName] = useState('') useEffect(() => { someResolve().then(() => { console.log(name) }) }, []) const onChange = (event) =>{ setName(event.target.value) } return ( <input value={name} onChange={onChange} /> ) } 上面的代码无法满足。当组件被创建,useEffect方法会捕获state和props的值,结果,用户即使输入了内容,console仍会打出一个空行。要告诉React使用最新的值,你必须将依赖项传给effect。props也是一样的逻辑。在这个例子中,effect比必须要使用compomentDidMount的class组件简单。 方法在不同的时刻被调用React可以确定何时在componentDidMount中同步状态,让我们看一下组件的实际生命周期: 组件被挂载 根据render中返回的内容创建DOM componentDidMount被调用,state被更新 DOM重新渲染,render中return的内容更新 可能有人希望我们在第一帧和第二帧中间看见闪烁,但是不会出现这样的情况,React检测到状态更新后,只打印第二帧。如果组件需要在DOM渲染时才计算元素的比例,这个是很有用的。 在组件挂载之后,hooks和useEffect都会执行。不同的是:hooks在DOM内容绘制之后执行。所以,如果在effect方法中同步更新state,用户会看到第二帧替换第一帧的闪烁。 你可以使用useEffectLayout获取带有hooks的旧行为,useEffectLayout在内容commit到页面前被调用。然而大多数都不会用到这个hook,大多数程序员仍坚持使用useEffect。 class组件围绕生命周期和渲染时间设计。函数组件旨在state和DOM的同步。思维方式的转变可能会导致一些奇怪的怪癖和错误,如果没有合适的知识很难去解决。简而言之,应该考虑“基于state,我的组件应该是什么样子,以及合适应该重新渲染”,这些问题将确保你的函数组件正常运行。]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[松本行弘的程序世界-怎么理解解决方案]]></title>
<url>%2F2020%2F08%2F23%2Fhow-to-create-solution%2F</url>
<content type="text"><![CDATA[解决方案概念上个季度有一个刷leetcode的计划,这个季度本来想延续,但是同时给我的建议是:刷题偏向于解决方法,现在增强的应该是出解决方案的能力。我很疑惑,解决一个现有问题不是算吗?我刷题能拓展我的思维,我觉得它是对我有好处的。现在的公司是做nlp的,我记得当时说的是:平台提供的是机器人平台,用户在这上面只要配置keyword或者流程,就可以解放客服的双手。他说这个不是,那到底什么才是解决方案呢?为龙湖提供的个性化回复属于解决方案。有痛点,有场景,结合现有的能力,出一个方案来解决这个痛点。 为什么会出现ruby最近恰好在读松本行弘的程序世界,松本是ruby语言的作者。在ruby出现之前已经有java、c++这样的语言了,那为什么还要再创造一个新的语言呢?左耳朵说过我觉得很有道理的话:一门新技术的出现,无非是三个目的:降低技术门槛、提高开发效率、提升稳定性。我仔细想了想,没毛病。c++的门槛很高,java很重。说到这里,对于下面的代码 class Person {} class Solution { viod handle(Person person) {} } 我拿着代码去问写过java的同事,问preson的传值是不是引用传值,他十分确定的说不管在哪个语言中,一定是引用传值。然后我又拿着代码去问了写c++的算法同学,他告诉我只要传值的不加&,再c++里面都是复制。这是多么令人迷惑的代码啊! 回到书上,当你准备写一门新的语言,已经有优秀的语言出现了,那么你创造一门新语言的初衷是什么呢?作者设计这个语言的目标是:简洁性、扩展性、稳定性。上一家公司用的是ror,有幸写过一些简单的ruby的代码,当时第一感觉是只有你想不到的,没有他做不到的。js很让人诟病的是,有一些package竟然只有一行代码。作为语言,js可能提供的太少。所以当时写ruby的代码的时候,特别幸福,常见的处理函数,它都有,常见的功能,社区也有gem去做,你只要在原来的基础上扩展一下即可。简洁性和扩展性都有感受到,但是一直没接触过稳定相关的。 简洁性和扩展性,我觉得很多是元编程的功劳,我在一个class中,仅仅增加几行代码即可。在我久远的记忆中,java对private的变量加getter和setter方法,我记得编辑器有快捷键能做这个事情,那如果加上元编程的话,是不是可以更简单一下:在定义变量的时候,直接指定上知否需要getter和setter即可。 在书中,能看出来作者一直为这三个目标努力,结合各种编程思想,考虑怎么才能出一个更好的方案。是选择静态还是动态语言?是要面向oop还是函数式?怎样同时享受便利?让我想来,真是难题啊。其实对于oop,我还是偏爱函数式编程,怎么说呢,它更倾向于解决问题的本质,但如果项目一大,oop的优势就凸显了,它有一个明确的概念:对象,可以帮助梳理思维,组织项目。 解决方案的实践和理解恰好最近在落地微前端的概念,这个概念提出好久了,但是到现在还是没有一个完整的方案。最出名的single-spa,怎么说呢,感觉它只是微前端中的一环,它不单独成立,它仅仅是体系中的一部分。但是现在的开发者都在你努力基于它,在完善概念。在基于现有概念,去做拆分的工作,拆成完全解耦的,发现现有的满足不了(好沮丧),于是又去找了刚开始提出微前端方案的文章,由看了后端的微服务的解决方案,感觉有一丝丝不一样。如果前端要做一个gateway完全解耦,现在的根本满足不了(qiankun中不能多个应用和url绑定),这样的话,平级只能调整成嵌套,但是一旦gateway包含了其他功能,稳定性就保证不了了,啊,纠结。带着疑问去问了别人,说我拆的太细了==😒。一时间不知道如何消化冲击,目前只能是先基于现实条件,先做出来一个雏形吧。 所以解决方案是什么?发现痛点,结合当前的条件,出一个方案来解决这个事情。这是我目前的理解。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于大旋转角度摆正图片的血泪汗]]></title>
<url>%2F2020%2F08%2F15%2Ftransform-image%2F</url>
<content type="text"><![CDATA[效果示例 原图2.旋转后的图 我要做的是将有旋转角度的图片摆正,目前只涉及大角度(90 180 270)的摆正,对于180,只是反转一下,长和宽还是原来的,我不需要对它进行太多的处理。但是对于90和270,由于旋转后,长变成宽,宽变成长,还必须将图片放在可视区域内,所以就必须调整原图片的宽高,使旋转后正好在可视区域内。业务上还有图片内容的标示,即使旋转后,我还需要记住原坐标在显示图片上的对应关系。 方案1刚开始的方案是,将图片直接按照图片原点旋转,原图假如是比较长的图片,旋转后,必定有一部分不在可视区域内,我需要再进行平移、调整宽高的操作,对于图片上的标示也同样进行一样的操作,实际做起来特别麻烦,于是在想有没有更好的办法 方案2返回的图片标示的坐标按照旋转后的图片的左上角为原点,这样的话,我前端只需要将图片摆正即可,标示坐标我只需要进行缩放率的计算即可。 我发现了一个很好的demo:https://developer.mozilla.org/zh-CN/docs/Web/CSS/transform-origin,上面明确的标示出来旋转的原点,说起来会更清晰。这个例子也正好和我的场景类似,为了效果明显,我需要将蓝色的部分调整成长方形(160*200),这样它旋转90/270度后,会变成(128*160) 1. 90度的处理由于中间涉及到调整蓝色块的宽高,旋转的原点是基于按照调整后的宽高,而不是原来的宽高。 首先调整宽高:160*200 -> 128*160 调整旋转原点,增加旋转角度transform-origin的默认值是 center center,所以调整后的旋转原点具体坐标在:64px 80px; 这样旋转后,调整后的高并没有在虚线框内,所以需要将旋转原点的x-offset往外挪一点,调整为宽的一半,这样保证旋转后的高正好在虚线框内。这个时候再进行旋转transform: rotate(90deg);,会发现效果刚刚好; 2. 270度的处理当时刚开发的时候,想着270不就是-90吗,效果应该和90一样,但是直到看到效果,才发现,旋转角度是反的,-90旋转后,是靠在原来高的下方,这样离视图区域的上方有一定距离。这个时候想了两个方法: 调整原来的方案,不调整旋转原点,90度和270旋转后,是在同一个位置,然后再使用margin去调整 再90度方案的处理上,再进行270度的处理 现在想来很清晰,方案一的margin的调整就等于(调整后的高 - 调整后的宽)/ 2,当时自己没想清楚,没找到公式,于是采用的第二个方案。旋转270后,你会发现它是靠在高的底部,于是我只要调整margin-top,将它挪到高的顶部即可,中间的距离就是调整后的高 - 调整后的宽。 总结今天总结下来很清晰,当时想方案的时候特别模糊,本来我一直以为原点是基于调整之前,后来自己研究的时候才发现是基于调整后的宽高进行的。另外业务里面的是图片,并不知道宽高,所以还需要进一步的计算。总的下来: // 90/270的旋转 let height = 'auto' let transformOrigin = 'center center' let marginTop = 0 if (imageAngle === 90 || imageAngle === 270) { // 1. 先得到图片当前的显示宽度 const { width: preShowWidth } = this.$img // 2. 设置图片的宽度,以得到原始的高度和宽度 this.$img.style.width = 'auto' const { height: realHeight, width: realWidth } = this.$img // 3. 调整后的高度是原来的宽度 height = preShowWidth ratio = preShowWidth / realHeight // 4. 设置原点的x-offset 为调整后的高的一半 transformOrigin = `${height / 2}px center` // 假如旋转角度是270,需要调整marginTop = -(showHeight - showWidth) // 由于在设置高度之前我是拿不到图片显示的宽度,于是引入了一个缩放比 // showWidth = realWidth * ratio(图片的缩放比) if (imageAngle === 270) { marginTop = (realWidth * ratio) - preShowWidth } }]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[今天聊一聊duck-typing]]></title>
<url>%2F2020%2F08%2F08%2Fduck-typing%2F</url>
<content type="text"><![CDATA[duck typing是动态类型的一种风格。If it walks like a duck and it quacks like a duck, then it must be a duck. 我只关心它的行为,不用考虑它是什么类型。 现在常见的语言分为静态语言和动态语言。例如java,强类型语言,类型需要一一对应,如果类型定义大仙不一致的地方,那么连编译一关都过不去。又例如javascript,只要没有语法错误,你就可以运行起来,至于错误只能在运行的时候才能被发现。 静态语言的类型定义,是一个辅助信息,对于最终的程序执行来说,是没有用的,因为不论是什么代码都会被编译成机器码执行。所以在开发阶段,对于静态语言来说,你想把精力集中去解决问题的时候,还需要去考虑类型的定义。 思考一个场景,你有一个打印日志的方法,这个方法有两个参数:输出对象和输出信息。 现在out的行为仅仅是将msg输出 void log_outs(ostream out, msg) 现在我想将msg输出到一个文件中,应该要怎么做呢?因为第一个变量已经指定了类型,你无法使用它,只能copy一下再去写一个新的方法。 上面的两个操作有共同的行为,只是需要处理的对象是不一样的。我使用ruby写一个例子 class Duck def run print "鸭子在跑" end def quack print "鸭子在叫" end end class Person def run print "模仿鸭子在跑" end def quack print "模仿鸭子在叫" end end def call_duck(duck) duck.run() duck.quack() end def game() duck = Duck.new person = Person.new call_duck(duck) call_duck(person) end game() call_duck只负责调用第一个变量上的方法,我不需要知道或者定义参数的类型。 鸭子类型的场景很多,在静态类型中对应的应该是多态了。 class Caller<T extends CallMe> { final T callee; Caller(T callee) { this.callee = callee; } public void go() { callee.call(); // should work now } } interface CallMe { void call(); } class Foo implements CallMe { public void call() { System.out.print("Foo"); } } class Bar implements CallMe { public void call() { System.out.print("Bar"); } } public class Main { public static void main(String args[]) { Caller<Foo> f = new Caller<>(new Foo()); Caller<Bar> b = new Caller<>(new Bar()); f.go(); b.go(); System.out.println(); } } 参考: 松本行弘的程序世界 https://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B https://stackoverflow.com/questions/46270804/using-java-and-cs-generics-to-simulate-duck-typing]]></content>
<categories>
<category>编程语言</category>
</categories>
<tags>
<tag>编程语言</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浏览器中,将pdf和tiff转换成常见格式的图片显示]]></title>
<url>%2F2020%2F08%2F08%2Ftransfer-img%2F</url>
<content type="text"><![CDATA[最近项目,要支持多种图片类型和pdf的显示,tiff在每个浏览器中的展示是不一样的,在使用最多的chrome中,就无法显示;pdf的话,是截取第一张显示出来。 1. 首先使用FileReader将文件转成base64// file可能是本地选择/或者通过xhr请求到 const file = request.response const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => { if (reader.result) { const readerResult = reader.result rsolve(readerResult, file) } } 2. pdf的转换找到了一个package(pdfjs-dist)能将pdf转换成图片,不过中间也需要canvas的支持,它可以将转换到的流画到canvas上,然后canvas再导出为图片。 安装yarn add pdfjs-dist 转换 import PDFJS from 'pdfjs-dist' // 这个cdn比较慢,可以下载到项目本地或上传至自己的cdn PDFJS.GlobalWorkerOptions.workerSrc = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS.version}/pdf.worker.js' PDFJS.getDocument(readerResult).promise .then((pdf) => { // 只取第一页 pdf.getPage(1) .then((page) => { const viewport = page.getViewport({ scale: 1 }) const context = $canvas.getContext('2d') $canvas.height = viewport.height $canvas.width = viewport.width const task = page.render({ canvasContext: context, viewport }) task.promise.then(() => { // 使用$canvas的api转换成图片 setImgUrl($canvas.toDataURL('image/jpeg')) }) }) }) .catch((error) => { console.error('pdf转图片失败', error) }) 打开浏览器,可以顺利显示;刚上线两天,有人报bug,一看他的pdf展示的时候,丢失了很多信息。我以为是只是前端的问题,但是后端同学尝试将pdf转成图片之后,发现图片也是有问题的。另外,我找到一个外婆家的发票试了一下,发现完全正常。所以,这有个坑,在本地正常显示的pdf,有可能无法转换成正常的图片。 3. tiff可以点这里看每种浏览器对每种图片格式的支持情况Chrome、Firefox都不支持显示😭,这个必须需要解决。幸运的是,也找到了package来做这个事,它是先将file转换成ArrayBuffer而不是base64,然后再进一步转换成常见的图片格式。 安装yarn add tiff.js 转换 const bufferReader = new FileReader() bufferReader.readAsArrayBuffer(file) bufferReader.onload = () => { if (bufferReader && bufferReader.result) { const image = new Tiff({ buffer: bufferReader.result }) setImgUrl(image.toDataURL('image/jpeg')) } else { cosole.error('上传失败, 请重试') } } OK,两种文件格式都已经可以转换成常见的图片格式在浏览器中展示。]]></content>
<categories>
<category>前端</category>
<category>react</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[怎样做一个安全的token]]></title>
<url>%2F2020%2F08%2F01%2Fcsrf-token%2F</url>
<content type="text"><![CDATA[周五分享的csrf新get到的点,csrf安全的前提一定是没有xss攻击,否则你做的防御根本起不到作用。参考文章https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html csrf攻击过程,你已经在网站A上登录了,这个时候就有了A网站下cookie;当你浏览B网站时,B网站有一个向A网站提交的表单,网站会诱导你去点击,点击后,就立即向A发送了一个请求,这个时候浏览器会自动携带A网站下的cookie,A网站在验证你的cookie后,就立即执行请求内容。于是,在用户不知情的情况下,可能操作了危险的操作,例如转账等… 分享过程中,提到了token,token可以和cookie配合使用,用来做双重认证。比较熟悉的就是小程序中的JWT。那么怎么做才能保证token足够安全呢? token使用原则:理论上一个token只能用一次,当你使用了token,服务器会再给你下发一个token,但是这样做的代价大,所以大部分时间用户登录后,会拿到一个token,保存本地,发请求的时候再带过去。 不使用redis,直接使用算法加密现在正在做的项目,后端同学直接在用户的id基础上进行加密,然后下发一个token给前端,但是这样带来的一个问题是,万一加密的密钥被获取到,因为用户的id是number类型,是可以枚举的,那么攻击者很有可能会去枚举用户id,进而获得每个用户的内容。如果发现泄漏,第一想到的解决办法是修改密钥,修改后,会导致全部的用户需要重新登录,嗯,这应该是一个事故… 在1的基础上,token后面增加随机字符串并且随机字符串增加过期时间,这就杜绝了即使攻击者去枚举用户的id得到用户的信息。一定不要将随机字符串放在数据库中,否则大量肉鸡就会将数据库拖垮。可以放在redis中,利用redis天然的过期特性。 在2的基础上,我加入多个密钥一个token经过多重加密,即使泄露了一个也没关系。 其他: 还有一个有趣的,微信抢红包,微信会给你下发一个token,前端收到token后,发送给后端,后端将token推到消息队列中,慢慢消费… PS: 去朋友家做客,体验了一把手机投屏,投完屏之后,我还可以在手机上看别的,感觉特别神奇,找了找相关资料,感兴趣点这里]]></content>
<categories>
<category>前端</category>
<category>react</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用react的context实现一个简单i18n]]></title>
<url>%2F2020%2F07%2F25%2Freact-i18n%2F</url>
<content type="text"><![CDATA[受withApollo的启示,仔细研究了一下context的API,在开发中还没用过context,但是想antd中的form这种跨组件传递数据,那i18n正适合啊 基本代码先来三个基本组件,到时候准备替换的就是title和desc import React, { useState } from 'react' const Title = () => { return ( <div>title</div> ) } const Descrption = () => { return ( <div>desc</div> ) } const Container = () => { return ( <div> <Title /> <Descrption /> </div> ) } export default Container 语言配置准备了en和cn的两个版本的语言配置文件: // en.js export default { locale: 'en', messages: { title: 'title', desc: 'this is description' } } // zh-CN.js export default { locale: 'zh-CN', messages: { title: '中文', desc: '这是一段描述' } } 加载语言配置, // load_language.js import en from './en' import zhCN from './zh-CN' const list = { [en.locale]: en.messages, [zhCN.locale]: zhCN.messages } 写到这,我想起来webpack的ignore的plugin,加载多语言配置,肯定会将所有的语言全都load,假如你只需要cn,那么其他的语言包对你来说都是没有用的。经常见的就是moment这个,你会使用ignorePlugin忽略它,然后再手动引入 使用context注入1、 准备conext // context.js import React from 'react' const LanguageContext = React.createContext('zh-CN') export default LanguageContext 2、向load_language增加消费: const t = (title) => { return ( <LanguageContext.Consumer> {language => ( list[language][title] )} </LanguageContext.Consumer> ) } 3、然后我就可以直接改造我的title和desc了: const Title = () => { return ( <div>{t('title')}</div> ) } const Descrption = () => { return ( <div>{t('desc')}</div> ) } 4、Container中增加切换语言的按钮: const [language, setLanguage] = useState('zh-CN') const changeLanguage = () => { setLanguage(language === 'zh-CN' ? 'en' : 'zh-CN') } return ( <LanguageContext.Provider value={language}> <button onClick={changeLanguage}>切换语言:当前是{language}</button> <Title /> <Descrption /> <Descrption22 /> </LanguageContext.Provider> ) } ok,可以了 使用hoc注入另外,t还可以使用hoc的形式注入 export const withLanguage = (Component) => (props) => { return ( <LanguageContext.Consumer> { language => ( <Component {...props} t={(title) => list[language][title] } /> ) } </LanguageContext.Consumer> ) } 在组件中使用: const Descrption2 = ({ t }) => { return ( <div>hoc: {t('desc')}</div> ) } const Descrption22 = withLanguage(Descrption2) 走到这,一个简单的i18n就完成了。 PS: 如果中文网站要做国际化的话,可能最快的方式还是开发一套英文网站,再它的基础上做国际化。中文转英文,长度是由短变长,排版上改的会让人崩溃。]]></content>
<categories>
<category>前端</category>
<category>react</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[http2基础教程-笔记]]></title>
<url>%2F2020%2F07%2F19%2Fhttp2-summary%2F</url>
<content type="text"><![CDATA[看完了这本书,想来还是要总结一下,不然就是看了就忘了 web页面请求主要是两个部分:资源获取、页面解析/渲染,过程主要发生的是: 把待请求的URL放入队列 解析URL中域名的IP地址 建议与目标主机的tcp连接 如果是https请求,初始化并完成TLS握手 向页面对应的URL发送请求 接收相应 如果(接收的)是主体HTML,那么解析它,并针对页面中的资源触发优先获得机 如果页面上的关键资源已经接收到,就开始渲染页面 接收其他资源,继续渲染,知道结束。 关键性能指标延迟、DNS查询、建议连接的时间、TLS协商时间、首字节时间(TTFB)等 HTTP1的问题 队头阻塞 低效的TCP利用,TCP的拥塞控制需要几次往返才能知道最佳的拥塞窗口的大小 臃肿的消息首部 受限的优先级设置 第三方资源 web性能的最佳实践 DNS查询优化(dns-prefetch) 优化TCP连接(preconnect) 避免重定向,使用rewrite来代替 客户端缓存 条件缓存 压缩代码极简化 HTTP1中的优化中的反模式 生成雪碧图和资源合并内联 域名拆分 禁用cookie HTTP2分帧带来的好处首部压缩、多路复用、加密传输 其他HTTP1和HTTP2都会受丢包的影响。HTTP3是QUIC协议,基于UDP,它又会带来什么样的变化呢?]]></content>
<categories>
<category>http</category>
</categories>
<tags>
<tag>http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[webpack的hash、chunkhash、contenthash]]></title>
<url>%2F2020%2F07%2F16%2Fwebpack-hash%2F</url>
<content type="text"><![CDATA[对于webpack的hash,常用于cdn缓存。我理解的是文件不变的情况下,最后打包出来的hash串也不会变。最近被问到了这是三个hash的区别,就查了一下,发现还很有讲究。 先看一下三个hash的解释: [hash] is a “unique hash generated for every build” [chunkhash] is “based on each chunks’ content” [contenthash] is “generated for extracted content” 这里有两次代码变更原代码: // file1.js console.log('file1') // file2.js console.log('file2') // file3.js console.log('file3') // index.js require('./file2') console.log('index') // detail.js require('./file1') console.log('detail') // webpack.config.js const path = require('path') const webpack = require('webpack') module.exports = { // mode: 'development', // mode: 'production', entry: { index: './src/index.js', detail: './src/detail.js', }, output: { filename: '[name].[hash].js', path: path.resolve(__dirname, 'dist') }, } 第一次变更: // file2.js console.log('file22') 第二次变更: // index.js require('./file2') require('./file3') console.log('index') 下面我会以我理解的顺序比较一下三个hash hash每次构建的生成唯一的一个hash,且所有的文件hash串是一样的。源代码构建: 第一次变更: 是不是看到非预期的地方了?我只改了file2.js,index.js的hash串变了,但是为什么detail.js为什么也变了?这还怎么有缓存的作用!不行,升级! chunkhash每一个文件最后的hash根据它引入的chunk决定 源代码构建: 第一次变更:这次文件的hash变化符合预期,index的变了,detail的没变 第二次变更:你会发现这次变更也是基于index的变更,但是实际上detail的文件内容没有变,那为什么它的hash也跟着变了? 原因是 module identifier,因为 index 新引入的模块改变了以后所有模块的 id 值,所以 detail 文件中引入的模块 id 值发生了改变,于是 detail 的 chunkhash 也随着发生改变。 不用怕,webpack已经提供方案了,解决方案是将默认的数字 id 命名规则换成路径的方式。webpack 4 中当 mode 为 development 会默认启动,但是production环境还是默认的id方式,webpack也提供了相应的plugin来解决这个问题 plugins: [ new webpack.HashedModuleIdsPlugin(), ], 加上这个plugin后,再走一遍上述代码的变更,你会发现第一次、第二次的变更后,detail的hash串仍然没有变化,符合预期。 在webpack中,有css的情况下,每个entry file会打包出来一个js文件和css文件,在使用chunkhash的情况下,js和css的文件的hash会是一样的,这个时候暴露出来的一个问题:你修一个react的bug,但是并没有改样式,最后更新后,js和css的文件的hash都变了。这个还是不太好,css文件的hash串不变最好,再继续升级! contenthashcontenthash是根据抽取到的内容来生成hash。 生产环境是不是会使用一个MiniCssExtractPlugin来进行css的压缩,这个时候我们在这个plugin里面指定hash为contenthash,你会发现修改js文件后,js文件的hash串变了,css的hash串没变!完美。 new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: '[name].[contenthash:8].css', chunkFilename: '[name].[contenthash:8].chunk.css' }) 将webpack中的全部hash都设置成contenthash的情况下,仅仅只修改css文件,不改js文件的情况下,css文件的hash串会变,js文件的不会变,这样能达到最小更新。 其他我查了两个流行的脚手架:create-react-app和umi,发现它们的entry file的配置都是contenthash output: { filename: '[name].[contenthash].js', chunkFilename: 'cfn_[name].[contenthash].js', }, umi使用了HashedModuleIdsPlugin来进行稳定的hash构建,但是cra没有,我看有人已经提issue了:https://github.com/facebook/create-react-app/issues/6909,作者说optimization.moduleIds: "hashed"这个也能满足需求,查了webpack5中optimization.moduleIds是可以的 所以目前最佳实践是contenthash+HashedModuleIdsPlugin/optimization.moduleIds: "hashed" 参考资料:https://stackoverflow.com/questions/35176489/what-is-the-purpose-of-webpack-hash-and-chunkhashhttps://imweb.io/topic/5b6f224a3cb5a02f33c013ba]]></content>
<categories>
<category>前端</category>
<category>webpack</category>
</categories>
<tags>
<tag>webpack</tag>
</tags>
</entry>
<entry>
<title><![CDATA[升级react-apollo v2 踩坑]]></title>
<url>%2F2020%2F07%2F11%2Freact-apollo-v2%2F</url>
<content type="text"><![CDATA[拖到现在才升级react-apollo,一直用的是v1,随着react的升级,是真的害怕会阻碍react的升级,于是这个Q决定升级它。 migrate V2文档点这里,跟着文档升级还是挺快速的。替换完成后,启动发现可以正常运行。但是目前server端会有返回401、403、405的状态,需要验证一下 问题出现根据文档上来,处理4xx的状态这样来: const errorLink = onError(({ networkError = {}, graphQLErrors }) => { if (networkError.statusCode === 401) { logout(); } }) 但是我在项目中,收到的networkError一直是字符串...failed to fetch(大概是这个意思),怎么也收不到statusCode这个状态码,于是去找到相关联的issue,还真有这个问题,issue300,issue中给的解决方法是调整networkError的ts类型,虽然刚开始比较疑惑数据为什么和类型有关,但是还是抱着希望试了试,发现并没有什么用😭。随即一直在issue中遨游,但是还是没有找到解决办法 问题·查没办法,只能一路去源码里面找了,看代码,没看出来啥问题,但就是不行。只能在怀疑的地方加断点debugger了,最后查到了是apollo-link-http-common中parseAndCheckHttpResponse函数的问题 var parseAndCheckHttpResponse = function (operations) { return function (response) { return (response .text() .then(function (bodyText) { try { return JSON.parse(bodyText); } catch (err) { var parseError = err; parseError.name = 'ServerParseError'; parseError.response = response; parseError.statusCode = response.status; parseError.bodyText = bodyText; return Promise.reject(parseError); } }) .then(function (result) { if (response.status >= 300) { throwServerError(response, result, "Response not successful: Received status code " + response.status); } if (!Array.isArray(result) && !result.hasOwnProperty('data') && !result.hasOwnProperty('errors')) { throwServerError(response, result, "Server response was missing for query '" + (Array.isArray(operations) ? operations.map(function (op) { return op.operationName; }) : operations.operationName) + "'."); } return result; })); }; }; 能看出来上面的代码有问题吗?是reponse.text()的时候出error了,这里抛出的error是一个string,导致走不到下面的then去判断status。 问题·查之reponse.text()react-apollo的fetch用的是浏览器原生的fetch,记得之前用fetch咋写吗 fetch(url) .then(response => response.json()) .then(res => { // 处理数据 }) .json方法是针对于server返回的json数据类型处理的,.text就是针对text/xxx处理。在stackoverflow上看了一个回复,加上我之前在issue上说可能是服务端返回的数据的问题,让我把目光转向了server。 首先200是可以的,是不是401的时候不能text吗?先找资料,没有,那可能是错的。写代码试试 var http = require(‘http’); var server = http.createServer(function (request, response) { console.log(request.method + ‘: ‘ + request.url); response.writeHead(401, {‘Content-Type’: ‘text/plain’, ‘Access-Control-Allow-Origin’: ‘*’}); response.end('Hello world!'); }); // 让服务器监听65534端口: server.listen(65534); console.log(‘Server is running at http://127.0.0.1:65534/'); 在chrome的控制台上用fetch,response.text()是可以的。那问题出在哪呢?在server的代码中我看到写的是res.end();,我尝试在end中加上hello world,结果可以了! 关于text()的解释是: The text() method of the Body mixin takes a Response stream and reads it to completion. It returns a promise that resolves with a USVString object (text). The response is always decoded using UTF-8. 难道是因为没有流导致的报错?这个时候我想起来了一个状态码204,它表示no content,就是没有任何返回数据,那对于这个请求,按照常规的fetch写法是不是也会进入catch中,结果真的在fetch的issue中找到了这个问题,里面说的解决方案是: const r = await fetch('https:...', { method: 'POST', .. }); if (r.ok) { const data = await r.json().catch(() => null); // ... } node-fecth也有类似的的讨论: https://github.com/node-fetch/node-fetch/issues/165#issuecomment-258718522,里面的意思是不太符合规范。额,这个…我还是很喜欢204这个状态的…]]></content>
<categories>
<category>前端</category>
<category>graphql</category>
</categories>
<tags>
<tag>graphql</tag>
</tags>
</entry>
<entry>
<title><![CDATA[阅读网站,操作按钮为什么放在左侧]]></title>
<url>%2F2020%2F07%2F04%2Fknow-product-2%2F</url>
<content type="text"><![CDATA[事情的起因是这样的,我去了之前公司的网站看文章,忽然发现操作按钮在左边,当时开发的时候没感觉出什么问题,今天一看觉得为什么要在左边? 想了想medium也是在左边,作为一个鼓掌和收藏的重度使用用户,每次鼓掌👏我还需要鼠标挪过去。 为什么为此我问了一位设计、一位产品同学 产品的观点:强调视线 大部分人的视线在左边 鼓掌是鼓励互动,但是因为视线在左边,右边一般是无关紧要的 我追问说我的鼠标一般在右边的中间,得到的是:鼠标位置不重要,重要的是视线 总结:左边是产品的基本意识 设计的观点这个我是拿着medium的截图问的 设计师为了版面整洁吧,功能不多就聚类放在了左边 分开放的话,操作成本左右可能没有太大差异,但信息检索成本变高了 因为网站的最大意义是看文章,操作是次要的 总结:普通做法,都放在边角 综上,我被说服了,应该放在左边。]]></content>
<categories>
<category>产品</category>
</categories>
<tags>
<tag>产品</tag>
</tags>
</entry>
<entry>
<title><![CDATA[webpack中优先打包node_modules中的style]]></title>
<url>%2F2020%2F07%2F04%2Fwebpack-cachegroup%2F</url>
<content type="text"><![CDATA[第一次的解决在网上看到的node_modules的打包都是放在一个chunk里面,但是对于大项目,如果继续这样,vendor文件简直大到暴,于是对node_modules进行了拆分,拆分的js文件大致符合预期,但是样式文件的顺序错乱了(之前用的css in js没暴露出问题,改成less后发现了),node_modules中样式文件穿插在了自己写的样式中间,导致好几个页面上样式出了问题。这个时候我又加了一个规则,想优先打包node_modules中的样式文件。 我的主要目的是将node_modules里面的样式文件先打包,但是test使用function,怎么也达不到目的 // 先把css结尾的文件打包,主要是node_modules中的文件 styles: { name: 'styles', test: (module) => { const name = module.resource return /node_modules/.test(name) && /\.css$/.test(name) }, chunks: 'all', enforce: true, priority: 20 } 为了解决这个问题,因为laiye-antd的按需加载使用的是css文件,不是less文件,于是就写了下面的正则,目前能解决样式文件顺序错乱的问题。 // 先把css结尾的文件打包,主要是node_modules中的文件 styles: { name: 'styles', test: /\.css$/, chunks: 'all', enforce: true, priority: 20 } 二次爆发新Q的计划是要将laiye-antd升级到基于antd4的1.x版本,但是样式上有一个很大的改动就是@border-radius-base从4px变成了2px,为了保证样式的兼容,我必须保证,即使升级了版本,组件的圆角仍然保持之前的4px。最容易想到的办法的是项目的配置不变,在组件库中改,但是该起来文件改的比较多,还有一个很大的原因,假如设计同学说我要把圆角从4px变成2px,这个时候我还要去发一次版本?这个很明显是不符合正常逻辑的。所以这个时候,我引用laiye-antd的时候,必须将css变成less,可满足随时css变量的修改。这个时候,我还是重回最初的问题:我要打包node_modules中的样式文件,我不管它是css结尾还是less结尾,最重点的是node_modules中的,于是开始了test之旅。 // 先把css结尾的文件打包,主要是node_modules中的文件 styles: { name: 'styles', test: /\.css$/, // 这个是可以的 test: (module) => { // 这个是不可以的 return /\.css$/.test(module.resource) }, chunks: 'all', enforce: true, priority: 20 } 我找了很多test为function的配置,基本上rep.test中是module.resource或者module.context,怎么改都不行,一直在圈里徘徊。 怎么解决方案一:正则我想要不还是正则吧,使用正则里面的与来解决,node_modules和(c|le)css结尾 // 先把css结尾的文件打包,主要是node_modules中的文件 styles: { name: 'styles', test: /(?=node_modules).*(?=.(c|le)ss$)/, chunks: 'all', enforce: true, priority: 20 } 打包出来一看,可以可以,满足了我的需求。但是很明显这个正则的效率不太高。还是不如两个test并起来 方案二:function我还是决定采用函数吧,我只能去看webpack的源码到底是怎么操作,到底问题出在哪里 // SplitChunksPlugin.js // 268行 else if (SplitChunksPlugin.checkTest(option.test, module)) { } // checkTest是何方神圣 static checkTest(test, module) { if (test === undefined) return true; if (typeof test === "function") { if (test.length !== 1) { return test(module, module.getChunks()); } return test(module); } if (typeof test === "boolean") return test; if (typeof test === "string") { if ( module.nameForCondition && module.nameForCondition().startsWith(test) ) { return true; } for (const chunk of module.chunksIterable) { if (chunk.name && chunk.name.startsWith(test)) { return true; } } return false; } if (test instanceof RegExp) { if (module.nameForCondition && test.test(module.nameForCondition())) { return true; } for (const chunk of module.chunksIterable) { if (chunk.name && test.test(chunk.name)) { return true; } } return false; } return false; } // nameForCondition又是什么呢?NormalModule.js文件中 nameForCondition() { const resource = this.matchResource || this.resource; const idx = resource.indexOf("?"); if (idx >= 0) return resource.substr(0, idx); return resource; } // 暂时先看到这个里 看到源码后,我尝试用nameForCondition()来代替resource styles: { name: 'styles', test: (module) => { const name = module.nameForCondition && module.nameForCondition() if (name && /node_modules/.test(name) && /.(c|le)ss$/.test(name)) { return true } return false }, chunks: 'all', enforce: true, priority: 20 } 可以了,但是matchResource、resource到底有什么区别呢?我用console.log打印了一下 可以看到有时候resource会是undefined的情况,内部的编译我也不清楚==。 再说一下module.chunksIterable,这个数组真的非常大,但是没关系,webpack5已经去掉了,在代码里面看不见这个循环了,期待5的正式版本😄 项目修改['import', { libraryName: 'laiye-antd', style: true }] // css => true // less-loader里面加上 options: { sourceMap: true, javascriptEnabled: true, modifyVars: { 'laiye-primary-color': 'red' } } 目前已经解决,撒花🎉 总结最后的最后,问题发生的原因还是因为我对webpack内部编译的原理不清楚,导致根据输入不能预估到输出,这是个很危险的事情。总之,继续学吧…]]></content>
<categories>
<category>前端</category>
<category>webpack</category>
</categories>
<tags>
<tag>webpack</tag>
</tags>
</entry>
<entry>
<title><![CDATA[聊一聊throttle and debounce]]></title>
<url>%2F2020%2F06%2F27%2Fthrottle-and-debounce%2F</url>
<content type="text"><![CDATA[两个都可以针对高频操作进行性能优化 debounce(防抖)debounce表示一段时间后要执行某种操作,假如在执行之前又有了执行信号,就取消之前的延迟操作,产生一个新的延迟操作。 代码实现: function debounce(fn, wait) { let timeout = null; fucntion debounced() { clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(this, arguments) }, wait) } return debounced } lodash的实现方式:https://github.com/lodash/lodash/blob/master/debounce.js,它优先使用requestAnimationFrame 它将触发频繁的事件合成一次执行,例如input、keyup、keydown、resize等 throttle(节流)throttle表示每隔一段时间就执行某种操作 代码实现: function throttle(fn, wait) { let timeout = null let firstRun = true function throttled() { if (firstRun) { firstRun = false fn.apply(this, arguments) } if (timeout) { return } timeout = setTimeout(() => { clearTimeout(timeout) fn.apply(this, arguments) }, wait) } return throttled } lodash的throttle是基于debounce的 它可以在一段时间中固定触发的次数, 例如scroll、resize等 PS: angular已经有10了🤯]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何用redis实现锁]]></title>
<url>%2F2020%2F06%2F15%2Fredis-lock%2F</url>
<content type="text"><![CDATA[今天遇到一个问题是,我正准备调用api的时候,突然来了一个取消的信号,这一瞬间,就导致调用api成功,取消也成功了。但是实际上真正想取消的没有取消。这个时候需要一个锁,来固定一下。使用的是redis来进加锁。 1.使用setnx来做setnx的意思是set if not exist,如果set成功就反悔字符串“ok”,不成功返回null client1.setnx(key, value) // 成功 client2.setnx(key, value) // 失败 client1.del(key) client2.setnx(key, value) // 成功 利用setnx的属性,成功了就说明抢到锁了;进行操作后,将锁删掉后,别人抢到锁后才能进行操作 剩下的方法待补充…]]></content>
<categories>
<category>后端</category>
<category>redis</category>
</categories>
<tags>
<tag>redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[总结一下做性能平台的过程]]></title>
<url>%2F2020%2F05%2F10%2Fsaas-performance-scheduler%2F</url>
<content type="text"><![CDATA[受康师傅委托,为大家写一个性能平台,上个Q一些基本的功能做好了,这个Q的目标是写一个api,将上一个Q写的基本功能自动化😂,称之为“调度”。 过程 有连接数据库的需求了,于是开始学习sequelizes,但是它太大了,比较着急,所以直接抓了一个现有的项目的数据库配置,模仿着写。虽然能够解决燃眉之急,但是最后下来还是稀里糊涂的,用的时候还是有很多迷惑点。所以一定要做好技术储备! 开始写查询了,真的好难🤯。没技术储备的缺点暴漏出来,本来写sql就不太行,连API都不知道,于是开始求助stackoverflow,一个sql抠个好长时间。需求是查询一个任务的执行次数,失败次数。 const taskHistorys = await History.findAll({ where: { taskId: taskIds }, order: [['auto_update_time', 'DESC']], attributes: [ 'taskId', ['auto_update_time', 'updateTime'], [sequelize.fn('COUNT', sequelize.col('*')), 'triggrCount'], [sequelize.literal('SUM(exec_status=3)'), 'failCount'], ], group: ['task_id'], raw: true, }); 看上去似乎还可以 和康师傅讨论过,为什么现在硬件做的很好,但是软件却落后了,原因之一就是模块化。我门在processon上共同配合,我将自己的想的画出来,结果看上去只是很简单,似乎是只是其中的一小部分。康师傅更新了一下,我一看,吆,差别果然很大。能很明显的看出来,他将一个流程划分成几个模块,模块之间相互协作。我想在里面增加新功能,只需要改动模块内部即可。真的非常好!接下来,开始动手就比较明确了。 对于模块,我决定定义几个class,这样看起来似乎一一对应。写函数式编程写多了,oop只是一个外皮,内里还是函数式。函数式也没啥问题,就是函数多传参。将功能大致实现之后,所有的class都new一个挂到了global上。还是发现了一个别扭的地方。每一个执行任务对应一个监视器,这个时候,我不能全局只有一个监视器,而是一个任务一个监视器,很oop。 最后还是心理方面,这个工作是第一次做,内心非常忐忑,很怕。我没做过,写不出来怎么办,所以刚开始不太积极。其实整个过程都感觉有些煎熬(嗯,这个我周一需要寻求一下经验),尽管之前已经深入的想过了,但是还有一些点没想好,只能是做的时候,遇见再解决。还是要勇敢一些。 ps: 还有点要说的 docker里面使用的路径不要以根路径开头,要以/home/用户名开头,不会担心有什么权限问题yu 前几天遇到表被锁的问题,导致后续所有读表全部pending,DBA小姐姐教了一招kill连接的操作select concat('kill ',id,';') from information_schema.processlist where db = '数据库名' and command <> 'Sleep'; 然后把结果复制,执行! 在服务器上执行rm -rf xxx,被服务器拒绝后,找运维小哥,得到的回复:咋,想删库跑路?😂]]></content>
<categories>
<category>后端</category>
<category>nodejs</category>
</categories>
<tags>
<tag>nodejs</tag>
</tags>
</entry>
<entry>
<title><![CDATA[终于要使用css modules]]></title>
<url>%2F2020%2F04%2F19%2Fcss-modules%2F</url>
<content type="text"><![CDATA[项目的庞大以及glamor的年久失修,不得不使用css modules了。我非常不喜欢css in js,它只会让css失去原有的魅力,仅仅是为了解决命名冲突的问题,搞得css书写特别困难,为了实现类似keyframe,而不得不做各种各样的api来规避它。用了它之后把整体的样式书写的乱七八糟,无尽的嵌套简直是噩梦。虽然不是UI工程师,但是也期待用最少的css代码来实现最好的样式。 css module的出现,简直是一道把人拉到正道上的曙光。具体的配置参见create-react-app的react-script 说一说的我的使用感受(我是使用的less预处理): .container { .title { // .... } } 如果这样写,styles中会有两个属性container和title,其实刚开始特别不了解,但是仔细想了想,现在都是用bem来命名,用bem正常的写法 .xxx { &-container { // ... } &-title { // .... } } 上面的写法能用的class应该是两个:xxx-container和xxx-title,所以这样暴露没什么问题。那如果你不想将上上面的title暴露出来怎么办,那这个时候你要用到global了 .container { :global { .title { // .... } } &:global { &:hover .title { // ... } } } 关于gloabl的用法,没找到太详细的资料。global指的是全局作用域,其实我理解的是:当前环境的全局作用域,即title用global包起来之后,title样式的生效还是依赖于container作用范围的。:global和&:global的区别不知,你如果将它们交换一下,那么css-loader一定会报错。 还有一个问题是css module中引用图片一定要以~开头。项目中自定义的alias也是以这个符号开头,但是~~static/image/xxx会导致图片找不到,无奈只能将~static改为@staic 参考资料 github css modules Stop using CSS in JavaScript for web development CSS Modules使用详解]]></content>
<categories>
<category>前端</category>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[现有项目接入single-spa,浏览器一直处于url重定向的状态==]]></title>
<url>%2F2020%2F03%2F25%2Fsingle-spa-react-route-prevent-back%2F</url>
<content type="text"><![CDATA[在项目中接入single-spa后,有一个菜单,只要点到它就直接死掉了,刚开始怀疑是点击的时候,右手动的重定向,检查了代码,发现没这个操作。找了组件里面有一行“阻止浏览器”后退的代码 // 加载后绑定Window的路由事件 componentDidMount() { if (window.history && window.history.pushState) { window.addEventListener('popstate', this.stopGoBack) window.history.pushState('forward', null, '#') window.history.forward(1) } } // 销毁前清除监听Window的路由事件 componentWillUnmount() { window.removeEventListener('popstate', this.stopGoBack) } // --- 生命周期函数区 End --- // --- 阻止回退 --- stopGoBack = () => { // 业务代码 window.history.pushState('forward', null, '#') window.history.forward(1) } 找了popstate的解释: 当活动历史记录条目更改时,将触发popstate事件。如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。 需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法) 不同的浏览器在加载页面时处理popstate事件的形式存在差异。页面加载时Chrome和Safari通常会触发(emit )popstate事件,但Firefox则不会。 在react-router中的BrowserHistory,在window.history的包装的。项目单独运行时,可以正常使用,接入single-spa后,就不行了,于是乎看了single-spa的代码 // navigation-events.js window.history.pushState = patchedUpdateState(window.history.pushState); window.history.replaceState = patchedUpdateState(window.history.replaceState); function patchedUpdateState(updateState) { return function() { const urlBefore = window.location.href; const result = updateState.apply(this, arguments); const urlAfter = window.location.href; if (!urlRerouteOnly || urlBefore !== urlAfter) { urlReroute(createPopStateEvent(window.history.state)); } return result; }; } function createPopStateEvent(state) { // https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49 // We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that // all the applications can reroute. try { return new PopStateEvent("popstate", { state }); } catch (err) { // IE 11 compatibility https://github.com/single-spa/single-spa/issues/299 // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd const evt = document.createEvent("PopStateEvent"); evt.initPopStateEvent("popstate", false, false, state); return evt; } } export function navigateToUrl(obj) { let url; if (typeof obj === "string") { url = obj; } else if (this && this.href) { url = this.href; } else if ( obj && obj.currentTarget && obj.currentTarget.href && obj.preventDefault ) { url = obj.currentTarget.href; obj.preventDefault(); } else { throw Error( formatErrorMessage( 14, __DEV__ && `singleSpaNavigate/navigateToUrl must be either called with a string url, with an <a> tag as its context, or with an event whose currentTarget is an <a> tag` ) ); } const current = parseUri(window.location.href); const destination = parseUri(url); if (url.indexOf("#") === 0) { window.location.hash = destination.hash; } else if (current.host !== destination.host && destination.host) { if (process.env.BABEL_ENV === "test") { return { wouldHaveReloadedThePage: true }; } else { window.location.href = url; } } else if ( destination.pathname === current.pathname && destination.search === current.pathname ) { window.location.hash = destination.hash; } else { // different path, host, or query params window.history.pushState(null, null, url); } } 那么popState是有在操作浏览器的前进后退才会触发,那single-spa是如何监控页面变化的呢? single-spa中跳转url给的api是navigateToUrl,它是基于history h5的api。想象一下,react-route人中提供的NavLink和Link应该也是基于pushState或者replaceState的。 所有的跳转操作都是基于pushState或replaceState的,single-spa就对history的pushState和replaceState进行了重写,在patchedUpdateState中判断url的变化,如果url变化,就手动触发popstate事件。 那么最上面的代码是如何导致死循环的呢? window.addEventListener('popstate', this.stopGoBack) // 监听popstate stopGoBack = () => { // pushState已经被重写,这里pushState后,就执行了patchedUpdateState,在patchedUpdateState中又手动触发popstate的事件,于是乎就成了自己掉自己 window.history.pushState('forward', null, '#') window.history.forward(1) } 走到这,我又尝试在react的spa应用中执行window.history.pushState(null, null, url),期待着能跳转页面,btw,url变了,但是组件挂载没有发生变化,于是乎,又去找了history的代码,能看出来createBrowserHistory中内部保存着一个history的状态,react-route接收根据history.location的值来进行match,match成功后挂载组件,并不是直接使用的window.history上的值进行判断的,所以在控制台中执行,地址栏会改变,但是组件挂载不会发生变化。]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[pm2在docker上以非root权限启动不了]]></title>
<url>%2F2020%2F02%2F20%2Fdocker-pm2-permission%2F</url>
<content type="text"><![CDATA[在docker-compose中,我是用works身份去启动去启动docker,在好几台机器上部署了,但是有的机器上可以正常启动,有的机器却显示权限有问题,具体错误是:docker Error: EACCES, permission denied '/root/.pm2’ 解决办法: ❌在启动pm2的时候指定用户, pm2 start -u works,这个方法不可行,仍旧是这个错误 ✅在dockerfile中设置PM2_HOME WORKDIR /apps # 设置成工作目录 ENV PM2_HOME="/apps/.pm2"]]></content>
<categories>
<category>docker</category>
</categories>
<tags>
<tag>docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[性能测试上docker]]></title>
<url>%2F2020%2F01%2F01%2Fperformance-docker%2F</url>
<content type="text"><![CDATA[在服务器上部署,需要做什么呢? 项目配置 增加docker文件 增加pm2, 在package.json中的scripts中增加"pm2": "pm2 startOrReload --no-daemon pm2.json" 增加docker文件夹,内部创建Dockerfile、start.sh # Dockerfile 文件 FROM xxx WORKDIR xxx #设置工作路径 # 一系列copy动作 COPY 项目的路径 docker中的路径 # 安装git, 创建文件,设置文件权限 RUN apk add --no-cache git \ && mkdir /apps/logs/ \ && chown -R node:node /apps/logs/ \ && yarn \ && chmod +x /start.sh \ && chown -R node:node /tmp CMD ["/start.sh"] # start.sh文件 #!/bin/sh # 根据环境不同设置传入参数CONF_NAME # 启动pm2 npm run pm2 --conf=/apps/conf/${CONF_NAME} 打镜像,测试机,推tag docker build -t tagName -f docker/Dockerfile . docker run -it -v 实际挂载路径:docker内部使用的路径 tagName 配置文件 在不同环境使用不同的配置文件,配置文件的格式可以自己定义,然后在server启动的时候读取配置文件 开机启动 docker-compose.yml version: '3' services: services_name: image: tagName container_name: container_name user: root # 用户启动身份 restart: always # 跟随docker服务重启 environment: # 环境变量 - HOST=${JMETER_PUBLISH_HOST} volumes: # 挂载变量 - xxxx:/apps/conf/online.conf - xxxx:/home/works/saas-performance-material-hub ports: # 端口映射 - 65534:65534 - 8001:8001 Redis 部署 直接在上面的docker-compose文件中增加redis的配置 测试验证项目运行之后,如何去查看log:docker exec -it container-id bash/sh 项目开发中的问题 项目中需要操作的文件,加一个:如果不存在,先创建,保障项目的可用性 项目中一定要有一个全局的try catch,一定要有app和process的 app.on('error', (err) => { console.error(`app.error出现错误,${err.message}`) }) process.on('uncaughtException', (err) => { console.error(`uncaughtException出现异常,${err.message}`) }) 项目中的redis一定要增加重试机制 options.retry_strategy = (options) => { // 前5次1分钟重连一次 超过五次,5分钟重连一次 let retryInterval = 1000 * 60 if (options.times_connected > 5) { retryInterval *= 5 } console.log(`---redis 正在尝试重连, ${retryInterval / (1000 * 60)}分钟之后重连`) return retryInterval } js文件中的websocket的地址写成灵活的,不能写成死的 一定要打出详细的log,以后好排查问题 遇到的问题 项目中有获取ip的操作,在docker中获取不到正确的ip,可以选择使用环境变量的形式传进去 docker虽然类似linux,但base的docker不一样的话,可能存在命令不存在的情况。刚开始在项目中使用curl命令,在本机调试的时候正常,但是部署上去不可以了,进入docker查看后,发现不存在curl命令 镜像导出导入的问题 docker save tag_id > filename.tar wget -P /保存文件的目录 文件下载地址 docker load < filename.tar docker tag tag_id xxxxx # 修改tag名字 docker开机自启动 systemctl enable docker.service # 启动docker-compose中的服务 vim /etc/rc.d/rc.local /usr/local/bin/docker-compose -f /www/docker/trace_fecshop/docker-compose.yml up -d docker中的权限问题,这是一个很头疼的问题。即使相同的权限,在不同的机器有的能起来有的起不来。相同的docker在不同机器上启动情况不一样,可能是文件权限的问题。]]></content>
<categories>
<category>nodejs</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[webpack官网所有的plugin]]></title>
<url>%2F2019%2F11%2F24%2Fwebpack-plugins%2F</url>
<content type="text"><![CDATA[dllPlugin这个plugin,webpack说是提高build time,它会将noede_modules里面的文件先build出来一个文件,每次hot reload的时候,只需要重新编译业务代码即可。 当然也可以使用它来提取多个项目的公共包,prodution也可以用参考链接,相比external来说,唯一的缺点就是会比原来的包的体积稍大一些。 对于react这样的包,dev和prod使用的js是不一样的,你需要针对不同的环境打不同的包。 另外还以一个缺点就是,不能正确的找到babel target版本的文件。例如,你的target是es next,他不会找xxx.es.min.js,只会找xxx.min.js。这样的话,也会导致最后的包增大。 所以,可不可以在production环境使用dllpligin打包出来的js文件,看业务需求即可。 下面是所有plugin]]></content>
<categories>
<category>前端</category>
<category>webpack</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[微前端-single-spa探索]]></title>
<url>%2F2019%2F11%2F16%2Fmicro-frontend-single-spa%2F</url>
<content type="text"><![CDATA[这个星期看了single-spa的整体思路和demo,差不多能解答上篇blog的问题了。 1. 如何匹配的路由sigle-spa在这册子项目的时候,需要指定路由在何种情况下挂载项目,这是不是意味着最好以url的prefix来划分项目更好一些? import {registerApplication, start} from 'single-spa' registerApplication( // Name of our single-spa application 'home', // Our loading function () => import('./src/home/home.app.js'), // Our activity function () => location.pathname === "" || location.pathname === "/" || location.pathname.startsWith('/home') ); start() 那么single-spa内部也是通过监听history的变化,进而判断如何挂载项目 // navigation/navigation-event // ... // We will trigger an app change for any routing events. window.addEventListener('hashchange', urlReroute); window.addEventListener('popstate', urlReroute); 另外single-spa为需要项目之间跳转的a标签,暴露了一个navigateToUrl的方法,我想了一下如果需要在js里面去进行项目之间的跳转,就是用最原始的方法: window.history.pushState(null, null, url); 2. 多个项目如何通信single-spa并不提供这个功能,但在github上看到了一个思路:(https://github.com/me-12/single-spa-portal-example)[https://github.com/me-12/single-spa-portal-example] 这个项目只要的思想是:在进行registerApplication的时候,主项目也会为每个子项目挂载一个store,并为每个子项目通过customProps将dispatch传递下去,那么dispatch主要做的就是将每个子项目的每次dispatch都会分发给每个子项目,子项目收到后,各自做自己的事情。 export class GlobalEventDistributor { constructor() { this.stores = []; } registerStore(store) { this.stores.push(store); } dispatch(event) { this.stores.forEach((s) => s.dispatch(event)); } } 3. 打包时,如何将多个子项目相同包合并这个是不正确的思路,如果每次打包的时候,都需要查找每个子项目的相同包,假如这个“相同”发生变化,那么就意味着主项目中打包出来的公共包需要改变,这是不正确的。微服务必须要尽可能的保持主项目即容器稳定,而不是每次迭代都有更改的可能。所以解决办法是公共包需要手动维护,并且更新的可能性最小。 对于打包这个功能,同样single-spa也不提供这个功能,但官网的文档上提供了三种解决方法:(https://single-spa.js.org/docs/separating-applications)[https://single-spa.js.org/docs/separating-applications] 比较靠谱的是第三个,为此,创建一个manifest文件,该文件在单Spa应用程序的部署过程中进行更新,该manifest控制“ SPA”哪些版本处于“活动”状态。然后根据manifest更改加载哪个javascript文件。 这个可以做到更新单个子项目,而不需要更新其他项目的目的。 暂未实施,这个做为下一步的重点关注。]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[微前端初探索]]></title>
<url>%2F2019%2F11%2F03%2Fmicro-frontend%2F</url>
<content type="text"><![CDATA[前几年的spa将前端推入热潮,但是当spa越来越大怎么办?整个团队都需要维护一个repo,碰上改文案的需求,整个项目需要全部更新,看起来真的好笨重。 现在项目的现状,是有两个项目的spa组合起来,虽然一定程度上减少了项目的耦合性,但是导致的问题就是,两个spa跳转时,整个页面刷新;两个项目中有一些重复的逻辑代码。因为页面是左右结构,左边是导航栏,我如果要改导航,我要改两个项目的代码,如果逻辑相同还好,逻辑不相同的话,就是double工作量。 微前端的概念提出来后,终于是有了解决方案的曙光。虽然还很年轻,但是值得探索。结合现在的项目,它能解决现在项目的痛点,真的非常适合。 现在的github上的解决方案很多。对于解决方案,我需要考虑些什么? 如何匹配的路由一个容器中包含多个子项目,每个项目有对应的路由。访问不同路由时,容器如何分发? 每个子项目在window上挂载对象,但这样做和按需加载时一个相悖的过程? 那就写一个adapter收集每个项目中路由,如何收集路由?假如子项目使用的技术栈不同,难道容器要穷举每种技术栈吗? 子项目主动在容器暴露的方法中挂载路由 匹配到路由后,执行相应的js,容器中需要存在一个路由指向状态,如果当前指向状态和需要跳转的状态相同,这个跳转就交给子项目的spa去做,不相同,就挂载并执行路由对应的js。 多个项目如何通信想把多个子项目用的的base数据放在容器中,问题是子项目如何从容易中拿数据? 之前看的文章说避免因为数据导致项目的耦合,但如果不将相同的数据放在一起,子项目中还是会存在相同的代码。容器负责维护base数据。 又出了一个问题,容器维护base数据,子项目加入使用不同的技术栈的话,是不是考虑一种最兼容的方式去维护数据? 打包时,如何将多个子项目相同包合并 打包的时候,容器遍历每个项目的package,抽出相同的,打包子项目的时候,不重复打包,但这个方法带来的问题是,我仅仅更新子项目的时候,如何知道哪些是重复的?这个pass 这个问题留给使用者,暴露接口让用户自行配置,打包的时候,还是照常打包package中的包。 另外可以出一个dev和pro的不同配置 // conf const devPackges = { key: value } const prodPackges = { key: value } // html Object.keys(devPackges/prodPackges).map(key => ( <script src={packages[key]} /> )) 11.4 新增–根据路由执行对应js,会导致内存泄漏吗?忽然想到如果single-spa没做出来,做一个兜底方案。受turbolinks启发,拦截两个项目之间所有的跳转链接,将跳转页面的请求转换成ajax请求,收到返回后,替换html中的js和css,这样就实现了无感知跳转。但这样,每次js标签的替换,都会导致js运行一遍(这个暂时这么想,我需要验证一下),来回跳转十次,内存不就炸了吗?照这个套路想,现在项目的spa,每个页面都是一个chunk,但是是一个累加的过程,react是如何做到匹配的路由执行对应的js,这个“执行”,是匹配到路由后,就执行对应的chunk,重复执行50次相同的js,内存不就满了吗?react是如何处理这的呢? 参考链接https://martinfowler.com/articles/micro-frontends.html]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[提高页面打开速度-优化打包]]></title>
<url>%2F2019%2F09%2F01%2Fincrease-page-open-speed%2F</url>
<content type="text"><![CDATA[cacheGroupswebpack4新有的一个cacheGroups, 现在我看大部分人都倾向于把node_modules里面的打成一个文件,像下面这样: vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor', chunks: 'all', minSize: 1, minChunks: 1, priority: 10, reuseExistingChunk: true } 但是比较尴尬的是,项目时使用spa写的,如果这样做的话,vendor文件会巨大无比,现在是打包压缩完是4M,非常庞大的文件。 仔细看了webpack的分析,类似echarts这样的大包也会打进去,但是echarts只在一个页面中使用了,如果一打开页面,就开始加载,太昂贵了。于是我调整了一下打包的策略,将“minChunks”改成2,最少两次引用的才会被打进vendor文件,这样导致的结果是有些页面的体验不是特别好,于是再为这些页面想想办法。 externals项目已经使用了http2,这样带来的好处是我可以使劲将文件切分,越小越好,像上面的问题,我只能在一打开页面的时候就去请求文件,这样的好处是等打开页面的时候,加载更快,体验更好。想了想,还是将文件放在cdn上,在webpack里面加入externals externals: { jsPlumb: 'jsPlumb' } 放在cdn上的好处是,只要用户访问过一次,就缓存了,即使经历迭代上线,也不会再去请求更新。 提取公共包?现在项目是有两个repo组成的,技术栈一模一样,上面的方法带来的启示是,我要不要将两个项目里面的包提取出来,放成一个cdn,这样既节省了打包速度,又减少了一次很大的包请求? 引入iconfont项目现在还没有使用iconfont,全都是用的图片,真的是非常想用iconfont代替它,因为使用图片有太多的局限,使用起来也很麻烦,但是实在是没有想到一个好办法来解决这和事情,真的是好纠结。说到这又想起来一个问题,之前尝试将iconfont.css加入到项目,但是webpack打包老是不过,现在打包是这样设置的: { test: /\.css$/, include: /node_modules/, use: isClient ? [MiniCssPlugin.loader, 'css-loader'] : 'css-loader/locals' } 真的是很对不起自己,没有看过webpack的文档,于是代入式的认为include表示的意思是在开发目录的基础上在包括的意思。在多次尝试无果的基础上,我看一了webpack的文档,终于找到了原因,include是需要完全匹配的,所以我需要将include的value改成/node_modules|iconfont/,嗯,做事之前一定要先阅读好说明书! 如何提高页面加载速度对于这个问题,项目依赖的东西是一定那么多的,如果是依靠文件什么的来提高加载速度,感觉不太现实,东西就那么多,只能这样那样拆来进行优化。 为了提高加载速度,项目也从http1换成了http2,看过这个概念,但是现在是想不起来了。 怎么说呢,提高加载速度有很多很多办法,但每种办法的收益不同,不要因为是做前端的而限制自己的视野。优化是一个持续的工作…]]></content>
<categories>
<category>webpack</category>
</categories>
<tags>
<tag>前端</tag>
<tag>webpack</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在高阶组件中传递ref]]></title>
<url>%2F2019%2F08%2F18%2Freact-hoc-ref%2F</url>
<content type="text"><![CDATA[在antd的基础上封装,做一个叫高阶组件封装,只是多了一个层。但是却拉了一个ref,ref通过不了这个层。 首先先来回顾高阶组件的写法 const Hoc = (Component) => { return class Wrapper extends React.Component { // xxxxxx render () { // 在这里做操作 <Component {...this.props} /> } } } export default Hoc(Input); 这个时候,你是用高阶组件包装后的Input发现this上并没有input的属性。这个时候才想起来ref不能通过props继续向下传下去。赶紧去找解决方案,发现React对于这个问题有相应的处理办法:Forwarding Refs const Hoc = (Component) => { class Wrapper extends React.Component { // xxxxxx render () { // 在这里做操作 <Component {...this.props} /> } } return React.forwardRef((props, ref) => { return <Wrapper {...props} ref={ref} />; }); } 在这里ref会存在在props中,但是不知道会不会有性能影响 真的需要强调ref的用途: Managing focus, text selection, or media playback. Triggering imperative animations. Integrating with third-party DOM libraries. 在代码中,经常会看到乱适应ref的情况,明明可以用过react本身就可以解决,非要用ref… 二次封装组件库上线后的问题 一定要保证和现有的样式兼容,因此样式是一个增量的增加,不是直接改,因为项目太大,如果全部替换不太现实。 props的增加也尽量保持灵活性。例如我在做modal的时候,我认为关闭按钮是一定存在的,上线后,发现有很多modal是自己增加的关闭按钮,于是和我的按钮就出现了重影。 像button这样的组件,我实际开发的时候,可能直接用的伪类hover、active多一点,忘了focus了=.=导致上线后,样式有些瑕疵 另外就是在封装组件时,没考虑到的问题,例如上面的ref问题。所以上到测试后,要跟着业务测试一下。以防想不到的情况。 还有一个最难受的一个,组件库是用ts,有很多类型啥的,我是第一次见,但是解决不了就不能compile,第一次input中遇到ts的问题,修了好久== PS: 发现的一篇精品文章Data Structures in JavaScript: Arrays, HashMaps, and Lists]]></content>
<categories>
<category>react</category>
</categories>
<tags>
<tag>前端</tag>
<tag>react</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用s3协议上传ali oss]]></title>
<url>%2F2019%2F08%2F10%2Fs3-replace-ali-oss%2F</url>
<content type="text"><![CDATA[前言最近要做的是将静态资源放在服务器上,目前项目的文件存储阿里的oss上,考虑业务和后续发展,以后会有更多的云供选择,所以找一个通用的协议来做这些上传。 问了后端的同学,ali oss兼容aws的上s3协议,但是s3不兼容ali oss,所以选用s3协议来上传ali oss。 s3是什么全称是Amazon Simple Storage Service介绍在这里 配置项目使用oss这里我用到的上传打包后的文件到oss的一个webpack的plugin是webpack-aliyun-oss 配置打包时文件的publicPath 配置打包后上传文件new WebpackAliyunOss({ dist: 'dist/', region: 'oss-region', accessKeyId: 'accessKeyId', accessKeySecret: 'accessKeySecret', bucket: 'bucket' }) 配置完后,测试一下没有问题即可。 使用s3上传oss使用aws-sdk来进行上传,说到这里,我去网上找demo,找到的一个java的例子,怎么到了js这里配置咋就这么麻烦呢? s3有好几种signatureVersion,本来想这个一个一个的尝试,试到v2的时候,总算是不报signature不匹配的问题了,幸亏问了一下后端的同学,才知道oss用的version时v4,完成配置后就可以正常上传了。 使用s3上传minio本来时改配置的事,但是会抱一个未知的inaccesshost的错误,直接找的同学解决了一下,s3将bucket放在endpoint中有两种方式: bucket.host.com host.com/bucket 使用s3ForcePathStyle设置,默认false,展现形式是第一种,设置成true就是第二种。 遗留问题 使用s3协议上传的图片,复制链接在浏览器打开只能下载,不能预览,这个是否可以通过设置“Content-type”来解决? 做成一个webpack的plugin? 2020.02.06补充 上传时指定Content-type // 设置参数 AWS.config.setPromisesDependency(Promise) AWS.config.update({ region, accessKeyId, secretAccessKey, endpoint, httpOptions: { timeout } }) // 实例化s3 const s3 = new AWS.S3({ signatureVersion = 'v4', Bucket: bucket, s3ForcePathStyle }) // 上传文件 const result = yield s3 .putObject({ Key: fileName, Bucket: bucket, Body: fs.readFileSync(filePath), ContentType: mime.lookup(fileName) }) .promise() 参照webpack-aliyun-oss,做出来一个webpack plugin.]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>后端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[优化项目中的包依赖]]></title>
<url>%2F2019%2F08%2F04%2Foptimization-package-dependencies%2F</url>
<content type="text"><![CDATA[跑了一下webpack analyse,发现项目中有两个moment版本,查了一下antd中依赖的moment版本和项目依赖的版本不同。 如何统一版本呢? 第一个计划,开始时想着不在项目中引用moment,直接使用antd中的moment,但是想到新来的同学会不会有疑问,项目里面明明没有引入moment但是为什么能用?于是,这个想法被弃用了。 第二个计划,我本来想着直接将项目中的moment版本直接设置成2.x,但是和同事商量了一下,依赖项目的依赖的依赖这个事情不太靠谱,看了antd4的计划又可能会对moment下手,因为moment太大了,有人在issue上提了可不可以将moment换成dayjs。antd的计划是在秋天,哦,秋天已经不远了。 于是采用的方法是写一个脚本来判断项目中依赖和依赖的依赖的moment版本是否一致 const { readFileSync } = require('fs') const antdPath = 'node_modules/antd/package.json' const projectPath = 'package.json' try { const momentVersionInAntd = getMomentVersion(readFileSync(antdPath)); const momentVersionInProject = getMomentVersion(readFileSync(projectPath)); if (momentVersionInAntd === momentVersionInProject) { console.log(`moment的版本一致, 版本号是${momentVersionInAntd}`) } else { console.log(`版本不一致,antd的版本是${momentVersionInAntd},项目中的版本是${momentVersionInProject},请查看changelog以统一版本`) } } catch (err) { console.log('---出现错误,原因:', err) } function getMomentVersion(content) { return JSON.parse(content).dependencies.moment } 统一了版本后,打包后的文件就少出来一个moment… 在就是lodash的tree shake,选的方法是使用lodash-es来代替lodash,可以进一步缩小js文件的大小。 import { get } from 'lodash'; // 变成下面这样的引用 import get from 'lodash-es/get'; 我看现在好多实践中都是酱node_modules中的全部达成一个包,现在项目里面也是这样的,但是光node_modules里面的打出来压缩完就3.9M这个也太多了,于是我想直将引入两次及以上的打进去,其他的单独是一个chunk,这样的话能从3.9变成2,只是试了一下,但还没有完整的探索一下~下周再试试]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[启示录--笔记]]></title>
<url>%2F2019%2F07%2F21%2Finspired-summary%2F</url>
<content type="text"><![CDATA[之前是对产品这个职业产生了兴趣,去网上查了一下,说启示录是产品的入门手册,于是选择这本书。 没有开始职业生涯的时候,就听说产品和技术是天敌,工作之后,是发现更多的是不理解,产品为什么这个设计,如果可以说服我,我可能会很欣喜的做这个,说服不了,也得做。hhh 新瓶装老酒这本书印象最深刻的一个词是:新瓶装老酒。成功的产品往往不是什么新鲜事物,只是新瓶装旧酒,之所以成功,是因为这个“新瓶”做的更好、更方便、更便宜,改变了消费者对“老酒”的印象。这个是想我想起了抖音,记得刚开始的是一个叫做小咖秀的app很流行,但是等我反应过来,抖音已经火起来了,操作真的非常方便,类似的应用“快手”,操作起来就没有那么便捷,所以我还是转回到抖音。 关注点产品经理应该关注的是什么?答:关注失望、不满、愤怒等一切负面的情绪。抛开无关紧要的问题,一心一意的解决已知的愤怒和不满,这正是skype成功的原因。 可用性和美感以我个人的感觉,这个真的非常非常重要有的系统的特点就是能用,但是很难用。一个是不太好看,随着设计师的迭代,色彩主色调被更换,系统外观处在分裂状态,看起来真的不太好看,迫切的分离感。kkkk 可用性的话,因为是第一次经历快速迭代,快速迭代的缺点,我觉得就是因为时间的关系没有时间来得及重构,导致的是一个堆积型的开发,越来越多,越来越重… 原型测试这里提到的原型测试,可能这一块要求的是可操作的,现在有很多原型制作的方式,但要是很的做这个,这是不是会成为产品的一个负担?或者是我还没get到点? 苹果公司的启示 硬件为软件服务 软件为用户体验服务 用户体验为情感服务 产品为真正的需求服务 提防有特殊要求的产品产品经理是满足大众的需求,这是产品公司和定制软件公司的区别。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[重构大组件的历程]]></title>
<url>%2F2019%2F07%2F20%2Frefactor-big-component%2F</url>
<content type="text"><![CDATA[在快速迭代下,如果没有来得及重构,一个组件会快速膨胀,慢慢的成为了一个历史悠久的组件,在没有完全了解业务的情况下,没有人敢动它,于是它成了以三千行闻名的组件=.=,这次优化把它排进去了,这个星期就先开始切分一下代码,正好也熟悉一下业务。 过程中最需要的是耐心,一定要稳住! 第一步:分离DOM一个组件大的原因是承载的东西太多了,首先是先将render函数中返回的DOM做一下切分。这样做的好处是,根据业务切分成多个小组件后,是有利于公共组件的构建,如果有部分样式改变也可以迅速的定位到问题。 const Article = () => { return ( <div className="article"> <div className="article-header"> <Title /> </div> <div className="article-footer"> <Title2 /> <Question /> <Answer /> </div> <div className="article-footer"> {/* 按钮组 */} </div> </div> ) } 上面看起来清爽多了,另外,在切分组件的时候,需要给组件传递很多数据和函数,可能会比原来的dom行数还好多,不要怕,继续做。后面会有办法解决。 第二步:数据降级这个和状态提升是一个相反的状态。什么时候需要状态提升呢?两个组件共享状态的时候,这个时候需要状态提升,将共享状态放在父组件中。状态降级是一个相反的过程。数据不仅仅限于状态还有一些挂载在组件上的数据。 在分离完dom后,去看一下哪些数据仅仅在一个子组件中使用,没有跨组件使用的情况,这个时候就可以将这个数据转移到子组件中,当然数据相关的操作函数也需要转移。在父子组件通信的过程中,可能也需要一个callback去操作子组件中的状态。 执行完这个过程,你会发现第一步中的props会减少,如果还是很多的情况下,就需要为方法写一个适配器去减少props的传递。例如add和delete可以合成一个change方法,add和delete就可以转移到子组件中。 第三步:整理代码由于每个人的需求开发对于其他人是相对封闭的,谁也不知道他改了什么?于是这个时候,会出现一些冗余代码,这个时候需要你有足够的耐心去浏览所有的函数。 首先找到组件所有的引用点,汇总所有的props,找到废弃的props,如果可以找到之前的负责人,可以问一下props废弃的原因,这样可能会发现更多的冗余代码。当然,你不确定的函数,一定不要动。 另外,每个人的代码质量是不一样的,在过代码的时候可以发现很多可以优化的地方,例如一个操作数组中每一个数据使用map,这个就一定要改掉。 最后的话对于这个组件,之所以是选择重构而不是重写的原因是,这个组件已经跨越了两年之久,开发人员和产品的人员变动,导致没有一个人了解组件涉及到的业务。我在过代码的时候,也发现了一些产品经理都不知道的逻辑。问之前的开发者,他说他也不是很清楚里面所有的业务,这也是组件增量的原因,不敢动! 另外,重写也是需要进行多个子组件的开发,这样下来,重写比重构的好处是多了代码质量的提高。但在快速迭代的压力下,没有足够的时间去进行。于是还是选择了重构,这样拆分下来,剩下的更多的是业务逻辑的梳理,里面的操作函数实在是太多了,代码也有些晦涩,在慢慢熟悉业务之后,会和产品一起梳理全部的逻辑,那个时候再进行操作函数的优化。 目前是这样的一个打算。]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
<tag>react</tag>
</tags>
</entry>
<entry>
<title><![CDATA[卓有成效的管理者--笔记]]></title>
<url>%2F2019%2F07%2F14%2Fthe-effective-executive%2F</url>
<content type="text"><![CDATA[怎么说呢?可能我目前了解的技术之后的路一个是管理还有一个是架构师,自身认为最难的是人心,对管理从来没有什么期待。但五月份的时候忽然转了性,觉得做管理也不错,于是选了“卓有成效的管理者”来了解一下。看完这本书,我觉得它不仅仅是针对每一个管理者,更适用于每个人。自我管理也是一门艺术 如果作为一名团队的管理者,那么你的管理风格是尤为重要的,本来是有一个很简单的想法,管理者必备的素质是为公司好,只要围绕着这个目标,一定可以做好管理,但是看到身边的例子发现并不可以,如何凝聚团队的核心力,手拉手往前冲呢? 首先书中提到如何做到卓有成效 记录并分析时间的使用情况 把眼光集中在贡献上 充分发挥人的长处 要事优先 有效的决策 首先你应该掌握自己的时间,精确记录会发现真实的时间占比如何,消除浪费时间的活动,统一安排自己的可支配时间。每个人对时间心中都会有一个大概的估算,但是在精确记录之后,你会发现结果会大大超出你的估算,其实你花在需要做的事情上面并不是自己想象的那样。 贡献,如何理解?我印象很深刻的一句话:不要问你要做什么,而是要问你能做什么。我之前问过我需要做什么?我能做什么其实你能为公司做什么,但这会不会限制你的创新力?安于现状还是有错创新,这是一个选择题。我能做什么是一个更大的期待吧。我想做的很多很多….能做的也不仅仅是限于业务部分。 后面的三个,怎么说呢?是一个很司空见惯的单词,应该是典型的说起来简单做起来难。 我目前理解的管理,仅仅是限于我能见到的。但是这个职业还有更多需要探索的,了解之后,会发现每个职业都有每个职业的痛点。其实看完这本书,更多的应该是我对职业的思考,我未来想做什么和可以做什么?每多一些了解,就会多一些天马行空的思考。虽然现在需要学习的东西还是很多。不管是做什么,最终的目的是使自己变得更好,越往前走,可能越恐惧,未知的东西太多,自己的年纪也越大。 国内程序员的黄金期是在35岁之前,想一下:22岁之前在读书,保守估算人可以活到七十岁。半半一下,人生的黄金期只有十三年,怎么可能? 对于一个团队来说,管理者的责任更大,与各个部分的人对齐,想想脑袋大了。这本书更大的收获实在自我管理上,如何管理自己,安排自己的时间,多问自己能做什么!再说多点可能就是鸡汤了…]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[提取公共组件-探索antd中的form]]></title>
<url>%2F2019%2F07%2F06%2Fexplore-form-in-antd%2F</url>
<content type="text"><![CDATA[目前遇到的问题是:有一个内部逻辑很复杂的公共子组件,对于它我的期望是我传给他一个初始值,随它内部怎么操作,最后我只要得到结果即可。这就要求数据和逻辑全部在自组件内部,但是我怎么在父组件里面得到子组件内部的数据?这似乎是违背了react单向数据流的思想,数据可以从父组件流向子组件,但不可以从子组件流向父组件。如果使用状态提升,这会导致我会在父组件里面创建state,写很多methods,既然这是一个公共组件,那每次使用一次我就在父组件里面增加一遍数据和函数吗?这似乎也不是最佳实践。这时候我想到了antd中的form。 在使用的时候你会注意到 Form.create({ name: 'normal_login' })(NormalLoginForm); 这里可以看出来Form.create会返回一个高阶组件,在NormalLoginForm里面得到form中的值也是通过props.form得到的。form里面有一个方法getFieldDecorator,文档的定义是:用于和表单进行双向绑定。为什么要进行双向绑定呢?form中的数据其实是存在高阶组件里面的,高阶组件和Input之间隔了一个你自己的组件,想到这里似乎有些明朗了,双向绑定是绑定Input和高阶组件的state。 我想了一下这个逻辑是可以解决我的问题的,于是想看一下antd是如何实现的 Form中create是static函数,它返回了一个createDOMForm高阶组件。mapPropsToFields是每次在getInitialState和componentWillReceiveProps中更新state的值 getFieldDecorator getFieldDecorator() { // ... return React.cloneElement(fieldElem, { ...props, ...this.fieldsStore.getFieldValuePropValue(fieldMeta), }); } 于是照着上面的步骤,写了一个高阶组件和公共组件,这个时候我在双向绑定上犯了难,antd是写了一个类似redux的store,有点闹不懂的是没有将数据放在state里面,组件如何重新render…因为目前业务需求不是很复杂,所以换一种思路。 实现1. 创建一个公共组件数据都是通过props来的 import React, { Fragment } from 'react'; const Reply = (props) => { const { list, delItem, addItem } = props; return ( <Fragment> <div className="list"> <p>这里是reply</p> { list.map((item, index) => ( <div key={index} onClick={() => delItem(index)}>{item}</div> )) } </div> <button onClick={addItem}>add</button> </Fragment> ) } export default Reply; 2. 创建一个高阶组件import React from 'react'; import Reply from './Reply'; const Hoc = (options) => (Component) => { class Wrapper extends React.Component { constructor() { super() // get initial state by options.mapPropsToFields this.state = { list: [1, 2] } } delItem = (index) => { const { list } = this.state this.setState({ list: [ ...list.slice(0, index), ...list.slice(index + 1) ] }) } addItem = () => { const text = prompt() if(text) { const { list } = this.state list.push(text) this.setState({ list }) } } getReplyCard = () => { return ( <Reply list={this.state.list} addItem={this.addItem} delItem={this.delItem} /> ) } getList = () => { return this.state.list } render() { const { list } = this.state; return ( <Component {...this.props} getList={this.getList} getReplyCard={this.getReplyCard} /> ) } } return Wrapper } export default Hoc; 3. 使用高阶组件import React from 'react'; import Reply from '../components/reply' class ContainReply extends React.Component { render () { return ( <div> <p>这是我的组件</p> {this.props.getReplyCard()} </div> ) } } export default Reply({})(ContainReply) 以上方式可以实现我的需求了….form还需再探索]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
<tag>react</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何二次封装组件库]]></title>
<url>%2F2019%2F06%2F16%2Fantd-custom%2F</url>
<content type="text"><![CDATA[前言现在已经有好多成熟的组件库,公司使用的是antd,但是样式和设计那边的有些出入。在快速迭代下,根本就没有精力去实现设计的要求,另外,每个人负责的也是不太相交的业务,所以即使你改了别人也不甚清楚,又是为了求速度,也是直接套用之前的样式,没有及时抽成组件,每个人写的也不兼容,直接后果就是导致项目越来越脏。 于是现在的一个方案是做一个基于antd的组件库。其实刚开始想的是一个组件一个组件的封装,但是这样的结果就是改每个地方的组件引用,虽然编辑器已经很优秀了,但是需要改动的地方实在是太多了。于是第二个方案出现了,我先做一个基于antd封装的组件库,打成包,组件慢慢的实现。如果遇见antd的升级的情况,就去升级自己的组件库,这样对于现在的开发项目其实是一个无感知的迁移。想来,第二个方案是一个最有选择。 如何封装呢大家都知道antd有一个按需加载,我也想我的组件库也有。怎么办呢?那我的文件组织和antd的一样,在webpack配置中只需要改一下name即可。 我看了antd的项目,发现在它的pacakge.json中的运行命令已经很完美了,如果我使用了它的所有配置,我又可以省好多精力了。 说到这里,相信你已经有些想法了。是的,继续往下… 实施首先去antd的项目里面下载zip包。antd中的组件都在components文件夹,那么我们也只需要更改这个文件夹了,至于其他的文件,你不需要的就可以删除, 1. 更改项目的依赖将package.json中依赖全部删除,增加antd 2.封装组件拿affix举例,更改affix文件夹中的style/index.less和index.tsx // index.less @import '~antd/lib/affix/style/index'; // index.tsx import Affix from 'antd/lib/affix'; export default Affix; 其他组件类推。 3.编译package.json中有compile命令,执行一下就有es5的代码了,这个时候你就可以去发布自己的包了。 4.更改组件如果更改组件的话,只需要去更改style/index.less和index.tsx这两个文件即可。 结语现在的组件库的文件组织都已经很清晰了,这次的研究其实是一个通用方法,以后遇到其他的组件库改造,这个方法也适用。 另外因为我没有研究更改组件这个开发过程….难道我要在另外一个项目中开发完,再复制粘贴到style/index.less和index.tsx中吗?感觉蠢蠢的,一定还有更好的方法去解决。]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
<tag>react</tag>
</tags>
</entry>
<entry>
<title><![CDATA[停止使用css in js进行web开发]]></title>
<url>%2F2019%2F06%2F09%2Ftranslate-stop-using-css-in-javascript-for-web-development%2F</url>
<content type="text"><![CDATA[CSS不会去任何地方。许多项目选择用JavaScript来设置文档样式是出于错误的原因。本文列出了常见的误解(神话)和现有的CSS解决方案。 本文中的任何内容均不构成对特定项目或个人的人身攻击 CSS和JavaScript的历史层叠样式表(CSS)语言用于描述用标记语言编写的文档的表示。JavaScript作为“粘合语言”被创建,它用于组合图像和插件等组件。多年来,JavaScript不断发展并变异以适应新的用例。 Ajax(2005年)的出现标志着一个重要的里程碑。 这就是Prototype,jQuery,MooTools等库吸引大量社区共同努力解决后台和跨浏览器不一致的数据获取问题。 这造成了一个新问题:如何管理所有数据? 快进到2010年,Backbone.js作为应用程序状态管理的行业标准。不久之后,Knockout和Angular将每个人都用双向绑定来吸引人。不久之后,React和Flux出现了。这开启了单页应用程序(SPA)、由组件组成的应用程序的时代。 那么CSS呢?借用styled-components的文档: The problem with plain CSS is that it was built in an era where the web consisted of documents. In 1993 the web was created to exchange mostly scientific documents, and CSS was introduced as a solution to style those documents. Nowadays however, we are building rich, interactive, user-facing applications, and CSS just isn’t built for this use-case. 我不同意。 CSS已经发展到捕获现代UI的要求。过去十年中新功能的数量超出了合理范围(pseudo-classes, pseudo-elements, CSS variables, media queries, keyframes, combinators, columns, flex, grid, computed values, …) 从UI的角度来看,“组件”是文档的孤立片段(<button />是组件)。CSS旨在设计文档样式,其中包含所有组件。有什么问题? 俗话说:“使用合适的工具来完成工作”。 Styled-componentsStyled-components使用标记模板文字在JavaScript中编写CSS。它删除了组件和样式之间的映射 - 组件被制成一个低级样式构造,例如, import React from 'react'; import styled from 'styled-components'; // Create a <Title> react component that renders an <h1> which is // centered, palevioletred and sized at 1.5em const Title = styled.h1` font-size: 1.5em; text-align: center; color: palevioletred; `; // Create a <Wrapper> react component that renders a <section> with // some padding and a papayawhip background const Wrapper = styled.section` padding: 4em; background: papayawhip; `; // Use them like any other React component – except they're styled! <Wrapper> <Title>Hello World, this is my first styled component!</Title> </Wrapper> 结果是: styled-components现在成为在React中设置组件样式的新方法。 让我们明白一点:样式组件只是CSS之上的高级抽象。它所做的就是解析用JavaScript定义的CSS并创建映射到CSS的JSX元素。 我不喜欢这种趋势,因为它被许多误解所包围。 下面是选择使用styled-components的原因列表,我结合原文谈一下我的观点。 1.它解决了全局命名空间和样式冲突听起来它似乎没有解决这些问题。 CSS模块,Shadow DOM和无数的命名约定(eg: BEM)很久以前就在社区中解决了这个问题。 styled-components(就像CSS模块一样)消除了人类命名。但就其本身而言,这并不是开始使用styled-components的充分理由。 2.使用styled-components可以获得更简洁的代码举一个例子 <TicketName></TicketName> <div className={styles.ticketName}></div> 首先 - 没关系。差异可以忽略不计。其次,事实并非如此。总字符数取决于样式名称。 <TinyBitLongerStyleName></TinyBitLongerStyleName> <div className={styles.longerStyleName}></div> 这样的做法和css中的选择器有什么差异吗 3. 使用styled-components可以让您更多地考虑语义语义和样式是两个不同的东西。 4.它可以轻松扩展样式css预处理器不是可以很轻松的解决这个问题 5.它使条件样式组件变得容易通过变量更改className不是一个好的解决办法吗? 6.它允许更好的代码组织这个完全不同意。都成一锅粥了…本来一个一百行的组件要是和样式写在一起,都要二百行了… 7.任何更好的性能,任何更小的捆绑尺寸js做了解析,在不同的页面挂载不同的css,小,我是同意的,但是对于一个spa来说,你的css没有办法进行缓存。 另外通过查看源码可以看到,css实际上都是挂载在一个HOC上面,这很明显也存在一定的性能问题。 如果使用ssr的话,你得到的可能是一个巨大的HTML。 8.它允许开发响应组件🙂️ 结语我现在知道很多公司正在使用css in js,我想大部分的因素应该是第一个css和组件的映射。换一种角度来说,我认同组件的定义,但是对于它和css的关系,我会认为是html和css的关系。如果要在css上做区分的话,我会选择给组件的className加一个rc的前缀。 对于命名,有很多成熟的方案,例如之前使用的BEM,其实我在css in js的代码里面看到最多的是嵌套语法,这个就很伤了,尽管处理器现在已经很快了,那也禁不住你三四层的套啊。 css是样式,js对于一个平台来说已经很巨大了,将css给它处理真的是一个累赘。还有一点开发过程,写css和写css in js完全是存在巨大差异的过程,没有各种简写,我只能选择浏览器作为我的第一开发工具😂。 参考链接]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[migrate-flow-to-ts]]></title>
<url>%2F2019%2F06%2F01%2Fmigrate-flow-to-ts%2F</url>
<content type="text"><![CDATA[现在经常开发的两个项目,一个是用的ts,一个用的flow。ts是现在一个主流的技术,新同学进来需要学习flow的语法,在开发过程中,在flow项目中也经常会写ts的语法,但是在js文件里面写了ts也不会什么作用啊。 找了从flow迁移到ts的文章,跟着里面的步骤实践了一遍,过程繁琐了点,但结果还是令人欣喜的。大致的步骤是: 更改文件后缀名 移除所有flow的注释 移除flow的配置文件和package,增加ts配置文件 在webpack中增加ts-loader 修复type error 1. 更改文件后缀名项目中所有的文件都是js文件,根据文件名可以选择后缀是tsx还是ts。使用fs为所有文件重命名。(这就体现了文件命名约定的好处了) const fs = require('fs'); const tsFileReg = /(style|styles|data|util|type|reducer|action|gql|index).js$/ const dfs = (path) => { fs.readdir(path, function (error, files) { if (error) { console.log(error); } else { files.forEach((filename) => { const currentPath = path + filename; const stats = fs.statSync(currentPath); if (stats.isDirectory()) { // 文件夹 dfs(currentPath + '/'); } else { // 文件重命名 let newSuffix = tsFileReg.test(filename) ? '.ts' : '.tsx' const newFileName = filename.replace(/\.js$/, newSuffix); fs.rename(currentPath, `${path}${newFileName}`, function (err) { if (err) throw err; console.log('renamed complete'); }); } }); } }); } dfs('./src/shared/'); 虽然在重命名的时候增加了规则,但是还是会出现错误的后缀名,原因是有一些文件命名不符合现在的命名规则。 2.移除所有flow的注释文件太多,我做不到一个一个删除了,于是选择了使用编辑器将// @flow变成空 3. 移除flow的配置文件和package,增加ts配置文件 删除tools/scripts/flow.js和tools/scripts/removeTypes.js 删除tools/flow/文件夹 删除.flowconfig 增加tsconfig.json 增加依赖: @types/react、@types/react-dom、tslint、tslint-immutable、tslint-react、typescript、ts-loader 删除依赖:eslint-plugin-flowtype、flow-typed 4. Add tslint and autofix code style增加tslint.json 5. Remove type identifier in import interfaces删除(import type { AppProps } from ‘../interfaces/app’)里面的type标示 6. Adjust webpack configmodule.exports = { test: /\.tsx?$/, use: 'ts-loader' }; 文件扩展名增加:ts, tsx 7. 修复type error和warning使用tslint -c tslint.json -p tsconfig.json -e \"**/*.d.ts\" \"src/**/*.{ts,tsx}\"去查找type error修复,这个虽然很简单,但是一个大工程… PS:2019/7/4号得知tslint被弃用了,现在用的是typescript-eslint]]></content>
<categories>
<category>前端</category>
<category>typescript</category>
</categories>
<tags>
<tag>typescript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[这周也是元气满满的一周]]></title>
<url>%2F2019%2F05%2F25%2Fsummary-work-4%2F</url>
<content type="text"><![CDATA[PureComponent为什么不管用了首先这个问题可以追溯到我之前的issue,这次拿出来看的时候,终于是找到了清楚的答案。 使用PureComponent可以提高性能,因为react会在shouldeComponentUpdate中进行一个浅比较,注意是浅比较,所以当你的UI组件中一旦有嵌套的对象属性或者函数,其实这个时候即使用了PureComponent也不会有什么作用,这个时候就需要你在shouldeComponentUpdate去进行手动判断是否rerender。 参考链接:你真的了解浅比较么? ssr当你访问一个spa时,第一次打开的速度很慢,白屏的时间也很长,打开控制台发现用了css in js导致html巨大,js文件也很巨大,这个时候最简单的是使用部分ssr,先显示一部分html在页面,然后等下载完js文件后执行即可。 项目里面用了css in js,想要把它干掉,不知道执行中会遇到什么问题,这是自己的一个挑战吧。 因为现在工作时间变长了,真的是很难调节自己的时间,每天回到家只想躺着,努力调整一下。]]></content>
<categories>
<category>前端</category>
<category>react</category>
</categories>
<tags>
<tag>react</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何更改antd的样式]]></title>
<url>%2F2019%2F05%2F19%2Fchange-antd-style%2F</url>
<content type="text"><![CDATA[新地方用的是antd组件,功能可以满足需求,但是样式需要调整一下,找到了两个方法:一个是粗犷的暴力覆盖样式,另一个是通过改变less里面的值进而达到更改样式 方法1: @import '~antd/lib/modal/style/index'; .ant-modal { &-header { border-bottom: none; } &-title { color: red; } } 方法2:因为之前使用的是sass预处理,变量支持default和import,但是less是不支持这个操作的,想要达到目标,我只能采取变量覆盖的方法,这就需要在引入定义变量的文件后面覆盖变量的值(和定义变量相同),这样就导致一个问题:我必须要了解组件style文件中的逻辑。这样一来,我的自定义组件中的样式架构就需要跟着antd的变化而改变。 @import '~antd/lib/style/themes/default'; @import '~antd/lib/style/mixins/index'; @border-width-base: 0; @heading-color: red; @import '~antd/lib/modal/style/modal'; @import '~antd/lib/modal/style/confirm'; 目前只想到这两个方法。 但是比较伤的是我发现项目中的antd版本不一样,那么就要意味着我写两个版本的组件样式库??? PS: 今晚等雨的我,怕是等不到雨了…]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[你需要有一些产品意识-1]]></title>
<url>%2F2019%2F05%2F11%2Fknow-product-1%2F</url>
<content type="text"><![CDATA[俗话说,技术的天敌是产品,但是懂些产品也是好的,对于这周的工作,主要是进行开发新功能,但是在测试中,遇到了一些反馈,讲真,以我的观点,为什么要这么做呢?于是问了产品,认真的做了笔记。 是否要设置默认状态这个起因于产品中有两个状态:在职和在校。我的想法是:用户主要是在职的多一点,既然每个用户都要点,我将状态设置成在职,这不就省了大部分人的点击操作吗? 产品:你不能决定用户的想法,另一方面,如果默认状态设置成在职会给在校的一个“我不是主要推广人群”的暗示。 我:那什么时候要设置默认状态? 产品:当你需要向用户传达你的期望的时候,可以将默认状态设置成你期望的操作,例如订阅操作,你是希望用户订阅,所以订阅的初始状态是“已订阅”。 swiper什么时候用点点什么时候用箭头这个是swiper的表现样式,我不确定是否和产品形态有问题,于是就问了,得到的答案是和产品无关,和视觉设计有关。 如何引起用户的注意这个源于“要给主要的消息通知加轮播”,我左思右想感觉这个没必要: 消息在页面的占比是页面的宽度*70的高度,你设置了轮播,太矮了,用户感知不到; 给消息加轮播的合适的时间间隔? 消息通知的样式是一个红色的icon加文字,红色的icon在页面上是一个显眼的存在,放个轮播在上面,谁会去读文字,我不如直接点击列表页去查看消息 产品:现在要做的是在不影响用户原有路径(关于路径说了好多,但是我听的不是太明白)的情况下,去优雅的提示用户:这里有可以查看的东西。高度虽然很矮,但是够长,如果轮播的话还是能够提醒到用户。时间间隔的话,因为有文字需要阅读,时间间隔的话选择3-5s秒。 我:我想起来,我见过别的网站上的消息通知,可以让消息文字前面的小icon加上晃动的特效来提示用户这里有需要注意的东西。 产品:icon晃动的确是能引起用户的注意,此时用户也只能看到铃铛晃动,这个操作不会引起他对文字的兴趣,所以如何做到引起用户的注意,首先是看场景。 我:云里雾里=。= 结语这让我想起了之前我在页面中加入了类似“纸牌反转”的效果,这个效果是我提出的,设计说也可行,于是就下手做了,我的初衷是:加入一些时尚的效果,使网站看起来更时尚。 but产品给我说这个效果的加入只会格格不入,不会是一个时尚的效果。仔细想了想,的确是,于是我又改回去了。这让我有了思考,设计的基础是产品的原型,类似hover、active的效果是在设计层面上,效果的目的是为了交互,交互友好可以让用户感知操作。那么产品层面的就是为什么这块东西我要放在这里,总之,你是在为用户规划操作路径,总之一个事物的存在都有产品的初衷。 前端是直接和用户打交道的,如果能了解设计和产品的base,相信开发出来的产品,交互更友好,传达更准确。]]></content>
<categories>
<category>产品</category>
</categories>
<tags>
<tag>产品</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端相关的问题解答]]></title>
<url>%2F2019%2F05%2F01%2Fsome-question-answer%2F</url>
<content type="text"><![CDATA[最近在公众号上看到一篇文章里面,虽然是前端的米啊是,但是里面涉及到的问题众多。链接在这里 redux的工作流程 component –> dispatch(action) –> reducer –> subscribe –> getState –> component react 16新增内容 ReactDOM.createPortal 新的生命周期 hooks context fragment 其实印象最深的是hooks,其他的新增也应该关注,最直接的方法是看react blog。 四次挥手详细介绍参考链接 TCP 有哪些手段保证可靠交付 校验和 序列号 确认应答 超时重传 连接管理 流量控制 拥塞控制 如何预防中间人攻击参考链接 DNS 解析会出错吗,为什么参考链接 ES6 的 Set 内部实现参考链接 如何应对流量劫持参考链接 算法:top-K 问题我是第一次听说这个问题,问题描述是:给定n个整数,求其前k大或前k小的问题,简称TOP-K问题 webpack 的 plugins 和 loaders 的实现原理参考链接参考链接 webpack 如何优化编译速度这个我能想到多线程编译参考链接 事件循环机制,node 和浏览器的事件循环机制区别参考链接 区别的话我会选择node和浏览器不同进行理解,例如node没有browser api 参考链接 rxjs 高阶数据流定义,常用高阶数据流操作符? RxJS 冷热流区别, RxJS 调试方法,RxJS 相对于其他状态管理方案的优势?我第一次看到这个东西,官网在这里,前端真的是任重而道远! nginx 负载均衡配置参考链接我服务器上的nginx还没配置好=。= JWT 优缺点感谢开发小程序中,让我认识到JWT,这里又想起了一个新的概念“单点登录” 参考链接 针对 React 的性能优化手段参考链接 forceUpdate 经历了哪些生命周期,子组件呢?参考链接 谈谈 XSS 防御,以及 Content-Security-Policy 细节参考链接 参考链接) canvas 优化绘制性能参考链接 谈谈 css 预处理器机制参考链接 ssr 性能优化,node 中间层细节处理现在越来越多的公司将node作为中间层去处理数据,转发数据。参考链接 发布订阅模式和观察者模式观察者模式维护一个observers,每当有变化,就通知观察者。发布订阅则是由一个中介再做这件事。 PS:写在最后的话跨域、性能优化应该是一个都关注的点,另外webpack也是一个关注点,但是我只停留在会用会配置的阶段=。= 整理起来真的感觉又杂又多,感觉自己已经很努力的学习前端的知识,但是新的东西越来越多的向你涌来。最近看了web性能优化,给了我很大的启示:自己对有些问题的理解只停留在表面,eg:使用雪碧图的目的是减少http请求,但从没想过为什么要减少http的请求。前端其实涉及到知识点众多,而不仅仅是html、js、css,学有所思才是王道。]]></content>
<categories>
<category>总结</category>
<category>前端</category>
</categories>
<tags>
<tag>总结</tag>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[跨域]]></title>
<url>%2F2019%2F05%2F01%2Fcross-domain%2F</url>
<content type="text"><![CDATA[所有的浏览器都有沙箱机制。 网络安全与沙箱将个别套接字的管理任务委托给浏览器还有另一个重要的用意:可以让浏览器运用沙箱机制,对不受信任的应用代码采取一致的安全和侧罗的限制。比如,浏览器不允许直接访问原始网络的套接字API,因为这样给恶意应用向任何主机发起的任意请求(端口扫描,连接邮件服务器或发送未知消息)提供可乘之机。 连接限制浏览器管理所有打开的套接字池并强制施加连接数限制,保护客户端和服务器的资源不会被耗尽。 请求格式化和响应处理浏览器格式化所有外发请求以保证格式一致和符合协议的语义,从而保护服务器,类似的,响应解码也会自动完成,以保护用户。 TLS协商浏览器执行TSL握手和必要的证书检查。任何证书有问题(比如服务器正在使用自己签发的证书),用户都会收到通知。 同源策略浏览器会限制应用只能向哪个来源发送请求。 上面的安全限制规则只是一部分,但已经体现了“最低特权”原则了,浏览器只想应用代码公开那些必要的API和资源:应用提供数据和URL,浏览器执行请求并负责管理每个连接的整个生命周期。 上面的同源策略中的源是由“应用协议、域名和端口”这三个要件共同定义。其中有一个不同就是跨域请求。 在刚接触跨域的时候,为了能访问到数据,我尝试修改xhr的首部,但是浏览器会拒绝绝对不安全的首部的重写,以保证应用不能假扮用户代理、用户或请求。 有时候,同源策略并不能满足我们的请求,这个时候就出现了“跨源资源共享” 如何实现跨域CORS请求针对CORS请求的选择同意认证机制由底层处理:请求发出后,浏览器自动追加受保护的Origin HTTP首部,包含着发出请求的来源。相应的,远程服务器也可以检查Origin首部,决定是否接受该请求,如果接受就返回Access-Control-Allow-Origin响应首部。 但是CORS还会提前采取一系列的安全措施: CORS请求会忽略cookie和HTTP认证等用户凭证 客户端被限制只能发送“简单的跨域请求”,包括智能使用特定的方法(GET、POST和HEAD),以及只能访问可以通过XHR发送并读取的HTTP首部。 要采用cookie和HTTP认证,客户端必须在发送请求的时候通过XHR对象发送额外的属性(withCredentials),而服务器也必须以适当的首部(Access-Control-Allow-Credential)响应,表示它允许应用发送用户隐私数据。类似的客户端需要写或者读自定义的HTTP首部,或者想要使用“不简单的方法“发送请求,那么它首先需要获取第三方服务器的许可,即向第三方服务器发送一个预备(optins)请求。options请求只会发生一次。 JSONP只能实现GET请求,因为浏览器允许资源请求可以不受同源的限制 <script type="text/javascript"> function dosomething(data){ //处理获得的数据 } </script> <img src="xxxx?callback=dosomething" /> postMessage待补充… nginx 反向代理待补充…]]></content>
<categories>
<category>总结</category>
<category>http</category>
</categories>
<tags>
<tag>总结</tag>
<tag>http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[web性能权威指南-读书笔记]]></title>
<url>%2F2019%2F04%2F30%2Fhigh-performance-browser-networking-summary%2F</url>
<content type="text"><![CDATA[首先买这本书是看了推荐来的,名字是“web性能权威指南”,翻了目录之后,看到里面是tcp、ip的相关的网络的东西,第一感觉是啊,web性能优化和网络有很大关系吗?直到今天,我看到英文名“high performace browser networking”…额,这个翻译名字… HTTP简史http 0.9只有一行协议,其主要功能是: 客户端/服务器、请求/响应协议 ASCII协议,运行于TCP/IP链接之上 设计用来传输超文本文档(HTML) 服务器和客户端之间的连接在每次请求之后都会关闭 因为传输类型的局限性,人们需要一种协议,它不仅能访问超文本文档,还能提供有关请求和享用的各种元数据,还要支持内容协商。于是HTTP1.0出现了,但HTTP 1.0最大的问题是对每个请求都打开一个新的TCP连接严重影响性能,经过修订,出现了HTTP 1.1。 HTTP 1.1标准厘清了之前版本中的很多歧义的地方,而且加入了很多重要的优化:持久连接、分块编码传输、字节范围请求、增强的缓存机制、传输编码及请求管道(详细内容请查阅HTTP权威指南)。 web性能要点更宏观的web性能优化(之前对与web性能优化从来没有关注过以下几点)有: 延迟和带宽对web性能的影响 传输协议(tcp)对http的限制 HTTP协议自身的功能和缺陷 web应用的发展趋势及性能需求 浏览器局限性和优化思路 限制web性能的主要因素是客户端和服务器之间的网络往返延迟。 现代浏览器为我们做了一些优化,我们可以在文档中嵌入提示,以触发浏览器提供的优化: 资源预取和排定优先次序 DNS预解析 TCP预连接 页面预渲染 HTTP 1.xsteve souder的“高性能网站建设指南”中的优化规则,有一半是针对网络优化的 减少DNS请求 减少HTTP请求 使用CDN 添加Expires首部并配置ETag标签 Gzip资源 避免HTTP重定向 以上建议都反映在两个根本方面:消除和减少不必要的网络延迟,把传输的字节数降到最少,这两个根本问题是永远优化的核心,对任何应用都有效。 这让我想起了,在写api的时候,我为了不在js中拼接html和共用模版,就直接在后端返回拼接好的html,相对于json数据,html的传输字节数较大,所以,类似这样的api,是选择json还是html是否存在一个中和? 持久连接的优点HTTP 1.1的一个主要改进就是引入持久的HTTP连接,一个新的TCP连接发送的HTTP请求所花的总时间,最少等于两次网络往返的时间:一次用于握手,一次用于请求和响应。你也许在请求头中看到过connection:Keep-Alive,这是在声明使用持久连接。 图片大小计算RGBA图片的像素需要占用4个字节,红黄蓝通道各占一个字节,Alpha通道占用1个字节,这样算下来,一个图片占用的内存量是:像素宽度 像素高度 4字节 度量和控制协议的开销HTTP 0.9当初是一行简单的只有一行的ASCII请求,用于取得一个超文本,这样导致的开销是最小的。HTTP 1.0增加了请求和响应头部,以便双方能够交换有关请求和响应的元信息,最终HTTP 1.1把这种格式变成标准:服务器和客户端都可以轻松的扩展首部,而且始终以纯文本形式发送,以保证与之前HTTP版本兼容。 今天,浏览器发起的HTTP请求,都会携带额外的500-800字节的HTTP元数据:用户代理字符串、很少改变的接受和传输首部、缓存指令等等。这还不算上http cookie。现在应用中有时候会使用cookie进行会话管理,综合在一起,所有这些未经压缩的HTTP元数据经常会给每个HTTP请求增加几千字节的协议开销。 cookie在很多应用中都是常见的性能瓶颈。 TCPTCP(传输层控制协议),负责在不可靠的传输信道上提供可靠的抽象层。 三次握手客户端与服务器在交换应用数据之前,必须就起始分组徐海以及其他的一些连接相关的细节达成一致,出于安全的考虑,序号由两端随机生成。 SYN:客户端随机选择一个随机序列号x,并发送一个SYN分组,其中还包括其他的TCP标志和选项。 SYN ACK:服务器给x加1,并选择自己的一个随机序列号y,并追加自己的标志和选项,然后返回响应。 ACK:客户端给x和y加1并发送握手期间的最后一个ACK分组。 三次握手完成后,客户端与服务器之间就可以通信了。客户端可以在发送ACK分组之后立即发送数据,而服务器必须等到接收到ACK分组之后才进行发送数据。 SYN ACK名词我也不清楚是什么,再考虑一下握手为什么是三次而不是二次或者四次?]]></content>
<categories>
<category>总结</category>
<category>http</category>
</categories>
<tags>
<tag>总结</tag>
<tag>http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[js中map那些事]]></title>
<url>%2F2019%2F04%2F25%2Fjs-map%2F</url>
<content type="text"><![CDATA[这是我看到leetcode中的第一题的python解答想到的map,求两数之和为target,只需要在数字num的基础上,查找数组中是否存在target-num即可,但是针对数组的查找,时间复杂度应该是O(n), 但是判断map是否含有某个key,它的时间复杂度应该是O(1)。我看别人的解答是使用map来降低时间复杂度,这里先不考虑空间复杂度。 es6中有map对象,虽然object也是key-value的形式,但是object中的key只能是字符串,而map的key可以是任何类型。当你使用一个object作为key时,可能在不使用之后导致内存得不到释放,于是js也提供了WeakMap类型。 map提供了一些很方便的方法,供使用。我在项目中使用map不多,想了解一下map的应用场景。参考链接 文章中的观点主要有 map主要用于快速搜索和查找数据中key的类型:在object中,key必须是简单类型-整数、字符串或symbols,但在map中,key可以是任何类型元素的顺序:map会保留元素对的原始顺序,在object中,不会保留。继承:map是object的一个实例,但是object不是map的实例map提供的语法比object中的语法更简单 map和object的应用场景 object适用于只有简单类型的key,给key赋值为函数 JSON直接支持object,在使用json的时候可以选择object 在有大量增加和删除key的场景中,map的性能会好一点 对需要保存key的顺序时,使用map map在大数据中表现的比object更优秀 另外删除object中的key,有两种方法 delete obj.id obj.id = undefined // for performance boost 第一种方法将从对象中完全删除该特定的属性,第二种方法实际上只是将key的映射值改成undefined,key还保留在对象中。不仅仅要考虑性能。]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[面试那些事儿]]></title>
<url>%2F2019%2F04%2F23%2Finterview-summary%2F</url>
<content type="text"><![CDATA[我的面试到此为止了。 前提:一定要准备我的面试经验少的可怜,但这次真正的准备面试中,还是收获了很多东西,正如之前的post说的,面试官和候选人总是需要一个互相引导的过程。但前提是如果如参加面试,候选人一定要好好准备,如果你在没有任何准备的情况下,面试过程和收获可能都不是满意的。 反思在刚毕业的时候,有过一段两个星期的面试经历。当时的状态是我觉得自己还不错,也是一个努力的人,那为什么我会在面试中发挥不出来我的最好水平呢?对此也反思过,但是找到工作之后,就停止了。 之后,我给我朋友说:我觉得自己不错,但在面试中发挥不了自己的能力呢?她震惊道:面试都是有技巧的,你不知道吗?说实话,我是真的不知道。我查过如何准备简历,嗯哼,要写上github和blog,ok我写了。但是在一份面试后,面试官说你的简历太板了,其实不是很理解板的的意思。现在我在筛简历的时候,深深的感觉到了“板”的意思。你可以写“熟练使用ajax”,但ajax对于前端来说是一个必备的技能,这句话体现不出来你的能力,与其这样你还不如写点ajax的本质。 其实我对我的写简历也不在行,上次改简历是参考了“方糖”的策略来改的简历,ok,写上自己的技能,更多的笔墨放在自己项目中的感受。其次,排版一定要清晰。 另外,这次我又专门找人给我过了简历,收到他的建议后,我添加了个人评价这一块,另一块把项目中用到的我熟悉的后端技能也写上,这样起码能让面试官对你目前的全部技术栈有一个大概的了解,如果遇上类似的技术栈及更好了。 面试过程对于一个面试,面试官和候选人都需要付出很大的精力。虽然我之前看过文章说,可以在工作之余经常性的面试一下,保持一下敏锐性,我觉得挺好的,如果遇到契合的面试官,相信你的收获会很大。 一个好的面试过程,是一个面试官和候选人相互引导的过程。面试过程中,万一面试官卡住,空气突然变得寂静,也会更紧张,这个时候你可以挑选面试过程中你有疑惑的地方问一下。如果有不清楚的地方,你可以抛出一个问题,表示需要面试官的引导。另外,面试题目一板都是连接性的,不会跳跃很大,因为这是在考察你的深度,所以你可以适当的联系一下上一个问题,来得到一个更好的答案。当遇到你真的不清楚的问题,你可以将问题往你清楚的问题上带,假如你不知道js中的继承,但是你知道prototype,你可以讲一下原型链,争取将你知道的全部发挥出来。 收获这次面试的三个,收获颇多。但是更多的收获就是我可以去更加深度的思考问题。想起之前我给同事们分享angular1的时候,我看了源码,我将代码中的步骤讲出来,那时候上司问我:angular是怎么做的,我就将它的步骤讲了一遍,他不满意。想想那个时候,他应该是让我将更深处的点讲出来,但是当时的我不是很理解。 例如:我知道elasticsearch是怎么加快查找速度的,哦,通过分词?那为什么分词呢?知道是降低时间复杂度。那内部分词就具体怎么做的?这个过程是如何进行的?是否还有其他的方式?这几种方式再对比一下?满满的都是问题。 结语面试就是一个你展示自己的过程,当然不要夸大(类似你干的基层,但你说的是leader),如果你将自己的百分之九十能展示出来,相信不会有很差的结果。]]></content>
<categories>
<category>总结</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[编程中的名词]]></title>
<url>%2F2019%2F04%2F19%2Fconcept-in-fe%2F</url>
<content type="text"><![CDATA[自省这个概念是学python同学遇到的一个概念。因为在python中一切皆对象,自省表示查看对象的一些特性,推断对象的结构和能力。 js中的伪数组伪数组是一个含有length属性的json对象。像函数中的arguments对象,还有getElementsByTagName,document.childNodes之类的返回NodeLists对象都是伪数组。 浏览器事件模型这个真的好久好久没见过,导致我忘的一干二净。事件发生定义成三个阶段:捕获阶段,目标阶段,冒泡阶段。 如何在箭头函数中得到argus箭头函数是不绑定arguments对象的,那么如何在函数内部获得呢? // 1 var bar = (...arguments) => console.log(arguments); // 2 var fapply = (fun, args) => fun(...args);]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[http中的状态码和http缓存]]></title>
<url>%2F2019%2F04%2F18%2Fhttp-status-code-and-cache%2F</url>
<content type="text"><![CDATA[status code如果被问到http的状态码,我可以会说出来200,404,500,204。3开头的表示的是重定向,其他的可能就over了。思来想去,还是做一个总结,有的看。 http的状态码中,1xx表示消息,2xx表示成功,3xx表示重定向,4xx表达客户端错误,5xx表示服务器错误。选择几个常见的状态码如下: 2xx 码 含义 200 OK,成功处理,有返回内容 204 No Content,成功处理,没有返回内容 3xx 码 含义 302 Found,重定向 304 Not Modified,资源未修改,不用重新传输(和http cache有关) 4xx 码 含义 400 Bad Request,客户端中明显错误,可能的原因:语法错误、无效请求 401 Unauthorized,未认证,表示当前请求需要用户认证 403 Forbidden,服务器已经正确理解请求,但拒绝执行。返回信息中应该存在拒绝的原因。 403 Not Found,请求的资源服务器上找不到 5xx 码 含义 500 Internal Server Error,服务器错误 502 Bad Gateway,网关或者代理的服务器执行请求时,从上游的服务器收到无效请求。 参考链接 cache通常情况下请求一个资源,返回的header中可以设置“Cache-Control”来设置缓存策略,浏览器在再次请求时,会先计算当前缓存是否过期,如果过期则去重新请求,如果未过期,则使用现在的缓存。那么这样的方式会有一个问题:当遇到一个紧急修改,这个时候本地的缓存是一个废弃状态怎么办?那么,如何在客户端缓存和快速更新两者之间找一个平衡点? 现在前端使用的自动化工具时webpack,可以去设置打包后的资源名后加上一串hash。服务器可以将缓存时间可以设置成一年甚至更多,当快速更新后,资源的hash改变之后,它对于客户端来说是一个全新的资源,客户端是去请求而不是使用之前的缓存。 参考链接]]></content>
<categories>
<category>http</category>
</categories>
<tags>
<tag>http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[margin+flex=love]]></title>
<url>%2F2019%2F04%2F16%2Fmargin-flex%2F</url>
<content type="text"><![CDATA[原因在这里为什么要写这篇文章呢?之前在一个总结中说过这个问题,这次单拎出来的原因是:之前印象太浅了 margin和flex如果想要一个盒子居中显示,我可以加上margin: 0 auto;如果想要一个盒子居右显示,我可以加上margin-left: auto;那么我在flex中,对于flex-item使用margin会有什么效果呢?margin应用在flex-item上会覆盖它的主轴和纵轴的指定方式。 可以实现这样的布局]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:理解js中的event loop]]></title>
<url>%2F2019%2F04%2F11%2Ftranslate-javascript-event-loop-explained%2F</url>
<content type="text"><![CDATA[之前看的文,感觉不太完整,于是找了篇详细的文章。原文链接 Javascript是如何异步和单线程的?简短的回答是javascript语言是单线程的,异步行为不是它的一部分,相反,它是建立在浏览器(或编程环境)中的核心JavaScript语言之上,并通过浏览器API访问。 现在为了得到答案,让我写两个示例代码片段。 基本架构 堆:对象在堆中分配,表示大多数非结构化的内存区域。 栈:这表示JavaScript代码执行提供的单个线程,函数调用形成一个栈。 浏览器或Web API是内置在你的web浏览器中,能够暴露浏览器和周围的计算机环境中的数据,并使用它执行游泳的复杂操作。它们不是JavaScript语言的一部分,而是基于核心JavaScript语言构建,为你的JavaScript代码中提供额外的超能力。例如,Geolocation API提供了一些简单的JavaScript构造,用于检索数据,所以你可以说,在Google map上绘制你的位置。在后台,浏览器实际上使用一些复杂的低级代码(例如C++)与设备的GPS硬件(或者任何可用于确定位置数据的信息)进行通信,检索位置数据,并将其返回到浏览器环境用来使用在你的代码中。但同样,这种复杂性是由API抽象出来的。 代码片段1:迷惑心灵function main(){ console.log('A'); setTimeout( function display(){ console.log('B'); } ,0); console.log('C'); } main(); // Output // A // C // B 这里,我们有一个主函数,它有两个console.log:将A和C输入到控制台上。它们中间是一个在0ms后将B输出到控制台上的setTimeOut调用。 > 调用主函数,首先将它推入到栈中。然后浏览器将主函数的第一个声明console.log('A')推入到栈中,执行此语句,并在完成后弹出,字母A显示在控制台上。 第二个声明setTimeout推入栈中并开始执行。setTimeout函数使用浏览器的API来延迟回调其中的函数。一旦交接给brower完成timer,这一帧就被推出。 当计时器在浏览器中运行来回调exec函数时,console.log(‘C’)被推入栈中。在这种特殊的情况下,由于提供的延迟是0ms,一旦浏览器接收到它(理想情况下),回调将被添加到消息队列中。 执行完主函数的最后一个语句,主函数从调用堆栈中弹出,从而堆栈为空。对于浏览器将任何消息从队列推送到调用堆栈中,调用堆栈必须为空。这就是为什么即使setTimeout中提供的延迟是0ms,exec的会带哦耶必须等到调用堆栈中的所有帧的执行完成。 现在exec的回调被推入调用栈,接着执行。字母C显示在控制台上。这就是JavaScript的事件循环。 因此setTimeout(function,delayTime)中的delay参数不代表执行函数之后的精确时间延迟。它代表最小等待时间,之后在某个时间点执行该功能。 代码片段2:深入理解function main(){ console.log('A'); setTimeout( function exec(){ console.log('B'); } , 0); runWhileLoopForNSeconds(3); console.log('C'); } main(); function runWhileLoopForNSeconds(sec){ let start = Date.now(), now = start; while (now - start < (sec*1000)) { now = Date.now(); } } // Output // A // C // B 函数runWhileLoopForNSeconds完全符合其名称所代表的含义,它会不断检查从调用时间开始经过的时间是否等于函数参数提供的秒数。要记住的要点是while循环(与许多其他循环一样)是一个阻塞语句,意味着它的执行发生在调用堆栈上,并且不使用浏览器API。因此它会阻止所有后续语句,知道它执行完成。 所以在上面的代码中,即使setTimeout的延迟为0ms且while循环运行3s,exec()回调也会卡在消息队列中。while循环继续在调用堆栈(单线程)上运行,直到3s已经过去。在调用堆栈变空之后,回调exec()被移动到调用堆栈并执行。 因此setTimeout()中的delay参数不保证在计时器完成延迟后开始执行。它是延迟部分的最短时间。]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在react中使用typescript]]></title>
<url>%2F2019%2F04%2F09%2Fuse-typescript-in-react%2F</url>
<content type="text"><![CDATA[安装yarn add typescript yarn add tslint tslint-config-prettier -dev 增加配置文件增加配置文件tsconfig.json tslint.json在webpack中增加loader module.exports = { test: /\.(ts|tsx)?$/, use: 'ts-loader' }; 改造 更改prototypes // 使用interface 代替 组件的proptypes interface ItemProps { path: string, title: string, description: string, published_at: string, remote: boolean, isHasFooterLeft: boolean, } class ArticleItem extends React.Component<ItemProps> { private static defaultProps = { } } 我之前习惯于将proptypes和defaultProps写在组件的外面,更改了proptypes,那么defaultProps为了代码的清晰度,就移入了组件的内部. 更改state const initialState = { checkedTags: [] }; type State = Readonly<typeof initialState>; class FilterWrappper extends React.Component<IProps> { readonly state: State = initialState; // xxx } props中的类型使用ts定义的话,类型名需要改变,我在设置类型functuon的时候,找了好久才找到正确的名字。 组件中的函数使用private或者public需要再斟酌一下。目前生命周期函数使用public,自定义的函数使用private。 遇到的问题使用ts-loader,ts文件中不能解析webpacke中alias,需要将webpack中的alias复制到tsconcifg文件中,形如: "baseUrl": "./", "paths": { "@components/*": ["frontend/components/*"], } 是否有必要引入ts最想要的是类型检查,但是引入ts的成本很高,如果对现有项目进行全部的迁移,工作量是巨大的。ts开始支持jsdoc了,以注释的形式增加类型支持。 是否使用ts,根据实际情况进行选择。]]></content>
<categories>
<category>前端</category>
<category>react</category>
</categories>
<tags>
<tag>react</tag>
<tag>typescript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[第一次当面试官的总结]]></title>
<url>%2F2019%2F04%2F04%2Ffirst-interviewer-experience%2F</url>
<content type="text"><![CDATA[yao特别开心的给我说起她当一个面试官的经历,说如果靠谱的人听到代码会眼睛放光,她已经面了六七个了。我的其他同学也有过面试别人的经历,我的面试经历本来就很少,面试别人也是一点没经验,于是我主动请缨,来尝试当一个面试官。 准备工作去网上搜索了很多关于面试的文章,最后决定了我的提问方式:先提问一些base的问题,再去问一下项目中的问题,并顺着面试者做的去扩展提问。 下面是我准备的问题: 盒模型transition和animation的区别rem 和 em垂直居中: flex position line-height table是否了解flex布局实现一个宽高为1:1的块浏览器渲染es6介绍JS数据类型,基本数据类型和引用数据类型的区别介绍闭包以及闭包为什么没清除继承:原型继承 构造继承 组合继承 寄生组合继承js如何实现多重继承(除了原型继承)作用域和执行上下文前端事件流 事件捕获 处于目标阶段 事件冒泡阶段事件委托 我准备捡着重要的问,毕竟问题太多。 面试过程我在提问的时候,可能对方没有听明白我的意思,于是回答的内容不是我想要的。 我在提问“js如何实现继承”,对方的回答是“不知道”。搞得我也很懵,本来想顺着这个问题接着问一些作用域、执行上下文之类的问题,但是听到这样的答案,我是觉得很失望,就没有继续问作用域和执行上下文的问题。最后的结果是,我准备的问题完全不够,尴了个尬了。 在问对方做过的项目的时候,他的技术栈是angular2,这就很尴尬了,因为我对angular还停留在1的版本上面,猛然听到2,忽然有一种不知所措的感觉。 再然后就是组长问问题,但是我因为对于面试者的回答持失望态度,就不在进行提问。 面试中的一些问题为了不让我的面试生涯中断,于是我让组长寻求了建议。 最好的方式是先广度在深度。 面试官把自己的问题描述清楚,如果候选人的回答是不,就尝试换一种更加通俗的说法去提问,如果得到的答案还是不的话,ok,类似的问题到此为止,更深度的东西也没必要去问。 假如候选人有种如鲠在喉的感觉,你可以去引导他。 对于面试,面试官和候选人都是需要付出精力来共同完成这件事情,所以,希望候选人在面试完也可以收获满满。这时候在面试过程中,遇到候选人不清楚的问题,可以为对方指一下方向。虽然我在面试的时候会主动申请纸和笔,但是为了给候选人留下一些东西,我决定以后会带着纸和笔,顺便也能考察一下候选人现场写代码的能力。 另外问了产品的意见,他的思路大概是上来先问一下做过的项目,这样你会对候选人有一个大概的了解。这种方式也很好,起码的话,广度是有了,面试最重要的是深度的考察。 在提问过程中,你需要对候选人进行一个实时的刻画。问完一个问题,你就应该知道继续往哪个方向提问。 对于不善于表达的候选人,为了减轻他的压力,你可以顺着他的话去继续问。组长的原话是:说的越多,破绽就越多。 面试的题目不是为了把候选人难住,而是要测试他的度到底在哪。 面试候选人,首先要知道你需要的是什么级别的人。假如仅仅让她写页面,就是css重点考察;假如你想要的是立马就能上手的,这个时候你要的就是react技术栈的人,你应该朝着react去深挖。 假如遇到我这种技术栈不对口的时候,你可以尝试问一下技术栈共同的东西,例如你是如何优化组件的,性能方面是怎么考虑的等等。 另外的话,组长说我问的“继承-prototype”没太有意义,正确的姿势应该是es6提供了继承的语法糖,它有哪些坑,是因为什么造成的?但是,我的想法是即使es6提供了类这个东西,但我期待的keyword还是prototype,prototype是js的特点之一,对于js我还是持函数型比较多,之前在m站上看到的一篇文章“再见,面向对象编程”,觉得非常棒。but,这个知识针对我我个人的,不知道这算不算一个怪癖…. 虽然前端设计的算法层面的东西不多,但是还是需要了解一下(我的java同学说需要一个手写排序算法的步骤)。 假如你是候选人真真正正的体会了那句话:当了面试官,你才隐约的感受到对于问题,你想要的是什么。 于是当我再去接触面试题目的时候,即使面试官给我抛出的是一个我隐隐约约听过的问题,我会尝试说出一个keyword去看一下面试官的反应,或者实在看不出来就问出来,如果说对点了,就继续说下去,如果不会,这个时候你可以要求面试官去进行引导。例如对方想你提问了‘单点登录’,但是我并知道这个名词的意思,面试官解释之后你大概有一个了解,这是个什么东西,恰巧我最近看的书是这一方面的,就说了“创建一个专门的session服务器,任何其他的服务器如果需要用户状态就需要想session服务器请求”,这个时候面试官追问“session服务器挂了怎么办”,额,这个我是真的不知道了,于是就想到了“再copy一个session服务器”,这个时候,面试官心中已经有了一个深度了。 想到这,一个会引导的面试官真的太重要了,以我有限的面试经验来说。要是收获多的,我能满满的记一张纸(组长说我面试走的时候,纸上记的满满的);反之遇到一个干巴巴的面试官,就浑浑噩噩,因为错了他不会说出来,还只会提问一些干巴巴的问题(可能就是第一次面试的我)。如果遇到一个会引导的,真的是聊的很欢乐(除了菜的时候,因为一问三不知)。 终于写完了,由于没有录音,我只能尽力的回想,看来录音笔要提上日程了…]]></content>
<categories>
<category>总结</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[power(x, n)有感]]></title>
<url>%2F2019%2F03%2F31%2Fleetcode-50-problem%2F</url>
<content type="text"><![CDATA[这个题目是求x的n次方。 错误求解n的值可以是正值或者负值,第一想到的最简单粗暴的办法是for循环,but runtime error了。接着就放在那了,继续解决别的问题了。 经过指点经过老师的指点,思路变成了power(2, 8) = power(power(power(2, 2), 2), 2),这样,八次就变成了三次,runtime error的问题解决了。但是让我写的,扣了好久也没扣来😅,老师给了答案 var myPow = function(x, n) { let result = 1; let sum = x; while(n > 0) { if (n % 2 === 1) { result *= sum; } sum *= sum; n = parseInt(n / 2); } return result; }; 再接着问,加入负数的处理,扣了好久有没扣出来。 优化早上想了想如何处理负数的情况,在js语言中测试了一下-1 / 2 = -1, 这一看无论值为正值还是负值,最后靠近的结果都是0,于是就有了下面的代码: var myPow = function(x, n) { const temp = n; let result = 1; let sum = x; while(n !== 0) { if (n % 2 !== 0) { result *= sum; } sum *= sum; n = parseInt(n / 2); } if(temp < 0) return 1 / result; return result; }; 提交代码accept,一切ok。 但是shared给老师看,老师说n % 2那一块可能会出问题,于是查了一下“负数求模”,果然是有问题的。于是我在ruby中尝试-1 / 2的结果竟然等于1!!!!!!啊,真的是一个很意外的结果。 老师给了一个解答,在一段代码中加入if(n < 0) return 1 / myPow(x, -n),so cool, 但是也很sad,不知道自己当时的鬼迷心窍。哎 总结负数不要进行求模运算。 将整个问题切成几块处理,在多线程的程序中,如果有3亿个数,可以使用三个线程分别去计算一亿个数的和,最后相加。 对于能合并处理的问题一定合并处理。]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:通过理解大O来通过编程面试,并且编写更快的代码]]></title>
<url>%2F2019%2F03%2F25%2Ftranslate-understanding-big-o-notation%2F</url>
<content type="text"><![CDATA[原文链接 世界顶级科技公司测试候选人的算法知识以及算法速度l,这是大O的用武之地。这篇文章解释了大O是什么,并且提供真实的算法以及大O的实际示例。 了解算法的速度是编程的必备技能。对于较小的数据集,这可能不会引起太多麻烦,但是具有大量用户、数据等,差异可能是惊人的。 尽管大O的知识只和大型科技公司的面试有关,但它仍然是需要知道如何是你的算法尽可能的更快。了解如何编写更有效的算法以及为什么它们更有效率在每个级别都很有用! 内容1. 为什么效率很重要2. 什么是大O3. 先决条件4. O(n) - 线性时间 - 线性时间)5. O(N²) - 二次时间 - 二次时间)6. 冒泡排序7. O(N³,ON⁴)- 立方时间,四次方时间等8. 为什么我们经常忽略倍数9. 为什么我们忽略除大的次方的一切10. 对数11. O(logN)- 二分法查找12. O(NlogN)- 快速排序13. 最后一句话:时间和空间的复杂性14. 总结15. 我想了解的更多 为什么效率很重要在解释大O之前,我想用一个愚蠢的例子来解释它为什么这个重要。假设你有十封电子邮件,每一个里面有包含一个你需要的电话号码,你不能使用搜索栏,那你怎么找到那个的电话号码呢? 好吧,它只有十封邮件,因此你可能一个一个的打开浏览,直到你找到电话号码,并且你最多检查十封邮件。那没关系,它只需要一两分钟。 但是想象一下,你有18亿封邮件,你仍然只需要一个电话号码。面对这种规模,你永远不会想着打开每一个邮件。即使你每秒都可以查看一封邮件而且从不睡觉,但仍需要57年才能找到你想要的电话号码。 这个例子非常傻,如果你不幸有18亿封邮件,你你肯定会使用搜索栏。但是这个例子清楚的表明:一个小规模的工作过程很大程度上是完全不切实际的。 我选择的数字为18亿,因为在2018年1月,这是互联网上活跃网站的估计数量,因此大致相仿于google等搜索引擎需要存储数据的网站数量。因此,像google搜索这样的服务利用最优化的算法来处理所有数据至关重要。 什么是大O在基本的层面上,大O为我们提供了一种快速评估算法速度或者更恰当的性能的方法。它测量的是随着输入的增加,运行时相对输入增长的速度。 对于很多算法,可以一目了额安的确定它们的效率,并使用大O来表示它。这就是为什么许多世界顶级的变成公司在面试中测试这些知识的原因。 像二分法搜索这样高效的算法,可能具有O(logN)的性能。而像Bogosort这样极其低效的算法可能具有O(log(N!))的性能。 如果这些看起来不熟悉,不用担心,我将会逐一介绍必要的细节。本文适用于初学者,但是它比我遇到的其他任何初学者的文章更深入。这是因为,在这篇文章中,我们不仅会介绍大O的理论,还会在介绍大O的部分中看一些剧名的搜索排序算法,以及这些算法的代码。 先决条件示例代码是使用js编写的,因此如果你对js有所了解,那么你将从本文中获得最大的收益。其中还涉及到有关数组、函数和循环的基本知识。 O(1)这表示运行时固定的算法,无论你输入数据的量如何,算法只会花费固定的时间。 在下面的例子中,我们向函数传递一个数组,数组无论有一个值或者一千个值都没关系,它将得花费同样的时间,因为它只是简单的返回一个值。 我们称之为O(1),无论输入的大小如何,函数只吐出一个值。这样的函数都可以被描述为具有O(1)的性能。 function(array) { return array[0]; } O(n) - 线性时间这个相当于手动浏览所有的电子邮件。随着数组的长度增加,所需的处理量以相同的效率增加。 在这个函数中,将数组的每一项都打印到控制台上。随着数组长度增加量与执行console.log的次数增加的数量是一样的。 function logArray(array) { for (let i = 0; i < array.length; i++) { console.log(array[i]); } } 请注意,大O基于最大可能的迭代次数。因此,即使函数可以提前完成,以下函数仍具有O(n)的算法效率,这就是为什么有些人将“最坏情况”看作衡量的标准。 function findTheMeaningOfLife(array){ for (let i = 0; i < array.length; i++) { if (array[i] === 42) { return i; } } } O(N²) - 二次时间效率为O(N²)的算法是以O(N)的相对速率的两倍增加,我将使用console.log作为演示。 假设我们有含有三个数字的数组:array = [1, 2, 3]。如果我将它传递给上面的logArray函数,那么它将只打印出数字1, 2, 3。但是这个方案怎么样呢? function(array){ for (let i = 0; i < array.length; i++){ for (let j = 0; j < array.length; j++){ console.log(array[j]); } } } 它将打印出1 2 3 1 2 3 1 2 3。与上面的logArray相比,这是九个数字,因此它遵循3² = 9,并且该功能具有的O(N²)性能。 技术说明:使用大O时,我们关注函数执行的操作,而不是它返回的结果。为了帮助初学者,我选择的都是返回的值的数量等于操作的数量的函数,但在实践中这种情况很少。 这是O(N²)的另一例子,如果数组中任意两个数字的和事42,则返回true。如上所述,它可以提前返回,但是我们会假设它会花费最长的时间。 function isSum42(array){ for (let i = 0; i < array.length; i++){ for (let j = 0; j < array.length; j++){ if (array[i] + array[j] === 42) { console.log('Found the meaning of life!'); return true; } } } } 冒泡排序现在我们的第一个实际排序算法的示例是冒泡排序。这是对一组值进行排序,其性能可以描述为O(N²)(原理我不翻译了)。 为什么冒泡排序的性能是O(N²)当你看了代码时,冒泡排序的性能为O(N²)的原因变得清晰。这有一个最简单的实现,它运行可能需要最大的传递次数(一个更复杂的冒泡排序会阻止算法在没有交换的情况下的继续进行,但为了简单期间,我把它留了下来)。 function bubbleSort(array) { let length = array.length; // This loop handles the passes for (let i = 0; i < length; i++) { // This loop handles the swapping during a single pass for (let j = 0; j < (length - i - 1); j++) { if (array[j] > array[j + 1]) { let tmp = array[j]; array[j] = array[j + 1]; array[j + 1] = tmp; } } } return array; } 正如你看到的那样,其中有你两个循环,第二个嵌套在第一个里面。这是O(N²)的经典标志,这是我们对算大的第一次真实性能分析。 O(N³),O(N⁴) - 立方时间,四次方时间等如果两个嵌套循环给出了O(N²),则三个嵌套循环给出了O(N³),四个嵌套循环给出了O(N⁴),以此类推。 让我们回到简单的数组[1, 2, 3], 并将其传递给性能为O(N³)的函数: function(array){ for (let i = 0; i < array.length; i++){ for (let j = 0; j < array.length; j++){ for (let k = 0; k < array.length; k++){ console.log(array[k]); } } } 正如我们期望的那样,输出是27(=3³)个数字,因为我们的输入N的值为3。 如果我们再添加一个循环,我们将获得: function(array){ for (let i = 0; i < array.length; i++){ for (let j = 0; j < array.length; j++){ for (let k = 0; k < array.length; k++){ for (let l = 0; l< array.length; l++){ console.log(l); } } } } } 正如我们预期的那样,这个O(N⁴)函数产生81(=3⁴)个数。 为什么我们经常忽略倍数假设对于我们放入函数的每个项目,它会对数据进行两次处理。例如,我们可以有这样的函数: function countUpThenDown(array) { for (let i = 0; i < array.length; i++) { console.log(i); }; for (let i = array.length; i > 0; i--) { console.log(i); }; } 我们可以称之为O(2N)。并且,如果我们在序列中添加了另一个for循环,那么它第三次处理数据就可以称之为O(3N)。 在小规模,差异非常重要。但是大O关注算法如何以非常高的规模运行。出于这个原因,我们倾向于忽略倍数。 一般来说,我们认为线性O(N)和二次O(N²)之间的差异,比同一类型的倍数之间的差异更重要。因此,在大多数情况下,您可以安全地忽略倍数。 为什么我们忽略除大的次方的一切对数O(logN)- 二分法查找O(NlogN)- 快速排序最后一句话:时间和空间的复杂性总结我想了解的更多]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>算法</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[高性能的JavaScript笔记1]]></title>
<url>%2F2019%2F03%2F15%2Fbook-summary-1%2F</url>
<content type="text"><![CDATA[作用域链和标识符解析在函数执行中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获得或存储数据。该过程搜索执行环境的作用域链,查找同名的标识符。搜索过程从作用域链头部开始,也就是当运行函数的活动对象。如果找到,就使用这个标识符对应的变量,如果没找到,就继续搜索作用域链中的下一个对象…正是这个搜索过程影响了性能。 一个标识符所在的位置越深,它的读写速度也就越慢。所以,如果某个跨作用域的值在函数中被引用一次以上,纳闷就把它存储在局部变量里面。 DOM的访问和修改DOM和javascript是两个独立的功能,两个独立的功能只要通过接口彼此连接就会产生消耗。通常的做法是减少访问DOM的次数,把运算尽量留在js这一段处理。 重绘和重排当DOM的变化影响了元素的几何属性,浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这个过程称为重排(reflow),完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这个过程叫做重绘(repaint)。例如改变元素背景色会导致重绘,改变元素大小,会引起重排和重绘。 重排何时发生 添加或删除可见的元素 元素位置改变 元素尺寸变化 内容改变 页面渲染器初始化 浏览器窗口尺寸改变 另外js使用属性和方法,也会引起重排。 offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop, scrollLeft, scrollWidth, scrollHeight client(top, left, width, height) getComputedStyle() (currentStyle in ie) 颠倒数组的顺序来体改循环性能for(let i = 0; i < arr.length; i++) // replace for(let i = arr.length; i--) // reason: 控制条件与true相比时,任何非零的数自动转换成true,而零值等同于false. 减少迭代次数最广为人知的模式被称为“达夫设备(duff’s device)”]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一行代码引起的思考]]></title>
<url>%2F2019%2F03%2F12%2Fanswer-of-leetcode-92%2F</url>
<content type="text"><![CDATA[leetcode的92题是一道反转从m到n的链表的题目。说起链表,就想起来刚接触链表的时候,随意改变指向的兴奋感。第一门接触的语言是C++,不过想要使用C++去尝试解题的时候,发现我之前接触的已经过时了(用c写链表比js舒服呀),还是拿着js来吧。 回到题目,我在想当m等于1的时候,算不算特殊情况?为了统一来说,还是不当做特殊情况,于是我决定,只拿出m开始的链表进行操作,最后再接上。 下面是我第一次解法: /** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */ /** * @param {ListNode} head * @param {number} m * @param {number} n * @return {ListNode} */ var reverseBetween = function (head, m, n) { if(m === n) return head; let startPrevNode = null; let startNode = null; let currentNode = head; let reverseHeadNode = null; let index = 1; while(index < m) { startPrevNode = currentNode; currentNode = currentNode.next; index++; } reverseHeadNode = startNode = currentNode; while(index < n) { index++; currentNode = currentNode.next; startNode.next = currentNode.next; currentNode.next = reverseHeadNode; reverseHeadNode = currentNode; // console.log('reverseHeadNode', reverseHeadNode) currentNode = startNode; } if (startPrevNode) { startPrevNode.next = reverseHeadNode; return head; } else { return reverseHeadNode; } }; 答案通过了,但是我的速度只超过了40%的人,内存消耗只少于7%的人,这结果真是沮丧。于是,我开始缕缕算法,发现每次移动的元素都是startNode后面的元素,这个时候,我只需要知道startNode,而不需要每次保存currentNode的值。所以我尝试改了第二个while中的操作。 while(index < n) { index++; currentNode = startNode.next; startNode.next = currentNode.next; currentNode.next = reverseHeadNode; reverseHeadNode = currentNode; } 更改之后,少了一次赋值操作,但是有了很大的提升,速度超了86%, 内存消耗少了15%,算起来效率提升了两倍,算法给人的想象真的是无穷的。我只是删除了一行代码,没想到会有这样的结果。同时最近看的“高性能的javascript”,尽管书出来很多年了,但是其中还是有很多的精髓的地方。 算法使我的code更美,更快,更小!]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[刷leetcode带来了什么]]></title>
<url>%2F2019%2F03%2F04%2Fleetcode-and-algorithm%2F</url>
<content type="text"><![CDATA[序言最近开始尝试刷leetcode,最直观的感受是:你可以知道自己最擅长和最不擅长解决什么问题。我是擅长解决关于字符串的问题,最不擅长解决排列,回溯,搜索问题。 解决排列问题一个很常见的面试题:从一个数组中找出和为sum的全部排列。现在还能记得那时候的想法:加加加如果大于和了怎么办?要退回去呀…这个“退”让我想起了迷宫问题,于是就朝这方面想办法,but结果可以预想。一开始的错误会让你永远都找不到答案。 再次思考这个问题,我想:一个一个循环找答案吧。抱着这个想法去找还有没有更好的方法。得到的答案也是这样的,不过里面得到的一个很重要的point:“剪枝”。就像之前看到的找八皇后答案的过程一样:如果过程中遇到不满足结果的答案,及时剔除。 同时,提交的答案,也会把程序的速度和空间复杂度展示出来。印象很深刻的一个题目是排序列表,题目中给的tip是不要尝试交换值。不过只是我第一想法,于是我就这么做了,但是达到的复杂度真的是差强人意。于是我开始尝试改变next的指向来进行排序,效率真的大大提升。 测试驱动开发很喜欢“测试驱动开发”这个观点,我看到题目会先将测试用例加上,在尝试写code。但是通不过测试,到底是为什么?这个时候,开始打日志,看问题到底是出在哪?还记得之前看过的:如何调试自己的代码?首先是看。看到底是哪里出错了。打日志得到的错误区域可能会帮你定位bug。但是给我的感觉就是很乱,我只是着眼于这一处,改的时候也是只关注这一点,可能的后果是其他地方除了问题。看是第一选择,实在是看不出来,再尝试debug。 边界处理里面还有一个是边界值处理。跑过了测试用例,但是得到一个“wrong answer”的结果,定眼一看,原来是边界值的问题。等再次code,会有一个潜移默化的想法,想的全面。 待解决到现在,对于排列的问题,我可以尝试去下手写,去寻找解决办法。开动脑筋,去找answer。最近没有解决的问题是大数处理和位运算。]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[我的对scss的错误认知]]></title>
<url>%2F2019%2F02%2F23%2Fscss%2F</url>
<content type="text"><![CDATA[之前的post介绍的重构的评论组件,我的重构工作仅仅从coding的复杂度去考虑,却没有进行深入的研究,例如渲染原理等等。用react实现实际上是不是比重构后更快,毕竟算法的复杂度已经降到O(n)了。这是一个很大的失误。 继上面的错误之后,有犯了一个很粗鄙的错误。没有正确的认识到scss作为css的预处理的作用。目前项目中scss的作用只用到变量定义和嵌套的。在我查看bootstrap的源代码的时候,粗略的看了一下,嗯,感受到的是一种形散神不散的感觉,就像它是一张网,但是你只是将它们看成一条一条的线。这个时候赶忙去看scss的doc,才发现它既然是一种预处理的语言,相对于css,它必然包含更多更好的功能。 变量变量也可以进行计算哦。 $red: red; body { color: $color; } ` 嵌套.btn { color: #fff; &.t-red { background: $red } } namespace.btn { font { size: 1.6rem; weight: normal; } } ` each and map这个功能真的很棒很棒,超级棒!棒到我都不想写之前的code。 $theme-colors: ( 'blue': $blue, 'pink': $pink, 'green': $green, 'yellow': $yellow, 'red': $red ); .badge { color: #fff; @each $color, $value in $theme-colors { &.t-#{$color} { background-color: $value; } } } ` function看到设计图里面很多颜色都是带透明度的,但是场景是不需要用到rgba,于是写了一个将带有透明度的颜色转化成不带透明度的颜色,带透明的颜色实际上是和白色以透明度去混合得到。 @function rgba-to-rgb($rgba, $background: #fff) { @return mix(rgb(red($rgba), green($rgba), blue($rgba)), $background, alpha($rgba) * 100%); } ` 避免使用@extend之前看到的文章里面专门来讲这个事情,不要使用@extend而是使用@inlcude,举个栗子: h3 { font-size: 1.8rem; } .home h3 { font-size: 2rem; } .h3 { @extend h3; } ` 这时候得到的css是: h3, .h3 { font-size: 1.8rem; } .home h3, .home .h3 { font-size: 2rem; } ` 你仅仅想要继承的是1.8的这个样式,但同时也把2的样式给继承了。 还有更多更好的功能去探索。]]></content>
<categories>
<category>前端</category>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用turbolinks和rails-ujs搭建website]]></title>
<url>%2F2019%2F02%2F21%2Fror-website%2F</url>
<content type="text"><![CDATA[这次开发的管理项目,比较简单,想选用一个比较快速而且简单的方式。于是我选择了turbolinks+ujs套装。 turbolinksturbolinks的主要作用是在页面跳转时,不会存在白屏,看起来是一个spa,其原理是拦截所有的链接跳转,使用ajax得到页面的html,替换body,合并头部的js和css文件。其中你的js文件一定要放在head中,而不是body底部。如果你担心第一次加载页面,空白时间太长,可以给script加上defer属性。 在项目中,使用webpack去打包所有的js文件(没有分包啊),所以在每次“跳转”页面的时候,需要重新执行一遍js,这个时候需要监听turbolinks的事件: document.addEventListener('turbolinks:load', () => { // ..... }); ujs对于rails的ujs,不得不称赞,真的很优秀。在页面中,你可以通过给元素增加data-remote=”true”来指示这是需要ujs来处理的。那么如何处理ujs返回的错误呢? $(document).on('ajax:error', (event) => { // ... }); 最后的代码如下: export default () => { RailsUJS.start(); turbolinks.start(); ajaxFeedback(); // handle ajax document.addEventListener('turbolinks:load', () => { // xxxx });]]></content>
<categories>
<category>rails</category>
</categories>
<tags>
<tag>rails</tag>
</tags>
</entry>
<entry>
<title><![CDATA[新的一年的第一周get了]]></title>
<url>%2F2019%2F02%2F17%2Fsummary-work-3%2F</url>
<content type="text"><![CDATA[1. 如何创建xhr拦截器override xhr的open方法参考链接 let oldXHROpen = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function(method, url, async, user, password) { // do something with the method, url and etc. this.addEventListener('load', function() { // do something with the response text console.log('load: ' + this.responseText); }); return oldXHROpen.apply(this, arguments); } 2. 当参数为object对象时,如果使用默认参数,怎么做?const spin = ({color: 'red'}) => {} // use default params spin(); // error span({}); // use dedault params const spin1 = ({color: 'red'} = {}) => {} spin1(); // use dedault params span1({}); // use dedault params 3. 布局是[x-y]——[z]时参考ruby-china中header的样式z通常使用flex布局,将x和y作为一部分但可以看成类似这样的布局[x]-[y]——[z],y的style为margin-right: auto; 4. 如何提交react的性能参考链接 减少不必要的渲染why-did-you-update 切割组件 Recompose Reselect Beware of Object Literals in JSX<Cropper style={{ maxHeight: 300 }} /> 上面的code中的{ maxHeight: 300 }在每一次render过程中都是一个新的对象,所以Cropper的render每次都会被执行,可以改成const style = { maxHeight: 300 }; <Cropper style={style} />]]></content>
<categories>
<category>总结</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[来写一个编辑器的plugin]]></title>
<url>%2F2019%2F01%2F25%2Fadd-editor-plugin%2F</url>
<content type="text"><![CDATA[今天在纠结是否要更换编辑器,调研了一个新的编辑器,发现功能不能满足需求,也不能增加插件。回过来头,还是在现在的编辑中增加功能吧–写一个plugin。 去看了document和api发现上面的方法不能实现我想要的结果(弹出来的modal)。接着我去看demo,demo中是可以弹出来的modal,我的第一个想法是将代码copy一遍,然后手动的去插入。于是只能尝试将插件【this】console出来,看一下里面有什么方法。这里还有一个问题,代码是被压缩过的,即使有你想要的方法,参数到底是什么还是需要去猜。技巧是有的,多试试就能知道了。 对于这个plugin,我几个月前是尝试写过的,但是那个时候,并没有写出来,现在看来是方向错了。时间会给人收获和更多的思考,fighting 推荐阅读:(链接)[https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536],这个作者总共写了6个part,读下来很通顺,读起来没有任何难度(语法很简单的)]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[重构评论组件]]></title>
<url>%2F2019%2F01%2F22%2Frefactor-comment%2F</url>
<content type="text"><![CDATA[初衷在年末我终于做了一个决定,重构评论组件。尽管之前也对它做过各种重构,重构过的代码依然看着很菜,这个组件是使用react去实现的,现在仔细分析一下,抛开react使用js实现更简单一点,所以我决定在一年的尾巴中开始清除这个“毒瘤”。 手机端和pc的评论样式是不一样的,但是实现的功能完全是一样的。之前是一个组件,但是样式的差别实在太大了,我改了组件的一部分,我就需要更改手机和pc端的样式,于是我就拆成两个组件,但是功能的实现差不多是一样的,存在很多重复代码,真的是炒鸡别扭,今天仔细的分析一下,觉得不使用react,实现起来也不是很复杂,于是我决定推倒重来。 步骤step 1:多少页面在使用这个组件我查看了有多少页面用到这个组件,绝大部分是主体的详情页。既然是组件,我希望它以后也是一个组件,现在要做的仅仅是更改使用组件的code。 step 2:组件中涉及的功能 增加评论 回复评论 删除评论 查看更多 增加评论这其中涉及的事件有:提交表单(ajax + 新增dom节点)、打开评论框(addClass)、输入框失焦(是否removeclass)。 回复评论涉及的事件:增加回复框(移除已经存在的回复框,在target后新增回复框)、提交表单(功能与增加评论类似) 删除评论send xhr => 移除target 查看更多send xhr => 新增dom节点 step 3:前后端交互xhr是给我返回json数据还是一段html,这个很纠结,如果是json数据,这就意味着相同的html片段我需要在view层写一遍,在js中又来了一遍。以可扩展性来说,用json是最好的选择。这里很值得思考🤔 重构后的预想结果功能实现的js部分只需要写一份,不同的就是view的呈现。 ##总结总的来说,这次的预想重构过程,我很大部分还是先看了我的心理结构(重构真的难上加难),如果这个重构需要一个星期甚至更久的时候,我会第一时间放弃。组件化真的是非常好的实践,我只用知道duck会叫,至于它是什么样的,我是不需要关心的。 这是我目前能预见重构过程涉及到的所有功能。尽量在这个星期refactor,看一下还需要这个过程还缺少哪些内容。 后续两天的时间,终于完成了。先是修改dom结构,手机端和移动端就不用写两套了,顺便把css也优化了一遍。至于返回数据的格式,我选择返回html-partial而不是json,至于以后数据多了,再去调整吧,这里的重构就到此为止吧。 这个真真的是react的国度使用,使用react时,插入回复框的时候,我需要把所有的list全部循环一遍看应该插入到哪条数据的后面。现在也回想不起来使用react的初衷了,可能是用着用着失去初心了…]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
<category>react</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>react</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:保持你的代码洁净]]></title>
<url>%2F2019%2F01%2F18%2Ftranslate-keeping-your-code-clean%2F</url>
<content type="text"><![CDATA[原文链接 我在座位上安顿下来,与我的团队成员一起解决问题。我说”我们必须赢的这场比赛”。在两天内埋头开发一个工作原型,大家的好胜心都被激发,都在争夺前三名。 几分钟后,其中的一个高级工程师走到我的办公桌前,脸上露出一丝不满,喃喃自语:你的代码不清晰,很乱!这是我迈向clean code旅程的开始。 clean code?嗯,这对于我来说并不奇怪,但是如果代码正常工作,这真的很重要吗。是的,它确实很重要,一千次。 在这次活动之前,我曾担任几年的软件工程师。我已经构建了应用,但是我刚刚被告知了让我代码不一样的东西。 我的问题很简单:我专注于完成工作,目的是编写有效的代码,反过来又招致了技术债务。 clean code 的方式当你读完clean code的所有章节时,就不会发生这种情况。它需要知识和不断的实践,你必须学习原理,模式和实践。这是艰苦的工作需要数年,但你可以从今天开始。 无论你怎么去clean你的code,总有一件或两件你能学到的事情使code变得更干净。 学习的最佳方式之一是阅读专家的书籍或者帖子,你应该在你的twitter中有他们的推特流,听取他们的交谈,在github上follow他们,学习他们的代码是如何写和组织的。 Your growth is limited as an engineer if you do not constantly learn from experts in your field. 保持你的函数短小精悍这可能是1337篇文章中的一篇,来强调保持函数尽可能短,人们很容易在这里弄错。 clean code不仅仅是写短的方法,而是编写清晰表达意图的代码。 当一个函数太长了,这说明它做的太多了,阅读者可能会无法完全解读它的功能,一个函数应该做一件事。 if($order->contains($status){ //do something with order } function contains($status){ $order_statuses=['accepted','delivered','rejected','processed']; if (in_array($status, $order_statuses)) { return true; } return false; } 我们可以重写它来使contains更清楚: function contains($status){ return in_array($status, $this->config->statuses); } 现在,contains不仅仅更简短,而且还能解耦。 变量和函数名字肯以体现其功能为函数命名是乏味的,但是这绝对物有所值。当代码改变时,你可以不用更新注释。 $date =date('Y-m-d'); //Ofcourse, it's a date but too generic! $orderCreationDate =date('Y-m-d'); //cleaner code 避免使用if和switch就个人而言,我花了一段时间来掌握这一点。你怎么能告诉我避免我的最爱呢?事实上,大多数的条件语句可以很容易的提取到单独的函数和类中。这并不是说你永远不应该使用if和switch语句,但在某些情况下可以避免。 这有一个很好的例子: class BookOrder { public function process() { switch ($this->book->type) { case 'audiocopy': return $this->processAudioBook(); break; case 'downloadablecopy': return $this->processDownloadableCopy(); break; case 'paperbookcopy': return $this->processPaperBookCopy(); break; default: } } } 更整洁、更易于维护的方法是: interface IBookOrder { public function process(); } class AudioBookOrder implements IBookOrder :void { public function process() { // TODO: Implement process() method. } } class PaperBookOrder implements IBookOrder: void { public function process() { // TODO: Implement process() method. } } class DownloadableBookOrder implements IBookOrder: void { public function process() { // TODO: Implement process() method. } } 避免心理映射clean code应该是更易于阅读,理解并且不应该留下任何猜测空间。 It is not the language that makes a program look simple, but the programmer who makes the language appear simple. Robert C. Martin 以下代码检查客户能否可以提取一定数额的资金,它有效但是很乱。 if($this->user->balance > $amount && $this->user->activeLoan===0){ $this->user->balance -=$amount; // withdraw amount; } 让我们把它变得更清晰: if($this->hasNoActiveLoan() && $this->canWithdrawAmount($amount)){ $this->withdraw($amount); } public function hasNoActiveLoan(){ return $this->user->activeLoan===0; } public function canWithdrawAmount(float $amount){ return $this->user->balance > $amount; } public function withdraw(float $amount){ $this->user->balance -=$amount; } 这不仅仅更容易理解而且更容易测试了。 理解和应用S.O.L.I.D原则S.O.L.I.D是由Robert C Martin定义的面向对象编程的前五个原则的首字母缩写。使用这些原则,你可以编写低耦合、高内聚和封装很好的代码。这些原则密切相关。 不要太为难自己想知道这一点为什么这个在名单上?陷入清洁代码的世界是很容易,想要一天吸收一切。悲伤的是:需要时间,数月,数年和奉献精神。必须学习和实践这些原则,但这一切都始于决定让事情变得更加清洁。]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:JavaScript中的组合函数]]></title>
<url>%2F2019%2F01%2F17%2Ftranslate-functional-composing-javascript%2F</url>
<content type="text"><![CDATA[原文链接这篇文章很有趣。 简介lodash和underscore无处不在,但仍然有一种超级高效的方法,实际上只有那些赶时髦的人使用:组合。 我们将研究组合,并且深入了解为什么这种方法会是你的代码更加具有可读性,更易于维护和更加优雅。 基础我们将会使用lodash的一些函数,只是因为: 我们不想编写自己的简单的实现,因为它们会分散我们关注的内容 lodash被广泛使用,可以很容易的被underscore或其他的库或者自己的实现替换 在我们深入研究一些基本的例子之前,让我们回顾一下“组合”实际做了什么,以及如果需要,我们如何实现我们自己的组合函数。 var compose = function(f, g) { return function(x) { return f(g(x)); }; }; 这是最基本的实现。仔细看看上面的函数,你将会注意到传入的函数实际上是从右向左调用的,意思是将右侧函数的结果传递给它左侧的函数。 现在仔细看看这段代码: function reverseAndUpper(str) { var reversed = reverse(str); return upperCase(reversed); } reverseAndUpper函数首先反转给定的字符串,然后变大写。我们可以借助基本的组合函数重写以下代码: var reverseAndUpper = compose(upperCase, reverse); 现在我们可以使用reverseAndUpper: reverseAndUpper('test'); // TSET 这相当于写成: function reverseAndUpper(str) { return upperCase(reverse(str)); } 仅仅更加优雅、可维护和可重复使用。 快速组合函数的能力和创建数据管道的能力可以通过多种方式加以利用,从而可以在很长的管道中进行数据转换。想象一下传递一个集合,映射集合,然后在管道的末尾但会一个最大值或者将给定字符串转换成布尔值。组合是我们能够轻松的连接多个函数用来构建更复杂的功能。 让我们实现一个可以处理任意数量的函数以及任意数量的参数的非常灵活的组合函数,之前的组合函数只适用于两个函数或者只接受第一个参数传入,我们可以重写组合函数如下: var compose = function() { var funcs = Array.protoype.slice.call(arguments); return funcs.reduce(function(f,g) { return function() { return f(g.apply(this, arguments)); }; }); }; 这个组合函数能够写出像这样的代码: var doSometing = compose(upperCase, reverse, doSomethingInitial); doSomething('foo', 'bar'); 存在大量的可以微我们实现组合的库。我们的组合函数应该只有助于理解underscores或scoreunders组成函数中真正发生的事情,显然具体实现在库之间有所不同。它们大部分仍然在做同样的事情:使它们各自的组成函数更加通用。现在我们已经知道什么叫组合了,让我们使用lodash中的 _.compose函数继续下面的例子。 栗子让我们从一个很基础的例子开始: function notEmpty(str) { return ! _.isEmpty(str); } 函数notEmpty简单的否定了_.isEmpty的结果。我们可以使用lodash中的_.compose写一个not函数来做同样的事情。 function not(x) { return !x; } var notEmpty = _.compose(not, _.isEmpty); 现在我们可以在给定的任意参数调用notEmpty notEmpty('foo'); // true notEmpty(''); // false notEmpty(); // false notEmpty(null); // false 第一个例子非常简单,下一个会更高级: findMaxForCollection会返回由id和value属性组成的给定对象集合的最大的id。 function findMaxForCollection(data) { var items = _.pluck(data, 'val'); return Math.max.apply(null, items); } var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data); 上面的例子可以使用组合函数重写。 var findMaxForCollection = _.compose(function(xs) { return Math.max.apply(null, xs); }, _.pluck); var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data, 'val'); // 6 我们可以在这重构很多。 _.pluck期待集合作为第一个参数,回调函数作为第二个参数。如果我们想要部分应用_.pluck怎么办?这种情况下,我们可以使用柯里化来反转参数。 function pluck(key) { return function(collection) { return _.pluck(collection, key); } } 我们的findMaxForCollection仍然需要更加精致,我们可以创建我们的max函数。 function max(xs) { return Math.max.apply(null, xs); } 它可以使我们重写组合函数来变得更加优雅: var findMaxForCollection = _.compose(max, pluck('val')); findMaxForCollection(data); 通过编写我们自己的pluck函数,我们可以用val部分地应用pluck。现在你可以明显的争辩,当lodash已经拥有更方便的_.pluck函数,为什么要编写自己的pluck方法?原因是_.pluck期望集合作为第一个参数,这不是我们想要的。通过反转参数,我们可以将部分应用key,只需要在返回的函数上调用数据。 我们还可以进一步重写pluck函数。lodash带来了一个更简单的方法:_.curry,这使我们能够编写如下的pluck函数: function plucked(key, collection) { return _.pluck(collection, key); } var pluck = _.curry(plucked); 我们只是想包裹原始的pluck函数,这样我们就可以翻转参数了。现在pluck仍简单的返回一个函数,这么长,直到所有的参数都被提供。让我们看一下最终的代码: function max(xs) { return Math.max.apply(null, xs); } function plucked(key, collection) { return _.pluck(collection, key); } var pluck = _.curry(plucked); var findMaxForCollection = _.compose(max, pluck('val')); var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data); // 6 findMaxForCollection可以很容易的从右向左阅读,这意味着集合先返回val的属性,然后获的所有给定值的最大值。 var findMaxForCollection = _.compose(max, pluck('val')); 这使代码更具有可维护、可重用,并且更加优雅。我们将看一下最后的例子,以突出组合函数的优雅。 让我们扩展前一个示例的数据,然后添加名为active的属性,现在的数据如下: var data = [{id: 1, val: 5, active: true}, {id: 2, val: 6, active: false }, {id: 3, val: 2, active: true }]; 我们有一个名为的getMaxIdForActiveItems(data)函数,它接收一组对象,过滤所有的active项,并从返回的过滤项中返回最大的id。 function getMaxIdForActiveItems(data) { var filtered = _.filter(data, function(item) { return item.active === true; }); var items = _.pluck(filtered, 'val'); return Math.max.apply(null, items); } 如果我们可以将上面的代码转换成更优雅的内容怎么办?我们已经有了max和pluck函数,所以我们现在要做的是添加过滤: var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter); getMaxIdForActiveItems(data, function(item) {return item.active === true; }); // 5 _.filter有和_.pluck一样的问题,这意味着我们不能将集合作为第一个参数来部分应用。我们可以通过包裹原生的filter实现,在filter上翻转参数。 function filter(fn) { return function(arr) { return arr.filter(fn); }; } 另一个改进是添加一个isActive函数,它只需要一个项目并检查active标志是否设置为true。 function isActive(item) { return item.active === true; } 我们可以在filter上部分应用isActive,使我们只能使用集合调用getMaxIdForActiveItems。 var getMaxIdForActiveItems = _.compose(max, pluck('val'), filter(isActive)); 现在我们需要传递的是数据: getMaxIdForActiveItems(data); // 5 这也使我们能够轻松编写一个函数,返回任何非active的最大id: var isNotActive = _.compose(not, isActive); var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive)); 总结组合函数可以很有趣,如前面的例子中所示,如果正确应用,可以产生更优雅和可重复使用的代码。 PS: 赞赞赞👍,一个简单的实现组合函数如下: const doubleValue = (x) => x * 2; const multiplyByFour = x => x + 4 const compose = (...funcs) => (value) => { return funcs.reduceRight((acc, func) => func(acc), value) } // (((f, g) => x) === f(g(x)) console.log(compose(doubleValue, multiplyByFour)(2)) // 12]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[忽然想写一些总结]]></title>
<url>%2F2019%2F01%2F13%2Fsummary-work-2%2F</url>
<content type="text"><![CDATA[最近情绪不是太好,想了也很多,忽然发现,工作这么久,我还没正经的写过总结,现在和之前刚毕业的自己有什么不一样…. 墙外的世界很美好之前自学阶段,似乎什么问题都很难解决,虽然有梯子,但是没有利用好,找答案在bd上根本找不到啊,第一步就是好好使用梯子,起码自己的问题通过g,百分之九十都能解决,另外的百分之十可能是方向错了,这个时候,需要从头捋一下。 拒绝熟练工刚开始的自己,两天能写一个页面吧,现在半天能写一个页面,写的多了,熟练了。这个时候,我是一个熟练工还是其他的一个角色?boss不缺熟练工。 代码规范性刚到的时候,对于em的使用产生了讨论,是使用rem还是em?两种方案都各有好处。项目积累到一定程度,在不进行重构的时候,还是先以rem为主。代码是写给人看的,大部分的情况是,你不可能去维护你的代码,这个时候代码的可读性非常重要。你可以写一个a的函数,也可以写一个getName的函数….eslint的出现使规范代码更便利。 及时重构(如果条件允许)当你写的不舒服的时候,这就是重构的信号。一定要及时重构啊,否则越往后写的越累。例如:提取公共动作,如果新加入一个功能或者修改一些参数,这个时候,你只需要在公共处进行修改;否则,你改的就不止一个地方了。 拒绝硬编码你对于下面放入示例代码如何看呢 // methods 1 const xxx => (id) { if (id === 1) { return 'a'; } else if (id === 2) { return 'b'; } // ..... else { return 'default'; } } // methods 2 const translate = { 1: 'a', 2: 'b', // ... } const xxx = (id) => { const res = translate[id]; if (res) return res; return 'default'; } 拒绝过度使用好吧,项目中允许使用react,react使用起来也很方便,我全部的功能使用react去实现,而不去想是否应该去使用,或许使用项目中的template更好呢? 优化or过度优化借用一句话:过度优化相当于现在想死后如果分财产。适度优化吧,度的话,颇有些只能意会,不能言传的感觉😂。 不要仅仅停留在读文档阶段新加的需求,通过找到一个插件,读完文档,实现了需求,内心很是开心啊,但不要仅仅停留在这个阶段,你可以尝试着去看一下插件内部是如何实现的…. 日常 尝试回顾之前的代码 你需要有plan b 解决不了的问题,放下,第二天再去解决 PS:2019也要red。]]></content>
<categories>
<category>总结</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[threejs初试]]></title>
<url>%2F2019%2F01%2F11%2Fuse-threejs%2F</url>
<content type="text"><![CDATA[最近需要一个效果,去看了demo,发现是使用three.js做的。虽然是第二次看threejs的demo,但是被闪瞎了眼,真的是太酷了(我需要去补点图形学的知识)。 api太长了,我准备先去找点demo的代码试一下,额,这代码看不懂啊,这是些什么?虽然不太清楚,先写出来能看的demo试试。找了好几圈,我不知道我想要的效果是使用什么api来解决。还是都看一下吧。。。 第二天,我带着满肚子的疑问先看了文档,终于对前一天看到的api有了大致的认识。具体细节的参数,还是需要斟酌一下。 我想要的是漫天繁星的效果,仔细想想的话,首先有两个重要的点:画星星、怎么让页面动起来(这个效果应该是像拿着望远镜一样看天空的感觉) 画星星 for (let i = 0; i < 300; i++) { let geometry = new THREE.SphereBufferGeometry(.6, 2, 2);; let material = new THREE.MeshBasicMaterial({ color: 0x4F4F4F }); let sphere = new THREE.Mesh(geometry, material); sphere.position.set( Math.random() - 0.5, Math.random() - 0.5, -Math.random() * 0.5 ).normalize().multiplyScalar(getRandomFloat(100, 300)); scene.add(sphere); stars.push(sphere); } 如何动起来为页面增加鼠标移动监听事件,在事件函数中改变全局变量 document.addEventListener('mousemove', onDocumentMouseMove, false); function onDocumentMouseMove(event) { mousePosition.x = event.clientX; mousePosition.y = event.clientY; normalizedOrientation.set( -((mousePosition.x / screenWidth) - 0.5) * cameraAmpl.x, ((mousePosition.y / screenHeight) - 0.5) * cameraAmpl.y, 0.5, ); } 代码戳这里]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:javascript中的对象,方括号和算法]]></title>
<url>%2F2019%2F01%2F09%2Ftranslate-javascript-objects-square-brackets-and-algorithms%2F</url>
<content type="text"><![CDATA[原文链接 JavaScript最强大的一个方面是能够动态引用对象的属性,在本文中,我们将了解他的工作原理以及它带给我们的好处。我们将快速浏览一下计算机科学中使用的一些数据结构。此外,我们将研究一种名为大O表示法的东西,用于描述算法的性能。 对象简介让我们从创建一个名为car的对象开始,每一个对象都有一个叫属性的东西。属性是属于一个对象的变量。car对象将有三个属性:make,model和color。 让我们看看它的样子: const car = { make: 'Ford', model: 'Fiesta', color: 'Red' }; 我们可以使用点表示法来引用对象的属性。例如,如果我们想要找出car的颜色,我们可以使用点表示法,就像这样:car.color 我们甚至可以使用console.log输出: console.log(car.color); //outputs: Red 引用属性的另一种方法是使用方括号表示法: console.log(car['color']); //outputs: Red 在上面的例子中,我们使用属性名称作为方括号内的字符串来获取与该属性名称对应的值。关于方括号表示法的优点在于我们还可以使用变量来动态获取属性。 也就是说,我们可以将其指定为变量中的字符串,而不是对特定属性名称进行硬编码: const propertyName = 'color'; const console.log(car[propertyName]); //outputs: Red 使用带方括号表示法的动态查找让我们看一个我们可以使用它的例子。假设我们经营一家餐馆,我们希望能够在菜单上获得商品的价格。这样做的一种方法是使用if / else语句。 让我们写一个接受名称并返回价格的函数: function getPrice(itemName){ if(itemName === 'burger') { return 10; } else if(itemName === 'fries') { return 3; } else if(itemName === 'coleslaw') { return 4; } else if(itemName === 'coke') { return 2; } else if(itemName === 'beer') { return 5; } } 虽然上面的方法有效,但是他不是理想的。在我们的code中有菜单的硬编码。现在如果我们的菜单改变了,我们不得不重写代码并且重新部署。此外,我们可以有一个很长的菜单,不得不写所有这些代码将是繁琐的。 更好的方法是分离我们的数据和逻辑。数据将包含我们的菜单,逻辑将从该菜单中查找价格。 我们可以将菜单表示为对象,其中属性名称(也称为键)对应于值。 在这种情况下,键将是项目名称,值将是项目价格: const menu = { burger: 10, fries: 3, coleslaw: 4, coke: 2, beer: 5 }; 使用方括号表示法,我们可以创建一个接受两个参数的函数:菜单对象和一个菜名并且返回菜品的价格: const menu = { burger: 10, fries: 3, coleslaw: 4, coke: 2, beer: 5 }; function getPrice(itemName, menu){ const itemPrice = menu[itemName]; return itemPrice; } const priceOfBurger = getPrice('burger', menu); console.log(priceOfBurger); // outputs: 10 这种方法的巧妙之处在于我们将数据与逻辑分开。在这个例子中,数据存在于我们的代码中,但它可以很容易地来自数据库或API。它不再与我们的查找逻辑紧密耦合,后者将菜品名称转换为菜品价格。 数据结构和算法计算机科学术语中的map是一种数据结构,它是键/值对的集合,其中每个键映射到相应的值。我们可以使用它来查找与特定键对应的值。 这就是我们在前面的例子中所做的。我们有一个菜品的名称,我们可以使用菜单对象查找该项目的相应价格。我们正在使用一个对象来实现一个map数据结构。 让我们看看为什么我们可能想要使用Map。 假设我们经营一家书店,并有一份书籍清单。 每本书都有一个名为国际标准书号(ISBN)的唯一标识符,这是一个13位数字。 我们将图书存储在一个数组中,希望能够使用ISBN查找它们。 一种方法是循环遍历数组,检查每本书的ISBN值,如果匹配则返回它: const books = [{ isbn: '978-0099540946', author: 'Mikhail Bulgakov', title: 'Master and Margarita' }, { isbn: '978-0596517748', author: 'Douglas Crockford', title: 'JavaScript: The Good Parts' }, { isbn: '978-1593275846', author: 'Marijn Haverbeke', title: 'Eloquent JavaScript' }]; function getBookByIsbn(isbn, books){ for(let i = 0; i < books.length; i++){ if(books[i].isbn === isbn) { return books[i]; } } } const myBook = getBookByIsbn('978-1593275846', books); 这个例子很好用,因为我们只有三本书(这是一个小书店)。但是,如果我们是亚马逊,那么迭代数以百万计的书籍可能会非常慢并且计算成本也很高。 计算机科学中使用大O表示法来描述算法的性能。例如,如果n是我们集合中的书籍数量,那么在最坏的情况下使用迭代来查找书籍的成本(我们要查找的书是列表中的最后一本)将是O(n)。这意味着如果我们集合中的书籍数量增加一倍,使用迭代查找书籍的成本也会翻倍。 让我们看看如何通过使用不同的数据结构使我们的算法更有效。 如上所述,可以使用映射来查找与键对应的值。 我们可以使用对象将数据结构化为map(这里指对象)而不是数组。 键将是ISBN,值将是相应的书对象: const books = { '978-0099540946':{ isbn: '978-0099540946', author: 'Mikhail Bulgakov', title: 'Master and Margarita' }, '978-0596517748': { isbn: '978-0596517748', author: 'Douglas Crockford', title: 'JavaScript: The Good Parts' }, '978-1593275846': { isbn: '978-1593275846', author: 'Marijn Haverbeke', title: 'Eloquent JavaScript' } }; function getBookByIsbn(isbn, books){ return books[isbn]; } const myBook = getBookByIsbn('978-1593275846', books); 我们现在可以使用ISBN的简单地图查找来获取我们的价值,而不是使用迭代。我们不再需要检查每个对象的ISBN值。 我们使用key直接从地图获取值。 在性能方面,地图查找将提供相对于迭代的巨大改进。 这是因为地图查找在计算方面具有不变的成本。 这可以使用大O表示法写为O(1)。如果我们有三三百万册书籍没关系,我们可以通过使用ISBN键进行地图查找来快速获得我们想要的书。 总结 我们可以使用点表示法和方括号表示法访问对象属性的值 我们学习了如何通过使用带方括号表示法的变量来动态查找属性值 我们还了解到,map数据结构将键映射到值。 我们可以使用键直接在我们使用对象实现的map中查找值。 我们初看了如何使用大O表示法描述算法性能。此外,我们还看到了如何通过将对象数组转换为map并使用直接查找而不是迭代来提高搜索性能。 ps: hardcode真的是很硬…]]></content>
<categories>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[重构那些事]]></title>
<url>%2F2019%2F01%2F02%2Fmixed-talk-3%2F</url>
<content type="text"><![CDATA[#1今天查代码,发现又有一些可以提取公共函数的代码…真的是,对于重构这个事,真的是“野火烧不尽,春风吹又生”。 #2我的习惯是,功能性重构,当我看到一个新的需求出来的时候,发现有一些行为是和已经存在的行为是类似的,这个时候我开始去着手重构,将相同的行为提取出来。但是这个过程会使我忽略另一部分重构工作…不大但是非常值得提取的行为,因为这样的过程太多了,写着写着就习惯了😂啊,这个不复杂,我可以直接写出来,时间就是金钱,争分夺秒写,然而这个过程就666了…. 今天查看代码的时候,一搜关键字好多重复的代码,第一反应是我尽然写了这个多遍,自己都佩服自己,接着开始自我怀疑,写的时候是不是处于不清醒的状态😂。恰恰是这些,最应该做成辅助函数的代码却零星的散落在四处…忍不了啊 #3反思一下,是自己对于重构的粒度是怎么定义,换一句话讲是对重构怎么定义的。之前也享受过重构的快乐。本来预计两天的工作,真的一下手写,发现只用更改一点点,就能满足新的需求,当时的那个喜悦真的是冲破天际。现在的自我的怀疑也是突破天际,真的是很基本的基础函数,写了这么多遍怎么久没感觉呢? “写代码的时候,如果觉得不舒服,不是方向错了就是需要重构”,这句话是我重构的标杆。思考🤔了很久,得出的答案是对自己的代码太宽容了…. #4在没有新需求或者调整架构时,我enhance code的方式是浏览每个文件,这个方式费心费力,下次应该调整方式,一次捋页面,查找里面的相同的点;并且粒度再小一点…. PS: 新的一年,掐指一算,又是写代码的一年😄]]></content>
<categories>
<category>杂谈</category>
</categories>
<tags>
<tag>杂谈</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:理解javascript中的作用域]]></title>
<url>%2F2018%2F12%2F24%2Ftranslate-understanding-scope-in-javascript%2F</url>
<content type="text"><![CDATA[原文链接 #简介JavaScript有一个特性叫做作用域。尽管作用域的概念对于许多初学者是不容易理解的,我会尽力在最简单的范围内解释。理解作用域会是你的代码更加清楚,减少错误,帮助你使用它制作强大的设计模式。 #什么是作用域作用域是运行时代码中某些特定部分中变量,函数和对象的可访问性。换句话说,作用域确定了代码中的变量和其他资源的可见性。 #作用域为何存在–最小访问性原则因此,限制变量的可见性的重点是什么,而不是所有的代码不是随处可见的。一个优点是作用域为你的代码提供了一定级别的安全性,计算机安全的一个常见的原则是用户应该一次只能访问他们需要的东西。 想想电脑的管理员,由于他们对公司的电脑有很多控制权,向他们的账户授予全部权限是没问题的。假设你有一个含有三个管理员的公司,他们都可以访问系统,一切都很顺利。但是突然,发生了一件坏事,其中的一个系统感染了病毒。现在你不知道到底是谁的错误导致的。你意识到你应该给他们基本的用户账户,只有在需要的时候才赋予完全访问的特权。这会帮助你追踪变化,记录谁做了什么。这叫做最小访问性原则。看起来很直观?这个原则也适用于程序语言的设计。它在大多数编程语言中称作作用域,包括我们接下来要研究的JavaScript。 随着你的编程之旅,你会意识到代码的作用域有助于提高效率,追踪bug并且减少bug。作用域也解决了在不同作用域中相同变量名的命名问题。切记不要吧作用域和上下文弄混淆了,它们是不同的特性。 #JavaScript中的作用域在JavaScript中有两种类型的作用域 全局作用域 局部作用域 函数内部的变量是在局部作用域,外部的是在全局作用域。每一个函数在调用的时候会创建一个新的作用域。 #全局作用域在文档中开始写JavaScript时,你已经在全局作用域中了。整个JavaScript文件中只有一个全局作用域,如果变量位于函数的外面,那么它是在全局作用域中。 // the scope is by default global var name = 'Hammad'; 位于全局作用域的变量可以在其他作用域被访问和修改。 var name = 'Hammad'; console.log(name); // logs 'Hammad' function logName() { console.log(name); // 'name' is accessible here and everywhere else } logName(); // logs 'Hammad' #局部作用域定义在函数内部的变量是在局部作用域中,每一次调用函数,它们会有不同的作用域。这意味着相同名字的变量可以在不同的函数中使用。这是因为这些变量绑定在它们各自的函数中,每一个有不同的作用域,并且在其他的函数中无法访问。 // Global Scope function someFunction() { // Local Scope #1 function someOtherFunction() { // Local Scope #2 } } // Global Scope function anotherFunction() { // Local Scope #3 } // Global Scope #块语句块语句类似if和switch条件或者for和while循环中,不像函数那样创建新的作用域。定义在块语句中的变量将保留在它们已经存在的作用域中。 if (true) { // this 'if' conditional block doesn't create a new scope var name = 'Hammad'; // name is still in the global scope } console.log(name); // logs 'Hammad' ECMAScript 6中采用let和const关键字,这些关键词可以代替var关键字。 var name = 'Hammad'; let likes = 'Coding'; const skills = 'Javascript and PHP'; 与var关键字相反,let和const关键字支持在块语句中声明局部作用域。 if (true) { // this 'if' conditional block doesn't create a scope // name is in the global scope because of the 'var' keyword var name = 'Hammad'; // likes is in the local scope because of the 'let' keyword let likes = 'Coding'; // skills is in the local scope because of the 'const' keyword const skills = 'JavaScript and PHP'; } console.log(name); // logs 'Hammad' console.log(likes); // Uncaught ReferenceError: likes is not defined console.log(skills); // Uncaught ReferenceError: skills is not defined 只要您的应用程序存在,全局作用域就会存在。只要调用和执行函数,本地作用域就会存在。 #上下文很多开发者经常混淆作用域和上下文,认为它们指的是相同的概念。但这种情况并非如此。作用域是我们上面讨论的,上下文用来在代码的某些特定部分引用this的值。作用域是指变量的可见性,而上下文是指在同一范围内的this的值。我们也可以使用函数方法更改上下文,我们将在后面讨论。在全局作用域中上下文始终是Window对象。 // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…} console.log(this); function logFunction() { console.log(this); } // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…} // because logFunction() is not a property of an object logFunction(); 如果作用域在一个对象的方法中,则上下文是该方法所属的对象。 class User { logName() { console.log(this); } } (new User).logName(); // logs User {} (new User).logName()是一种将对象存储在变量中然后在其上调用logName函数的简短办法。在这里,你不需要创建新的对象。 你会注意到如果使用new调用你的函数,上下文的值表现是不一样的。上下文将会被设置为调用函数的实例。考虑上面的一个示例,使用new关键字调用该函数。 function logFunction() { console.log(this); } new logFunction(); // logs logFunction {} 在严格模式下调用函数时,上下文默认为undefined。 #执行上下文要消除我们上面学的内容的混淆,执行上下文中的上下文指的是作用域而不是上下文。这是一个奇怪的命名约定,但是由于JavaScript的规范,我们与之相关。 JavaScript是一个单线程的语言,所以它一次只能执行一个任务。其余的任务在执行上下文中排队。正如我之前告诉你的,当JavaScript解释器开始执行你的代码时,默认情况下,上下文(作用域)设置成全局。此全局上下文附加到您的执行上下文,该上下文实际上是启动执行上下文的第一个上下文。 之后,每个函数调用都会将其上下文附加到执行上下文。当在该函数内部或其他地方调用另一个函数时,会发生同样的事情。 每个函数都创建自己的执行上下文 一旦浏览器完成该上下文中的代码,那么该上下文将从执行上下文中弹出,并且执行上下文中的当前上下文的状态将被传送到父上下文。 浏览器总是执行位于执行堆栈顶部的执行上下文(实际上是代码中最内层的范围)。 只能有一个全局上下文,但有任意数量的函数上下文。 执行上下文有两个创建阶段和代码执行阶段。 创建阶段当调用函数但其代码尚未执行时,存在创建阶段的第一个阶段。在创建阶段发生的三件主要事情是: 创建可变对象 创建作用域链 设置上下文的值(this) 可变对象可变对象(也称为激活对象)包含在执行上下文的特定分支中定义的所有变量,函数和其他声明。 调用函数时,解释器会扫描所有资源,包括函数参数,变量和其他声明。 当打包到单个对象中时,所有内容都将成为可变对象。 'variableObject': { // contains function arguments, inner variable and function declarations } 作用域链在创建阶段的运行上下文中,作用域链在可变对象后被创建。作用域链本身包含变量对象。作用域链被用来解决变量。当被要求解析变量时,JavaScript始终从代码嵌套的最内层开始,并一直跳回到父作用域,直到它找到正在寻找的变量或任何其他资源。作用域链可以简单地定义为包含其自己的执行上下文的可变对象的对象,以及它父对象的所有其他执行上下文,该对象拥有一堆其他对象。 'scopeChain': { // contains its own variable object and other variable objects of the parent execution contexts } 执行上下文对象执行上下文对象可以表示为这样的抽象对象: executionContextObject = { 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts 'variableObject': {}, // contains function arguments, inner variable and function declarations 'this': valueOfThis } 代码执行阶段在执行上下文的第二阶段是代码执行阶段,其他的值被分配,代码最终运行。 #语法作用域语法作用域意味着嵌套在函数组中,内部的函数可以访问其父作用域的变量和其他资源。这意味着子函数在语法上绑定了其父函数的执行上下文。语法作用域有时也被称为静态作用域。 function grandfather() { var name = 'Hammad'; // likes is not accessible here function parent() { // name is accessible here // likes is not accessible here function child() { // Innermost level of the scope chain // name is also accessible here var likes = 'Coding'; } } } 你会注意到关于语法作用域的事情是它向前工作,这意味着name可以通过其子项的执行上下文来访问。但是他不会像父母一样向后工作,这意味着变量likes不能被它的父函数访问。这也告诉我们在不同执行上下文中具有相同名称的变量从执行堆栈的顶部到底部优先获得。一个变量和其他变量具有相同的名称,在最里面的函数(执行堆栈的最顶部的上下文)具有最高的优先权。 #闭包闭包的概念和我们上面学习的语法作用域密切相关。当内部函数尝试访问其外部函数的作用域链时创建Closure,这意味着语法作用域之外的变量。闭包包含自己的范围链,父母的范围链和全局范围。 闭包不仅可以访问外部函数中定义的变量,还可以访问外部函数的参数。 即使在函数返回后,闭包也可以访问其外部函数的变量。这允许返回的函数维护对外部函数的所有资源的访问。 当您从函数返回内部函数时,这时候您尝试调用外部函数时,将不会调用返回的函数。你必须首先将外部函数的调用保存在单独的变量中,然后将该变量作为函数调用。思考这个例子: function greet() { name = 'Hammad'; return function () { console.log('Hi ' + name); } } greet(); // nothing happens, no errors // the returned function from greet() gets saved in greetLetter greetLetter = greet(); // calling greetLetter calls the returned function from the greet() function greetLetter(); // logs 'Hi Hammad' 这里要注意的关键点是函数greetLetter在greet函数返回的情况下任然可以访问变量name。在没有变量赋值的情况下从greet函数调用返回函数的一种方法是使用括号两次,如下所示: function greet() { name = 'Hammad'; return function () { console.log('Hi ' + name); } } greet()(); // logs 'Hi Hammad' PS:我觉得上面的例子没有很好的反映出来闭包的概念,让我看来它只是一个高阶函数的代表。我第一次接触闭包是我之前在写一个手风琴的demo时,为每一个li标签添加事件,但是写出来的结果是只能为最后一个li标签添加事件,其他的列表没有添加上!what?我是来了一个for循环加上的呀…这时候我开始寻找问题的原因,进而了解到闭包这个概念。 #public作用域和private作用域在很多其他的编程语言中,你可以使用private、public、private和protected设置类方法和类属性的可见性。使用PHP语言思考这个例子: // Public Scope public $property; public function method() { // ... } // Private Sccpe private $property; private function method() { // ... } // Protected Scope protected $property; protected function method() { // ... } 封装来自public作用域的函数可以使他们免受易攻击。但是在JavaScript中,共有和私有的概念都没有。然而,我们可以使用闭包来模拟这个特性。为了使所有内容与全局分离,我们必须首先将函数封装在如下的函数中: (function () { // private scope })(); 函数的尾部告诉解释器无需调用立即就可以执行它。我们向其中增加函数和变量,在外部是不可以访问的。但是,如果我们想在外部访问它们,意味着我们希望一部分是public一部分是private?我们可以使用的另一种闭包叫做模块模式,这允许我们使用对象中的public和private作用域来界定我们的函数。 模块模式模块模式看起来像这样: var Module = (function() { function privateMethod() { // do something } return { publicMethod: function() { // can call privateMethod(); } }; })(); 模块的返回声明中包括我们的public函数,private函数并不会返回。不反悔的函数是在模块命名空间外不能访问。但是我们的共有方法是可以访问我们的私有函数,这些函数一般是辅助函数,例如ajax调用和一切其他的。 Module.publicMethod(); // works Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined 一种惯例是私有函数的命名是以下划线开头,并返回包含我们的公共函数的匿名对象。这使得它们易于在长对象中进行管理。这就是它的样子: var Module = (function () { function _privateMethod() { // do something } function publicMethod() { // do something } return { publicMethod: publicMethod, } })(); 立即执行函数(IIFE)另一种闭包的类型是立即执行函数。这是在window的上下文中自调用的匿名函数,这意味着this的值是window。这暴露了一个与之交互的全局接口。它看起来是这样: (function(window) { // do anything })(this); #使用.call(), .apply() 和 .bind()改变上下文call和apply函数用于在调用函数时更改上下文,这为您提供了令人难以置信的编程能力(以及统治世界的一些终极能力)。要使用call或apply函数,只需要在函数上调用它,而不是使用一对括号调用函数,并将上下文作为第一个参数传递。函数自己的参数可以在上下文之后传递。 function hello() { // do something... } hello(); // the way you usually call it hello.call(context); // here you can pass the context(value of this) as the first argument hello.apply(context); // here you can pass the context(value of this) as the first argument .call()和.apply()之间的区别在于,在call中,您将其余参数作为以逗号分隔的列表传递,而apply允许您传递数组中的参数。 function introduce(name, interest) { console.log('Hi! I\'m '+ name +' and I like '+ interest +'.'); console.log('The value of this is '+ this +'.') } introduce('Hammad', 'Coding'); // the way you usually call it introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context // Output: // Hi! I'm Hammad and I like Coding. // The value of this is [object Window]. // Hi! I'm Batman and I like to save Gotham. // The value of this is [object Window]. // Hi! I'm Bruce Wayne and I like businesses. // The value of this is Hi. call性能略高于apply。 以下示例获取文档中的项目列表,并将它们逐个打印到控制台: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Things to learn</title> </head> <body> <h1>Things to Learn to Rule the World</h1> <ul> <li>Learn PHP</li> <li>Learn Laravel</li> <li>Learn JavaScript</li> <li>Learn VueJS</li> <li>Learn CLI</li> <li>Learn Git</li> <li>Learn Astral Projection</li> </ul> <script> // Saves a NodeList of all list items on the page in listItems var listItems = document.querySelectorAll('ul li'); // Loops through each of the Node in the listItems NodeList and logs its content for (var i = 0; i < listItems.length; i++) { (function () { console.log(this.innerHTML); }).call(listItems[i]); } // Output logs: // Learn PHP // Learn Laravel // Learn JavaScript // Learn VueJS // Learn CLI // Learn Git // Learn Astral Projection </script> </body> </html> HTML仅包含无序的项列表。然后JavaScript从DOM中选择所有的列表。循环列表。在循环内部,我们将列表项的内容记录到控制台。 此日志语句包含在括在括号中的函数中,在该函数中调用调用函数。相应的列表项将传递给调用函数,以便控制台语句中的关键字记录正确对象的innerHTML。 对象可以有这些方法,同样函数对象也可以有这些方法。事实上,JavaScript函数带有四个内置方法,它们是: Function.prototype.apply() Function.prototype.bind() (Introduced in ECMAScript 5 (ES5)) Function.prototype.call() Function.prototype.toString() Function.prototype.toString()返回函数源代码的字符串表示形式。 到目前为止,我们已经讨论过.call(),.apply()和toString()。 与call和apply不同,bind本身不调用该函数,它只能在调用函数之前用于绑定上下文和其他参数的值。 在上面的一个例子中使用bind: (function introduce(name, interest) { console.log('Hi! I\'m '+ name +' and I like '+ interest +'.'); console.log('The value of this is '+ this +'.') }).bind(window, 'Hammad', 'Cosmology')(); // logs: // Hi! I'm Hammad and I like Cosmology. // The value of this is [object Window]. bind就像call函数一样,它允许你一个接一个地用逗号分隔其余的参数,而不是像apply一样,在数组中传递参数。 #总结这些概念对JavaScript来说是激进的,如果您想要处理更高级的话题,这一点很重要。 我希望你能更好地理解JavaScript Scope及其周围的事情。如果有什么疑问,请随时在下面的评论中询问。 扩展您的代码,直到那时,快乐编码! Scope up your code and till then, Happy Coding! PS终于理解编辑的累了,这么长的文章!!!这是分好几次做的…]]></content>
<categories>
<category>javascript</category>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title><![CDATA[哇哇,我的第一个task]]></title>
<url>%2F2018%2F12%2F21%2Ffirst-task%2F</url>
<content type="text"><![CDATA[最近有一些数据迁移,心中已有蓝图,但是不知如何下手,请教了后端的小哥哥,写出了我的第一个task, namespace :migrate do desc 'migrate info to apply' task :to_apply => :environment do Membership.all.each do |membership| user = membership.user apply = Apply.new( user_id: user.id, # .... ); apply.save!(validate: false) end end end 本来是想写一个数据库的migrate,征求了后端的意见,选择了写一个task,migrate在被合并的时候,这次的数据迁移可能会被吃掉,于是写了一个task,一个简单的数据迁移…]]></content>
<categories>
<category>rails</category>
</categories>
<tags>
<tag>rails</tag>
</tags>
</entry>
<entry>
<title><![CDATA[小程序web-view的实践]]></title>
<url>%2F2018%2F12%2F21%2Fweapp-web-view%2F</url>
<content type="text"><![CDATA[因为小程序的标签有限,像iframe这样的标签不能在小程序里面很好的展示,于是只能选择使用web-view去展示小程序的内容,点击web-view中iframe期望跳到小程序中的某个页面。 根据小程序的文档来,小程序嵌入web-view是一个很简单的事情 <web-view src="{{url}}"></web-view> 如何在页面中跳入小程序的页面,为了防止污染现有的页面,我选择新建一个页面,引入小程序的sdk, - if browser.wechat? = javascript_include_tag 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js' // ..... wx.miniProgram.navigateTo({ url }) ok,成功!]]></content>
<categories>
<category>小程序</category>
</categories>
<tags>
<tag>小程序</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Boolean隐形类型转换]]></title>
<url>%2F2018%2F12%2F17%2Fjs-boolean%2F</url>
<content type="text"><![CDATA[在if类型中,为了使代码看起来好看一些,你往往会使用最简洁的判断,但是其中涉及的隐形类型转换,一不小心就出了bug。 if (count) { // .... } 在运行过程中,if代码块死活不执行,仔细想了想,count等于零的时候,是不执行的,这不是我的本意呀,我想着存在着这个值的时候,就执行,所以我应该这样写,才能达到我的目的。 if (count !== undefined) { // .... } 这里有一份列表,表中的值类型转换完一定为false undefined null false +0, -0, and NaN “” 不在上面列表中,一定为true。 思考一下: var a = new Boolean( false ); var b = new Number( 0 ); var c = new String( "" ); var d = Boolean( a && b && c ); d; // true 你会不会感到疑惑?看一下值时候在上面的列表中!记住只要不在列表中的值,类型转换之后,一定为true 这里补充一下+c的意思是将c转换成number类型,那么"foo"++"5",就是先执行+"5"]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[小程序二级地址选择器]]></title>
<url>%2F2018%2F12%2F11%2Fweapp-address%2F</url>
<content type="text"><![CDATA[今天的任务里面有一个表单,表单里面有地址选择器,小程序官方的组件提供的是国内三级地址选择器,但是有些地方还需要海外的地址,地址又不需要这么详细,到二级足矣… 我的想法是共有两了picker,第一个选择国家,如果选择的是中国的话,第二个输入框为省市picker,如果是海外,第二个为输入框。 那么第二个输入框的数据从哪来呢?是否是需要请求后端?想了想延迟,还是从前端来吧,数据的来源就是某宝了,数据也是最新的。数据格式我存储成下面这样,向后端传值的时候,就传代码就可以了。 export const province = [ { id: "110000", name: "北京" }, // ... } export const city = { "110000": [{ "id": "110100", "name": "北京" }], "110100": [{ "id": "110101", "name": "东城" }, { // .... }, // ... ], // ..... } 其中有一个很坑的地方是,在picker的bingchange函数中,如果直接使用event中的detail值直接赋值,会会发现你的数据从number变成string类型,我在第二个显示输入框还是picker的时候,死活显示不出来,都已经给设计要planb了😂,我说是小程序的锅吗后来搜了一圈,也没发现类似问题…最后使用Number强制类型转换了一下。 下面是部分小程序的代码了: <picker name="city" bindchange="bindCountryChange" value="{{countryIndex}}" range="{{country}}"> <text>{{country[countryIndex]}} </text> </picker> <view class="{{countryIndex === 0 ? '' : 'is-hidden'}}"> <picker name="external" mode="multiSelector" range-key="name" bindchange="bindCityChange" bindcolumnchange="bindCityColumnChange" value="{{cityIndex}}" range="{{city}}" > <view class="u-text-limit--one" name="internal"> {{city[0][cityIndex[0]].name}}{{city[1][cityIndex[1]].name}} </view> </picker> </view> <view class="{{countryIndex === 0 ? 'is-hidden' : ''}}"> <input type="text" name="internal" placeholder="请输入城市" /> </view> bindCityChange: function(event) { const index = Number(event.detail.value); this.setData({ cityIndex: index }) }, bindCityColumnChange(e) { const data = { city: this.data.city, cityIndex: this.data.cityIndex } data.cityIndex[e.detail.column] = e.detail.value if (e.detail.column === 0) { const provinceIndex = data.cityIndex[0]; const provinceId = data.city[0][provinceIndex].id; data.city[1] = city[provinceId]; data.cityIndex[1] = 0; } this.setData(data) },]]></content>
<categories>
<category>小程序</category>
</categories>
<tags>
<tag>小程序</tag>
</tags>
</entry>
<entry>
<title><![CDATA[今日杂谈]]></title>
<url>%2F2018%2F12%2F10%2Fmixed-talk-2%2F</url>
<content type="text"><![CDATA[今天看到写出来的api不尽人意,于是我就问了我后端的小伙伴:他们是怎么写api,他说是照着原型来…对于这个答案,我也是一脸懵逼…. 还记得之前实习的我和同学一起做了一个排班系统,前端是按照星期展示的,写的过程中,我的同学问我:你什么样的数据方便啊,我就给你什么类型的数据?嗯,当时很年轻,我要这这样的数据,巴拉巴拉的。。最后做出来的系统非常满意,最后交工的时候,CTO看了代码之后,就说:你这样给前端数据,假如前端的ui变了,需要两星期一行展示数据怎么办?你还是需要改后端的接口,这是一个完全颠倒的过程…api应该是后端主导,而不是前端去主导,api的职责是提供数据,至于数据如何展示,应该是前端的职责。 当时说到这样的建议,也是非常惭愧,真的是出于无知,做出无知的事情。 虽然小目标是成为一个全栈工程师,目前是前端,写简单的api,我谨记这些话,以防整个过程是由前端来主导。 我只能当作对方出于人道主义精神,写出让我处理起来简单的API,否则真的是无法忍受这样的api… 今天在接api的时候,发现小程序不接受patch请求…why 最近想了很多,也寻求了很多建议,权衡各方面的利弊,虽然寒冬来临,希望自己能有一片温暖的小天地…]]></content>
<categories>
<category>杂谈</category>
</categories>
<tags>
<tag>杂谈</tag>
</tags>
</entry>
<entry>
<title><![CDATA[翻译:js中的神奇的类型转换]]></title>
<url>%2F2018%2F12%2F07%2Fjs-type-coercion%2F</url>
<content type="text"><![CDATA[来源这边文章主要讲的是运算符中的隐式类型转换,我只摘了下面的例子 true + false // 1 12 / "6" // 2 "number" + 15 + 3 // 'number153' 15 + 3 + "number" // '18number' [1] > null // true "foo" + + "bar" // 'fooNaN' 'true' == true // false false == 'false' // false null == '' // false !!"false" == !!"true" // true ['x'] == 'x' // true [] + null + 1 // 'null1' [1,2,3] == [1,2,3] // false 0 || "0" && {} // {} {}+[]+{}+[1] // '0[object Object]1' !+[]+[]+![] // 'truefalse' new Date(0) - 0 // 0 new Date(0) + 0 // 'Thu Jan 01 1970 02:00:00(EET)0' true + false加法运算符会触发数字转换 true + false ==> 1 + 0 ==> 1 12 / “6”除法运算符会触发字符串的数字转换 “number” + 15 + 3加法运算符是从左到右结合,所以先执行"number" + 15,因为第一个操作数是字符串,加法运算符对15进行字符串转换,第二步也是如此。 15 + 3 + “number”如同上一个一样,先执行15+3,因为都是数字,所以不需要进行类型转换。第二步中,因为一个操作数中有字符串,所以对数字进行字符串转换。 [1] > null比较运算符会触发对操作数的数字转换 [1] > null ==> '1' > 0 ==> 1 > 0 ==> true “foo” + + “bar”右边运算符比左边运算符具有更高的优先级,因此+'bar'表达式先执行。一元加号运算符会触发对字符串进行数字类型转换,由于字符串转换之后是一个非法的数字,结果是NaN。第二步执行'foo' + NaN "foo" + + "bar" ==> "foo" + (+"bar") ==> "foo" + NaN ==> "fooNaN" ‘true’ == true and false == ‘false’==运算符会触发数字类型转换,字符串'true'被转换成NaN,布尔true被转换成1 null == ‘’==运算符通常会触发数字类型转换,但不是null的情况,null只能和null和undefined进行比较,不能和其他值进行比较。 !!”false” == !!”true”!!运算符将这两个字符串转换成布尔值true,因为他们不是空的字符串, ==不进行类型转换,只对布尔值的true进行比较 [‘x’] == ‘x’==运算符会触发对一个数组进行数组转换,数组的valueOf()方法会返回数组自己,但是因为数组不是基本类型,velueOf会被忽略。数组的toString()仅仅将['x']转换成字符串'x' [] + null + 1+会触发将[]进行数字类型转换,和上一个一样进行toString操作,会返回一个空的字符串,接下来就进行'' + null + 1 [1,2,3] == [1,2,3]两边有同样类型,不进行类型转换,由于==检查值相等而不是引用相等,两个数组是两个不同的实例,所以结果是false 0 || “0” && {}逻辑运算符||、&&会进行布尔类型转换,但是会返回原始操作数。0是false,'0'是true,{}是true,最后返回{} {}+[]+{}+[1]所有的操作数都不是基本类型,所以+开始从左边的触发数字转换,数组和对象的valueOf方法被忽略。使用后备toString方法。这里有一个诀窍,第一个{}不被看作对象,而是被视为块声明语句,因此它被忽略。于是开始从后面的+[]开始进行评估,[]的toString返回一个空字符串,再对空字符串进行数字转换是0 {}+[]+{}+[1] ==> +[]+{}+[1] ==> 0 + {} + [1] ==> 0 + '[object Object]' + [1] ==> '0[object Object]' + [1] ==> '0[object Object]' + '1' ==> '0[object Object]1' !+[]+[]+![]这个可以根据运算符的优先级去解释 !+[]+[]+![] ==> (!+[]) + [] + (![]) ==> !0 + [] + false ==> true + [] + false ==> true + '' + false ==> 'truefalse' new Date(0) - 0减号运算符会进行数字转换。Date.valueOf()返回自Unix纪元以来的毫秒数 new Date(0) - 0 ==> 0 - 0 ==> 0 new Date(0) + 0加号运算符触发默认转换。 Date假定字符串转换为默认值,因此使用toString()方法,而不是valueOf()。]]></content>
<categories>
<category>前端</category>
<category>javascript</category>
<category>翻译</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>