-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
488 lines (310 loc) · 287 KB
/
atom.xml
File metadata and controls
488 lines (310 loc) · 287 KB
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>This Js</title>
<subtitle>小凡的秘密花园</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://www.thisjs.com/"/>
<updated>2021-06-04T06:20:02.318Z</updated>
<id>http://www.thisjs.com/</id>
<author>
<name>张树源</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>借助nginx统计前端页面PV信息</title>
<link href="http://www.thisjs.com/2021/06/04/with-the-help-of-front-page-pv-nginx-statistics-information/"/>
<id>http://www.thisjs.com/2021/06/04/with-the-help-of-front-page-pv-nginx-statistics-information/</id>
<published>2021-06-04T04:28:39.000Z</published>
<updated>2021-06-04T06:20:02.318Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p>为了解决前端静态网站统计 PV 数据的问题,需要有个接口返回页面的访问数量。</p><p>我们不打算使用写接口来接受用户访问,然后数据库字段++ 的方案,因为需要维护 node 服务器、还有数据库,成本太高。</p><p>最后决定直接读取 nginx 的 log 日志信息,来获取用户的访问量。</p><a id="more"></a><p>要实现该功能,可以从以下几步来实现:</p><ol><li>格式化 nginx 输出日志</li><li>统计日志中某个文件(例如 index.html)的日志数量</li><li>将数据输出到 json 文件中,供前端请求</li><li>定时不断执行 2、3 任务输出到文件中</li></ol><p>下面介绍如何来实现以上功能</p><h2 id="格式化-nginx-输出日志"><a href="#格式化-nginx-输出日志" class="headerlink" title="格式化 nginx 输出日志"></a>格式化 nginx 输出日志</h2><p>借助 nginx 的 <strong>log_format</strong> 我们可以自定义一个日志输出格式。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">http {</span><br><span class="line"> # ... 其他配置...</span><br><span class="line"> log_format analysis '$remote_addr - [$time_local] "$request" '</span><br><span class="line"> '"$http_referer" "$http_user_agent" "$http_x_forwarded_for" ';</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>修改我们的 server 配置</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">server {</span><br><span class="line"> listen 3000;</span><br><span class="line"> access_log /var/lib/nginx/log/website.log analysis;</span><br><span class="line"> # ... 其他配置...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意 analysis 为我们自己的命名,只要保证相同即可。</p><p>这样拿到的请求数据日志就会如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">10.232.140.148 - [21/May/2021:16:14:48 +0800] "GET /assets/js/runtime~main.1e8e793d.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:48 +0800] "GET /img/logo.svg HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/css/styles.a72913c9.css HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/main.b489375c.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/486.ab98b521.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/611.58bc5d65.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/125.1dc4a115.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/c4f5d8e4.75ce3469.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/1be78505.085d4311.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/935f2afb.1a476284.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /img/favicon.ico HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:49 +0800] "GET /assets/js/c4f5d8e4.75ce3469.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:50 +0800] "GET /assets/js/17896441.f8ee3a49.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br><span class="line">10.232.140.148 - [21/May/2021:16:14:50 +0800] "GET /assets/js/bbe1322f.7a2bf3d6.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-"</span><br></pre></td></tr></table></figure><p>有了以上数据,我们便可以分析 UV/PV 等信息,当前需求是页面访问量,所以我们只需要取 index.html 的请求次数即可。</p><blockquote><p>实践中,index.html 作为请求次数不是一个好的方案,因为 nginx 配置,请求/也会直接返回 index.html 文件,因此数据可能会少很多,因此我们采用统计<strong>runtime~main.js</strong>的请求次数作为请求次数的统计。</p></blockquote><h2 id="统计日志中某个文件的日志数量"><a href="#统计日志中某个文件的日志数量" class="headerlink" title="统计日志中某个文件的日志数量"></a>统计日志中某个文件的日志数量</h2><p>我们使用<strong>grep</strong>命令从日志中过滤符合条件的文本,然后使用<strong>wc</strong>(word count)来统计出现的行数,这样就能拿到日志的行数,进而拿到了请求次数。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">grep runtime /var/lib/nginx/log/website.log |wc -l</span><br></pre></td></tr></table></figure><h2 id="将数据输出到json文件中,供前端请求"><a href="#将数据输出到json文件中,供前端请求" class="headerlink" title="将数据输出到json文件中,供前端请求"></a>将数据输出到json文件中,供前端请求</h2><p>我们只需要将输出的内容,写入到前端对应的目录下,前端使用ajax请求对应的文件即可,假设我们的前端项目部署在:<strong>/var/lib/nginx/html/</strong>目录下,则可以这样输出:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">grep runtime /var/lib/nginx/log/website.log |wc -l > /var/lib/nginx/html/pv.json</span><br></pre></td></tr></table></figure><p>这样,在代码中我们便可以直接请求该文件即可</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">fetch(<span class="string">"/pv.json"</span>)</span><br><span class="line"> .then(<span class="function"><span class="params">response</span> =></span> response.json())</span><br><span class="line"> .then(<span class="function"><span class="params">count</span> =></span> <span class="built_in">console</span>.log(<span class="string">"PV数为"</span>, count))</span><br></pre></td></tr></table></figure><h2 id="定时不断执行2、3任务输出到文件中"><a href="#定时不断执行2、3任务输出到文件中" class="headerlink" title="定时不断执行2、3任务输出到文件中"></a>定时不断执行2、3任务输出到文件中</h2><p>只有一次的数据输出,是无法满足不断统计用户请求数量的,因此我们可以借助<strong>crontab</strong>来执行定时任务。<br>创建名为<strong>pvcron</strong>的任务文件,编辑内容</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">* * * * * grep runtime /var/lib/nginx/<span class="built_in">log</span>/website.log |wc -l > /var/lib/nginx/html/pv.json</span><br></pre></td></tr></table></figure><p>前面的<code>* * * * *</code>的意思是每分钟执行一次该写入任务,也可以根据需要定制cron任务,比如:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">* 8-22 * 5 *</span><br></pre></td></tr></table></figure><p>该内容的意思是<strong>在5月份的每天8点-22点之间,每分钟执行一次</strong></p><p>接下来执行</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">crontab pvcron</span><br></pre></td></tr></table></figure><p>现在该文件已经提交给cron进程,它将每分钟运行一次来将PV数写入json文件供前端请求。</p><h2 id="备注"><a href="#备注" class="headerlink" title="备注"></a>备注</h2><p>该方案有多个点会导致数据不够精确</p><ol><li>grep 过滤runtime内容时,并不准确,因此可以使用正则表达式来提高准确性</li><li>如果文件进行了缓存,可能会导致请求没有发到nginx中,也会导致计算不准确</li><li>crontab最小精度为分钟,所以时间粒度不够精细</li></ol><p>因此,在以上问题都可接受的情况下,才可以采用此方案。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p><img src="https://cdn.thisjs.com/blog_images/20210521165208.png" alt></p>]]></content>
<summary type="html">
<p>为了解决前端静态网站统计 PV 数据的问题,需要有个接口返回页面的访问数量。</p>
<p>我们不打算使用写接口来接受用户访问,然后数据库字段++ 的方案,因为需要维护 node 服务器、还有数据库,成本太高。</p>
<p>最后决定直接读取 nginx 的 log 日志信息,来获取用户的访问量。</p>
</summary>
<category term="nginx" scheme="http://www.thisjs.com/tags/nginx/"/>
</entry>
<entry>
<title>React函数式组件使用同时转发子组件的ref</title>
<link href="http://www.thisjs.com/2020/11/12/use-forward-ref/"/>
<id>http://www.thisjs.com/2020/11/12/use-forward-ref/</id>
<published>2020-11-12T15:15:54.000Z</published>
<updated>2020-11-13T02:59:07.476Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020111223142282.png" alt="use forward ref"></p><p>React 中常常会用到 Ref 对组件进行命令式的调用,官方对不同 ref 值的介绍如下:</p><p>ref 的值根据节点的类型而有所不同:</p><ul><li>当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。</li><li>当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。</li><li><strong>你不能在函数组件上使用 ref 属性</strong>,因为他们没有实例。</li></ul><a id="more"></a><p>但是函数式组件,可以使用 forwardRef 与 useImperativeHandle 组合使用,来实现对外暴露组件调用命令的效果。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// cool-input.tsx</span></span><br><span class="line"><span class="keyword">const</span> CoolInput = forwardRef<CoolInputForward, CoolInputProps><span class="function">(<span class="params">(<span class="params">props, forwardedRef</span>) => {</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> [value, setValue] = useState<<span class="built_in">string</span>>(<span class="params">""</span>);</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> inputRef = useRef<HTMLInputElement>(<span class="params"><span class="literal">null</span></span>);</span></span></span><br><span class="line"><span class="function"><span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="comment">// 对外暴露命令式方法</span></span></span></span><br><span class="line"><span class="function"><span class="params"> useImperativeHandle(<span class="params">forwardedRef, (<span class="params"></span>) => (<span class="params">{</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> getValue(<span class="params"></span>) {</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> <span class="keyword">return</span> value;</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> },</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> <span class="comment">// 聚焦Input框</span></span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> focusInput(<span class="params"></span>) {</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> inputRef.current?.focus(<span class="params"></span>);</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> },</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"><span class="params"> }</span>)</span>);</span></span></span><br><span class="line"><span class="function"><span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">return</span> (<span class="params"></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> <input ref={inputRef} value={value} onChange={(<span class="params">{ target: { value } }</span>) => setValue(<span class="params">value</span>)} /></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> </span>);</span></span></span><br><span class="line"><span class="function"><span class="params">}</span>);</span></span><br></pre></td></tr></table></figure><p>同时,使用 forwardRef,可以实现将函数式组件中的子组件转发给父组件使用,让父组件直接调用对应的命令。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// cool-input.tsx</span></span><br><span class="line"><span class="keyword">const</span> CoolInput = forwardRef<HTMLInputElement, CoolInputProps><span class="function">(<span class="params">(<span class="params">props, forwardedRef</span>) => {</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> [value, setValue] = useState<<span class="built_in">string</span>>(<span class="params">""</span>);</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">return</span> (<span class="params"></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> <input ref={forwardedRef} value={value} onChange={(<span class="params">{ target: { value } }</span>) => setValue(<span class="params">value</span>)} /></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> </span>);</span></span></span><br><span class="line"><span class="function"><span class="params">}</span>);</span></span><br></pre></td></tr></table></figure><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.tsx</span></span><br><span class="line"><span class="keyword">const</span> Index: React.FC<IndexProps> = <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> coolInputRef = useRef<HTMLInputElement>(<span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">const</span> onFocusCool = useCallback(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> coolInputRef.current?.focus();</span><br><span class="line"> }, []);</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <div></span><br><span class="line"> <CoolInput ref={coolInputRef} /></span><br><span class="line"> <button onClick={onFocusCool}>聚焦输入框<<span class="regexp">/button></span></span><br><span class="line"><span class="regexp"> </</span>div></span><br><span class="line"> );</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>这样就可以在父组件中,直接调用函数组件中的子组件的事件了。但是有一种特殊的情况:<strong>函数组件中在转发子组件的同时,也需要使用该组件的 Ref</strong>。我们期望实现的效果是,父组件(index.tsx)点击「聚焦输入框」可以正确的使 input 触发 focus 事件,同时,函数组件也可以监听 input 的 focus 事件,打印输入框的值。</p><p>这个时候,我们便需要将 input 元素同时赋给多个 Ref,因此需要根据 ref 的类型来进行批量赋值。ref 赋值有三种方式:</p><h2 id="1-字符串定义"><a href="#1-字符串定义" class="headerlink" title="1. 字符串定义"></a>1. 字符串定义</h2><blockquote><p>不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除</p></blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><input type=<span class="string">'text'</span> ref=<span class="string">'textInput'</span> />;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用ref</span></span><br><span class="line"><span class="keyword">this</span>.refs.textInput;</span><br></pre></td></tr></table></figure><h2 id="2-使用回调函数"><a href="#2-使用回调函数" class="headerlink" title="2. 使用回调函数"></a>2. 使用回调函数</h2><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.setTextInputRef = <span class="function">(<span class="params">element</span>) =></span> {</span><br><span class="line"> <span class="keyword">this</span>.textInput = element;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><input type=<span class="string">'text'</span> ref={<span class="keyword">this</span>.setTextInputRef} />;</span><br></pre></td></tr></table></figure><h2 id="3-使用-Ref-对象"><a href="#3-使用-Ref-对象" class="headerlink" title="3. 使用 Ref 对象"></a>3. 使用 Ref 对象</h2><p>在 React 16.3 版本之后,使用 createRef 使得对象引用变得更加方便。因此正常情况下,推荐使用该方式。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 声明</span></span><br><span class="line"><span class="keyword">this</span>.textInput = React.createRef();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 赋值</span></span><br><span class="line"><input type=<span class="string">'text'</span> ref={<span class="keyword">this</span>.textInput} />;</span><br></pre></td></tr></table></figure><p>在函数组件中,一样可以<strong>在函数组件内部使用 ref 属性</strong>,使用方法便是 useRef,创建一个 Ref 对象。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> inputRef = useRef<HTMLInputElement>(<span class="literal">null</span>);</span><br><span class="line"><input ref={inputRef} <span class="keyword">type</span>=<span class="string">'text'</span> />;</span><br></pre></td></tr></table></figure><p>回归问题,我们只需要判断 ref 的类型,然后根据当前类型,依次赋值即可。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> CoolInput = forwardRef<HTMLInputElement, CoolInputProps><span class="function">(<span class="params">(<span class="params">props, forwardedRef</span>) => {</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> inputRef = useRef<HTMLInputElement>(<span class="params"><span class="literal">null</span></span>); <span class="comment">// Object类型的Ref</span></span></span></span><br><span class="line"><span class="function"><span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="comment">/** 根据ref类型设置ref的值 */</span></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">function</span> setRef(<span class="params">ref: React.Ref<HTMLInputElement>, value: HTMLInputElement | <span class="literal">null</span></span>) {</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">if</span> (<span class="params"><span class="keyword">typeof</span> ref === "<span class="keyword">function</span>"</span>) {</span></span></span><br><span class="line"><span class="function"><span class="params"> ref(<span class="params">value</span>);</span></span></span><br><span class="line"><span class="function"><span class="params"> } <span class="keyword">else</span> <span class="keyword">if</span> (<span class="params">ref !== <span class="literal">null</span></span>) {</span></span></span><br><span class="line"><span class="function"><span class="params"> (<span class="params">ref <span class="keyword">as</span> MutableRefObject<HTMLInputElement | <span class="literal">null</span>></span>).current = value;</span></span></span><br><span class="line"><span class="function"><span class="params"> }</span></span></span><br><span class="line"><span class="function"><span class="params"> }</span></span></span><br><span class="line"><span class="function"><span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">return</span> (<span class="params"></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> <input</span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> ref={(<span class="params">instance</span>) => {</span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> <span class="comment">// 依次设置ref的值</span></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> setRef(<span class="params">forwardedRef, instance</span>);</span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> setRef(<span class="params">inputRef, instance</span>);</span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> }}</span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> onFocus={(<span class="params"></span>) => <span class="built_in">console</span>.log(<span class="params">inputRef.current?.value</span>)}</span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> /></span></span></span></span><br><span class="line"><span class="function"><span class="params"><span class="params"> </span>);</span></span></span><br><span class="line"><span class="function"><span class="params">}</span>);</span></span><br></pre></td></tr></table></figure><p>思路便是以上的内容,即将示例依次赋值给对应的 ref 即可,但是这样编写起来比较麻烦,我可以将该功能提取为一个 hooks 使用,这样就不需要单独去赋值了。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { MutableRefObject, Ref, useCallback } <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 合并多个ref为一个</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useCombinedRefs = <T <span class="keyword">extends</span> <span class="built_in">any</span>>(...refs: <span class="built_in">Array</span><Ref<T>>): Ref<T> =></span><br><span class="line"> useCallback(</span><br><span class="line"> (element: T) =></span><br><span class="line"> refs.forEach(<span class="function">(<span class="params">ref</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!ref) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> ref === <span class="string">"function"</span>) {</span><br><span class="line"> <span class="keyword">return</span> ref(element);</span><br><span class="line"> }</span><br><span class="line"> (ref <span class="keyword">as</span> MutableRefObject<T>).current = element;</span><br><span class="line"> }),</span><br><span class="line"> refs</span><br><span class="line"> );</span><br></pre></td></tr></table></figure><p>这样我们即可在页面中直接使用即可合并所有相同的 Ref 为一个,直接赋值到元素上即可。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> CoolInput = forwardRef<HTMLInputElement, CoolInputProps><span class="function">(<span class="params">(<span class="params">props, forwardedRef</span>) => {</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> inputRef = useRef<HTMLInputElement>(<span class="params"><span class="literal">null</span></span>); <span class="comment">// Object类型的Ref</span></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> combineRef = useCombinedRefs(<span class="params">forwardedRef, inputRef</span>);</span></span></span><br><span class="line"><span class="function"><span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">return</span> <input ref={combineRef} onFocus={(<span class="params"></span>) => <span class="built_in">console</span>.log(<span class="params">inputRef.current?.value</span>)} />;</span></span></span><br><span class="line"><span class="function"><span class="params">}</span>);</span></span><br></pre></td></tr></table></figure><p>这时,父组件也可以直接对 input 元素使用.focus()方法,函数组件中也可以随时在 onFocus 中使用 ref 打印当前输入框的值了。</p><iframe src="https://codesandbox.io/embed/hebingzhuanfazujianderef-vekvj?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="合并转发组件的ref" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://github.com/facebook/react/issues/13029" target="_blank" rel="noopener">Share ref with multiple ref handlers</a></li><li><a href="https://zh-hans.reactjs.org/docs/refs-and-the-dom.html" target="_blank" rel="noopener">Refs and the DOM —— React官方文档</a></li><li><a href="https://juejin.im/post/6844903581435297806" target="_blank" rel="noopener">React ref 指北教程</a></li></ul>]]></content>
<summary type="html">
<p><img src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020111223142282.png" alt="use forward ref"></p>
<p>React 中常常会用到 Ref 对组件进行命令式的调用,官方对不同 ref 值的介绍如下:</p>
<p>ref 的值根据节点的类型而有所不同:</p>
<ul>
<li>当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。</li>
<li>当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。</li>
<li><strong>你不能在函数组件上使用 ref 属性</strong>,因为他们没有实例。</li>
</ul>
</summary>
<category term="javascript" scheme="http://www.thisjs.com/tags/javascript/"/>
<category term="react" scheme="http://www.thisjs.com/tags/react/"/>
</entry>
<entry>
<title>React受控组件与非受控组件</title>
<link href="http://www.thisjs.com/2020/11/11/react-controlled-and-uncontrolled-components/"/>
<id>http://www.thisjs.com/2020/11/11/react-controlled-and-uncontrolled-components/</id>
<published>2020-11-11T14:46:13.000Z</published>
<updated>2021-06-04T03:24:48.745Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p>在React开发中经常会遇到controlled和uncontrolled组件的问题处理,最近仔细阅读了官方文档以及相关的介绍,整理一份基础笔记。</p><a id="more"></a><h2 id="非受控组件"><a href="#非受控组件" class="headerlink" title="非受控组件"></a>非受控组件</h2><blockquote><p>当用户将数据输入到表单字段(例如 input,dropdown 等)时,React 不需要做任何事情就可以映射更新后的信息</p></blockquote><p>非受控组件就像是我们常见的DOM元素,Input节点元素内部保存输入的内容,然后在需要的时候可以通过 ref 获取它们的值。如下:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, { useCallback, useRef } <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> UncontrolledComponentProps {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> UncontrolledComponent: React.FC<UncontrolledComponentProps> = <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"><span class="keyword">const</span> nameInputRef = useRef<HTMLInputElement | <span class="literal">null</span>>(<span class="literal">null</span>);</span><br><span class="line"><span class="keyword">const</span> onSubmitForm = useCallback(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// 主动提取表单中的数据</span></span><br><span class="line"><span class="built_in">console</span>.log(nameInputRef.current?.value);</span><br><span class="line"> }, [])</span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line"><div></span><br><span class="line"><input <span class="keyword">type</span>=<span class="string">"text"</span> ref={ nameInputRef } /></span><br><span class="line"><button onClick={ onSubmitForm }>获取表单数据<<span class="regexp">/button></span></span><br><span class="line"><span class="regexp"></</span>div></span><br><span class="line">);</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> UncontrolledComponent;</span><br></pre></td></tr></table></figure><p>非受控组建的特点就是,不会实时的向父组件发送数据的变化,只有在父组件需要的时候,才会使用ref主动的从该组件中「提取」最终结果数据。</p><p>非受控组件的应用场景比如一个应用场景比较单一的Form表单,它的特点如下:</p><ul><li>组件自己保存全部数据变化</li><li>组件完善地处理错误提醒、正则数据验证、联动内容(disable/展示隐藏)等功能</li><li>父组件只需要最终的结果,不会实时改动太多表单的内容</li></ul><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, { useRef } <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> { useImperativeHandle } <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> UncontrolledFormProps {}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 组件Ref暴露的接口</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> UFRefProps {</span><br><span class="line"> getFormValues: <span class="function"><span class="params">()</span> =></span> { [key: <span class="built_in">string</span>]: <span class="built_in">string</span> };</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> UncontrolledForm = React.forwardRef<UFRefProps, UncontrolledFormProps>(</span><br><span class="line"> (props, fowardedRef) => {</span><br><span class="line"> <span class="keyword">const</span> nameInputRef = useRef<HTMLInputElement | <span class="literal">null</span>>(<span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">const</span> passwordInputRef = useRef<HTMLInputElement | <span class="literal">null</span>>(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 自定义暴露给父组件的实例值</span></span><br><span class="line"> useImperativeHandle(fowardedRef, <span class="function"><span class="params">()</span> =></span> ({</span><br><span class="line"> <span class="comment">/** 获取表单的全部值 */</span></span><br><span class="line"> getFormValues() {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> username: nameInputRef.current?.value ?? <span class="string">""</span>,</span><br><span class="line"> password: passwordInputRef.current?.value ?? <span class="string">""</span>,</span><br><span class="line"> };</span><br><span class="line"> },</span><br><span class="line"> }));</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <div></span><br><span class="line"> <form></span><br><span class="line"> <input placeholder=<span class="string">'用户名'</span> <span class="keyword">type</span>=<span class="string">'text'</span> ref={nameInputRef} /></span><br><span class="line"> <input autoComplete=<span class="string">'on'</span> placeholder=<span class="string">'密码'</span> <span class="keyword">type</span>=<span class="string">'password'</span> ref={passwordInputRef} /></span><br><span class="line"> <<span class="regexp">/form></span></span><br><span class="line"><span class="regexp"> </</span>div></span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> UncontrolledForm;</span><br></pre></td></tr></table></figure><p>以上便是一个非常简单的非受控组件的例子,它通过forwardRef和useImperativeHandle 暴露出获取全部Form数据的接口,父组件在使用时,可以直接通过该接口「拉取」数据即可。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.tsx</span></span><br><span class="line"><span class="keyword">const</span> formRef = useRef<UFRefProps | <span class="literal">null</span>>(<span class="literal">null</span>);</span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line"> <div></span><br><span class="line"> <UncontrolledForm ref={formRef} /></span><br><span class="line"> <button onClick={<span class="function"><span class="params">()</span> =></span> <span class="built_in">console</span>.log(formRef.current?.getFormValues())}>获取表单数据<<span class="regexp">/button></span></span><br><span class="line"><span class="regexp"> <div></span></span><br><span class="line"><span class="regexp">)</span></span><br></pre></td></tr></table></figure><p>在开发上,该组件的优点便是快速,完全不用考虑可能传递进来的参数的问题,快速完成功能。</p><h2 id="受控组件"><a href="#受控组件" class="headerlink" title="受控组件"></a>受控组件</h2><blockquote><p>如果一个 input 表单元素的值是由 React 控制,就其称为受控组件</p></blockquote><p>与非受控组件对应的,便是受控组件,特点也是与之对应,受控组件有以下特点:</p><ul><li>有一个函数实时的与父组件更新数据,</li><li>有一个参数接受它的当前值。</li></ul><p>参考<a href="https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/" target="_blank" rel="noopener">「Controlled and uncontrolled form inputs in React don’t have to be complicated」</a>文章介绍,它的流程如下:</p><p><img src="https://cdn.thisjs.com/blog_images/20210604111653.png" alt></p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> ControlledInputProps {</span><br><span class="line"> value: <span class="built_in">string</span>;</span><br><span class="line"> onChange: <span class="function">(<span class="params">value: <span class="built_in">string</span></span>) =></span> <span class="built_in">void</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> ControlledInput: React.FC<ControlledInputProps> = <span class="function">(<span class="params">{ value, onChange }</span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> <input value={value} onChange={<span class="function">(<span class="params">{ target: { value } }</span>) =></span> onChange(value)} />;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> ControlledInput;</span><br></pre></td></tr></table></figure><p>受控组件的当前值(value)与更新事件(onChange)一般是成对出现的,否则会导致组件变为readOnly组件,无法更新数据。在React开发模式可能会报warning:</p><blockquote><p>Warning: Failed prop type: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue.</p></blockquote><h2 id="使用对比"><a href="#使用对比" class="headerlink" title="使用对比"></a>使用对比</h2><p>受控组件是React官方推荐的开发风格,大部分的组件都应该遵循该模式来实现。在实际使用中,因为父组件可以实时获取、控制数据,因此对处理表单的错误提醒、其他子组件的disabled等状态变化都有非常好的体验。同时,对于异步初始化更新子组件数据也会变得非常方便。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [name, setName] = useState<<span class="built_in">string</span>>(<span class="string">""</span>);</span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line"> <ControlledInput value={name} onChange={setName} /></span><br><span class="line"> <button onClick={ <span class="function"><span class="params">()</span>=></span> setName(<span class="string">'helloworld'</span>) }>主动更新组件数据<<span class="regexp">/button></span></span><br><span class="line"><span class="regexp"> <Button disabled={!name}>提交数据</</span>Button></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="应用场景对比"><a href="#应用场景对比" class="headerlink" title="应用场景对比"></a>应用场景对比</h2><p>大部分的组件都应该使用受控组件。但是对于一些指令式的操作,使用非受控组件可以更好实现,比如:</p><ul><li>管理焦点,文本选择,媒体播放。</li><li>触发命令动画。</li><li>与第三方 DOM 库集成。</li></ul><p>之前有遇到过这样的业务:同一个页面是由多个Form组成,它们初始化数据是由多个接口请求到的数据来初始化,但是修改后使用同一个提交按钮同一个接口进行提交。</p><p>这样使用非受控组件的方案可以是每个组件单独去请求该数据,然后决定是否可以编辑,父组件点击提交时,从子组件中拉取全部数据,然后统一提交。</p><p>一般非受控组件,也会有一个defaultValue可以用来初始化组件的值,但是之后该值的改变,便不会实时更新。实现方法如下。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> UncontrolledInput: React.FC<UncontrolledInputProps> = <span class="function">(<span class="params">{ defaultValue: initialValue }</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> [value, setValue] = useState<<span class="built_in">string</span>>(initialValue);</span><br><span class="line"> <span class="keyword">return</span> <input value={value} onChange={<span class="function">(<span class="params">{ target: { value } }</span>) =></span> setValue(value)} />;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>利用了useState函数的参数值,<strong>只会在第一次创建的时候使用</strong>,之后再修改该props,并不会再对state的值引起改变的特点。</p><h2 id="协调-reconciliation-时的问题"><a href="#协调-reconciliation-时的问题" class="headerlink" title="协调(reconciliation)时的问题"></a>协调(reconciliation)时的问题</h2><blockquote><p>当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。</p></blockquote><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"before"</span> <span class="attr">title</span>=<span class="string">"stuff"</span> /></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"after"</span> <span class="attr">title</span>=<span class="string">"stuff"</span> /></span></span><br></pre></td></tr></table></figure><p>通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className 属性。</p><p>这时,非受控组件的问题就凸显出来了,再次更新的defaultValue值并不会更新组件的值,在一些特殊场景,比如切换Tab时,会出现将第一个Tab组件里的值,带入到第二个组件中去的问题。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><div></span><br><span class="line"> <Button <span class="keyword">type</span>=<span class="string">'primary'</span> onClick={<span class="function"><span class="params">()</span> =></span> setShowFirst(!showFirst)}></span><br><span class="line"> 切换展示 {showFirst + <span class="string">""</span>}</span><br><span class="line"> <<span class="regexp">/Button>;</span></span><br><span class="line"><span class="regexp"> {</span></span><br><span class="line"><span class="regexp"> showFirst ? <UncontrolledInput defaultValue='hi1' /</span>> : <UncontrolledInput defaultValue=<span class="string">'hi2'</span> />;</span><br><span class="line"> }</span><br><span class="line"><<span class="regexp">/div></span></span><br></pre></td></tr></table></figure><p>在该案例中,我们期待的是切换后,Input组件会切换两个默认值hi1和hi2的,但是实际效果并不如意,不仅如此,我们修改其中一个输入内容之后,点击切换,输入内容会带入到第二个中去。</p><p>我们通过对非受控组件中模拟mount和unmount事件,来查看切换时的效果。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> UncontrolledInputProps {</span><br><span class="line"> defaultValue: <span class="built_in">string</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> UncontrolledInput: React.FC<UncontrolledInputProps> = <span class="function">(<span class="params">{ defaultValue: initialValue }</span>) =></span> {</span><br><span class="line"></span><br><span class="line"> useEffect(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"mount"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"clean Up"</span>);</span><br><span class="line"> };</span><br><span class="line"> }, []);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> [value, setValue] = useState<<span class="built_in">string</span>>(initialValue);</span><br><span class="line"> <span class="keyword">return</span> <input value={value} onChange={<span class="function">(<span class="params">{ target: { value } }</span>) =></span> setValue(value)} />;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>观察结果发现,组件并没有被销毁,而是被重用了,这样就导致出现了状态被重用的问题。参考官方文档介绍,我们可以通过两种方案修复:</p><ul><li>为其声明不同的父级元素</li><li>使用不同的key</li></ul><p>当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。因此使用不同的父节点元素可以解决该问题。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> showFirst ? (</span><br><span class="line"> <span></span><br><span class="line"> <UncontrolledInput defaultValue=<span class="string">'hi1'</span> /></span><br><span class="line"> <<span class="regexp">/span></span></span><br><span class="line"><span class="regexp"> ) : (</span></span><br><span class="line"><span class="regexp"> <b></span></span><br><span class="line"><span class="regexp"> <UncontrolledInput defaultValue='hi2' /</span>></span><br><span class="line"> <<span class="regexp">/b></span></span><br><span class="line"><span class="regexp"> );</span></span><br><span class="line"><span class="regexp">}</span></span><br></pre></td></tr></table></figure><figure class="wp-block-video"><video controls src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020111211433655.mov"></video><figcaption>正常卸载组件</figcaption></figure><p>通过观察可以发现,每次切换,都会卸载上一个组件,重新创建一个新的组件。这样就解决了之前的问题。但是,使用不同的父组件会让代码变得很难理解,因此,更推荐使用官方提供的添加key的方式。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> showFirst ? (</span><br><span class="line"> <UncontrolledInput key=<span class="string">'hi1'</span> defaultValue=<span class="string">'hi1'</span> /></span><br><span class="line"> ) : (</span><br><span class="line"> <UncontrolledInput key=<span class="string">'hi2'</span> defaultValue=<span class="string">'hi2'</span> /></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素,这样key不相同,便不会复用原来的元素,这样就会保证了正常的切换。</p><p>受控组件,在表现上不会存在该问题,因为其value值的变化,都会实时的同步到元素状态中。但是,<strong>受控组件一样会复用该组件</strong>,就会导致组件内的状态(比如:定时器)会被复用,同样有一些隐患。因此,在切换受控组件时,也同样推荐增加key属性。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle" target="_blank" rel="noopener">useImperativeHandle – react 文档</a></li><li><a href="https://zh-hans.reactjs.org/docs/forwarding-refs.html" target="_blank" rel="noopener"> Refs 转发 – react文档</a></li><li><a href="https://zh-hans.reactjs.org/docs/glossary.html#%E5%8F%97%E6%8E%A7%E7%BB%84%E4%BB%B6-vs-%E9%9D%9E%E5%8F%97%E6%8E%A7%E7%BB%84%E4%BB%B6" target="_blank" rel="noopener">受控组件 vs 非受控组件 – React文档</a></li><li><a href="https://muyunyun.cn/posts/8bdf2cdf/" target="_blank" rel="noopener">组件设计 —— 重新认识受控与非受控组件</a></li><li><a href="https://zh-hans.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html" target="_blank" rel="noopener">你可能不需要使用派生 state</a></li></ul>]]></content>
<summary type="html">
<p>在React开发中经常会遇到controlled和uncontrolled组件的问题处理,最近仔细阅读了官方文档以及相关的介绍,整理一份基础笔记。</p>
</summary>
<category term="react" scheme="http://www.thisjs.com/tags/react/"/>
</entry>
<entry>
<title>JavaScript网络请求(二):前端下载方案</title>
<link href="http://www.thisjs.com/2020/11/09/javascript-web-request-2-the-front-end-download/"/>
<id>http://www.thisjs.com/2020/11/09/javascript-web-request-2-the-front-end-download/</id>
<published>2020-11-09T14:33:11.000Z</published>
<updated>2021-06-04T03:13:01.738Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/blog_images/fe-download.png" alt="前端下载方案"></p><p>浏览器端的下载,广泛应用于文件下载、导出等操作,前端发起下载的方案有各种的方案,但是都与后端的协议息息相关,比如最简单的甚至可以直接使用链接即可发起下载。</p><a id="more"></a><h2 id="a-标签下载"><a href="#a-标签下载" class="headerlink" title="a 标签下载"></a>a 标签下载</h2><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">a</span> <span class="attr">href</span>=<span class="string">"https://url.com/file"</span> <span class="attr">download</span>=<span class="string">"logo-image"</span>></span>下载<span class="tag"></<span class="name">a</span>></span></span><br></pre></td></tr></table></figure><p>这样既可在高版本浏览器中,指定一次下载,download属性便是指定该链接为下载的属性,属性值有三种形式:</p><ul><li>省缺:不填写的情况下会根据返回头中的 “Content-Disposition” 来生成名称。</li><li>文件名:优先使用该值作为文件名,后缀名根据Content-Disposition来生成。</li><li>文件名.后缀: 使用该文件名与后缀来下载文件。</li></ul><p>以下为浏览器的支持情况:</p><p><img src="https://cdn.thisjs.com/blog_images/20210604110942.png" alt="浏览器支持情况"></p><p>在实际开发中,更多的情况可能会是使用JavaScript来发起下载,而不是直接写好一个连接。因此我们可以通过动态的创建a标签,并触发点击事件,来完成下载。一个标准的下载工具函数如下:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> download = <span class="function">(<span class="params">url: string, fileName: string</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> elementLink = <span class="built_in">document</span>.createElement(<span class="string">"a"</span>);</span><br><span class="line"> elementLink.style.display = <span class="string">"none"</span>;</span><br><span class="line"> elementLink.download = fileName;</span><br><span class="line"> elementLink.href = url;</span><br><span class="line"> <span class="built_in">document</span>.body.appendChild(elementLink);</span><br><span class="line"> elementLink.click();</span><br><span class="line"> <span class="built_in">document</span>.body.removeChild(elementLink);</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>直接使用a标签来发起下载,在不同的浏览器端,会有一些不符合预期的表现形式,比如在测试中遇到的情况就是如果返回的文件是PDF格式,并且指定了’Content-Type’ Header为 ‘application/pdf’,则在不同的浏览器上有不同的表现。</p><ul><li>FireFox download指定的文件名失效</li><li>Chrome 会直接跳转到PDF阅读页面,而不是发起下载</li></ul><p>以下为不同浏览器表现的视频效果:</p><figure class="wp-block-video"><video controls src="https://cdn.thisjs.com/video/%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B62020-11-06%20%E4%B8%8B%E5%8D%885.05.53.mov"></video><figcaption>Chrome 中点击下载,则会跳转到阅读页面</figcaption></figure><figure class="wp-block-video"><video controls src="https://cdn.thisjs.com/video/%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B62020-11-06%20%E4%B8%8B%E5%8D%885.09.01.mov"></video><figcaption>firefox中则是指定的文件名失效</figcaption></figure><p>经测试,在使用JavaScript动态创建的标签下载时,不会出现该问题。和使用a标签链接对应的文件类似,我们同样可以使用其他的方式打开链接,让浏览器决定下载内容。</p><h2 id="location-href-window-open"><a href="#location-href-window-open" class="headerlink" title="location.href / window.open"></a>location.href / window.open</h2><p>两种实现方法比较类似,都是使用浏览器直接打开对应的链接,然后浏览器根据Header中的content-disposition 的信息来决定是展示还是下载。</p><p>对于普通的二进制文件正常都是直接触发下载,但是对于图片、video等浏览器可以直接解析的内容,就可能会出现直接在浏览器展示,而不是下载。这其中的主要原因依旧是content-disposition Header的缘故。</p><p><strong>content-disposition</strong>在作为response header时候,可以有两种类型,分别是:</p><ul><li>inline (默认值)—— 如果浏览器可以解析,则使用浏览器展示内容</li><li>attachment —— 作为附件下载,可以指定文件名</li></ul><p>对于这两种文件类型的效果区别,可以分别点击以下链接查看Header信息里的区别:</p><p>inline <a href="https://blog-demos.vercel.app/api/download/inline-image" target="_blank" rel="noopener">https://blog-demos.vercel.app/api/download/inline-image</a><br>attachement <a href="https://blog-demos.vercel.app/api/download/attachment-image" target="_blank" rel="noopener">https://blog-demos.vercel.app/api/download/attachment-image</a></p><p>因此,如果希望浏览器直接打开的方式下载文件,需要服务端配置好对应的content-disposition为attachment值。</p><h2 id="XMLHttpRequest-a标签下载"><a href="#XMLHttpRequest-a标签下载" class="headerlink" title="XMLHttpRequest + a标签下载"></a>XMLHttpRequest + a标签下载</h2><p>使用XHR下载主要可以处理以下几种情况,其中前两项为比较常见的场景:</p><ul><li><strong>认证信息存在于Header中,而不是cookie中</strong></li><li><strong>错误信息回调</strong></li><li>JS端获取最新的下载进度,展示下载效果</li><li>控制下载取消</li></ul><p>使用XHR下载的思路是,请求的responseType为blob格式,在获取到文件之后,使用createObjectURL转换为对象地址,赋值给a标签的href属性,完成下载。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 触发下载blob文件</span></span><br><span class="line"><span class="keyword">const</span> triggerDownload = useCallback(<span class="function">(<span class="params">data: <span class="built_in">any</span>, fileName: <span class="built_in">string</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">let</span> url = <span class="built_in">window</span>.URL.createObjectURL(<span class="keyword">new</span> Blob([data]));</span><br><span class="line"> <span class="keyword">let</span> link = <span class="built_in">document</span>.createElement(<span class="string">"a"</span>);</span><br><span class="line"> link.style.display = <span class="string">"none"</span>;</span><br><span class="line"> link.href = url;</span><br><span class="line"> link.setAttribute(<span class="string">"download"</span>, fileName);</span><br><span class="line"> <span class="built_in">document</span>.body.appendChild(link);</span><br><span class="line"> link.click();</span><br><span class="line">}, []);</span><br><span class="line"></span><br><span class="line"><span class="comment">// XHR请求文件内容</span></span><br><span class="line"><span class="keyword">const</span> onDownload = useCallback(</span><br><span class="line"> <span class="keyword">async</span> (url: <span class="built_in">string</span>) => {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> Axios.get(url, {</span><br><span class="line"> <span class="comment">// 文件类型设置</span></span><br><span class="line"> responseType: <span class="string">"blob"</span>,</span><br><span class="line"> headers: {</span><br><span class="line"> Authorization: <span class="string">`Bearer Token`</span>, <span class="comment">// 授权信息</span></span><br><span class="line"> },</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">if</span> (response.status === <span class="number">200</span>) {</span><br><span class="line"> <span class="keyword">let</span> fileName = <span class="string">""</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"content-disposition"</span> <span class="keyword">in</span> response.headers) {</span><br><span class="line"> <span class="comment">// 从Header中获取文件名</span></span><br><span class="line"> <span class="keyword">const</span> dispositionParams = contentDisposition.parse(</span><br><span class="line"> response.headers[<span class="string">"content-disposition"</span>]</span><br><span class="line"> ).parameters;</span><br><span class="line"> fileName = dispositionParams[<span class="string">"filename"</span>];</span><br><span class="line"> }</span><br><span class="line"> triggerDownload(response.data, fileName);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> message.error(error.message);</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [triggerDownload]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="错误信息回调"><a href="#错误信息回调" class="headerlink" title="错误信息回调"></a>错误信息回调</h3><p>以上便是一个较为完整的使用XHR下载的方法,基于此,我们为其增加错误信息回调。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> onDownload = useCallback(</span><br><span class="line"> (url: <span class="built_in">string</span>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="keyword">async</span> (resolve, reject) => {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">//...code...</span></span><br><span class="line"> <span class="keyword">if</span> (response.status === <span class="number">200</span>) {</span><br><span class="line"> <span class="comment">//...code...</span></span><br><span class="line"> triggerDownload(response.data, fileName);</span><br><span class="line"> resolve(<span class="string">"success"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> reject(<span class="string">"response status"</span> + response.status);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="comment">// Axios的错误类型,读取返回数据中的JSON内容</span></span><br><span class="line"> <span class="keyword">if</span> (error.isAxiosError) {</span><br><span class="line"> <span class="comment">// 因为数据默认设置为Blob文件类型,因此需要使用fileReader将数据从Blob中读取出来</span></span><br><span class="line"> <span class="keyword">const</span> fr = <span class="keyword">new</span> FileReader();</span><br><span class="line"> fr.onload = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> result = <span class="built_in">JSON</span>.parse((<span class="keyword">this</span>.result <span class="keyword">as</span> <span class="built_in">string</span>) ?? <span class="string">"{}"</span>);</span><br><span class="line"> reject(result.message);</span><br><span class="line"> };</span><br><span class="line"> fr.readAsText(error.response.data);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 其他错误类型,直接返回消息体</span></span><br><span class="line"> reject(error.message);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> },</span><br><span class="line"> [triggerDownload]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>使用fileReader将后端返回的错误JSON解析出来,这样在使用该函数的过程中,就可以获取到成功或者错误的回调信息了。</p><h3 id="进度和取消"><a href="#进度和取消" class="headerlink" title="进度和取消"></a>进度和取消</h3><p>取消网络请求可以使用xhr.abort,对应Axios的实现为cancelToken,取消网络请求相关的介绍内容可以参照之前内容:<a href="https://www.thisjs.com/2020/11/04/javascript-web-request-a-race-condition-problem/">「JavaScript网络请求(一):处理race condition竞态问题#abort上一次网络请求」</a></p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> onDownload = useCallback(</span><br><span class="line"> (url: <span class="built_in">string</span>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="keyword">async</span> (resolve, reject) => {</span><br><span class="line"> <span class="keyword">const</span> CancelToken = Axios.CancelToken;</span><br><span class="line"> cancelSource.current = CancelToken.source();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> Axios.get(url, {</span><br><span class="line"> <span class="comment">// 文件类型设置</span></span><br><span class="line"> responseType: <span class="string">"blob"</span>,</span><br><span class="line"> headers: {</span><br><span class="line"> Authorization: <span class="string">`Bearer Token`</span>, <span class="comment">// 授权信息</span></span><br><span class="line"> },</span><br><span class="line"> cancelToken: cancelSource.current.token,</span><br><span class="line"> onDownloadProgress(evt) {</span><br><span class="line"> setPercentage(<span class="built_in">Math</span>.ceil((evt.loaded / evt.total) * <span class="number">100</span>));</span><br><span class="line"> },</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">if</span> (response.status === <span class="number">200</span>) {</span><br><span class="line"> <span class="comment">// ...code...</span></span><br><span class="line"> resolve(<span class="string">"success"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> reject(<span class="string">"response status"</span> + response.status);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="comment">// ...code...</span></span><br><span class="line"> });</span><br><span class="line"> },</span><br><span class="line"> [triggerDownload]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>在线测试效果地址:<a href="https://blog-demos.vercel.app/admin/blog-demos/user-download" target="_blank" rel="noopener">https://blog-demos.vercel.app/admin/blog-demos/user-download</a></p><p><img src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110915453868.png" alt></p><p>完整代码地址:<a href="https://gist.github.com/mrxf/14091139d710a7e4d9d79b71e1472243" target="_blank" rel="noopener">https://gist.github.com/mrxf/14091139d710a7e4d9d79b71e1472243</a></p><h2 id="FileSaver-js"><a href="#FileSaver-js" class="headerlink" title="FileSaver.js"></a>FileSaver.js</h2><p>filesaver.js是非常强大的前端文件保存方案,不仅限于下载文件,包括canvas等内容都可以直接保存,是非常成熟的方案。</p><p>下载文件也非常的方便,如果有其他的保存需求,可以使用该库。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">FileSaver.saveAs(<span class="string">"https://httpbin.org/image"</span>, <span class="string">"image.jpg"</span>);</span><br></pre></td></tr></table></figure><h2 id="附录:"><a href="#附录:" class="headerlink" title="附录:"></a>附录:</h2><p>🤔 Q: <strong>在一些返回中,并不能看到content-disposition Header信息,导致无法获取文件名,应该怎么办呢?</strong></p><p>✅ 解决思路:检查Access-Control-Expose-Headers 的配置信息,该Header控制响应Header暴露那些Header到外部,默认不展示content-disposition。</p><p>🤔 Q: <strong>XHR的responseType还有哪些格式?</strong></p><p>🙋♂️ Answer:text(省缺值)、json、document、arraybuffer、blob、ms-stream(IE支持的下载类型)</p><p>🤓 Q:<strong>content-disposition的attachment类型的规范是怎么样的呢?</strong></p><p>🙋♂️ Answer:除了可以直接设置attachment值之外,还可以指明文件名,格式如下:</p><ul><li>attachment; filename=”filename.jpg”</li><li>attachment; filename=”???.png”; filename*=UTF-8’’%E4%B8%AD%E6%96%87%E5%90%8D.png</li><li>filename<em>“采用了 RFC 5987 中规定的编码方式,和filename同时出现时,应该优先使用filename</em>,主要在非英文名时会用到</li></ul><p>因此在获取content-disposition的文件名的时候,直接用正则匹配可能效果并不好,尤其是在出现了 filename*的时候,处理的条件就更加复杂了。建议直接使用第三方工具包 content-disposition</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install content-disposition</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://github.com/eligrey/FileSaver.js" target="_blank" rel="noopener">https://github.com/eligrey/FileSaver.js</a></li><li><a href="http://wangyn.net/2018/06/20/the-principle-of-aerolite-and-the-comparation-with-community-solution.html" target="_blank" rel="noopener">aerolite 实现原理及社区解决方案对比分析 ——有更多社区方案</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers" target="_blank" rel="noopener">Access-Control-Expose-Headers</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition" target="_blank" rel="noopener">Content-Disposition</a></li><li><a href="https://stackoverflow.com/questions/23054475/javascript-regex-for-extracting-filename-from-content-disposition-header" target="_blank" rel="noopener">javascript-regex-for-extracting-filename-from-content-disposition-header —— 用正则匹配Header中的文件名</a></li><li><a href="https://www.npmjs.com/package/content-disposition" target="_blank" rel="noopener">content-disposition - npm包</a></li><li><a href="https://juejin.im/post/6844903763359039501" target="_blank" rel="noopener">这应该是你见过的最全前端下载总结</a></li></ul>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/blog_images/fe-download.png" alt="前端下载方案"></p>
<p>浏览器端的下载,广泛应用于文件下载、导出等操作,前端发起下载的方案有各种的方案,但是都与后端的协议息息相关,比如最简单的甚至可以直接使用链接即可发起下载。</p>
</summary>
<category term="javascript" scheme="http://www.thisjs.com/tags/javascript/"/>
</entry>
<entry>
<title>JavaScript网络请求(一):处理race condition竞态问题</title>
<link href="http://www.thisjs.com/2020/11/04/javascript-web-request-a-race-condition-problem/"/>
<id>http://www.thisjs.com/2020/11/04/javascript-web-request-a-race-condition-problem/</id>
<published>2020-11-04T16:31:42.000Z</published>
<updated>2021-11-24T09:27:58.918Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><blockquote><p>竞争危害(race hazard)又名竞态条件、竞争条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机</p></blockquote><p>在前端中的表现常见于异步操作的结果返回的时间顺序和预期不同,比如常见的网络请求结果的顺序。</p><a id="more"></a><p>首先看一个异步请求返回结果顺序不同,导致数据不符合预期的场景。</p><figure><br> <video controls src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110409184214.mp4"></video><br> <figcaption>先请求的数据后返回,导致覆盖掉了最新的请求</figcaption><br></figure><p>以上功能的完整实现代码如下,我们接下来需要一点点的对其进行改动。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> UserSearch: React.FC = <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> [data, setData] = useState<<span class="built_in">any</span>>({});</span><br><span class="line"> <span class="keyword">const</span> [loading, setLoading] = useState<<span class="built_in">boolean</span>>(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> run = useCallback(<span class="keyword">async</span> (name: <span class="built_in">string</span>) => {</span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, { params: { name } });</span><br><span class="line"> setData(data.data);</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line"> }, []);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> onSearch = useCallback(</span><br><span class="line"> (e: React.ChangeEvent<HTMLInputElement>) => {</span><br><span class="line"> <span class="keyword">if</span> (e.target.value) {</span><br><span class="line"> run(e.target.value);</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [run]</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <div className=<span class="string">'inner-card'</span>></span><br><span class="line"> <Card></span><br><span class="line"> <Input.Search onChange={onSearch} placeholder=<span class="string">'请输入用户名进行搜索'</span> /></span><br><span class="line"> <<span class="regexp">/Card></span></span><br><span class="line"><span class="regexp"> <Card></span></span><br><span class="line"><span class="regexp"> <List</span></span><br><span class="line"><span class="regexp"> loading={loading}</span></span><br><span class="line"><span class="regexp"> itemLayout='horizontal'</span></span><br><span class="line"><span class="regexp"> dataSource={data?.items}</span></span><br><span class="line"><span class="regexp"> renderItem={(item: any) => (</span></span><br><span class="line"><span class="regexp"> <List.Item></span></span><br><span class="line"><span class="regexp"> <List.Item.Meta</span></span><br><span class="line"><span class="regexp"> avatar={<Avatar src={item.avatar_url} /</span>>}</span><br><span class="line"> title={<a href={item.html_url}>{item.login}<<span class="regexp">/a>}</span></span><br><span class="line"><span class="regexp"> description={item.node_id}</span></span><br><span class="line"><span class="regexp"> /</span>></span><br><span class="line"> <<span class="regexp">/List.Item></span></span><br><span class="line"><span class="regexp"> )}</span></span><br><span class="line"><span class="regexp"> /</span>></span><br><span class="line"> <<span class="regexp">/Card></span></span><br><span class="line"><span class="regexp"> </</span>div></span><br><span class="line"> );</span><br><span class="line">};</span><br></pre></td></tr></table></figure><h2 id="使用debounce"><a href="#使用debounce" class="headerlink" title="使用debounce"></a>使用debounce</h2><p>最常见的<strong>缓解</strong>该状况的方案便是使用debounce,在用户输入完成之后,再发起请求。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> run = useCallback(</span><br><span class="line"> debounce(<span class="keyword">async</span> (name: <span class="built_in">string</span>) => {</span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, { params: { name } });</span><br><span class="line"> setData(data.data);</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line"> }, <span class="number">500</span>),</span><br><span class="line"> []</span><br><span class="line">);</span><br></pre></td></tr></table></figure><figure class="wp-block-video"><video controls src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110410115425.mov"></video><figcaption>使用debounce后,在全部输入结束后再发起请求</figcaption></figure><p>这样,就可以极大的<strong>缓解</strong>了请求返回顺序不同导致的错误问题。</p><p>但是这样已经改变了需求的内容,原本期望的是实时更新,现在变成了等输入完成之后再更新,如果用户输入的内容比较长,界面就会一直空白,因此还需要考虑其他的解决方案。</p><h2 id="根据请求ID,对所需的数据做取舍"><a href="#根据请求ID,对所需的数据做取舍" class="headerlink" title="根据请求ID,对所需的数据做取舍"></a>根据请求ID,对所需的数据做取舍</h2><p>我们在每次请求的时候,都生成一个随机的字符串数据(自增ID,时间戳、nanoID)带入请求中去,后端在返回数据的同时,再原封不动的将该requestID返回;本地也有一个全局变量,记录最新的requestID。</p><p>在数据返回之后,我们拿数据中的requestID与本地的最新的requestID做比对,如果相等,则展示数据,不相等,则舍掉数据。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 只记录最新的请求ID</span></span><br><span class="line"><span class="keyword">const</span> reqId = useRef<<span class="built_in">number</span>>(+<span class="keyword">new</span> <span class="built_in">Date</span>());</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> run = useCallback(<span class="keyword">async</span> (name: <span class="built_in">string</span>) => {</span><br><span class="line"> reqId.current = +<span class="keyword">new</span> <span class="built_in">Date</span>();</span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, { params: { name, reqId: reqId.current } });</span><br><span class="line"> <span class="keyword">if</span> (reqId.current.toString() === data.data.reqId) {</span><br><span class="line"> setData(data.data);</span><br><span class="line"> }</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line">}, []);</span><br></pre></td></tr></table></figure><p>后端返回的数据格式:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="string">"reqId"</span>: <span class="string">"1604456870887"</span>,</span><br><span class="line"> <span class="string">"total_count"</span>: <span class="number">228912</span>,</span><br><span class="line"> <span class="string">"incomplete_results"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="string">"items"</span>: []</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样,根据请求ID的比对,我们就可以将旧的请求舍弃掉。</p><figure class="wp-block-video"><video controls src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110410301420.mov"></video><figcaption>即使先请求的数据后返回,也会被舍弃</figcaption></figure><p>这种方案的弊端太明显,需要依赖后端的配合,因此尝试寻找完全由前端处理的方案。</p><h2 id="使用useEffect"><a href="#使用useEffect" class="headerlink" title="使用useEffect"></a>使用useEffect</h2><p>在使用ahooks的useRequest的时候,官方的文档中提到了这样一个API介绍</p><blockquote><p>默认情况下,新请求会覆盖旧请求。如果设置了 fetchKey,则可以实现多个请求并行,fetches 存储了多个请求的状态。外层的状态为最新触发的 fetches 数据。</p></blockquote><p>那么意味着useRequest是完全可以处理竞态问题的。我们用useRequest来尝试一下。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> { run, data, loading } = useRequest(</span><br><span class="line"> (name: <span class="built_in">string</span>) => {</span><br><span class="line"> <span class="keyword">const</span> param = <span class="keyword">new</span> URLSearchParams({</span><br><span class="line"> name,</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> url: <span class="string">"/thirdparty/users?"</span> + param,</span><br><span class="line"> method: <span class="string">"get"</span>,</span><br><span class="line"> };</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> manual: <span class="literal">true</span>,</span><br><span class="line"> initialData: { items: [] },</span><br><span class="line"> }</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> onSearch = useCallback(</span><br><span class="line"> (e: React.ChangeEvent<HTMLInputElement>) => {</span><br><span class="line"> <span class="keyword">if</span> (e.target.value) {</span><br><span class="line"> run(e.target.value);</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [run]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这个时候再次进行请求之后发现,完全符合我们的预期。</p><p>那么我们如果也期望自己去实现该效果的话,则可以借助React的useEffect来实现。</p><p>首先我们看一下React Hooks的执行流程。</p><p><img src="https://cdn.thisjs.com/blog_images/20211103163446.png" alt="react hooks flow"></p><p>可以发现,在每次Update阶段,都会先执行一次cleanUp Effects,然后再执行Effects函数。借助此效果,我们可以定义一个Ref外部变量,每次Effect clean阶段自增该值。在effect阶段,将该值赋给局部变量。在请求结束之后,比对局部变量与自增的Ref值是否相等,然后即可对数据做取舍。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [name, setName] = useState<<span class="built_in">string</span>>(<span class="string">""</span>);</span><br><span class="line"><span class="keyword">const</span> counter = useRef<<span class="built_in">number</span>>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">useEffect(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (name) {</span><br><span class="line"> <span class="keyword">const</span> reqId = counter.current;</span><br><span class="line"> <span class="keyword">const</span> run = <span class="keyword">async</span> () => {</span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, { params: { name } });</span><br><span class="line"> <span class="keyword">if</span> (reqId === counter.current) {</span><br><span class="line"> setData(data.data);</span><br><span class="line"> }</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line"> };</span><br><span class="line"> run();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> counter.current++;</span><br><span class="line"> };</span><br><span class="line">}, [name]);</span><br></pre></td></tr></table></figure><figure class="wp-block-video"><video controls src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110410582661.mov"></video><figcaption>使用useEffect来处理返回数据</figcaption></figure><p>同时,Dan Abramov 在<a href="https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions" target="_blank" rel="noopener">A Complete Guide to useEffect</a> 也提到了该问题的解决方案,基于此,我们可以取消掉Ref这个外部变量来实现。这个可以实现的原因和该情况类似。</p><p>因为useEffect的cleanup函数不是和effects一一对应执行的,<strong>并不是</strong>Mount阶段执行的Effect,一定在UnMount阶段执行对应的cleanup。<strong>而是在下一次effect执行之前,执行上一个effect的cleanup函数。</strong></p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">useEffect(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">let</span> didCancel = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (name) {</span><br><span class="line"> <span class="keyword">const</span> run = <span class="keyword">async</span> () => {</span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, { params: { name } });</span><br><span class="line"> <span class="keyword">if</span> (!didCancel) {</span><br><span class="line"> setData(data.data);</span><br><span class="line"> }</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line"> };</span><br><span class="line"> run();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// 用于下一次effect clean的时候更新该值</span></span><br><span class="line"> didCancel = <span class="literal">true</span>;</span><br><span class="line"> };</span><br><span class="line">}, [name]);</span><br></pre></td></tr></table></figure><h2 id="提取为单独的Hooks"><a href="#提取为单独的Hooks" class="headerlink" title="提取为单独的Hooks"></a>提取为单独的Hooks</h2><p>可以看到,使用react useEffect是可以满足我们的需求的。但是,针对每一个请求都要写一次这样的代码,会做很多重复性的操作,因此我们可以将该功能提取为一个单独的hooks使用。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { useEffect } <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用函数返回值的方式保证原来的数据</span></span><br><span class="line"><span class="keyword">type</span> ICurrentCallback = <span class="function"><span class="params">()</span> =></span> <span class="built_in">boolean</span>;</span><br><span class="line"><span class="keyword">type</span> IEffectCallback = <span class="function">(<span class="params">getCurrent: ICurrentCallback</span>) =></span> <span class="built_in">void</span> | <span class="function">(<span class="params">(<span class="params"></span>) => <span class="built_in">void</span> | <span class="literal">undefined</span></span>);</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="params">export</span> <span class="params">const</span> <span class="params">useCurrentEffect</span> = (<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> effect: IEffectCallback,</span></span></span><br><span class="line"><span class="function"><span class="params"> deps: React.DependencyList | <span class="literal">undefined</span></span></span></span><br><span class="line"><span class="function"><span class="params"></span>) =></span> {</span><br><span class="line"> useEffect(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">let</span> isCurrent = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果返回了清理函数,则放在cleanup阶段执行</span></span><br><span class="line"> <span class="keyword">const</span> cleanup = effect(<span class="function"><span class="params">()</span> =></span> isCurrent);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> isCurrent = <span class="literal">false</span>;</span><br><span class="line"> cleanup && cleanup();</span><br><span class="line"> };</span><br><span class="line"> }, deps);</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>使用时,直接获取最新的current值即可。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">useCurrentEffect(</span><br><span class="line"> (getCurrent) => {</span><br><span class="line"> <span class="keyword">if</span> (name) {</span><br><span class="line"> <span class="keyword">const</span> run = <span class="keyword">async</span> () => {</span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, { params: { name } });</span><br><span class="line"> <span class="keyword">if</span> (getCurrent()) {</span><br><span class="line"> setData(data.data);</span><br><span class="line"> }</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line"> };</span><br><span class="line"> run();</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [name]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="abort上一次网络请求"><a href="#abort上一次网络请求" class="headerlink" title="abort上一次网络请求"></a>abort上一次网络请求</h2><p>还有一种实现思路,我们可以在下一次请求的时候,直接将上一次未结束的网络请求取消掉。取消网络请求,针对不同的协议对应的api是</p><ul><li>XMLHttpRequest.abort()</li><li>fetch 的 AbortController</li></ul><p>Axios中的具体实现,对应的为 CancelToken。使用CancelToken的方式官方介绍地址 <a href="https://github.com/axios/axios#cancellation" target="_blank" rel="noopener">https://github.com/axios/axios#cancellation</a> ,被取消的请求会执行Promise的reject,错误内容为cancel的内容。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cancelSource = useRef<CancelTokenSource | <span class="literal">null</span>>(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> run = useCallback(<span class="keyword">async</span> (name: <span class="built_in">string</span>) => {</span><br><span class="line"> <span class="keyword">if</span> (cancelSource.current) {</span><br><span class="line"> <span class="comment">// 如果之前存在请求,则取消掉</span></span><br><span class="line"> cancelSource.current.cancel();</span><br><span class="line"> cancelSource.current = <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> CancelToken = Axios.CancelToken;</span><br><span class="line"> cancelSource.current = CancelToken.source();</span><br><span class="line"></span><br><span class="line"> setLoading(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="keyword">await</span> Axios.get(<span class="string">"/thirdparty/users"</span>, {</span><br><span class="line"> params: { name },</span><br><span class="line"> cancelToken: cancelSource.current.token,</span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 清空cancelSource值</span></span><br><span class="line"> cancelSource.current = <span class="literal">null</span>;</span><br><span class="line"> setData(data.data);</span><br><span class="line"> setLoading(<span class="literal">false</span>);</span><br><span class="line">}, []);</span><br></pre></td></tr></table></figure><p><img src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110414210895.jpg" alt="canceled"></p><p>看效果这样也满足了我们的需求,这样先请求的数据,如果还没有完成,就不会再回来了,也就不会出现覆盖的情况了。</p><figure class="wp-block-video"><video controls src="https://blog.thisjs.com/wp-content/uploads/2020/11/2020110414223253.mov"></video><figcaption>使用cancel处理请求</figcaption></figure><p>Axios的cancelToken的实现方式是利用了一个cancel token的Promise,如果存在该Promise,并且该promise resolve了,则执行xhr.abort()方法来取消掉请求。</p><p>具体代码地址: <a href="https://github.com/axios/axios/blob/16aa2ce7fa42e7c46407b78966b7521d8e588a72/lib/adapters/http.js#L268" target="_blank" rel="noopener">https://github.com/axios/axios/blob/16aa2ce7fa42e7c46407b78966b7521d8e588a72/lib/adapters/http.js#L268</a></p><h2 id="rxjs的SwitchMap"><a href="#rxjs的SwitchMap" class="headerlink" title="rxjs的SwitchMap"></a>rxjs的SwitchMap</h2><blockquote><p>A reactive programming library for JavaScript</p></blockquote><p>使用rxjs处理异步操作,非常的轻松,因此处理该问题时,在不依赖react hooks直接使用rxjs的switchMap 操作符即可完成。</p><p>switchMap特点是在每次发出时,会取消前一个内部 observable 的订阅,然后订阅一个新的 observable。这个思想和react的useEffect的clearup类似,因此也是一样的实现思路。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { map, switchMap } <span class="keyword">from</span> <span class="string">"rxjs/operators"</span>;</span><br><span class="line"><span class="keyword">import</span> { fromEvent, <span class="keyword">from</span> } <span class="keyword">from</span> <span class="string">"rxjs"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建一个基于keyup事件的Observer</span></span><br><span class="line"><span class="keyword">let</span> userTypesInSearchBox = fromEvent(<span class="built_in">document</span>.getElementById(<span class="string">"#search-box"</span>), <span class="string">"keyup"</span>).pipe(</span><br><span class="line">map(<span class="function">(<span class="params">event</span>) =></span> event.currentTarget.value),</span><br><span class="line">switchMap(<span class="function">(<span class="params">name</span>) =></span> {</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">from</span>(Axios.get(<span class="string">"/thirdparty/users"</span>, { <span class="attr">params</span>: { name } }));</span><br><span class="line">}),</span><br><span class="line">map(<span class="function">(<span class="params">res</span>) =></span> res.data)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">userTypesInSearchBox.subscribe(<span class="function">(<span class="params">data</span>) =></span> {</span><br><span class="line"><span class="keyword">if</span> (data) {</span><br><span class="line">render(data);</span><br><span class="line">}</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params">data</span>) </span>{</span><br><span class="line"><span class="comment">// 渲染数据</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://medium.com/hackernoon/avoiding-race-conditions-when-fetching-data-with-react-hooks-220d6fd0f663" target="_blank" rel="noopener">Avoiding Race Conditions when Fetching Data with React Hooks</a></p><p><a href="https://flufd.github.io/avoiding-race-conditions-use-current-effect/" target="_blank" rel="noopener">Avoiding useEffect race conditions with a custom hook</a></p><p><a href="https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions" target="_blank" rel="noopener">A Complete Guide to useEffect</a></p><p><a href="https://github.com/axios/axios#cancellation" target="_blank" rel="noopener">Axios - GitHub</a></p><p><a href="https://juejin.im/post/6844903933735878663" target="_blank" rel="noopener">Axios取消请求CancelToken</a></p><p><a href="https://stackoverflow.com/questions/54666401/how-to-use-throttle-or-debounce-with-react-hook" target="_blank" rel="noopener">How to use throttle or debounce with React Hook?</a></p>]]></content>
<summary type="html">
<blockquote>
<p>竞争危害(race hazard)又名竞态条件、竞争条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机</p>
</blockquote>
<p>在前端中的表现常见于异步操作的结果返回的时间顺序和预期不同,比如常见的网络请求结果的顺序。</p>
</summary>
<category term="javascript" scheme="http://www.thisjs.com/tags/javascript/"/>
<category term="react" scheme="http://www.thisjs.com/tags/react/"/>
<category term="rxjs" scheme="http://www.thisjs.com/tags/rxjs/"/>
</entry>
<entry>
<title>探索Docker在前端开发中的应用一:背景</title>
<link href="http://www.thisjs.com/2020/05/08/explore-the-application-scenario-of-docker-in-fe-development/"/>
<id>http://www.thisjs.com/2020/05/08/explore-the-application-scenario-of-docker-in-fe-development/</id>
<published>2020-05-08T04:45:17.000Z</published>
<updated>2020-05-09T09:53:31.119Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="/images/docker-nodejs-cover.png" alt="Docker with node"></p><span style="border-bottom: 1px dashed #389fff;"><span class="hint--top hint--rounded hint--info" aria-label="Empowering App Development for Developers" ontouchstart>Docker</span></span>是一项具有划时代意义的开源项目,它在服务端开发以及部署中大放异彩,本着对优秀技术的不断学习态度,非常建议将Docker作为你下一项学习的技术。<br><br>本文作为Docker在前端中应用的背景篇章,会对Docker及其优势做精简的介绍,然后列举一些前端开发、测试、部署过程中遇到的问题,希望可以借助Docker优雅地解决。<br><br><a id="more"></a><h1 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h1><h2 id="一、在开发过程中,有没有遇到过以下场景:"><a href="#一、在开发过程中,有没有遇到过以下场景:" class="headerlink" title="一、在开发过程中,有没有遇到过以下场景:"></a>一、在<strong>开发</strong>过程中,有没有遇到过以下场景:</h2><ol><li>需要自己配置proxy解决跨域请求问题</li><li>接手一个新项目之后,想要跑起来,并没有想象中的那么流畅,除了前端依赖,还要配置若干系统内容<ul><li>npm i</li><li>npm start</li><li>Error XXX服务未配置</li></ul></li><li>一个Node服务(比如SSR、打印服务、普通的数据处理服务),在部署的时候,脚本控制权并不在前端手中,容易出现一些配合问题</li><li>后端项目需要在本地运行,你需要安装全部的后端环境,然后这些环境在你开发下一个项目的时候,你还在考虑如何能清理干净。<ul><li>MySql</li><li>MongoDb</li><li>Java</li><li>PHP</li><li>Redis</li><li>Nginx</li><li>…</li></ul></li></ol><h2 id="二、在搭建团队基础服务过程中,有没有遇到过以下场景:"><a href="#二、在搭建团队基础服务过程中,有没有遇到过以下场景:" class="headerlink" title="二、在搭建团队基础服务过程中,有没有遇到过以下场景:"></a>二、在<strong>搭建团队基础服务</strong>过程中,有没有遇到过以下场景:</h2><ol><li>需要在服务器上部署一个项目要安装若干的基础服务</li><li>你发现系统上已经存在了旧版本的服务,你不确定升级该服务,会影响系统上的哪些应用,但是不升级,你的应用不支持安装</li></ol><h2 id="三、在学习过程中,是否有过以下场景:"><a href="#三、在学习过程中,是否有过以下场景:" class="headerlink" title="三、在学习过程中,是否有过以下场景:"></a>三、在<strong>学习</strong>过程中,是否有过以下场景:</h2><ol><li>搭建一个普通的博客,都需要花费大量时间安装一些基础服务:<ul><li>PHP环境</li><li>Apache</li><li>各种数据库服务</li></ul></li><li>如果有一天你不需要了,还要花费时间去清理</li><li>项目开发完之后,还要考虑服务端的环境问题</li><li>你的一个Demo项目,希望在任何服务器都能快速部署</li></ol><h1 id="需求总结"><a href="#需求总结" class="headerlink" title="需求总结"></a>需求总结</h1><p>我们将我们的需求汇总一下,我们希望的功能有:</p><ol><li>开发所需的非前端环境一键配置</li><li>所需的服务方便配置、简单移除</li><li>项目开发配置完成后,方便部署在不同平台并且运行效果一致</li><li>其他人接手项目后,能够快速搭建好环境,并且运行起项目</li></ol><h1 id="Docker介绍"><a href="#Docker介绍" class="headerlink" title="Docker介绍"></a>Docker介绍</h1><p>Docker的官方化介绍,可以在其官网<a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/" target="_blank" rel="noopener">[1]</a>中了解到,对于我们来说Docker就是一个将项目运行所需所有环境打包到一个虚拟化容器中的工具,<br>同时,借助Docker,我们也可以非常方便的获取所需的依赖环境。</p><h2 id="举个例子:我的项目需要在本地安装MySQL服务。"><a href="#举个例子:我的项目需要在本地安装MySQL服务。" class="headerlink" title="举个例子:我的项目需要在本地安装MySQL服务。"></a>举个例子:我的项目需要在本地安装MySQL服务。</h2><p>只需要在命令行使用Docker使用mariadb镜像启动一个容器即可。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">--name some-mariadb --rm \</span><br><span class="line">-p 3306:3306 -e MYSQL_ROOT_PASSWORD=password \</span><br><span class="line">mariadb:latest</span><br></pre></td></tr></table></figure><p>使用用户名<code>root</code>,密码:<code>password</code>即可成功连接MySQL。</p><p><img src="/images/navicat-mysql-01.jpg" alt="docker mysql"></p><p>如果想停止,直接按下Ctrl+\,服务又从电脑中移除,非常干净。</p><blockquote><p><strong>备注</strong>:为何这里不是Ctrl+C来终止命令,可以参阅本文<a href="https://github.com/docker-library/mariadb/issues/82#issuecomment-339242301" target="_blank" rel="noopener">[3]</a></p></blockquote><p>该命令中有几个参数具体分享一下:</p><ul><li>–rm 容器结束时,移除container,与之相对的有-d</li><li>-p 映射容器端口到主机端口,主机端口:容器端口</li><li>-e Env 环境变量</li><li>-d –detach 后台运行,和–rm不能同时使用</li><li>-v 挂载数据卷,bind的方式</li></ul><p>接着启动数据库的例子,我们会发现,在结束掉服务后,下次再启动会发现数据全没了,我们如果希望将自己的数据保存在本地,那么可以增加-v参数。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">--name some-mariadb --rm \</span><br><span class="line">-p 3306:3306 -e MYSQL_ROOT_PASSWORD=password \</span><br><span class="line">-v /you/data/path:/var/lib/mysql</span><br><span class="line">mariadb:latest</span><br></pre></td></tr></table></figure><p>这样本地路径<code>/you/data/path</code>就会将MySQL产生的数据持久化保存了,每次启动MariaDB都会还原之前的数据了。</p><p>以上我们便体验了Docker的一大特点<strong>快速部署</strong>,即快速将MySQL服务部署到你的电脑上,无论是Windows/OSX/Linux系统,都是一段命令搞定,大大节省了时间和精力。</p><p>这样在我们的开发服务器上,只需要存储一些配置和数据文件,执行文件完全交由Docker管理。</p><h2 id="举个例子:我需要使用Nginx做一个接口转发"><a href="#举个例子:我需要使用Nginx做一个接口转发" class="headerlink" title="举个例子:我需要使用Nginx做一个接口转发"></a>举个例子:我需要使用Nginx做一个接口转发</h2><p>我们需要在开发中需要用到该接口:<code>https://yapi.thisjs.com/mock/21/antd/games</code>,我需要使用nginx对其进行一些代理配置,这样我们只需要创建一个.conf文件,然后使用Docker启动一个Nginx挂载该conf文件到<code>conf.d/</code>目录下即可。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"># proxy.conf</span><br><span class="line"></span><br><span class="line">server {</span><br><span class="line"> listen 8080;</span><br><span class="line"> location / {</span><br><span class="line"> add_header 'Access-Control-Allow-Origin' '*';</span><br><span class="line"> add_header 'Access-Control-Allow-Credentials' 'true';</span><br><span class="line"> add_header Cache-Control private;</span><br><span class="line"> add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';</span><br><span class="line"> add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';</span><br><span class="line"> proxy_pass https://yapi.thisjs.com/mock/21/antd/;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在配置目录下执行该命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">--name proxy-server \</span><br><span class="line">-v <span class="string">"<span class="variable">$(pwd)</span>/proxy.conf"</span>:/etc/nginx/conf.d/proxy.conf:ro \</span><br><span class="line">-d -p 8080:8080 \</span><br><span class="line">nginx</span><br></pre></td></tr></table></figure><p>这时候,我们即可访问 <code>http://localhost:8080/games</code>,即可获取到所需要的数据。</p><p>当我们开发/调试结束后,如果需要将nginx容器停止并移除可以参考以下命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看正在运行中的container</span></span><br><span class="line">docker ps</span><br><span class="line"><span class="comment"># 停止名称为 proxy-server 的container</span></span><br><span class="line">docker stop proxy-server</span><br><span class="line"><span class="comment"># 删除名称为 proxy-server 的container</span></span><br><span class="line">docker container rm proxy-server</span><br></pre></td></tr></table></figure><p>至此,Docker已经基本满足了我们的两个需求:</p><ol><li>开发所需的非前端环境一键配置</li><li>所需的服务方便配置、简单移除</li></ol><p>但是还没有完全达到,我们希望所需环境<strong>一键配置</strong>,而不是一键又一键的配置,因此希望能在下一个篇章里介绍Docker更轻松维护服务的相关内容。</p><h1 id="安装Docker"><a href="#安装Docker" class="headerlink" title="安装Docker"></a>安装Docker</h1><p>看到这,你可能还没有安装Docker,其实Docker安装非常的简单。比如Mac,只需要下载镜像然后直接安装即可:</p><p><img src="/images/install-mac-dmg.png" alt="安装Docker"></p><p>其他系统的Docker安装都非常的简单,可以参考这篇文章介绍:</p><p><a href="https://yeasy.gitbook.io/docker_practice/install" target="_blank" rel="noopener">安装 Docker</a></p><h2 id="安装完之后"><a href="#安装完之后" class="headerlink" title="安装完之后"></a>安装完之后</h2><p>安装完之后,不如尝试启动一个普通的前端项目。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">-p 80:80 --name react-demo \</span><br><span class="line">--rm \</span><br><span class="line">mrxf/craantdbasic:latest</span><br></pre></td></tr></table></figure><p>这是一个基于create react app的前端项目,启动之后直接访问 <code>localhost</code>即可看到页面。</p><h1 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h1><p>不能<strong>为了用Docker而用Docker</strong>,否则会出现Docker也成了我们开发中的一项没有必要的服务。遇到问题,还是要以前端的方式解决。</p><p>以下以2个场景进行分析:</p><h2 id="1-开发中的代理请求"><a href="#1-开发中的代理请求" class="headerlink" title="1.开发中的代理请求"></a>1.开发中的代理请求</h2><p>如果是单一的代理请求来解决跨域问题的场景,那么最佳方案是<strong>以前端的方案解决</strong></p><p>在现代前端框架脚手架中都集成了非常方便的proxy方案,以Create React App<a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/" target="_blank" rel="noopener">[2]</a>为例,就提供了http和https代理功能,在未eject的项目的<code>package.json</code>文件中增加一项配置即可。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">"proxy": "http://url.to.poxy",</span><br></pre></td></tr></table></figure></p><p>Creact React App在执行eject之后,与其他基于Webpack的项目都可以通过配置<code>webpack.config.js</code>,在<code>devServer</code>中新增proxy即可。<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">proxy: {</span><br><span class="line"> <span class="string">'/api'</span>: {</span><br><span class="line"> target: <span class="string">'http://localhost:3000'</span>,</span><br><span class="line"> pathRewrite: {<span class="string">'^/api'</span> : <span class="string">''</span>}</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>只有在遇到比较特殊的情况下,我们才会去配置Nginx</p><ol><li>项目请求多个URL,我们需要将某些地址做转发,以阻止其访问原来的地址。</li><li>直播数据流地址</li></ol><p>对于场景1,我们需要Nginx + Hosts的配合来实现。首先修改Hosts将原来的访问地址转发到本地,然后使用nginx来将其指向目标地址。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> listen 80;</span><br><span class="line"> server_name api.serverurl.com;</span><br><span class="line"> location / {</span><br><span class="line"> ...其他配置...</span><br><span class="line"> proxy_pass http://127.0.0.1:4300;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="2-前端项目部署问题"><a href="#2-前端项目部署问题" class="headerlink" title="2.前端项目部署问题"></a>2.前端项目部署问题</h2><p>前端线上部署到底要不要用Docker?<strong>纯静态前端项目</strong>完全不需要,因为前端项目更依赖网络资源和浏览器资源,因此常规场景下,<strong>CDN</strong>才是纯静态项目的部署方案。</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><p><a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/" target="_blank" rel="noopener">[1]Docker: Empowering App Development for Developers</a></p><p><a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/" target="_blank" rel="noopener">[2]Proxying API Requests in Development</a></p><p><a href="https://github.com/docker-library/mariadb/issues/82#issuecomment-339242301" target="_blank" rel="noopener">[3]Ctrl+C does not kill mysqld</a></p>]]></content>
<summary type="html">
<p><img src="/images/docker-nodejs-cover.png" alt="Docker with node"></p>
<span style="border-bottom: 1px dashed #389fff;"><span class="hint--top hint--rounded hint--info" aria-label="Empowering App Development for Developers" ontouchstart>Docker</span></span>是一项具有划时代意义的开源项目,它在服务端开发以及部署中大放异彩,本着对优秀技术的不断学习态度,非常建议将Docker作为你下一项学习的技术。<br><br>本文作为Docker在前端中应用的背景篇章,会对Docker及其优势做精简的介绍,然后列举一些前端开发、测试、部署过程中遇到的问题,希望可以借助Docker优雅地解决。<br><br>
</summary>
<category term="docker" scheme="http://www.thisjs.com/tags/docker/"/>
</entry>
<entry>
<title>使用MiTM的方式进行无线抓包调试</title>
<link href="http://www.thisjs.com/2018/04/07/using-man-in-the-middle-for-capture/"/>
<id>http://www.thisjs.com/2018/04/07/using-man-in-the-middle-for-capture/</id>
<published>2018-04-07T14:03:51.000Z</published>
<updated>2020-05-08T03:25:09.744Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><a href="https://cdn.thisjs.com/img/20180405173639.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/20180405173639.png" alt></a></p><h2 id="背景介绍"><a href="#背景介绍" class="headerlink" title="背景介绍"></a>背景介绍</h2><p>在开发的过程中,常常会遇到需要抓包,查看请求数据的情况。</p><ul><li>在Windows平台上,常用的软件是 <a href="https://www.telerik.com/fiddler" target="_blank" rel="noopener"><u>Fiddler</u></a></li><li>而在OS X系统中,要使用的软件则是 <a href="https://www.wireshark.org/" target="_blank" rel="noopener"><u>Wireshark</u></a></li><li>在Android平台上,使用的则是 <a href="https://play.google.com/store/apps/details?id=app.greyshirts.sslcapture" target="_blank" rel="noopener"><u>Packet Capture</u></a></li></ul><p>本来各自负责各自的平台,非常的和平。但是我们会遇到在一个平台上调试其他设备的数据请求情况。<br><a id="more"></a></p><p>比如在Windows上调试手机设备,我们可以在Fiddler中开启<strong>允许其他设备远程连接</strong>,然后在手机设备中设置VPN为电脑IP,这样手机的数据会通过电脑进行请求,这样我们就可以在Fiddler中抓取手机中的数据包了。</p><p>这项操作其实还可以简化,那就是不需要手机进行任何设置,我们就可以直接直接获取手机上的数据包。这时候,我们就可以使用神奇的 <strong>Ettercap</strong> 了,该软件可以实现一个中间人攻击的思路,进行抓包分析。</p><p><img src="https://cdn.thisjs.com/img/mitmblogeng-1.png" alt></p><blockquote><p>中间人攻击是指<strong>攻击</strong>者与通讯的两端分别建立独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制</p></blockquote><p>基于此,我们便不需要通过手机端的设置或者允许,我们在这个环节中,扮演攻击者,就可以快速的开始对其抓包分析了。</p><p>以下介绍在os X系统中进行中间人攻击抓包的方式。</p><h2 id="安装工具包"><a href="#安装工具包" class="headerlink" title="安装工具包"></a>安装工具包</h2><p>我们需要的几个工具如下:</p><ul><li>nmap (<em>端口扫描器</em>)</li><li>ettercap (<em>中间人攻击工具</em>)</li><li>Wireshark (<em>包分析工具</em>)<br>使用Homebrew安装这几个包非常方便。<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install nmap</span><br></pre></td></tr></table></figure></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install ettercap</span><br></pre></td></tr></table></figure><p>在安装Ettercap的时候可以选择带GUI界面的,只需要在后面追加<code>--with-gtk+</code> 参数即可。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install wireshark --with-qt</span><br></pre></td></tr></table></figure><h2 id="具体操作"><a href="#具体操作" class="headerlink" title="具体操作"></a>具体操作</h2><h3 id="1-查看局域网IP信息"><a href="#1-查看局域网IP信息" class="headerlink" title="1. 查看局域网IP信息"></a>1. 查看局域网IP信息</h3><p>首先,<strong>电脑要与手机在同一个局域网中</strong>。接下来,通过IP查看局域网使用的网段。在终端中,使用以下其中一个命令,查看IP地址。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ipconfig getifaddr en0 # 使用无线网连接</span><br><span class="line">ipconfig getifaddr en1 # 使用以太网连接</span><br><span class="line">ipconfig getifaddr en3 # 使用其他适配器连接</span><br></pre></td></tr></table></figure><h3 id="2-扫描同一局域网中的网络使用情况。"><a href="#2-扫描同一局域网中的网络使用情况。" class="headerlink" title="2. 扫描同一局域网中的网络使用情况。"></a>2. 扫描同一局域网中的网络使用情况。</h3><p>接下来我们使用namp查看同一网段下,有哪些设备在连接。会得到类似以下结果。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">$ nmap -sP 192.168.199.0/24</span><br><span class="line"></span><br><span class="line">Starting Nmap 7.60 ( https://nmap.org ) at 2018-04-05 18:10 CST</span><br><span class="line">Nmap scan report for Hiwifi.lan (192.168.199.1)</span><br><span class="line">Host is up (0.0030s latency).</span><br><span class="line">Nmap scan report for android-5ea1fea3b816a66.lan (192.168.199.153)</span><br><span class="line">Host is up (0.031s latency).</span><br><span class="line">Nmap scan report for zMBP.lan (192.168.199.169)</span><br><span class="line">Host is up (0.0021s latency).</span><br><span class="line">Nmap scan report for RedmiNote4X-hongmish.lan (192.168.199.198)</span><br><span class="line">Host is up (0.035s latency).</span><br><span class="line">Nmap scan report for iPad.lan (192.168.199.202)</span><br><span class="line">Host is up (0.037s latency).</span><br><span class="line">Nmap scan report for iPhone-7.lan (192.168.199.234)</span><br><span class="line">Host is up (0.0068s latency).</span><br><span class="line">Nmap done: 256 IP addresses (6 hosts up) scanned in 3.07 seconds</span><br></pre></td></tr></table></figure><p><del>当然,如果你通过手机的链接信息中,直接获取到手机IP的话,该步骤可以省略。</del></p><p>可以看到这里有多个设备在连接,而我本次需要测试的是<code>android…….lan (192.168.199.153)</code> 这一个IP。</p><h3 id="3-开始Ettercap"><a href="#3-开始Ettercap" class="headerlink" title="3. 开始Ettercap"></a>3. 开始Ettercap</h3><p>这里使用curses图形化界面启动,参数为<code>-C</code>,如果使用GUI界面的话,参数为<code>-G</code></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo ettercap -C</span><br></pre></td></tr></table></figure><p><a href="https://cdn.thisjs.com/img/etttercap-index.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/etttercap-index.png" alt></a></p><p>进入该界面后,依次选择<code>Sniff</code> -> <code>Unified sniffing...U</code> -> 输入网络类型值(<em>参考上面查询IP的参数,默认en0</em>) -> <code>Hosts</code> -> <code>Scan for hosts</code> -> <code>Hosts list</code></p><p><a href="https://cdn.thisjs.com/img/ettercap-hosts-list.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/ettercap-hosts-list.png" alt></a></p><p>这里可以看到扫描出来的同网段IP,在编写该文章的时候,有些其他设备已经离线了,因此本列表中扫描到的与使用namp扫描出来数量不同。但是如果记住了对应设备的IP,依旧可以使用。</p><p>这里,<code>192.168.199.1</code> 为网关,本次中间人攻击就是要实现欺骗设备 <code>192.168.199.153</code> 与网关 <code>192.168.199.1</code>之间的通讯。</p><p>接下来,需要将这两个IP分别加入嗅探的目标中,依次进入<code>Targets</code> -> <code>Select TARGET(s)</code> -> 在TARGET1中输入/<code>192.168.199.153//</code> TARGET1中输入/<code>192.168.199.1//</code></p><p><strong>备注:这里的Target格式为 <code>MAC/IPs/PORTs/</code></strong></p><p>这时,查看Current targets可以看到当前的目标列表。</p><p><a href="https://cdn.thisjs.com/img/ettercap-current-targets.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/ettercap-current-targets.png" alt></a></p><p>执行<code>MiTM</code> -> <code>ARP poisoning...</code> -> Parameters为空即可</p><p>这时,已经通过ARP欺骗的方式,成功开始了中间人攻击。可以通过<code>View</code> -> <code>Statistics</code>查看该设备的数据情况。</p><p><a href="https://cdn.thisjs.com/img/ettercap-statistics-view.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/ettercap-statistics-view.png" alt></a></p><h3 id="4-分析数据"><a href="#4-分析数据" class="headerlink" title="4. 分析数据"></a>4. 分析数据</h3><p>现在,我们已经成功监听了设备和网关之间数据。现在需要试着分析这些数据了。那么就要使用Wireshark了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo wireshark</span><br></pre></td></tr></table></figure><p>我们简单做一下筛选,只展示IP地址为192.168.199.153的POST请求。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ip.addr == 192.168.199.153 &amp;&amp; http.request.method == "POST"</span><br></pre></td></tr></table></figure></p><p>我在手机的一个非https网站(<a href="http://www.div.io)中进行了登录测试。可以在wireshark中获取到了POST的JSON数据信息。" target="_blank" rel="noopener">www.div.io)中进行了登录测试。可以在wireshark中获取到了POST的JSON数据信息。</a></p><p><a href="https://cdn.thisjs.com/img/wireshark-post-userinfo-data.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/wireshark-post-userinfo-data.png" alt></a></p><p>可以看到登录的用户名密码都是以明文的方式传输的,非常方便的进行了数据抓包调试。</p><h3 id="4-2-延伸"><a href="#4-2-延伸" class="headerlink" title="4.2 延伸"></a>4.2 延伸</h3><p>我们一直使用POST方式来获取该设备的登录信息,但是如果该设备已经登录过了,我们应该如何抓取到可以使用的信息呢?——当然是Cookie信息了。</p><p>将过滤的请求方式改为GET,在随便找到一个HTML页面之后,会发现其中带有Cookie信息。</p><p><a href="https://cdn.thisjs.com/img/wireshark-cookie-info.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/wireshark-cookie-info.png" alt></a></p><p>将该Cookie信息,保存下来,在任意浏览器中导入该Cookie信息,即可实现『登录』的效果。</p><p><a href="https://cdn.thisjs.com/img/wireshark-cookie-login.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/wireshark-cookie-login.png" alt></a></p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>至此,我们已经利用非常古老的的中间人攻击的方式,实现了不需要手机任何操作,就可以抓取手机数据包的功能。该方法在设置好之后,非常方便,可以快速切换设备,也可以多个设备同时抓包测试。</p><p>当然,我们也发现了其中的问题,那就是如果使用该方式对其他人的手机进行渗入,是不是就会导致数据泄露呢?理论上是会出现这种情况的,但是前面也提到,这是比较古老的攻击方式,只要设备上安装了任意的『XX安全卫士』『xx管家』,不要随便连接公共的WIFI,都可以保证我们的设备安全。</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://youyuejiajia.wordpress.com/2016/03/20/%E5%90%8C%E4%B8%80%E5%B1%80%E5%9F%9F%E7%BD%91%E7%8E%AF%E5%A2%83%E4%B8%8B%E7%9A%84arp%E6%AC%BA%E9%AA%97%E5%92%8C%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB%EF%BC%88mac%EF%BC%89/" target="_blank" rel="noopener">同一局域网环境下的arp欺骗和中间人攻击(mac)</a></li><li><a href="https://jingyan.baidu.com/article/c35dbcb0866b698916fcbc81.html" target="_blank" rel="noopener">wireshark怎么抓包 wireshark抓包详细图文教程</a></li><li><a href="https://www.youtube.com/watch?v=0a7o9FKzWOc" target="_blank" rel="noopener">Man In The Middle Attack! (ARP Poisoning) using ettercap to sniff login information</a></li><li><a href="http://blog.51cto.com/isnull/1738199" target="_blank" rel="noopener">使用ettercap实现中间人攻击</a></li><li><a href="http://www.freebuf.com/sectool/125104.html" target="_blank" rel="noopener">如何用Ettercap实现“中间人攻击”</a></li><li><a href="https://blog.csdn.net/xukai871105/article/details/31008635" target="_blank" rel="noopener">Wireshark学习笔记——如何快速抓取HTTP数据包</a></li><li><a href="http://blog.51cto.com/wxfplane/1749951" target="_blank" rel="noopener">Ettercap的arp攻击方法</a></li></ul>]]></content>
<summary type="html">
<p><a href="https://cdn.thisjs.com/img/20180405173639.png" target="_blank" rel="noopener"><img src="https://cdn.thisjs.com/img/20180405173639.png" alt></a></p>
<h2 id="背景介绍"><a href="#背景介绍" class="headerlink" title="背景介绍"></a>背景介绍</h2><p>在开发的过程中,常常会遇到需要抓包,查看请求数据的情况。</p>
<ul>
<li>在Windows平台上,常用的软件是 <a href="https://www.telerik.com/fiddler" target="_blank" rel="noopener"><u>Fiddler</u></a></li>
<li>而在OS X系统中,要使用的软件则是 <a href="https://www.wireshark.org/" target="_blank" rel="noopener"><u>Wireshark</u></a></li>
<li>在Android平台上,使用的则是 <a href="https://play.google.com/store/apps/details?id=app.greyshirts.sslcapture" target="_blank" rel="noopener"><u>Packet Capture</u></a></li>
</ul>
<p>本来各自负责各自的平台,非常的和平。但是我们会遇到在一个平台上调试其他设备的数据请求情况。<br>
</summary>
<category term="中间人" scheme="http://www.thisjs.com/tags/%E4%B8%AD%E9%97%B4%E4%BA%BA/"/>
<category term="开发" scheme="http://www.thisjs.com/tags/%E5%BC%80%E5%8F%91/"/>
<category term="抓包" scheme="http://www.thisjs.com/tags/%E6%8A%93%E5%8C%85/"/>
</entry>
<entry>
<title>使用JavaScript判断元素中是否含有某个样式</title>
<link href="http://www.thisjs.com/2018/03/28/use-javascript-to-judge-whether-the-element-contains-a-certain-style/"/>
<id>http://www.thisjs.com/2018/03/28/use-javascript-to-judge-whether-the-element-contains-a-certain-style/</id>
<published>2018-03-28T12:18:26.000Z</published>
<updated>2020-05-08T03:25:09.742Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/img/hero.png" alt="题图"></p><p>看到这个题目,立马想到的就是 <strong>element.classList.contains()</strong> 和 <strong>$(element).hasClass()</strong> 方法。</p><p>但是,在一些低版本浏览器中,classList无法使用,这个时候就可以自己实现类似jQuery的hasClass()函数。</p><a id="more"></a><p><img src="https://cdn.thisjs.com/img/classlist-can-use.png" alt="classList的兼容性"></p><p style="text-align: center;font-size: 10px;"> <i>classList的兼容性</i> </p><p>假如我们有如下测试元素</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">id</span>=<span class="string">"LL"</span> <span class="attr">class</span>=<span class="string">"a b hello-world"</span>></span><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure><h2 id="简单正则匹配法"><a href="#简单正则匹配法" class="headerlink" title="简单正则匹配法"></a>简单正则匹配法</h2><p>最开始我们找到的方法如下,即使用正则判断单词边界的方式判断是否存在className</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"> <span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> regExp = <span class="keyword">new</span> <span class="built_in">RegExp</span>(<span class="string">"\\b"</span>+className+<span class="string">"\\b"</span>, <span class="string">"gi"</span>);</span><br><span class="line"> <span class="keyword">return</span> regExp.test(element.className);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在样式的名字为hello-world之类的带有-连接符的情况,测试hello和world都会返回true,这并不满足我们的预期。</p><h2 id="IndexOf方法"><a href="#IndexOf方法" class="headerlink" title="IndexOf方法"></a>IndexOf方法</h2><p>同理,还有简单的使用 <strong>indexOf()</strong> 方法判断,也会导致这样的问题。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> element.className.indexOf(className) > <span class="number">-1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时候,不仅hello和world返回true,h/e/l/等单个字符都会返回true。</p><p>我们改进一下该方法:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> (<span class="string">" "</span> + element.className + <span class="string">" "</span> ).indexOf(<span class="string">" "</span>+className+<span class="string">" "</span>) > <span class="number">-1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在根据样式名称加” “的方式,判断一个元素是否含有该样式。在大部分的测试中,已经没有了问题。</p><p>但是!!!我们遇到了这样的神奇代码:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">id</span>=<span class="string">"tab"</span> <span class="attr">class</span>=<span class="string">"hello-worldabworld"</span>></span><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure><p>看上去和正常的代码没有太大区别,然而样式名称间的分隔符居然 <strong>是TAB,不是空格!</strong></p><p>那么使用空格作为分隔符检索的方式就失效了。不过我们可以在检索之前将内容格式化一下即可。将tab和多余的空格一起替换为空格即可。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> replacedName = element.className.replace(<span class="regexp">/\t*\s+/g</span>, <span class="string">' '</span>);</span><br><span class="line"> <span class="keyword">return</span> (<span class="string">" "</span> + replacedName + <span class="string">" "</span> ).indexOf(<span class="string">" "</span>+className+<span class="string">" "</span>) > <span class="number">-1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样,无论是遇到tab键,还是-连接符问题都可以很好的解决了。好方法 <strong>get√</strong></p><h2 id="优化正则匹配法"><a href="#优化正则匹配法" class="headerlink" title="优化正则匹配法"></a>优化正则匹配法</h2><p>另外,在查阅<a href="http://youmightnotneedjquery.com/" target="_blank" rel="noopener">YOU MIGHT NOT NEED JQUERY</a>时,遇到了如下方法。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">RegExp</span>(<span class="string">'(^| )'</span> + className + <span class="string">'( |$)'</span>, <span class="string">'gi'</span>).test(element.className);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该方法也是基于样式左右的空格,使用正则进行匹配,同时考虑到了样式开头如果没有空格的问题。但是依旧没有考虑到 <strong>TAB</strong> 作为分隔符的问题,我们可以将格式化的字符串作为匹配内容,也可以直接将该情况添加到正则中即可。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">RegExp</span>(<span class="string">'(^| |\\t)'</span> + className + <span class="string">'(\\t| |$)'</span>, <span class="string">'gi'</span>).test(element.className);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>好啦,现在又有一个新方法<strong>get√</strong></p><p>现在,非常方便的方法我们已经拥有了2个,那么我们开始天马行空(<del>不考虑性能</del>)的部分吧。</p><h2 id="边界匹配法变种"><a href="#边界匹配法变种" class="headerlink" title="边界匹配法变种"></a>边界匹配法变种</h2><p>同样是利用了样式边界的思路。我们将className字符串进行分割,然后使用for循环,进行查找。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> replacedName = element.className.replace(<span class="regexp">/\t*\s+/g</span>, <span class="string">' '</span>);</span><br><span class="line"> <span class="keyword">const</span> aClassName = replacedName.split(<span class="string">' '</span>)</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i < aClassName.length; i++) {</span><br><span class="line"> <span class="keyword">if</span> (className === aClassName[i]) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>至于为什么不用filter,includes等高阶函数,主要是为了照顾低版本浏览器,如果浏览器版本支持的话,还是用 <strong>classList.contains</strong> 吧。</p><h2 id="使用getElementsByClassName-法"><a href="#使用getElementsByClassName-法" class="headerlink" title="使用getElementsByClassName()法"></a>使用getElementsByClassName()法</h2><p>思路:根据className匹配元素数组,然后查找其中是否含有对应的元素。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hasClass</span>(<span class="params">element, className</span>) </span>{</span><br><span class="line"> <span class="keyword">var</span> aSameClassEle = <span class="built_in">document</span>.getElementsByClassName(className);</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">var</span> i=<span class="number">0</span>; i < aSameClassEle.length; i++) {</span><br><span class="line"> <span class="keyword">if</span>(aSameClassEle[i] === element) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>好了,这样在判断一个元素是否含有某个样式的时候,就有不同的方法可以用了。如果要做兼容性的话,只需要在前面加个判断if(element.classList),在有classList方法的浏览器中使用classList.contains()方法,其他浏览器使用这些方法中的一个即可。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p><a href="https://stackoverflow.com/questions/10960573/what-is-the-best-way-to-check-if-element-has-a-class" target="_blank" rel="noopener">What is the best way to check if element has a class?</a></p><p><a href="http://youmightnotneedjquery.com/" target="_blank" rel="noopener">YOU MIGHT NOT NEED JQUERY</a></p>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/img/hero.png" alt="题图"></p>
<p>看到这个题目,立马想到的就是 <strong>element.classList.contains()</strong> 和 <strong>$(element).hasClass()</strong> 方法。</p>
<p>但是,在一些低版本浏览器中,classList无法使用,这个时候就可以自己实现类似jQuery的hasClass()函数。</p>
</summary>
<category term="JavaScript" scheme="http://www.thisjs.com/tags/JavaScript/"/>
</entry>
<entry>
<title>使用Aria2为OneDrive增加离线下载功能</title>
<link href="http://www.thisjs.com/2018/03/07/use-aria2-increase-offline-download-function-for-onedrive/"/>
<id>http://www.thisjs.com/2018/03/07/use-aria2-increase-offline-download-function-for-onedrive/</id>
<published>2018-03-07T13:22:10.000Z</published>
<updated>2020-05-08T03:25:09.742Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/blog/Microsoft-OneDrive.jpg" alt="OneDrive"></p><p>本文介绍如何利用云服务器,为OneDrive增加离线下载功能。在充分利用云服务器空闲资源的同时,享受OneDrive强大的在线影音、文档编辑功能。</p><a id="more"></a><h2 id="太长不看的背景介绍"><a href="#太长不看的背景介绍" class="headerlink" title="太长不看的背景介绍"></a>太长不看的背景介绍</h2><details><br> <summary>还是点开看看吧</summary><br><br>微软推出的OneDrive拥有非常强大的功能,可以在多个终端管理自己的文件,国内访问速度非常快,下载文件可以达到满速,不会像百度云那样即使有百兆网络,也只能每秒100k左右的下载速度。如果无法愉快的使用GoogleDrive,那么OneDrive是一个非常不错的选择。<br><br>在国内的主流云盘中,都会有一个离线下载的功能,即允许用户添加下载任务,服务器会自动将资源下载到云盘中,用户过段时间就可以查看自己的文件了。<br><br>使用<code>离线下载</code>功能一般有如下两个目的<br><br>1. 将需要观看的影视资源,下载到服务器中,过段时间就可以直接在线观看 <em>(百度云)</em><br><br>2. 利用服务器的高速网络,将本来比较慢速的资源提前下载好,再取回本地 <em>(迅雷离线)</em><br><br>而本次为OneDrive搭建的离线下载功能,主要推荐的使用方法如下:<br><br>1. 将需要阅读的文档资源,离线下载到OneDrive中,随时随地查看<br>2. 将需要编辑的Office资源离线保存,使用Office Online编辑<br>3. 将喜欢的音乐离线保存,随时随地听<br>4. 将喜欢的影视作品离线保存,随时随地观看<br>5. <del>将墙外的一些影视、图像资源保存到自己的OneDrive中(需要国外服务器)</del><br><br>不推荐的使用方法:<br><br>1. 将喜欢的游戏离线保存到服务器,过段时间再下载到电脑上<br>2. 将需要安装的大型软件离线保存<br></details><h2 id="实现思路:"><a href="#实现思路:" class="headerlink" title="实现思路:"></a>实现思路:</h2><p>添加下载任务 => 将资源保存到服务器中 => 在服务器上将资源同步到OneDrive中 => 在OneDrive中查看资源</p><h2 id="准备材料:"><a href="#准备材料:" class="headerlink" title="准备材料:"></a>准备材料:</h2><ul><li>一台云服务器</li><li>OneDrive</li></ul><blockquote><p><strong>备注:</strong> 本次使用的云服务器安装的是<strong>CentOs 7.2</strong>系统</p></blockquote><h2 id="首先实现将服务器上的资源同步到OneDrive"><a href="#首先实现将服务器上的资源同步到OneDrive" class="headerlink" title="首先实现将服务器上的资源同步到OneDrive"></a>首先实现将服务器上的资源同步到OneDrive</h2><p>我们采用了<a href="https://github.com/skilion/onedrive" target="_blank" rel="noopener">Linux OneDrive</a>的开源项目。</p><h3 id="安装git用于Clone-GitHub上的资源"><a href="#安装git用于Clone-GitHub上的资源" class="headerlink" title="安装git用于Clone GitHub上的资源"></a>安装git用于Clone GitHub上的资源</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install git</span><br></pre></td></tr></table></figure><h3 id="开始安装onedrive"><a href="#开始安装onedrive" class="headerlink" title="开始安装onedrive"></a>开始安装onedrive</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># 安装依赖</span><br><span class="line">sudo yum install libcurl-devel</span><br><span class="line">sudo yum install sqlite-devel</span><br><span class="line">curl -fsS https://dlang.org/install.sh | bash -s dmd</span><br><span class="line"></span><br><span class="line"># 安装OneDrive</span><br><span class="line">git clone https://github.com/skilion/onedrive.git</span><br><span class="line">cd onedrive</span><br><span class="line">make</span><br><span class="line">sudo make install</span><br></pre></td></tr></table></figure><p>如果你在make过程中遇到了<code>dmd:命令未找到</code>错误,请先激活dmd,方法如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 激活</span><br><span class="line">source ~/dlang/dmd-2.079.0/activate</span><br><span class="line"># 取消激活</span><br><span class="line">deactivate</span><br></pre></td></tr></table></figure><p>安装完成之后,需要配置一下需要同步的内容,因为Onedrive默认会将服务器上所有的内容都同步下来,这样非常慢。</p><p>在onedrive 目录下执行以下三行命令,创建OneDrive配置文件<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mkdir -p ~/.config/onedrive</span><br><span class="line">cp ./config ~/.config/onedrive/config</span><br><span class="line">vim ~/.config/onedrive/config</span><br></pre></td></tr></table></figure></p><p>配置信息可以参考如下<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 本地同步的位置</span><br><span class="line">sync_dir = "/home/download/onedrive"</span><br><span class="line"># 符合以下规则的目录或者内容,将跳过同步</span><br><span class="line">skip_file = "影视|软件工具"</span><br></pre></td></tr></table></figure></p><ul><li>这里使用<code>/home/download/onedrive</code>作为同步目录,是为了给Aria2留出下载目录,可以根据自己需要随便修改</li><li>skip_file可以使用|添加多个规则</li></ul><p>接下来为OneDrive执行授权,在命令行中执行</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">onedrive</span><br></pre></td></tr></table></figure><p>会输出一个授权地址,复制授权地址到本地浏览器中打开,授权登录之后,将授权后的<strong>全部地址</strong>拷贝过来粘贴即可</p><p>从现在开始,只要执行OneDrive即可将本地的资源与服务端的内容同步。</p><p>但是我们希望在关闭SSH终端之后,依然可以自动同步。</p><p>官方推荐的方案是:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">systemctl --user enable onedrive</span><br><span class="line">systemctl --user start onedrive</span><br></pre></td></tr></table></figure><p>但是在Centos 7.2中会出现错误,因此可以使用<code>nohup</code>、<code>screen</code>等命令允许在关闭SSH终端之后,继续执行,执行以下命令即可</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nohup onedrive -m &</span><br></pre></td></tr></table></figure><p>现在,我们在服务器上的文件操作,都会同步到OneDrive中了。</p><p>如果需要结束后台同步,找到ID,结束即可<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[root@onedrive ~]# ps -ef|grep onedrive</span><br><span class="line">root 40504 1 0 12:21 ? 00:00:02 onedrive -m</span><br><span class="line">[root@onedrive ~]# kill 40504</span><br></pre></td></tr></table></figure></p><h2 id="安装Aria2实现远程下载"><a href="#安装Aria2实现远程下载" class="headerlink" title="安装Aria2实现远程下载"></a>安装Aria2实现远程下载</h2><p>首先安装Aria2</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install aria2</span><br></pre></td></tr></table></figure><p>配置<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mkdir /home/soft/aria2c -p</span><br><span class="line">touch /home/soft/aria2c/aria2.session</span><br><span class="line">vim /home/soft/aria2c/aria2.conf</span><br></pre></td></tr></table></figure></p><p>配置内容参考如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">#用户名</span><br><span class="line">#rpc-user=user</span><br><span class="line">#密码</span><br><span class="line">#rpc-passwd=passwd</span><br><span class="line">#上面的认证方式不建议使用,建议使用下面的token方式</span><br><span class="line">#设置加密的密钥</span><br><span class="line">rpc-secret=token</span><br><span class="line">#允许rpc</span><br><span class="line">enable-rpc=true</span><br><span class="line">#允许所有来源, web界面跨域权限需要</span><br><span class="line">rpc-allow-origin-all=true</span><br><span class="line">#允许外部访问,false的话只监听本地端口</span><br><span class="line">rpc-listen-all=true</span><br><span class="line">#RPC端口, 仅当默认端口被占用时修改</span><br><span class="line">rpc-listen-port=6800</span><br><span class="line">#最大同时下载数(任务数), 路由建议值: 3</span><br><span class="line">max-concurrent-downloads=3</span><br><span class="line">#断点续传</span><br><span class="line">continue=true</span><br><span class="line">#同服务器连接数</span><br><span class="line">max-connection-per-server=3</span><br><span class="line">#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要</span><br><span class="line">min-split-size=10M</span><br><span class="line">#单文件最大线程数, 路由建议值: 5</span><br><span class="line">split=10</span><br><span class="line">#下载速度限制</span><br><span class="line">max-overall-download-limit=0</span><br><span class="line">#单文件速度限制</span><br><span class="line">max-download-limit=0</span><br><span class="line">#上传速度限制</span><br><span class="line">max-overall-upload-limit=0</span><br><span class="line">#单文件速度限制</span><br><span class="line">max-upload-limit=0</span><br><span class="line">#断开速度过慢的连接</span><br><span class="line">#lowest-speed-limit=0</span><br><span class="line">#验证用,需要1.16.1之后的release版本</span><br><span class="line">#referer=*</span><br><span class="line">#文件保存路径, 默认为当前启动位置</span><br><span class="line">dir=/home/download/onedrive</span><br><span class="line">input-file=/home/soft/aria2c/aria2.session</span><br><span class="line">save-session=/home/soft/aria2c/aria2.session</span><br><span class="line">save-session-interval=60</span><br><span class="line">#文件缓存, 使用内置的文件缓存, 如果你不相信Linux内核文件缓存和磁盘内置缓存时使用, 需要1.16及以上版本</span><br><span class="line">#disk-cache=0</span><br><span class="line">#另一种Linux文件缓存方式, 使用前确保您使用的内核支持此选项, 需要1.15及以上版本(?)</span><br><span class="line">#enable-mmap=true</span><br><span class="line">#文件预分配, 能有效降低文件碎片, 提高磁盘性能. 缺点是预分配时间较长</span><br><span class="line">#所需时间 none < falloc ? trunc << prealloc, falloc和trunc需要文件系统和内核支持</span><br><span class="line">file-allocation=prealloc</span><br></pre></td></tr></table></figure><p><strong>几个关键内容:</strong></p><ul><li><code>rpc-secret</code>用于设置访问token</li><li><code>dir</code> 设置到OneDrive的目录</li></ul><p>启动Aria2服务:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">aria2c --conf-path=/home/soft/aria2c/aria2.conf -D</span><br></pre></td></tr></table></figure></p><p><strong>接下来安装UI界面</strong></p><p>UI界面采用<a href="https://github.com/ziahamza/webui-aria2" target="_blank" rel="noopener">webui-aria2</a></p><p>进入<code>/home/wwwroot</code>目录,克隆项目</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git clone https://github.com/ziahamza/webui-aria2.git</span><br></pre></td></tr></table></figure><p><strong>使用Nginx启动界面服务</strong></p><p>安装nginx<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># 安装</span><br><span class="line">sudo yum install nginx</span><br><span class="line"></span><br><span class="line"># 作为服务启动</span><br><span class="line">sudo systemctl start nginx</span><br></pre></td></tr></table></figure></p><h1 id="配置Nginx"><a href="#配置Nginx" class="headerlink" title="配置Nginx"></a>配置Nginx</h1><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim vim /etc/nginx/nginx.conf</span><br></pre></td></tr></table></figure><p>修改root目录到项目所在位置</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">server {</span><br><span class="line"> listen 80 default_server;</span><br><span class="line"> listen [::]:80 default_server;</span><br><span class="line"> server_name _;</span><br><span class="line"> root /home/wwwroot/webui-aria2;</span><br><span class="line"></span><br><span class="line"> # Load configuration files for the default server block.</span><br><span class="line"> include /etc/nginx/default.d/*.conf;</span><br><span class="line"></span><br><span class="line"> location / {</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>重启Nginx</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure><p>打开对应地址,发现已经成功了</p><p><img src="https://cdn.thisjs.com/blog/linksuccess.png" alt="成功界面"></p><p>测试下载文件</p><p><img src="https://cdn.thisjs.com/blog/downafile.png" alt="下载文件"></p><p>注意设置dir为OneDrive下的目录</p><p><img src="https://cdn.thisjs.com/blog/down-upload-success.png" alt="成功转存"></p><p>过一会儿在Onedrive上,就会发现文件已经成功转存。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p><a href="https://www.micronbot.com/Linux/aria2c.html" target="_blank" rel="noopener">Ubuntu 14 安装aria2c与web ui将老旧笔记本改装成下载机</a></p><p><a href="https://www.zhihu.com/question/20709809/answer/15939097" target="_blank" rel="noopener">Linux ssh状态下如何后台运行程序? - yegle的回答</a></p>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/blog/Microsoft-OneDrive.jpg" alt="OneDrive"></p>
<p>本文介绍如何利用云服务器,为OneDrive增加离线下载功能。在充分利用云服务器空闲资源的同时,享受OneDrive强大的在线影音、文档编辑功能。</p>
</summary>
<category term="centos" scheme="http://www.thisjs.com/tags/centos/"/>
</entry>
<entry>
<title>前后端分离的身份认证(一):JSON WEB TOKEN介绍</title>
<link href="http://www.thisjs.com/2017/09/25/using-the-jwt-for-api-to-add-authentication/"/>
<id>http://www.thisjs.com/2017/09/25/using-the-jwt-for-api-to-add-authentication/</id>
<published>2017-09-25T00:25:59.000Z</published>
<updated>2020-05-08T03:25:09.744Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/blog/software-720x380.jpg" alt="安全"></p><p>随着前端单页面APP的发展,前后端分离成为了现在开发的一种趋势,用户身份认证,发生了一系列的变化。传统的Cookie, Session验证方式存在跨域、扩展性的限制,Token验证方式成为了一个很好的替代选择。</p><a id="more"></a><p>这是一篇前导文章,之后会发布一系列关于JSON WEB TOKEN的项目实践。因此,这里将自己了解的相关知识和自己的一些观点汇集于此,以供查阅。</p><h1 id="传统验证方式的不足"><a href="#传统验证方式的不足" class="headerlink" title="传统验证方式的不足"></a>传统验证方式的不足</h1><blockquote><p>当然,传统验证方式并不是一文不值的,这里只是列出其中的不足,然后使用JSON WEB TOKEN来弥补其中的缺点。</p></blockquote><ul><li><strong>服务端性能消耗</strong> 每次与用户建立会话之后,都会在服务端保存该信息,例如:PHP Session是保存在文件中,而Java Session则是保存在内存中,随着用户量的提升,会大量占用服务器的资源。</li><li><strong>限制了分布式部署</strong> 当服务器处于分布式环境下,Session共享问题便随之而出,因此需要单独的服务器资源来解决Session共享问题。</li><li><strong>与Restful API的stateless冲突</strong> Restful思想正在逐步推广,而Session则引入了新的“状态”,与Restful思想矛盾。</li><li><strong>不方便移动APP的开发</strong> 使用Session验证方式,限制了原生Android,IOS APP的数据交互。</li><li><strong>XSS</strong> Session的提交方式,是将Session信息存储在Cookie中,提交到服务器端,因此很容易被客户端注入的javascript代码,截获Cookie信息。</li><li><strong>XSRF</strong> 基于Session的验证方式,有可能会被跨站请求伪造。</li></ul><h1 id="JSON-WEB-TOKEN"><a href="#JSON-WEB-TOKEN" class="headerlink" title="JSON WEB TOKEN"></a>JSON WEB TOKEN</h1><h2 id="简单介绍"><a href="#简单介绍" class="headerlink" title="简单介绍"></a>简单介绍</h2><p>JWT包含3部分数据信息,使用”.”分割,格式示例如下<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hhhhhhh.pppppp.sssss</span><br></pre></td></tr></table></figure></p><p>三部分信息分别为:</p><p><code>Signature</code>: 签名</p><h3 id="Header-头信息"><a href="#Header-头信息" class="headerlink" title="Header 头信息"></a>Header 头信息</h3><p>Header中一般包含Token类型和哈希算法,例如:<br><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{<span class="attr">"alg"</span>:<span class="string">"HS256"</span>,<span class="attr">"typ"</span>:<span class="string">"JWT"</span>}</span><br></pre></td></tr></table></figure></p><h3 id="Payload-有效荷载"><a href="#Payload-有效荷载" class="headerlink" title="Payload 有效荷载"></a>Payload 有效荷载</h3><p>Payload中包含声明信息,例如<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="string">"username"</span>: <span class="string">"admin"</span>,</span><br><span class="line"> <span class="string">"iat"</span>:<span class="number">1506320911</span>, <span class="comment">// 创建时间</span></span><br><span class="line"> <span class="string">"exp"</span>:<span class="number">1506324511</span> <span class="comment">// 过期时间</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><blockquote><p><strong>注意:</strong> Payload和Header中的信息是BASE64编码,不是加密,因此不要再payload中添加敏感信息</p></blockquote><h3 id="Signature-签名"><a href="#Signature-签名" class="headerlink" title="Signature 签名"></a>Signature 签名</h3><p>签名用来校验JWT的发送方属实,以及确认消息在传递途中没有被更改。<br>例如,使用HS256算法,签名将采用如下方式创建:<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">HS256(</span><br><span class="line"> base64UrlEncode(header) + <span class="string">"."</span> + </span><br><span class="line"> base64UrlEncode(payload), </span><br><span class="line"> secret)</span><br></pre></td></tr></table></figure></p><p>这里对于jwt的介绍只是简单介绍,详细关于JWT的信息可以参阅<a href="http://www.jianshu.com/p/576dbf44b2ae" target="_blank" rel="noopener">[2]</a>,<a href="https://github.com/smilingsun/blog/issues/1" target="_blank" rel="noopener">[3]</a>这两篇文章。</p><h2 id="JWT的优点"><a href="#JWT的优点" class="headerlink" title="JWT的优点"></a>JWT的优点</h2><ul><li><strong>可以实现跨域请求</strong> 因为JWT不依赖于Cookie,它可以添加在请求的<code>Header</code>,<code>body</code>,<code>参数</code>中,因此只要服务器允许跨域请求,那么带有授权Token的客户端,可以任意访问不同服务器下的服务,因此,非常适合SSO单点登录系统。</li><li><strong>减少服务器消耗</strong> 服务器在生成Token之后,就将Token返回给客户端,客户端保存Token用于下次请求。服务端不进行储存Token,只验证Token,减少了服务器的消耗。同时,带有Token的请求在请求不同服务时,不用考虑是与哪台服务器生成的Session问题,非常适用于云服务。</li><li><strong>通用性</strong> 因为JSON的通用性,所以JWT可以在Nodejs,JAVA,PHP等不同平台使用。</li></ul><h2 id="JWT示意图"><a href="#JWT示意图" class="headerlink" title="JWT示意图"></a>JWT示意图</h2><img src="http://www.plantuml.com/plantuml/svg/IqmkoIzIU3vbnREExLosjWel-hO_xPyMaf-TcgTWQyi5aL3GjLDuDgVtQTV2fpDNM2aydxdxwTwfN70HDEcNgtfUh6-4A7goV-6pnlKmOsnqaTMzs3_bwUnvOAkWolBoIr8LSbBJIgmKdkoV-tJj3B1aJDxJZWtFvgnvEcS3o8LFzku76eVVXKztB7koemkivlqUe6ksFzlOv1ZWIS39OFNutFfiwePdlUi0"><h2 id="安全问题"><a href="#安全问题" class="headerlink" title="安全问题"></a>安全问题</h2><ul><li>Payload中的内容是BASE64编码,如果需要,可以在编码前,对内容进行加密</li><li>生成签名的密钥除了妥善保存之外,可以使用<strong>动态密钥</strong>,在启动服务时生成密钥,这样就不会被轻易获取</li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><p>[1] <a href="http://lion1ou.win/2017/01/18/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io" target="_blank" rel="noopener">前后端分离之JWT用户认证</a></p><p>[2] <a href="http://www.jianshu.com/p/576dbf44b2ae" target="_blank" rel="noopener">什么是 JWT – JSON WEB TOKEN</a></p><p>[3] <a href="https://github.com/smilingsun/blog/issues/1" target="_blank" rel="noopener">适用于前后端分离的下一代认证机制 —— JSON Web Token</a></p><p>[4] <a href="http://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2016&filename=XDJS201616018" target="_blank" rel="noopener">基于JSON Web Token的无状态账户系统的设计</a></p><p>[5] <a href="http://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2016&filename=SZJT201602087" target="_blank" rel="noopener">JWT认证技术及其在WEB中的应用</a></p>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/blog/software-720x380.jpg" alt="安全"></p>
<p>随着前端单页面APP的发展,前后端分离成为了现在开发的一种趋势,用户身份认证,发生了一系列的变化。传统的Cookie, Session验证方式存在跨域、扩展性的限制,Token验证方式成为了一个很好的替代选择。</p>
</summary>
<category term="jwt" scheme="http://www.thisjs.com/tags/jwt/"/>
<category term="前端" scheme="http://www.thisjs.com/tags/%E5%89%8D%E7%AB%AF/"/>
</entry>
<entry>
<title>一个前端眼中的斐波那契数列</title>
<link href="http://www.thisjs.com/2017/09/21/my-view-of-fibonacci/"/>
<id>http://www.thisjs.com/2017/09/21/my-view-of-fibonacci/</id>
<published>2017-09-21T07:29:01.000Z</published>
<updated>2020-05-08T03:25:09.738Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/blog/fibonacci.jpeg" alt="斐波那契数字游戏"></p><p>大学时期,每学习一门新编程语言,就会被要求重新实现一遍斐波那契数列算法。那时,常用的方法即递归法和递推法。那时只对结果感兴趣,只要结果出来了,其他的仿佛就无所谓了。</p><a id="more"></a><p>现在,成为一名前端工程师之后,再看这个问题,要考虑的情况,也变得更广泛了,可以用的方法也更多了。所以现在希望应用自己了解的知识,再计算一次斐波那契数列。</p><p><img src="https://cdn.thisjs.com/blog/46c741e0cab6469d7e1c54bc054947c9_b.jpg" alt="格式"></p><p>首先,斐波那契数列从第0个开始,分别是<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……</span><br></pre></td></tr></table></figure></p><p>因此要根据该规则,返回第n个斐波那契数</p><h1 id="递归法"><a href="#递归法" class="headerlink" title="递归法"></a>递归法</h1><p>首先,先把之前的递归方法再再再实现一遍。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>)</span>{</span><br><span class="line"> <span class="keyword">if</span>(n === <span class="number">1</span> || n === <span class="number">0</span> ) <span class="keyword">return</span> n;</span><br><span class="line"> <span class="keyword">return</span> fibonacci(n<span class="number">-1</span>) + fibonacci(n<span class="number">-2</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>递归的思路很简单,即不断调用自身方法,直到n为1或0之后,开始一层层返回数据。</p><p>使用递归计算大数字时,性能会特别低,原因有以下2点:</p><p>① 在递归过程中,每创建一个新函数,解释器都会创建一个新的函数栈帧,并且压在当前函数的栈帧上,这就形成了调用栈。因而,当递归层数过大之后,就可能造成调用栈占用内存过大或者溢出。</p><p>另外,我们在return前加入以下语句,打印一下递归的计算过程。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">console</span>.log(<span class="string">`fibonacci(<span class="subst">${n<span class="number">-1</span>}</span>) + fibonacci(<span class="subst">${n<span class="number">-2</span>}</span>)`</span>)</span><br></pre></td></tr></table></figure></p><p>当,n为6时,得到的结果为<br><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">fibonacci(<span class="number">5</span>) + fibonacci(<span class="number">4</span>)</span><br><span class="line">fibonacci(<span class="number">4</span>) + fibonacci(<span class="number">3</span>)</span><br><span class="line">fibonacci(<span class="number">3</span>) + fibonacci(<span class="number">2</span>)</span><br><span class="line">fibonacci(<span class="number">2</span>) + fibonacci(<span class="number">1</span>)</span><br><span class="line">fibonacci(<span class="number">1</span>) + fibonacci(<span class="number">0</span>)</span><br><span class="line">fibonacci(<span class="number">1</span>) + fibonacci(<span class="number">0</span>)</span><br><span class="line">fibonacci(<span class="number">2</span>) + fibonacci(<span class="number">1</span>)</span><br><span class="line">fibonacci(<span class="number">1</span>) + fibonacci(<span class="number">0</span>)</span><br><span class="line">fibonacci(<span class="number">3</span>) + fibonacci(<span class="number">2</span>)</span><br><span class="line">fibonacci(<span class="number">2</span>) + fibonacci(<span class="number">1</span>)</span><br><span class="line">fibonacci(<span class="number">1</span>) + fibonacci(<span class="number">0</span>)</span><br><span class="line">fibonacci(<span class="number">1</span>) + fibonacci(<span class="number">0</span>)</span><br></pre></td></tr></table></figure></p><p>② 分析可以发现,递归造成了大量的重复计算。</p><p>递归的以上两种缺点,我们可以使用<strong>尾调用优化</strong>和<strong>递推法</strong>来解决。</p><h1 id="尾调用优化"><a href="#尾调用优化" class="headerlink" title="尾调用优化"></a>尾调用优化</h1><p>首先,什么是尾调用。</p><blockquote><p><strong>尾调用</strong>是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。WikiPad<a href="https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8" target="_blank" rel="noopener">[1]</a></p></blockquote><p>用代码来说,就是B函数的返回值被A函数返回了。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">B</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">A</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> B(); <span class="comment">// return 1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>什么时候会执行尾调用优化呢?</p><p>在ES6中,strict模式下,满足以下条件,尾调用优化会开启,此时引擎不会创建一个新的栈帧,而是清除当前栈帧的数据并复用:<a href="http://www.cnblogs.com/xfshen/p/6001581.html" target="_blank" rel="noopener">[2]</a></p><ol><li><p>尾调用函数不需要访问当前栈帧中的变量</p></li><li><p>尾调用返回后,函数没有语句需要继续执行</p></li><li><p>尾调用的结果就是函数的返回值</p></li></ol><p>举例说明:</p><p>以下函数即可开启尾调用优化<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">doA</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> doB();</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>以下函数无法开启尾调用优化<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">doC</span>(<span class="params"></span>) </span>{</span><br><span class="line"> doD(); <span class="comment">// 尾调用的结果不是函数的返回值</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">doE</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span> + doF(); <span class="comment">// 尾调用返回后,函数仍然有语句要运行</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们使用尾调用优化,重写函数。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">'use strict'</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n, current, next</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span>(n === <span class="number">1</span>) <span class="keyword">return</span> next;</span><br><span class="line"> <span class="keyword">if</span>(n === <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">return</span> fibonacci(n - <span class="number">1</span>, next, current + next);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>我们可以使用如下方法调用该函数<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fibonacci(<span class="number">6</span>, <span class="number">0</span>, <span class="number">1</span>);</span><br></pre></td></tr></table></figure></p><p>这时,在执行该函数时,由于<code>引擎不会创建一个新的栈帧,而是清除当前栈帧的数据并复用</code>,就不会出现内存占用过大的情况了。</p><p>得益于ES2015的<code>默认参数</code>特性,我们可以将以上函数改写。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">'use strict'</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n, current = <span class="number">0</span>, next = <span class="number">1</span></span>) </span>{</span><br><span class="line"> <span class="keyword">if</span>(n === <span class="number">1</span>) <span class="keyword">return</span> next;</span><br><span class="line"> <span class="keyword">if</span>(n === <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">return</span> fibonacci(n - <span class="number">1</span>, next, current + next);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>这样在调用时,只需要传递一个参数即可<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fibonacci(<span class="number">6</span>);</span><br></pre></td></tr></table></figure></p><p>这时,我们在return语句之前,打印其调用过程<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">console</span>.log(<span class="string">`fibonacci(<span class="subst">${n}</span>, <span class="subst">${next}</span>, <span class="subst">${current + next}</span>)`</span>);</span><br></pre></td></tr></table></figure></p><p>会发现调用过程大大减少<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">fibonacci(6, 1, 1)</span><br><span class="line">fibonacci(5, 1, 2)</span><br><span class="line">fibonacci(4, 2, 3)</span><br><span class="line">fibonacci(3, 3, 5)</span><br><span class="line">fibonacci(2, 5, 8)</span><br></pre></td></tr></table></figure></p><blockquote><p><strong>注意:</strong> 在ES 2015中,只有在strict模式下,才会开启尾调用优化</p></blockquote><h1 id="递推法"><a href="#递推法" class="headerlink" title="递推法"></a>递推法</h1><p>递推法的思路非常符合计算思路,即,f(0)开始,一个个计算下去,直到加到我们需要的值。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> aFi = <span class="keyword">new</span> <span class="built_in">Array</span>(n+<span class="number">1</span>);</span><br><span class="line"> aFi[<span class="number">0</span>] = <span class="number">0</span>; aFi[<span class="number">1</span>] = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">2</span>; i<= n; i++){</span><br><span class="line"> aFi[i] = aFi[i<span class="number">-1</span>] + aFi[i<span class="number">-2</span>];</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> aFi[n];</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>这里我们定义了一个数组来容纳<strong>所有</strong>的计算结果,但是实际上,我们仅仅需要<code>f(n-1)</code>和<code>f(n-2)</code>两个值,因此我们可以用两个变量存储这两个值来减少内存开销。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> current = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">let</span> next = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">let</span> temp;</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">let</span> i = <span class="number">0</span>; i < n; i++){</span><br><span class="line"> temp = current;</span><br><span class="line"> current = next;</span><br><span class="line"> next += temp;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>基于此思路,我们对此使用不同的方法进行改写。</p><h2 id="变种一-ES2015-结构赋值法"><a href="#变种一-ES2015-结构赋值法" class="headerlink" title="变种一 ES2015 结构赋值法"></a>变种一 ES2015 结构赋值法</h2><p>结构赋值<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment" target="_blank" rel="noopener">[3]</a>允许我们将值直接从数组中提取到不同变量中。因此我们可以用结构赋值,省略temp中间变量。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> current = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">let</span> next = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">let</span> i = <span class="number">0</span>; i < n; i++){</span><br><span class="line"> [current, next] = [next, current + next];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="变种二-while的–-gt-形式"><a href="#变种二-while的–-gt-形式" class="headerlink" title="变种二 while的–>形式"></a>变种二 while的–>形式</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> current = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">let</span> next = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">while</span>(n --> <span class="number">0</span>){</span><br><span class="line"> [current, next] = [next, current + next];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的<code>--></code>并不是limit运算符,这只是两个操作符的缩写。即–和>。</p><p>这里的<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span>(n --> <span class="number">0</span>){}</span><br></pre></td></tr></table></figure></p><p>可以改写为<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span>(n><span class="number">0</span>) {n--}</span><br></pre></td></tr></table></figure></p><p>这里解释一下为什么是这样。</p><p>n先进行–操作,n自身的值变为n-1。</p><p>然后使用n–的<strong>返回值</strong>与0进行比较大小,而<strong>n–的返回值是n</strong>。</p><p>所以,只要<code>n>0</code>,那么就会执行<code>n--</code></p><h2 id="变种三-高级函数"><a href="#变种三-高级函数" class="headerlink" title="变种三 高级函数"></a>变种三 高级函数</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>)</span>{</span><br><span class="line"><span class="keyword">let</span> seed = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">return</span> [...Array(n)].reduce(<span class="function"><span class="params">p</span> =></span> {</span><br><span class="line"><span class="keyword">const</span> temp = p + seed; </span><br><span class="line">seed = p;</span><br><span class="line"><span class="keyword">return</span> temp;</span><br><span class="line">},<span class="number">0</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里利用Reduce高级函数<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce" target="_blank" rel="noopener">[5]</a>的特性,第一个参数为上一次计算的值,因此这里的pp保存F(n-1)值,而seed则保存F(n-2)的值。</p><h2 id="变种四-Generator生成器"><a href="#变种四-Generator生成器" class="headerlink" title="变种四 Generator生成器"></a>变种四 Generator生成器</h2><p><br>Generator是ES2015的新特性,得益于该特性,我们可以使用生成器方法,制作一个斐波那契数列生成器。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span>* <span class="title">fibonacci</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">let</span> current = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">let</span> next = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">yield</span> current;</span><br><span class="line"> <span class="keyword">yield</span> next;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> [current, next] = [next, current + next];</span><br><span class="line"> <span class="keyword">yield</span> next;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>使用方法即<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> fibo = fibonacci();</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>; i< <span class="number">10</span>;i ++){</span><br><span class="line"> <span class="built_in">console</span>.log(fibo.next().value);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>但是这一个生成器并不是可以生成指定n的函数,详细实现方法,以及可能遇到的坑可以参阅这篇文章<a href="http://www.zcfy.cc/article/473" target="_blank" rel="noopener">我从用 JavaScript 写斐波那契生成器中学到的令人惊讶的 7 件事</a>。</p><h1 id="通项公式法"><a href="#通项公式法" class="headerlink" title="通项公式法"></a>通项公式法</h1><p><img src="https://cdn.thisjs.com/blog/503d269759ee3d6db9e6f1e046166d224f4adefd.jpg" alt="通项公式"></p><p>斐波那契的通项公式证明,可以参阅<a href="https://baike.baidu.com/item/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97" target="_blank" rel="noopener">百度百科</a>。比照该公式,可以实现如下代码<a href="https://segmentfault.com/a/1190000007115162" target="_blank" rel="noopener">[8]</a>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fibonacci</span>(<span class="params">n</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> SQRT_FIVE = <span class="built_in">Math</span>.sqrt(<span class="number">5</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Math</span>.round(<span class="number">1</span>/SQRT_FIVE * (<span class="built_in">Math</span>.pow(<span class="number">0.5</span> + SQRT_FIVE/<span class="number">2</span>, n) - <span class="built_in">Math</span>.pow(<span class="number">0.5</span> - SQRT_FIVE/<span class="number">2</span>, n)));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以上,便是我当前学习到的解决方案。如果你有更好的解决方案,或者对一些方法有异议,也希望可以在评论区不吝赐教。</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><p>[1] <a href="https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8" target="_blank" rel="noopener">尾调用 - 维基百科,自由的百科全书</a></p><p>[2] <a href="http://www.cnblogs.com/xfshen/p/6001581.html" target="_blank" rel="noopener">《理解 ES6》阅读整理:函数(Functions)(八)Tail Call Optimization</a></p><p>[3] <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment" target="_blank" rel="noopener">解构赋值 - JavaScript | MDN</a></p><p>[4] <a href="https://www.zhihu.com/question/65662523/answer/233405655" target="_blank" rel="noopener">关于–>的运算顺序问题</a></p><p>[5] <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce" target="_blank" rel="noopener">Array.prototype.reduce() - JavaScript | MDN</a></p><p>[6] <a href="https://baike.baidu.com/item/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97" target="_blank" rel="noopener">斐波那契数列_百度百科</a></p><p>[7] <a href="http://www.zcfy.cc/article/473" target="_blank" rel="noopener">我从用 JavaScript 写斐波那契生成器中学到的令人惊讶的 7 件事</a></p><p>[8] <a href="https://segmentfault.com/a/1190000007115162" target="_blank" rel="noopener">斐波那契数列求和的js方案以及优化</a></p><p>[9] <a href="http://www.ruanyifeng.com/blog/2015/04/tail-call.html" target="_blank" rel="noopener">尾调用优化 - 阮一峰的网络日志</a></p><p>[10] <a href="http://www.cnblogs.com/myoleole/archive/2012/12/01/2797709.html" target="_blank" rel="noopener">斐波那契数列算法优化</a></p>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/blog/fibonacci.jpeg" alt="斐波那契数字游戏"></p>
<p>大学时期,每学习一门新编程语言,就会被要求重新实现一遍斐波那契数列算法。那时,常用的方法即递归法和递推法。那时只对结果感兴趣,只要结果出来了,其他的仿佛就无所谓了。</p>
</summary>
<category term="Javascript" scheme="http://www.thisjs.com/tags/Javascript/"/>
</entry>
<entry>
<title>探索无头浏览器的应用场景</title>
<link href="http://www.thisjs.com/2017/09/16/explore-the-headless-browser-application-scenario/"/>
<id>http://www.thisjs.com/2017/09/16/explore-the-headless-browser-application-scenario/</id>
<published>2017-09-16T02:27:09.000Z</published>
<updated>2020-05-08T03:25:09.736Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/blog/automateexpenses.jpg" alt="自动化操作"></p><p><a href="https://en.wikipedia.org/wiki/Headless_browser" target="_blank" rel="noopener">Headless browser</a>会带给我非常亲切的感觉,因为总能让我回想起<a href="http://www.anjian.com/" target="_blank" rel="noopener">按键精灵</a>和<a href="https://www.autohotkey.com/" target="_blank" rel="noopener">AutoHotKey</a>这两款非常实用的小工具。</p><a id="more"></a><p>能有这样的感觉,大概是因为它们都操作基于用户界面,但是在运行时,可以让用户忽略用户界面吧。</p><p>无头浏览器有哪些实用的使用场景呢?</p><h1 id="1-自动化E2E测试"><a href="#1-自动化E2E测试" class="headerlink" title="1. 自动化E2E测试"></a>1. 自动化E2E测试</h1><p>常用的E2E测试工具如<code>nightwatch</code>,<code>Karma</code>,都支持无头浏览器,这样在测试时,无需打开UI界面,即可完成对应的测试内容。</p><h1 id="2-解决登录问题"><a href="#2-解决登录问题" class="headerlink" title="2. 解决登录问题"></a>2. 解决登录问题</h1><p>在使用一些网站API时,会遇到一些网站需要先登录的情况。</p><p>标准的网站,允许使用Post方法发送用户名及密码,返回对应的Token,之后的请求即可使用该Token,这时候我们可以直接使用<a href="https://www.npmjs.com/package/request" target="_blank" rel="noopener">Request</a>包即可。</p><p>但是遇到一些网站,并没有对外开放API接口,每次请求数据是通过登录后的Cookis进行判断。这时候我们也可以使用Request,截取<code>Set-Cookie</code> 头部信息即可。</p><p>但是,还有一些网站,在登录时候,需要添加服务器发送给客户端的安全码,这个时候如果单单使用<code>Request</code>就有些费力了。<br><img src="https://cdn.thisjs.com/random.png" alt="识别码"></p><p>这时,使用无头浏览器可以很好的解决这个问题,这里使用Google Chrome的<a href="https://github.com/GoogleChrome/puppeteer" target="_blank" rel="noopener">puppeteer</a>编写例子。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">'puppeteer'</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () => {</span><br><span class="line"> <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.launch();</span><br><span class="line"> <span class="keyword">const</span> page = <span class="keyword">await</span> browser.newPage();</span><br><span class="line"> <span class="keyword">await</span> page.goto(<span class="string">'http://youruri/api?redirect=anotheruri'</span>); <span class="comment">// 进入对应的登录页面</span></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 这里测试的页面用户名input为autofocus属性</span></span><br><span class="line"><span class="comment"> * 不同页面可以使用选择器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">await</span> page.type(<span class="string">'username'</span>);</span><br><span class="line"> <span class="keyword">await</span> page.press(<span class="string">'Tab'</span>);</span><br><span class="line"> <span class="keyword">await</span> page.type(<span class="string">'password'</span>);</span><br><span class="line"> <span class="keyword">await</span> page.press(<span class="string">'Enter'</span>);</span><br><span class="line"> page.on(<span class="string">'response'</span>, res => {</span><br><span class="line"> <span class="keyword">if</span>(res.hasOwnProperty(<span class="string">'headers'</span>)){</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">let</span> key <span class="keyword">in</span> res.headers){</span><br><span class="line"> <span class="keyword">if</span>(key === <span class="string">'set-cookie'</span>){</span><br><span class="line"> <span class="comment">// 在这里进行获取Cookie信息操作</span></span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">await</span> page.waitForNavigation();</span><br><span class="line"> browser.close();</span><br><span class="line">})();</span><br></pre></td></tr></table></figure><p>简单几步,就可以获取到对应的Cookie信息,将该Cookie信息保存起来,就可以在其他请求中使用了。</p><h1 id="3-网络爬虫"><a href="#3-网络爬虫" class="headerlink" title="3. 网络爬虫"></a>3. 网络爬虫</h1><p>在爬取一些网页时,对于普通的网页,我们可以直接使用Request, 发送GET请求,获取页面内容,然后进行分析,获取其中的数据。</p><p>但是这里有一个缺陷,即我们只能获取到网页的HTML内容,无法获取到页面XHR获取到的内容,即我们无法执行页面的JS。<br>这就导致我们无法获取那些动态加载的数据,甚至大部分单页面APP。</p><p>这时无头浏览器的作用就非常明显了,无头浏览器即没有用户界面的浏览器,浏览器功能全部存在,因此执行JS也不在话下。</p><h2 id="例:"><a href="#例:" class="headerlink" title="例:"></a>例:</h2><p>我们使用Request,get请求知乎某用户的关注列表<a href="https://www.zhihu.com/people/zhang-shu-yuan-18/following" target="_blank" rel="noopener">https://www.zhihu.com/people/zhang-shu-yuan-18/following</a>,然后使用<a href="https://www.npmjs.com/package/cheerio" target="_blank" rel="noopener">Cheerio</a>获取关注的用户名。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> request = <span class="built_in">require</span>(<span class="string">'request'</span>);</span><br><span class="line"><span class="keyword">const</span> cheerio = <span class="built_in">require</span>(<span class="string">'cheerio'</span>);</span><br><span class="line"></span><br><span class="line">request.get(<span class="string">'https://www.zhihu.com/people/zhang-shu-yuan-18/following'</span>, (error, res, body) => {</span><br><span class="line"> <span class="keyword">const</span> $ = cheerio.load(body);</span><br><span class="line"> $(<span class="string">'.UserLink-link'</span>).each(<span class="function">(<span class="params">index, item</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log($(item).text());</span><br><span class="line"> })</span><br><span class="line">})</span><br></pre></td></tr></table></figure></p><p>会发现只有三个结果<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">柳佳</span><br><span class="line">李沫霖</span><br><span class="line">Jim Liu</span><br></pre></td></tr></table></figure></p><p>这是因为剩余的内容需要Ajax加载,这时,我们使用<a href="https://github.com/GoogleChrome/puppeteer" target="_blank" rel="noopener">puppeteer</a>进行获取。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">'puppeteer'</span>);</span><br><span class="line"><span class="keyword">const</span> cheerio = <span class="built_in">require</span>(<span class="string">'cheerio'</span>);</span><br><span class="line">(<span class="keyword">async</span> <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.launch();</span><br><span class="line"> <span class="keyword">const</span> page = <span class="keyword">await</span> browser.newPage();</span><br><span class="line"> <span class="keyword">await</span> page.goto(<span class="string">'https://www.zhihu.com/people/zhang-shu-yuan-18/following'</span>);</span><br><span class="line"> <span class="keyword">let</span> pageContent =<span class="keyword">await</span> page.content();</span><br><span class="line"> <span class="keyword">const</span> $ = cheerio.load(pageContent);</span><br><span class="line"> $(<span class="string">'.UserLink-link'</span>).each(<span class="function">(<span class="params">index, item</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log($(item).text());</span><br><span class="line"> })</span><br><span class="line"> browser.close();</span><br><span class="line">})()</span><br></pre></td></tr></table></figure><p>这时,一整页的数据全部加在进来了,打印<code>$('.UserLink-link').length</code>会发现有40条数据。</p><blockquote><p>当然,如果找到了该页面加载用户的API,直接使用该API请求数据是最方便的了</p></blockquote><h1 id="4-SSR服务端渲染"><a href="#4-SSR服务端渲染" class="headerlink" title="4. SSR服务端渲染"></a>4. SSR服务端渲染</h1><p>由于有些搜索引擎在抓取页面的时候,并不执行页面里的JS,因此会导致很多单页面APP的内容无法被搜索引擎更好的收录。</p><p>这时,可以使用无头浏览器,做服务端渲染。在判断访问来路为<code>XXX-spider</code>之后,将页面内容,在服务端使用无头浏览器执行一遍,将执行后的HTML内容,返回给搜索引擎,这样搜索引擎就可以获取到执行JS后的内容了。</p><blockquote><p>最后,这里收集了一些常用的无头浏览器</p></blockquote><ul><li><a href="http://phantomjs.org/" target="_blank" rel="noopener">Phantomjs</a> Webkit内核的无头浏览器,广泛应用于E2E测试</li><li><a href="https://slimerjs.org/" target="_blank" rel="noopener">SlimerJS</a> 类似Phantomjs,使用Gecko内核</li><li><a href="https://github.com/GoogleChrome/puppeteer" target="_blank" rel="noopener">puppeteer</a> Google Chrome团队推出的,可以直接在node中使用</li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://stackoverflow.com/questions/18539491/headless-browser-and-scraping-solutions" target="_blank" rel="noopener">Headless Browser and scraping</a></li><li><a href="http://www.infoq.com/cn/news/2014/02/nightwatch" target="_blank" rel="noopener">使用Nightwatch进行端到端测试</a></li><li><a href="http://python.jobbole.com/86415/" target="_blank" rel="noopener">运用phantomjs无头浏览器破解四种反爬虫技术</a></li></ul>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/blog/automateexpenses.jpg" alt="自动化操作"></p>
<p><a href="https://en.wikipedia.org/wiki/Headless_browser" target="_blank" rel="noopener">Headless browser</a>会带给我非常亲切的感觉,因为总能让我回想起<a href="http://www.anjian.com/" target="_blank" rel="noopener">按键精灵</a>和<a href="https://www.autohotkey.com/" target="_blank" rel="noopener">AutoHotKey</a>这两款非常实用的小工具。</p>
</summary>
<category term="无头浏览器" scheme="http://www.thisjs.com/tags/%E6%97%A0%E5%A4%B4%E6%B5%8F%E8%A7%88%E5%99%A8/"/>
<category term="爬虫" scheme="http://www.thisjs.com/tags/%E7%88%AC%E8%99%AB/"/>
</entry>
<entry>
<title>移动应用展示收纳效果</title>
<link href="http://www.thisjs.com/2017/04/24/mobile-app-showcase-effect/"/>
<id>http://www.thisjs.com/2017/04/24/mobile-app-showcase-effect/</id>
<published>2017-04-24T08:51:55.000Z</published>
<updated>2020-05-08T03:25:09.738Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/demo/phone/appshowcase.png" alt="手机展示效果" title="手机展示效果"></p><p>最近在阅读<a href="(https://tympanus.net/codrops/2013/08/01/3d-effect-for-mobile-app-showcase/)">Codrops</a>时,遇到了一个不错的手机APP效果,想着可以用在视差滚动宣传页中,便尝试着也制作了一下。</p><a id="more"></a><p>整体思路不是很复杂,即旋转整体,展示图片</p><p>主要用到的CSS3属性有</p><ul><li><a href="(https://developer.mozilla.org/zh-CN/docs/Web/CSS/perspective)">perspective</a> // 这是为了让手机旋转的时候,有3D效果</li><li><a href="(https://developer.mozilla.org/zh-CN/docs/Web/CSS/perspective-origin)">perspective-origin</a> // 设置观察消失点</li><li>transition // 设置过渡效果</li><li>transform // 变换</li></ul><h2 id="整个手机设备的transform效果"><a href="#整个手机设备的transform效果" class="headerlink" title="整个手机设备的transform效果"></a>整个手机设备的transform效果</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">transform: rotateY(50deg) rotateX(20deg) translateZ(-$dv-height/2 + $depth);</span><br></pre></td></tr></table></figure><h2 id="宣传图像的变换效果"><a href="#宣传图像的变换效果" class="headerlink" title="宣传图像的变换效果"></a>宣传图像的变换效果</h2><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">@for</span> $i from <span class="number">1</span> through <span class="number">5</span> {</span><br><span class="line">.expand-view .page-#{$i} {</span><br><span class="line">transform: translateZ($depth/2 + $screengap * $i);</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="为图像添加鼠标滑过效果"><a href="#为图像添加鼠标滑过效果" class="headerlink" title="为图像添加鼠标滑过效果"></a>为图像添加鼠标滑过效果</h2><blockquote><p>在页面展开之后,鼠标滑过每个图层,其他图层透明度为0.1</p></blockquote><ol><li>获取鼠标滑过的图层的兄弟节点,设置他们的style</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Array</span>.prototype.filter.call(el.parentNode.children, <span class="function"><span class="keyword">function</span>(<span class="params">child</span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> child !== el;</span><br><span class="line">});</span><br></pre></td></tr></table></figure><ol start="2"><li>为鼠标滑过的图层添加<code>active</code> Class,通过css :not()选择器,选择非<code>.active</code> Class的元素,设置他们的透明度</li></ol><p>这里采用的是<strong>第二种</strong>方法。</p><h2 id="最终效果预览"><a href="#最终效果预览" class="headerlink" title="最终效果预览"></a>最终效果预览</h2><script async src="//jsrun.net/YxkKp/embed/all/light/"></script>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/demo/phone/appshowcase.png" alt="手机展示效果" title="手机展示效果"></p>
<p>最近在阅读<a href="(https://tympanus.net/codrops/2013/08/01/3d-effect-for-mobile-app-showcase/)">Codrops</a>时,遇到了一个不错的手机APP效果,想着可以用在视差滚动宣传页中,便尝试着也制作了一下。</p>
</summary>
<category term="css3" scheme="http://www.thisjs.com/tags/css3/"/>
</entry>
<entry>
<title>OS X中使用brew管理多个node版本</title>
<link href="http://www.thisjs.com/2017/04/18/osx-using-the-brew-to-manage-multiple-node-version/"/>
<id>http://www.thisjs.com/2017/04/18/osx-using-the-brew-to-manage-multiple-node-version/</id>
<published>2017-04-18T13:58:16.000Z</published>
<updated>2020-05-08T03:25:09.739Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/thisjs/nodejs_header-03e90275ca.svg" width="1050" alt="node list" align="center"></p><p>在使用Node进行开发的时候,有时候需要在不同的Node版本中进行切换。首先,跨平台的<a href="https://github.com/creationix/nvm" target="_blank" rel="noopener"><strong>NVM</strong>(Node Version Manager)</a>可以帮助我们很好的进行版本管理。</p><p>在OS X系统中,<strong>HomeBrew</strong>也是一个很方便的Node版本切换工具。</p><p>以下是使用HomeBrew管理Node的一些操作</p><a id="more"></a><h2 id="查看可用版本"><a href="#查看可用版本" class="headerlink" title="查看可用版本"></a>查看可用版本</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew search node</span><br></pre></td></tr></table></figure><p>即可看到当前可用的版本</p><p> <img src="https://cdn.thisjs.com/thisjs/brew%20list.png" width="680" alt="node list" align="center"></p><h2 id="安装需要版本"><a href="#安装需要版本" class="headerlink" title="安装需要版本"></a>安装需要版本</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install node@6</span><br></pre></td></tr></table></figure><p>如果需要6.x.x中最新版本,可以使用</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install node6-lts</span><br></pre></td></tr></table></figure><h2 id="切换版本"><a href="#切换版本" class="headerlink" title="切换版本"></a>切换版本</h2><ul><li>首先取消当前版本</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew unlink node</span><br></pre></td></tr></table></figure><ul><li>切换到需要的版本</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew link node@6 [--force]</span><br></pre></td></tr></table></figure><blockquote><p>注意:在切换版本的时候,可能会需要用到 –force命令,强制执行。</p></blockquote><p>在切换版本时,可能会有一些文件需要删除,注意提示内容,执行即可</p><p><strong>例如:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ rm <span class="string">'/usr/local/include/node/pthread-fixes.h'</span></span><br></pre></td></tr></table></figure><h2 id="检查切换是否成功"><a href="#检查切换是否成功" class="headerlink" title="检查切换是否成功"></a>检查切换是否成功</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ node -v</span><br></pre></td></tr></table></figure><h2 id="添加Path"><a href="#添加Path" class="headerlink" title="添加Path"></a>添加Path</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">which</span> node <span class="comment"># => /usr/local/bin/node</span></span><br><span class="line">$ <span class="built_in">export</span> NODE_PATH=<span class="string">'/usr/local/lib/node_modules'</span> <span class="comment"># <--- add this ~/.bashrc</span></span><br></pre></td></tr></table></figure><h2 id="删除所有Node"><a href="#删除所有Node" class="headerlink" title="删除所有Node"></a>删除所有Node</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ brew uninstall node</span><br><span class="line"><span class="comment"># 或者 `brew uninstall --force node` 移除所有版本</span></span><br><span class="line">$ brew prune</span><br><span class="line">$ rm -f /usr/<span class="built_in">local</span>/bin/npm /usr/<span class="built_in">local</span>/lib/dtrace/node.d</span><br><span class="line">$ rm -rf ~/.npm</span><br></pre></td></tr></table></figure><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>出现这个问题是在安装Yarn的时候遇到的。在使用<code>HomeBrew</code>安装<code>Yarn</code>的时候,需要<code>brew link node</code>,但是link之后的版本是最新的7.9。</p><p>本来新版本的Node带来了更多的特性,然而在使用<code>ng-cli</code>生成的项目中,打包的时候,<code>node-sass</code>一直出问题,因此需要工具来管理Node版本,固有此总结。</p><p>同时,Yarn也是一个很方便的包管理器,推荐在安装包时尝试一下<code>Yarn</code></p><h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><blockquote><p><a href="https://github.com/yarnpkg/yarn/issues/1505" target="_blank" rel="noopener"><code>brew link node</code> required for Yarn install #1505</a></p></blockquote><blockquote><p><a href="http://stackoverflow.com/questions/11177954/how-do-i-completely-uninstall-node-js-and-reinstall-from-beginning-mac-os-x" target="_blank" rel="noopener">How do I completely uninstall Node.js, and reinstall from beginning (Mac OS X)</a></p></blockquote><blockquote><p><a href="https://apple.stackexchange.com/questions/171530/how-do-i-downgrade-node-or-install-a-specific-previous-version-using-homebrew" target="_blank" rel="noopener">How do I downgrade node or install a specific previous version using homebrew?</a></p></blockquote>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/thisjs/nodejs_header-03e90275ca.svg" width="1050" alt="node list" align="center"></p>
<p>在使用Node进行开发的时候,有时候需要在不同的Node版本中进行切换。首先,跨平台的<a href="https://github.com/creationix/nvm" target="_blank" rel="noopener"><strong>NVM</strong>(Node Version Manager)</a>可以帮助我们很好的进行版本管理。</p>
<p>在OS X系统中,<strong>HomeBrew</strong>也是一个很方便的Node版本切换工具。</p>
<p>以下是使用HomeBrew管理Node的一些操作</p>
</summary>
<category term="node" scheme="http://www.thisjs.com/tags/node/"/>
<category term="osx" scheme="http://www.thisjs.com/tags/osx/"/>
</entry>
<entry>
<title>前端命令行工具代理设置</title>
<link href="http://www.thisjs.com/2017/04/17/the-front-command-line-tools-proxy-settings/"/>
<id>http://www.thisjs.com/2017/04/17/the-front-command-line-tools-proxy-settings/</id>
<published>2017-04-17T06:39:18.000Z</published>
<updated>2020-05-08T03:25:09.740Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/thisjs/tumblr_inline_nn489p271Z1t68bpr_500.png" alt="npm"></p><p>在开发过程中,有时候需要安装墙外一些包文件,前端常用的包管理工具有node/bower/sass,以及需要git发布内容,解决方案一般有三种:</p><ul><li>使用国内镜像</li><li>设置代理</li><li>本地安装</li></ul><a id="more"></a><h1 id="国内镜像"><a href="#国内镜像" class="headerlink" title="国内镜像"></a>国内镜像</h1><p>使用国内镜像的好处是省去搭建梯子的过程,利用国内连接速度优势,快速下载</p><h2 id="NPM"><a href="#NPM" class="headerlink" title="NPM"></a>NPM</h2><blockquote><p>使用淘宝镜像 <code>https://registry.npm.taobao.org</code></p></blockquote><p>安装时启用</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --registry=https://registry.npm.taobao.org</span><br></pre></td></tr></table></figure><p>设置全局镜像</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm config <span class="built_in">set</span> registry < registry url ></span><br></pre></td></tr></table></figure><blockquote><p><a href="http://npm.taobao.org/" target="_blank" rel="noopener">使用CNPM</a></p></blockquote><p>cnpm可以很快的安装完包,但是有些项目,比如Angular,有些包可能会安装出现问题。</p><h2 id="Gem"><a href="#Gem" class="headerlink" title="Gem"></a>Gem</h2><blockquote><p>使用<a href="http://gems.ruby-china.org/" target="_blank" rel="noopener">Ruby-China</a></p></blockquote><p>设置镜像</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/</span><br></pre></td></tr></table></figure><h1 id="设置代理"><a href="#设置代理" class="headerlink" title="设置代理"></a>设置代理</h1><p>设置代理需要有代理服务,保证可以访问到对应的地址</p><h2 id="NPM-1"><a href="#NPM-1" class="headerlink" title="NPM"></a>NPM</h2><p>设置代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm config <span class="built_in">set</span> proxy http://server:port</span><br><span class="line">npm config <span class="built_in">set</span> https-proxy http://server:port</span><br></pre></td></tr></table></figure><p>取消代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm config delete proxy</span><br><span class="line">npm config delete https-proxy</span><br></pre></td></tr></table></figure><p>查看代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm config list</span><br></pre></td></tr></table></figure><p>如果代理不支持https,修改npm存放package的网站地址为非https地址</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm config <span class="built_in">set</span> registry <span class="string">"http://registry.npmjs.org/"</span></span><br></pre></td></tr></table></figure><h2 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h2><p>设置代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git config --global http.proxy http://server:port</span><br><span class="line"></span><br><span class="line">$ git config --global https.proxy http://server:port</span><br></pre></td></tr></table></figure><p>删除代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global --<span class="built_in">unset</span> http.proxy</span><br><span class="line">git config --global --<span class="built_in">unset</span> https.proxy</span><br></pre></td></tr></table></figure><p>查看代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global --get http.proxy</span><br><span class="line">git config --global --get https.proxy</span><br></pre></td></tr></table></figure><h2 id="Gem-1"><a href="#Gem-1" class="headerlink" title="Gem"></a>Gem</h2><p>设置代理</p><blockquote><p>安装时加上 –http-proxy 参数</p></blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gem install --http-proxy http://proxy:port sass</span><br></pre></td></tr></table></figure><h2 id="bower"><a href="#bower" class="headerlink" title="bower"></a>bower</h2><p>设置代理</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 修改 .bowerrc 文件(如无则新增):</span><br><span class="line"></span><br><span class="line">{</span><br><span class="line"> "proxy": "http://proxy:port",</span><br><span class="line"> "https-proxy": "http://proxy:port"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="apm"><a href="#apm" class="headerlink" title="apm"></a>apm</h2><p>apm是github出品的Atom编辑器的包管理器,它默认使用npm的设置,如果需要单独设置</p><p>设置代理</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ apm config <span class="built_in">set</span> https-proxy https://server:port</span><br></pre></td></tr></table></figure><p>查看设置</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ apm config list</span><br></pre></td></tr></table></figure><h2 id="设置命令行代理"><a href="#设置命令行代理" class="headerlink" title="设置命令行代理"></a>设置命令行代理</h2><blockquote><p>可以将命令行直接设置代理,这样命令行里的数据链接都会通过代理</p></blockquote><ul><li><strong>windows</strong></li></ul><p>这种设置只对本命令行窗口启用</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">set</span> http_proxy=http://proxy:port</span><br><span class="line"></span><br><span class="line"><span class="comment"># 用户名密码则输入</span></span><br><span class="line"><span class="built_in">set</span> http_proxy_user=< username ></span><br><span class="line"><span class="built_in">set</span> http_proxy_pass=< password ></span><br></pre></td></tr></table></figure><ul><li><strong>OS X</strong></li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo networksetup -setwebproxy <span class="string">"Ethernet"</span> http://proxy port</span><br></pre></td></tr></table></figure><h1 id="本地安装"><a href="#本地安装" class="headerlink" title="本地安装"></a>本地安装</h1><h2 id="NPM-2"><a href="#NPM-2" class="headerlink" title="NPM"></a>NPM</h2><blockquote><p>对于有些几乎没有依赖的包,可以通过直接从node_modules文件夹中拷贝的方法实现安装</p></blockquote><h2 id="Gem-2"><a href="#Gem-2" class="headerlink" title="Gem"></a>Gem</h2><ol><li>首先通过(rubygems)[<a href="https://rubygems.org/]" target="_blank" rel="noopener">https://rubygems.org/]</a> 下载对应的包</li><li>通过本地安装</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gem install --<span class="built_in">local</span> sass.gem</span><br></pre></td></tr></table></figure><h1 id="关于OS-X的代理"><a href="#关于OS-X的代理" class="headerlink" title="关于OS X的代理"></a>关于OS X的代理</h1><p>OS X上有很多其他的下载需要代理,那么我们可以使用<code>Proxychains</code> 配合 <code>shadowsocks</code> 实现每个命令都可以使用代理</p><ol><li>安装工具</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install proxychains-ng</span><br></pre></td></tr></table></figure><ol start="2"><li>设置 Proxychains 安装目录下的 <code>proxychains.conf</code> 文件</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim /usr/<span class="built_in">local</span>/etc/proxychains.conf</span><br></pre></td></tr></table></figure><p>在<code>[ProxyList]</code>下加入<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">socks5 127.0.0.1 1080</span><br></pre></td></tr></table></figure></p><ol start="3"><li>使用 <code>proxychains4</code> 为命令代理</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">proxychains4 curl https://www.twitter.com/</span><br><span class="line">proxychains4 git push origin master</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/thisjs/tumblr_inline_nn489p271Z1t68bpr_500.png" alt="npm"></p>
<p>在开发过程中,有时候需要安装墙外一些包文件,前端常用的包管理工具有node/bower/sass,以及需要git发布内容,解决方案一般有三种:</p>
<ul>
<li>使用国内镜像</li>
<li>设置代理</li>
<li>本地安装</li>
</ul>
</summary>
<category term="node" scheme="http://www.thisjs.com/tags/node/"/>
<category term="git" scheme="http://www.thisjs.com/tags/git/"/>
</entry>
<entry>
<title>实现一个URL中的中文路径英文化工具</title>
<link href="http://www.thisjs.com/2017/03/21/realize-the-path-of-chinese-into-english/"/>
<id>http://www.thisjs.com/2017/03/21/realize-the-path-of-chinese-into-english/</id>
<published>2017-03-21T02:21:15.000Z</published>
<updated>2020-05-08T03:25:09.739Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/url-address.jpg?imageView/2/w/500" alt="题图"></p><p>在使用<a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>创建博客的时候,所有的博客内容为了与主题内容相同,使用了中文命名,导致生成的链接也是中文目录。</p><a id="more"></a><p>最近将博客迁移到Centos中之后,由于中文文件名导致404问题。所以决定将所有的中文文件名改为对应的英文名,希望仿照<a href="http://www.w3cplus.com/" target="_blank" rel="noopener">W3CPlus</a>的命名方式</p><ul><li>对应英文翻译</li><li>所有字母小写</li><li>空格变为- </li></ul><p><img src="https://cdn.thisjs.com/github/en-demo.png" alt="W3C命名方式"></p><p>最初使用手动方式将文件名拷贝到<a href="http://translate.google.cn/" target="_blank" rel="noopener">谷歌翻译</a>,得到翻译结果之后,将翻译结果变为小写,将空格替换为”-“,由于重复操作太多,所以决定写个小工具,来进行后面的2步操作。</p><p>由于<code>Vue</code>的双向数据绑定的便利,所以使用Vue对数据进行监听修改,采用<a href="https://lodash.com/" target="_blank" rel="noopener">loadash</a>来进行数据处理</p><h3 id="所有字母小写"><a href="#所有字母小写" class="headerlink" title="所有字母小写"></a>所有字母小写</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_.lowerCase(str)</span><br></pre></td></tr></table></figure><h3 id="空格变为"><a href="#空格变为" class="headerlink" title="空格变为-"></a>空格变为-</h3><p>使用正则表达式替换即可</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">str.replace(<span class="regexp">/\s+/g</span>, <span class="string">"-"</span>)</span><br></pre></td></tr></table></figure><p>但是这时候可能会出现一个问题,在字符串前后都有空格的时候<code>" Hello World "</code>,会生成<code>"-hello-world-"</code>,这不是我们需要的</p><p>所以在替换之前将首尾空格去掉即可</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_.trim(str)</span><br></pre></td></tr></table></figure><p>所以初版就是这样子的</p><p><img src="https://cdn.thisjs.com/github/first-result.png" alt="第一版功能"></p><h2 id="添加翻译、复制功能"><a href="#添加翻译、复制功能" class="headerlink" title="添加翻译、复制功能"></a>添加翻译、复制功能</h2><p>但是这样还是需要切换页面进行复制粘贴,因此可以直接将翻译过程省略,首先想到的是<a href="http://fanyi.youdao.com/openapi" target="_blank" rel="noopener">有道翻译api</a>,申请完key之后,发现如果使用json方式获取数据,那么会有跨域问题,只能使用jsonp方式,但是vue官方推荐的<a href="https://github.com/mzabriskie/axios" target="_blank" rel="noopener">axios</a>并不支持jsonp,所以采用<a href="https://github.com/pagekit/vue-resource" target="_blank" rel="noopener">vue-resource</a>。</p><img src="http://www.plantuml.com/plantuml/svg/AyaioKbLUB5--xEDJS-sDZvVqSt55PQPMgwWQwSGdbYId95Qf62by6pgXcUzwvvDsVcqVmON5qmSgCGtjIGZFwKOgEJ5XkRdKrOycxRpsUQfN5mmSLsWccTpEbysJdw-V_UBzHD12Y5sWKtauaAHr4MO2BFpnVufJsVFGp8GsKweinRiURgd_sdFD1HLO4zOAJpTEFNvabqGXGfS0000"><p>但是每次输入框发生变化,就会触发一次数据请求,而有道翻译每天提供<strong>1000</strong>次请求,所以,使用lodash的debounce方法,减少请求次数,在输入结束500ms之后,再发起请求。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">getTrans:_.debounce(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{},<span class="number">500</span>)</span><br></pre></td></tr></table></figure><p>最后使用<a href="https://clipboardjs.com/" target="_blank" rel="noopener">clipboardjs</a>为格式化的结果提供一个复制功能。这样就更加方便了。</p><script async src="//jsrun.net/yPkKp/embed/all/light/"></script><blockquote><p>但是这样还是有些不方便,因为仍然需要选择文件名,然后粘贴,再复制粘贴,多了很多重复操作,所以可以使用Node的文档读取与操作功能实现该功能。</p></blockquote>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/url-address.jpg?imageView/2/w/500" alt="题图"></p>
<p>在使用<a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>创建博客的时候,所有的博客内容为了与主题内容相同,使用了中文命名,导致生成的链接也是中文目录。</p>
</summary>
<category term="vue" scheme="http://www.thisjs.com/tags/vue/"/>
<category term="lodash" scheme="http://www.thisjs.com/tags/lodash/"/>
</entry>
<entry>
<title>通过node搭建shadowsocks服务器</title>
<link href="http://www.thisjs.com/2017/02/28/build-shadowsocks-server-through-the-node/"/>
<id>http://www.thisjs.com/2017/02/28/build-shadowsocks-server-through-the-node/</id>
<published>2017-02-28T09:14:14.000Z</published>
<updated>2020-05-08T03:25:09.736Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/github/ss.png" alt="shadowsocks"></p><p>在企业开发项目时候,有时需要通过外网网络访问内部服务器,这时候可以通过搭建一个shadowsocks服务器,然后通过shadowsocks客户端连接服务器,访问内网内容。</p><p>安装过程如下,服务器已经安装好node服务,并且可以使用<code>npm</code>命令</p><a id="more"></a><ol><li><p>全局安装shadowsocks模块</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g shadowsocks</span><br></pre></td></tr></table></figure></li><li><p>找到安装目录<code>C:\Users\Administrator\AppData\Roaming\npm\node_modules\shadowsocks</code>,编辑<code>config.json</code>文件</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"> { </span><br><span class="line"> "server":"IP地址", </span><br><span class="line"> "server_port":443, </span><br><span class="line"> "local_address":"127.0.0.1", </span><br><span class="line"> "local_port":1080, </span><br><span class="line"> "password":"密码", </span><br><span class="line"> "timeout":600, </span><br><span class="line"> "method":"rc4-md5"</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>启动服务,执行命令</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssserver</span><br></pre></td></tr></table></figure></li></ol>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/github/ss.png" alt="shadowsocks"></p>
<p>在企业开发项目时候,有时需要通过外网网络访问内部服务器,这时候可以通过搭建一个shadowsocks服务器,然后通过shadowsocks客户端连接服务器,访问内网内容。</p>
<p>安装过程如下,服务器已经安装好node服务,并且可以使用<code>npm</code>命令</p>
</summary>
<category term="node" scheme="http://www.thisjs.com/tags/node/"/>
</entry>
<entry>
<title>让vs2015忽略某个文件夹</title>
<link href="http://www.thisjs.com/2017/02/24/let-vs-2015-ignore-a-folder/"/>
<id>http://www.thisjs.com/2017/02/24/let-vs-2015-ignore-a-folder/</id>
<published>2017-02-24T02:09:28.000Z</published>
<updated>2020-05-08T03:25:09.738Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/thisjs/images.png" width="500" alt="Vs2015"></p><p>在使用vs2015开发前端项目的时候,将整个网站项目引用进解决方案之后,软件会扫描全部的文件夹。<br>但是<code>node_modules</code>,<code>bower_components</code>的文件夹嵌套,会严重影响扫描的速度</p><a id="more"></a><p>暂时的解决方案是,将不需要被扫描的文件夹隐藏即可,但是要取消掉隐藏二级目录</p>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/thisjs/images.png" width="500" alt="Vs2015"></p>
<p>在使用vs2015开发前端项目的时候,将整个网站项目引用进解决方案之后,软件会扫描全部的文件夹。<br>但是<code>node_modules</code>,<code>bower_components</code>的文件夹嵌套,会严重影响扫描的速度</p>
</summary>
<category term="vs2015" scheme="http://www.thisjs.com/tags/vs2015/"/>
<category term="开发工具" scheme="http://www.thisjs.com/tags/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>解决流式布局元素高度不统一导致布局不整齐问题</title>
<link href="http://www.thisjs.com/2016/12/01/to-solve-the-fluid-layout-element-height-not-unified-in-layout-and-tidy/"/>
<id>http://www.thisjs.com/2016/12/01/to-solve-the-fluid-layout-element-height-not-unified-in-layout-and-tidy/</id>
<published>2016-12-01T09:47:19.000Z</published>
<updated>2020-05-08T03:25:09.741Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p>在页面布局中,在使用float布局大量相同属性元素的时候,如果元素的高度不固定,某个元素的高度过高,可能会阻挡元素的“流动”,会出现如图的情况。</p><p><img src="https://cdn.thisjs.com/github/probfloat-long.png" alt="float"></p><a id="more"></a><p>这时我们只要保证后面的高度也大于或等于该元素高度,即可让后面的元素流动到前面</p><p><img src="https://cdn.thisjs.com/github/probfloat-long2.png" alt="float-succ"></p><p>所以一种常见的解决方案是</p><h2 id="瀑布流"><a href="#瀑布流" class="headerlink" title="瀑布流"></a>瀑布流</h2><p><img src="https://cdn.thisjs.com/github/probwaterfull.png" alt="waterfull"></p><p>瀑布流的实现方法,网上已经有大量教程,详情参阅</p><p> <a href="https://www.jb51.net/article/34141.htm" target="_blank" rel="noopener">脚本之家—实现瀑布流</a></p><p> <a href="https://www.wufangbo.com/tag/%E7%80%91%E5%B8%83%E6%B5%81js/" target="_blank" rel="noopener">前端开发—瀑布流的实现方法</a></p><h2 id="将同一列设置为统一高度"><a href="#将同一列设置为统一高度" class="headerlink" title="将同一列设置为统一高度"></a>将同一列设置为统一高度</h2><p> 有时候,我们可能并不需要瀑布流的布局,因为在展示某些数据的时候,会显得比较混乱。</p><p> <img src="https://cdn.thisjs.com/github/probfloat-long3.png" alt="float3"></p><p> 要实现该效果,只需如下几步</p><ol><li>获取所有元素</li><li>获取相同offsetTop值的元素,即同一行的元素</li><li>比较同一行元素的高度,取最大的height值,赋给每一个元素即可</li></ol><p>如果遇到使用ng-repeat生成的元素无法获取自动高度问题,可以参考如下文章</p><blockquote><p><a href="https://mrxf.github.io/2016/12/01/%E8%A7%A3%E5%86%B3%E6%97%A0%E6%B3%95%E8%8E%B7%E5%8F%96ngRepeat%E7%94%9F%E6%88%90%E5%85%83%E7%B4%A0%E6%A0%B7%E5%BC%8F%E7%9A%84%E9%97%AE%E9%A2%98/" target="_blank" rel="noopener">解决无法获取ngRepeat生成元素样式的问题</a></p></blockquote>]]></content>
<summary type="html">
<p>在页面布局中,在使用float布局大量相同属性元素的时候,如果元素的高度不固定,某个元素的高度过高,可能会阻挡元素的“流动”,会出现如图的情况。</p>
<p><img src="https://cdn.thisjs.com/github/probfloat-long.png" alt="float"></p>
</summary>
<category term="javascript" scheme="http://www.thisjs.com/tags/javascript/"/>
</entry>
<entry>
<title>解决无法获取ngRepeat生成元素样式的问题</title>
<link href="http://www.thisjs.com/2016/12/01/the-problem-of-unable-to-get-ng-repeat-generating-element-styles/"/>
<id>http://www.thisjs.com/2016/12/01/the-problem-of-unable-to-get-ng-repeat-generating-element-styles/</id>
<published>2016-12-01T07:44:08.000Z</published>
<updated>2020-05-08T03:25:09.741Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-tag-hint@0.2.1/css/hint.min.css"><p><img src="https://cdn.thisjs.com/github/ngrepeatAngularJS-header-image.png" alt="ng"></p><p>在使用Angular进行开发的过程中,使用ng-repeat生成多个元素之后,如果元素的宽高是auto,那么我们在使用<br><code>css()</code>、<code>getComputedStyle</code>、<code>offsetHeight</code>或者<code>clientHeight</code>都会获取到0,我们无法获取到元素的实际高度。</p><p>这是因为DOM的渲染是异步的,导致计算元素属性信息在DOM渲染完成之前就已经完成了,因此无法获取到DOM真正渲染结束之后属性。</p><p>在Angular中,我们可以使用以下几种方法进行处理</p><a id="more"></a><h2 id="使用-watch方法来进行脏值检查"><a href="#使用-watch方法来进行脏值检查" class="headerlink" title="使用$watch方法来进行脏值检查"></a>使用$watch方法来进行脏值检查</h2><p>当元素信息发生改变之后,将最新的数据赋值给变量即可</p><p>例如:</p><p><strong>Directive</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">myApp.directive(<span class="string">'heightBind'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> scope: {</span><br><span class="line"> heightValue: <span class="string">'='</span></span><br><span class="line"> },</span><br><span class="line"> link: <span class="function"><span class="keyword">function</span>(<span class="params">$scope, $element</span>) </span>{</span><br><span class="line"> $scope.$watch(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> $scope.heightValue = $element.height();</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p><strong>HTML</strong><br><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">< <span class="attr">div</span> <span class="attr">height-bind</span> <span class="attr">height-value</span>=<span class="string">"containerHeight"</span>></span><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure></p><h2 id="当然,也可以使用-apply来完成同样的事情"><a href="#当然,也可以使用-apply来完成同样的事情" class="headerlink" title="当然,也可以使用$apply来完成同样的事情"></a>当然,也可以使用<code>$apply</code>来完成同样的事情</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"> setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> $scope.containerHeight = $(<span class="string">'#container'</span>).height()</span><br><span class="line"> $scope.$apply();</span><br><span class="line">}, <span class="number">0</span>);</span><br></pre></td></tr></table></figure><h2 id="使用自带的脏值检查方法"><a href="#使用自带的脏值检查方法" class="headerlink" title="使用自带的脏值检查方法"></a>使用自带的脏值检查方法</h2><p>我们知道,angular的一些方法会自动进行脏值检查,因此我们可以将上面的方法稍微改动一下即可</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$timeout(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</span><br><span class="line"> <span class="comment">// 获取元素信息</span></span><br><span class="line">});</span><br></pre></td></tr></table></figure><blockquote><p>参考资料 <a href="http://stackoverflow.com/questions/25108780/height-of-container-with-ng-repeat-directive-is-zero" target="_blank" rel="noopener">http://stackoverflow.com/questions/25108780/height-of-container-with-ng-repeat-directive-is-zero</a></p></blockquote>]]></content>
<summary type="html">
<p><img src="https://cdn.thisjs.com/github/ngrepeatAngularJS-header-image.png" alt="ng"></p>
<p>在使用Angular进行开发的过程中,使用ng-repeat生成多个元素之后,如果元素的宽高是auto,那么我们在使用<br><code>css()</code>、<code>getComputedStyle</code>、<code>offsetHeight</code>或者<code>clientHeight</code>都会获取到0,我们无法获取到元素的实际高度。</p>
<p>这是因为DOM的渲染是异步的,导致计算元素属性信息在DOM渲染完成之前就已经完成了,因此无法获取到DOM真正渲染结束之后属性。</p>
<p>在Angular中,我们可以使用以下几种方法进行处理</p>
</summary>
<category term="javascript" scheme="http://www.thisjs.com/tags/javascript/"/>
<category term="angular" scheme="http://www.thisjs.com/tags/angular/"/>
</entry>
</feed>