-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
656 lines (372 loc) · 348 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Tychlog</title>
<subtitle>Front-end Thinking</subtitle>
<link href="https://blog.tychio.net/atom.xml" rel="self"/>
<link href="https://blog.tychio.net/"/>
<updated>2023-05-04T06:26:55.717Z</updated>
<id>https://blog.tychio.net/</id>
<author>
<name>Zhengzheng Zhang</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>远程居家工作心得</title>
<link href="https://blog.tychio.net/2023/03/18/freelancer/"/>
<id>https://blog.tychio.net/2023/03/18/freelancer/</id>
<published>2023-03-18T11:53:45.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/office.png" alt="office"></p><p>远程/居家工作已经成为了一种趋势,更多的人选择了这种工作方式。在远程工作中,如何保持高效的开发流程、便捷的沟通与协作、灵活的项目切换,以及不断提升自己的技能和能力是非常重要的。在本文中,我将以自身多年远程居家工作的经验为基础,介绍一些工具和技术,来帮助你更好地适应远程工作的环境。</p><span id="more"></span><h2 id="高效的碎片化开发工作流"><a href="#高效的碎片化开发工作流" class="headerlink" title="高效的碎片化开发工作流"></a>高效的碎片化开发工作流</h2><p>远程居家工作的特点会让工作无法“持续”,因为同事或客户可能并不能了解你的实时工作情况,所以经常会出现专注工作被打扰和中断的情况;家庭的环境也同样可能造成这种困扰。因此,碎片化开发工作流将是提高生产力和保持竞争力的关键。它允许你和团队将大型项目分解为更小、更易于管理的部分,从而提高协作效率、减少错误并加快交付速度。</p><h4 id="拆分和列出任务"><a href="#拆分和列出任务" class="headerlink" title="拆分和列出任务"></a>拆分和列出任务</h4><p>在开始工作前,你需要将任务拆分成更小的部分,这样可以更好地管理和控制工作进度。你可以使用Trello或Notion等工具,将任务拆分成更小的部分,然后按优先级和状态进行分组和排序。这样可以让你更清晰地了解自己的工作进度和任务步骤。</p><p>Trello是一款流行的项目管理工具,我通常也作为个人笔记来使用。它提供了丰富的拆分和分组功能,可以根据不同的需求和优先级,将任务拆分为不同的子任务,并按状态和优先级进行分组和排序。它还支持标签和注释功能,可以为任务添加标签和注释,方便查看和编辑任务。还有许多插件和扩展,可以根据不同的需求和场景,选择适合自己的插件和扩展,扩展Trello的功能和应用范围。我可以很轻松的拆分和列出当前任务或用户故事,让开发的节奏更灵活,粒度更小。</p><p>Notion是一款功能强大的协作工具,它基于笔记形式,提供了灵活的页面设计可以通过拖拽方式自由组合不同的块和页面元素,创建个性化的页面布局和风格。还有多种集成功能,包括文本块、待办事项、嵌入式网页、表格、媒体文件等,可以根据不同的需求和场景选择适合的页面块。以及多种集成功能,可以将其他应用程序和服务集成到Notion中,例如Google Drive、Slack、Trello等。不过对我来说,缺点就是太自由了,当我希望保持一种布局风格的时候,可能会多花一些时间,或者治好强迫症。</p><h4 id="使用Git小步提交"><a href="#使用Git小步提交" class="headerlink" title="使用Git小步提交"></a>使用Git小步提交</h4><p>使用Git可以随时保存你的工作状态,更重要的是可以培养出良好的思维习惯。在Git提交时,你需要考虑如何描述这些代码。如果你经常使用<code>fix xxx issue</code>这种概括而又没有内容的描述,或者是这样<code>do xxxx and update xxxx and xxxx</code>经常出现and连接了很多意图的描述,就需要警惕了。你可能已经在一次提交中包含了太多的逻辑和意图。在这种情况下,需要回滚部分功能,或者检查部分代码时就成了灾难。而且别人在理解你的代码时也会难以阅读和理解。这时就需要考虑以下几点:</p><ul><li>将笨重的模块,分解成小巧的功能;</li><li>将复杂的需求,拆开成不同层面的逻辑;</li><li>将模糊的用户故事,挖掘出细化的任务;</li></ul><p>比如,需要添加一个API接口通过一系列算法得到结果后修改数据库。如果一次写好所有代码可能需要一个小时以上,这就使得工作跨度变得太长,你很难在无人打扰的专注状态下开发完成。试着添加一个API接口,不做任何事,只是正确的接受到参数并打印。这时就可以提交一次,然后冲杯咖啡看看窗外的美景休息一下。接着开发一系列算法,还可以继续拆分,先保证‘Happy path’,当输入数值是一个正常的数值时,得到了一个结果。只要没有破坏程序的运行,就可以提交一次,之后再添加一些单元测试,和处理边界值等问题,每个问题也都可以单独提交一次。最后的提交再存入数据库。这就是碎片化的提交,只要保证程序没有被破坏,你就可以提交一次。这样Git提交的描述也很容易编写,看起来会更加清晰。每次提交可能只需要几分钟几行代码,你可以随时停下来处理其他事务,不会再被暴躁的打断了。</p><p>提交时还可以使用<code>-p</code>参数,无论是<code>add</code>还是<code>checkout</code>等命令,都可以加上,这样就可以一段一段的代码选择是否要进行该操作。只将你需要的代码添加或回滚,杜绝console.log的误添加。</p><p>如果你的团队开发流程要求一次只能有一个提交,那么你可以<code>git checkout -b feature/xxx</code>为这次的任务创建一个分支,然后小步提交,等到合并时<code>git checkout develop</code>使用<code>git merge --squash feature/xxx</code>将之前的提交打碎合并成一个commit。</p><h2 id="便捷沟通与协作"><a href="#便捷沟通与协作" class="headerlink" title="便捷沟通与协作"></a>便捷沟通与协作</h2><p>对于所有远程工作来说,沟通永远是一项艰巨的任务。并不仅仅是沟通的方式,还隐含着一些思维上的转变。因为你和同事不再是面对面了,你需要思考何时沟通,如何沟通,以及表达方式等等。这些都需要你花时间去思考和实践,但是这些都是值得的。</p><h4 id="文字沟通"><a href="#文字沟通" class="headerlink" title="文字沟通"></a>文字沟通</h4><p>通常来说,文字沟通的成本最低,但可能也是最低效的。所以当你只是需要通知一下某些常规的事情或约定时,文字沟通才是最方便的。</p><ul><li>Slack - 我们团队通常使用Slack进行常规的文字沟通,因为当你加入一个团队或公司时,你就可以联系所有成员了。无论是每天的工作日常更新,还是一些简单的问题,都可以通过Slack来解决。Slack的消息可以通过@来提醒某个人,也可以通过#来提醒某个频道。Slack还提供了一些集成功能,可以将其他应用程序和服务集成到Slack中,例如Google Drive、Trello等。这样就可以在Slack中直接查看和操作这些应用程序和服务中的内容。Slack还提供了一些插件,可以通过插件来增强Slack的功能,例如可以通过插件来实现一些自动化的功能,例如每天早上自动发送一条消息,提醒大家今天的工作计划等等。另外它也很有效的捕捉到了电脑的工作状态,可以轻易的告知别人自己的状态。</li><li>Skype - 有的客户会喜欢使用Skype。Skype不同于Slack是公司团队式的,而是个人式的。你可以添加你的客户为好友,然后通过Skype来沟通。另外它的性能也不太好,尤其是在视频时可能会阻碍文字沟通。</li><li>Team - 是由微软开发的一款协作软件,旨在提供一个集成了聊天、视频会议、文件共享、应用程序集成和团队工作流程的平台,以帮助团队进行协作和沟通。它和Slack一样,只要加入团队就可以联系所有成员。区别是它可以和微软其他产品更好的关联。因此如果你的团队使用了微软的其他产品,那么Team可能是一个不错的选择。</li></ul><h4 id="视频会议及屏幕分享"><a href="#视频会议及屏幕分享" class="headerlink" title="视频会议及屏幕分享"></a>视频会议及屏幕分享</h4><p>当你需要沟通一些复杂的问题时,文字沟通就会变得很麻烦。因为你需要花费更多的时间来表达你的想法,而且你的同事也需要花费更多的时间来理解你的想法。这时候就需要考虑其他的沟通方式了,比如视频会议。</p><p>而且基本上可以开启语音及视频会议的软件或插件都可以进行屏幕分享。有些软件或插件的屏幕分享功能更加强大,甚至可以共享某个应用程序的窗口。</p><ul><li>Hunddle - 是Slack的一个插件,可以在Slack中直接发起一个视频会议,可以邀请多个人参加。这样就可以在Slack中直接进行视频会议,不需要额外的软件。它的性能很不错,也很方便,但是只能在Slack中使用。</li><li>Zoom - 是一款视频会议软件,可以在多个平台上使用,例如Windows、Mac、Linux、iOS和Android。虽然开启一个会议时对于一对一的视频聊天有点繁琐,但是可以通过一个链接邀请其他人加入会议。它的性能也很不错,但是需要额外的软件。</li><li>腾讯会议 - 感觉和Zoom很类似。国内的客户基本上都喜欢用腾讯会议。</li><li>Google Meet - 有的客户会使用Google Meet,但不如Zoom直接发送连接方便,需要加好友。</li><li>Microsoft Teams - 和Meet类似,不过基本没有遇到过客户使用这个。可能是在团队内部使用比较方便。</li><li>TeamViewer - 是一款专注于远程桌面控制和协作的软件,提供了屏幕共享功能,可以方便地分享桌面和应用程序。如果有很复杂的共享和协作需求的话可以尝试一下。</li></ul><h4 id="云存储共享文档"><a href="#云存储共享文档" class="headerlink" title="云存储共享文档"></a>云存储共享文档</h4><p>当你需要共享一些文档时,你可以通过邮件或者其他的文件共享软件来共享文档。但是这样的方式有一个很大的问题,就是你需要不断的发送文件,而且你需要不断的接收文件。这样会浪费很多时间。所以最好的方式就是使用云存储来共享文档,而且还可以云协作,团队共同编辑同一个文档,省去了开启会议共享屏幕的时间。</p><ul><li>Google Drive和Docs - 是Google提供的一款云存储和文档编辑服务。可以在线共享和编辑文档,也可以在各个平台安装客户端来使用。我们团队一般都会使用Google服务进行共享文档,比如编写一些技术手册和规格说明书等。</li><li>OneDrive和Word - 是微软提供的一款云存储和文档编辑服务。和Google Docs类似,可以在线共享和编辑文档,也可以在各个平台安装客户端来使用。一般只有在接受到严格需要Word格式的文档时才会使用。</li></ul><h4 id="任务列表或看板"><a href="#任务列表或看板" class="headerlink" title="任务列表或看板"></a>任务列表或看板</h4><p>一般来说,客户都会提供一个任务面板,用来管理项目的任务。每天完成客户指派给我的任务即可。但是有时候客户可能没有提供任务面板,这时候就需要自己选择一个任务面板来管理任务了。</p><ul><li>Trello是我自己开启任务管理的首选,一些小项目的客户也会选择它。它的界面非常简洁,可以方便地创建任务列表和看板。它的免费版也足够使用了。</li><li>Asana是最近几年比较流行的任务管理服务。它的界面色彩分明,可以方便地创建任务列表和看板,但又比Trello更加强大一些,方便追踪进度和成员记录。</li><li>JIRA是最为强大和复杂的面向敏捷的项目管理服务。除了类似于其他服务的管理任务和追踪进度等功能,JIRA对于缺陷bug的管理和追踪也非常强大。但是它的界面比较复杂,对于新手来说可能会有一定的学习成本。</li></ul><h2 id="灵活切换项目"><a href="#灵活切换项目" class="headerlink" title="灵活切换项目"></a>灵活切换项目</h2><p>对于我来说,自由的工作选择和安排意味着更多的项目。有时候可能需要在一天之内推进好几个不同客户和项目的进度,所以灵活切换项目就成了一个必不可少的能力。</p><h4 id="VS-Code-多语言全栈开发的编辑器"><a href="#VS-Code-多语言全栈开发的编辑器" class="headerlink" title="VS Code 多语言全栈开发的编辑器"></a>VS Code 多语言全栈开发的编辑器</h4><p>如果你只使用一种语言,当然可以选择习惯或喜好的代码编辑器。但远程工作往往意味着灵活,你对于工作的灵活,同时项目、团队甚至公司和客户也是灵活的。因此,需要一个可以适用于各种语言和开发习惯的代码编辑器。</p><p>VS Code支持多种语言,并且为每种语言提供了基本的语法高亮和智能感知功能。你还可以根据需要安装扩展来增强对特定语言的支持,按<code>Ctrl+Shift+P</code>打开命令面板,输入<code>install</code>选择Install Extensions就可以查找和安装各种插件。例如,如果你使用Python,可以安装Python扩展来获得更好的代码补全、调试和代码检查功能。如果你使用JavaScript或TypeScript,可以安装相应的扩展来获得更好的代码补全和提示,以及重构功能。此外,你还可以根据需要配置VS Code的设置来优化特定语言的开发体验。例如,你可以配置缩进大小、行尾字符和文件编码等选项。总之,VS Code为不同语言提供了基本支持,然后你可以通过安装扩展和配置设置来进一步增强对特定语言的支持。庞大的插件社区可以满足各种开发语言需求。</p><p>还可以使用<code>code</code>命令在终端命令行中使用,让VSCode打开当前目录。如果没有该命令,可以在VSCode的命令面板中输入<code>Shell Command: Install 'code' command in PATH</code>,然后按照提示进行环境变量的添加。</p><h4 id="综合数据库客户端-MySQL-SQL-Server-PostgrasSQL-MariaSQL随时切换"><a href="#综合数据库客户端-MySQL-SQL-Server-PostgrasSQL-MariaSQL随时切换" class="headerlink" title="综合数据库客户端 MySQL, SQL Server, PostgrasSQL, MariaSQL随时切换"></a>综合数据库客户端 MySQL, SQL Server, PostgrasSQL, MariaSQL随时切换</h4><ul><li>DBeaver是一款免费开源的多平台数据库管理工具,支持多种数据库,如MySQL、Oracle、PostgreSQL、SQL Server等。DBeaver提供了直观的GUI界面,可以方便地进行SQL查询、数据导入导出、数据库管理等操作。支持MacOS,Windows,Linux三大平台。</li><li>Navicat是一款功能强大的综合数据库管理工具,支持多种数据库,如MySQL、Oracle、SQL Server等。Navicat提供了直观的GUI界面,可以方便地进行SQL查询、数据导入导出、数据库管理等操作,并且支持数据可视化。支持MacOS和Windows平台,Linux只有MySQL版本。</li><li>SQuirreL SQL是一款免费开源的Java平台数据库客户端,支持多种数据库,如MySQL、Oracle、PostgreSQL等。SQuirreL SQL提供了直观的GUI界面,可以方便地进行SQL查询、数据导入导出、数据库管理等操作,并且支持插件扩展。同样支持MacOS,Windows,Linux三大平台。</li><li>HeidiSQL是一款免费开源的MySQL数据库管理工具,提供了直观的GUI界面,可以方便地进行SQL查询、数据导入导出、数据库管理等操作。但仅支持Windows系统。</li><li>SQLyog是一款MySQL数据库管理工具,提供了直观的GUI界面,可以方便地进行SQL查询、数据导入导出、数据库管理等操作,并且支持SSH和HTTP隧道连接。支持MacOS和Windows平台。</li></ul><p>我通常使用DBeaver,开源免费。支持目前我所遇到的所有数据库类型和版本细分,并且还可以SSH通道连接。虽然它的操作有点不太友好,我猜可能都是程序员做的,没有设计师参与这个开源项目,但功能确实强大,很多需求都能满足。</p><h4 id="使用Docker构建快速切换的开发环境"><a href="#使用Docker构建快速切换的开发环境" class="headerlink" title="使用Docker构建快速切换的开发环境"></a>使用Docker构建快速切换的开发环境</h4><p>通常可以使用Dockerfile,在项目中直接启动需要的开发环境。比如Python环境,Java环境,还有Node.js环境等。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">FROM node:18-alpine</span><br><span class="line">WORKDIR /app</span><br><span class="line">COPY . .</span><br><span class="line">RUN yarn install --production</span><br><span class="line">CMD ["node", "src/index.js"]</span><br><span class="line">EXPOSE 3000</span><br></pre></td></tr></table></figure><p>然后构建:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">docker build -t getting-started .</span><br></pre></td></tr></table></figure><p>构建完成后,运行就可以了:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">docker run -dp 3000:3000 getting-started</span><br></pre></td></tr></table></figure><p>还有一些常用的服务,比如数据库和服务器等,也可以使用Dockerfile,或者Docker compose启动。例如,如果你需要使用MySQL数据库,可以使用<a href="https://hub.docker.com/_/mysql">官方镜像</a>,利用Docker Compose,创建一个yaml配置文件<code>stack.yml</code>:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># Use root/example as user/password credentials</span><br><span class="line">version: '3.1'</span><br><span class="line"></span><br><span class="line">services:</span><br><span class="line"></span><br><span class="line"> db:</span><br><span class="line"> image: mysql</span><br><span class="line"> # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password</span><br><span class="line"> # (this is just an example, not intended to be a production configuration)</span><br><span class="line"> command: --default-authentication-plugin=mysql_native_password</span><br><span class="line"> restart: always</span><br><span class="line"> environment:</span><br><span class="line"> MYSQL_ROOT_PASSWORD: example</span><br><span class="line"></span><br><span class="line"> adminer:</span><br><span class="line"> image: adminer</span><br><span class="line"> restart: always</span><br><span class="line"> ports:</span><br><span class="line"> - 8080:8080</span><br></pre></td></tr></table></figure><p>然后运行:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">docker stack deploy -c stack.yml mysql</span><br></pre></td></tr></table></figure><p>或者:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">docker-compose -f stack.yml up</span><br></pre></td></tr></table></figure><p>这样,你就可以在项目中使用MySQL数据库了(访问<a href="http://swarm-ip:8080/">http://swarm-ip:8080</a>, <a href="http://localhost:8080/">http://localhost:8080</a>, 或 <a href="http://host-ip:8080)。相应地,Redis,Elasticsearch,Nginx等也是类似的。">http://host-ip:8080)。相应地,Redis,Elasticsearch,Nginx等也是类似的。</a></p><p>通过利用Docker Compose,我可以快速的启动和停止整套针对于某个项目的所有服务,而不需要在本地安装和配置这些服务,也不需要担心版本的问题。这样,我就可以在不同的项目中,快速的切换开发环境,而不需要担心它们相互之间有冲突等问题。</p><h4 id="SSH地址和Key管理"><a href="#SSH地址和Key管理" class="headerlink" title="SSH地址和Key管理"></a>SSH地址和Key管理</h4><p>在不同的项目中切换,还有一个问题需要考虑。服务器SSH所需要的地址和Key都是不一样的,而且可能还需要不同的账户。这样,就需要在不同的项目中,切换不同的SSH地址和Key。</p><p>使用SSH Config文件可以有效映射对应的地址和Key。方便管理你的SSH Key与域名地址的关联。在~/.ssh目录下编辑或创建config文件,然后添加基础默认配置:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Host *</span><br><span class="line"> ServerAliveInterval 30</span><br><span class="line"> ServerAliveCountMax 5</span><br><span class="line"> IdentitiesOnly yes</span><br><span class="line"> StrictHostKeyChecking no</span><br><span class="line"> PasswordAuthentication no</span><br></pre></td></tr></table></figure><p>然后添加不同项目对应的SSH地址和Key:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Host tychio_host</span><br><span class="line"> HostName server.tychio.net</span><br><span class="line"> User root</span><br><span class="line"> IdentityFile ~/.ssh/tychio_alienware</span><br></pre></td></tr></table></figure><p>这样我就可以直接使用<code>ssh tychio_host</code>命令SSH登录到服务器,而不需要使用<code>ssh [email protected] -i ~/.ssh/keys/tychio_alienware</code>这样冗长而难记的命令,甚至有时候服务器地址是IP地址,而不是域名,这样就更难记了。</p><p>另外,每个项目所使用的Git仓库和权限账户也都是不一样的,如果是不同的Git仓库提供商,比如Github, Gitlab, Bitbucket等,还可以区分。但很多项目都使用了GitHub,却是不同的账户。这样,就需要在不同的项目中,切换不同的SSH地址和Key。和刚才的SSH登录类似,只是使用不同的名字标记:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Host github.com-projecta</span><br><span class="line"> HostName github.com</span><br><span class="line"> User git</span><br><span class="line"> IdentityFile ~/.ssh/projecta</span><br><span class="line">Host github.com-projectb</span><br><span class="line"> HostName github.com</span><br><span class="line"> User git</span><br><span class="line"> IdentityFile ~/.ssh/projectb</span><br></pre></td></tr></table></figure><p>然后在项目目录下配置对应的Git仓库地址就行了:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">git remote set-url origin [email protected]:tychio/projecta.git</span><br></pre></td></tr></table></figure><p>这里原本的正确路径应该是<code>[email protected]:tychio/projecta.git</code>,由于我添加了SSH Config,所以可以使用<code>[email protected]:tychio/projecta.git</code>,这样就自动附带了相应的SSH Key用于权限认证。</p><h4 id="多账户管理"><a href="#多账户管理" class="headerlink" title="多账户管理"></a>多账户管理</h4><p>每个项目都有相关的一些账户,包括AWS之类的云服务,共享文档,Invision等设计图,还有测试账户等等。所以多账户和密码的管理也是一项重要任务。</p><p>作为个人来说,Chrome的用户就足够了,因为可以将账户密码保存在Chrome中,然后利用Chrome的Profile用户对应项目或客户进行区分。好处是不需要额外安装什么服务,用起来很方便,打开页面就可以自动填写。缺点是账户无法分享,而且不够安全。</p><p>所以团队使用的话,1Password等密码管理和分享服务就十分重要了。可以将账户密码保存在1Password中,然后利用Chrome的1Password插件,打开页面就可以自动填写。好处是可以分享账户,甚至于还包含了URL地址,可以直接打开并填写;而且更加安全有保障。缺点是需要额外安装1Password服务,而且需要额外的费用。</p><p>除了1Password以外,还有很多类似的服务,都可以用于团队的账户管理,比如:</p><ul><li>LastPass和Dashlane类似,他们都是流行的密码管理工具,支持多平台同步,可以自动填充密码、生成密码等功能,同时还支持密码共享、密码安全性评估等功能。不过没有加密保护等措施,而且安全漏洞监控需要手动输入信息。</li><li>Bitwarden是一个廉价的选项,它也支持多平台同步,可以自动填充密码、生成密码等功能,同时还支持密码共享、密码安全性评估等功能。同样没有加密保护等措施,而且安全漏洞监控需要手动输入信息。但它的价格只有前面几种的三分之一不到。</li><li>Roboform和Sticky Password对于安全方面的措施做的都不太好,而且价格也不便宜,所以不推荐使用。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>无论是沟通还是开发,远程自由工作的核心就是灵活和细节。没有什么是不变的常量,所有的东西都应该是像环境变量一样的东西,随着你当前工作的项目而改变。并且这些东西的粒度是越小越好,你要拥有“警觉性”,随时可以抽身出来,也随时可以投入工作。</p>]]></content>
<summary type="html"><p><img src="/images/office.png" alt="office"></p>
<p>远程/居家工作已经成为了一种趋势,更多的人选择了这种工作方式。在远程工作中,如何保持高效的开发流程、便捷的沟通与协作、灵活的项目切换,以及不断提升自己的技能和能力是非常重要的。在本文中,我将以自身多年远程居家工作的经验为基础,介绍一些工具和技术,来帮助你更好地适应远程工作的环境。</p></summary>
<category term="Work" scheme="https://blog.tychio.net/categories/Work/"/>
<category term="remote" scheme="https://blog.tychio.net/tags/remote/"/>
<category term="works" scheme="https://blog.tychio.net/tags/works/"/>
<category term="online" scheme="https://blog.tychio.net/tags/online/"/>
<category term="home" scheme="https://blog.tychio.net/tags/home/"/>
<category term="freelancer" scheme="https://blog.tychio.net/tags/freelancer/"/>
</entry>
<entry>
<title>使用S3FS将AWS S3挂载到EC2上</title>
<link href="https://blog.tychio.net/2022/10/08/aws-ec2-sftp/"/>
<id>https://blog.tychio.net/2022/10/08/aws-ec2-sftp/</id>
<published>2022-10-08T03:21:40.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/terminal.png" alt="terminal"></p><p>最近有一个客户想在AWS EC2上安装SFTP服务,其实之前已经给他设置了AWS Transfer Family服务,可以直接为S3开通SFTP服务。不过这个客户觉得太贵,所以还是想在已经存在的EC2服务器上安装SFTP服务,并且连接到S3。本来以为很简单的事情,没想到花了一天时间,还差点把服务器弄坏了。</p><span id="more"></span><h2 id="在EC2上安装S3FS"><a href="#在EC2上安装S3FS" class="headerlink" title="在EC2上安装S3FS"></a>在EC2上安装S3FS</h2><p>EC2也就是linux云服务器,为其安装SFTP其实很简单。创建一个专门的用户,生成ssh key,配置SSH,分配权限就可以了。</p><h5 id="创建SFTP用户"><a href="#创建SFTP用户" class="headerlink" title="创建SFTP用户"></a>创建SFTP用户</h5><p>创建SFTP的用户和用户组,并分配用户到组。最后修改用户目录权限。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">$ groupadd sftp</span><br><span class="line">$ useradd -d /home/mysftpuser -s /bin/false -G sftp mysftpuser</span><br><span class="line">$ sudo chmod 700 /home/mysftpuser</span><br></pre></td></tr></table></figure><p>接着编辑<code>/etc/ssh/sshd_config</code></p><p>注释掉<code>Subsystem sftp /usr/lib/openssh/sftp-server</code>,换成<code>Subsystem sftp internal-sftp</code></p><p>再添加下面的部分到最后</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Match user mysftpuser</span><br><span class="line"> X11Forwarding no</span><br><span class="line"> AllowTcpForwarding no</span><br><span class="line"> ChrootDirectory /home/mysftpuser</span><br><span class="line"> ForceCommand internal-sftp</span><br></pre></td></tr></table></figure><p>然后用命令<code>sshd -t service sshd restart</code>重启ssh服务就完成了。</p><p>但因为最后这一段是我从网上找到复制的,刚好有问题。重启ssh之后,实际上ssh已经挂了。直到过了一会我退出再重新登录的时候,AWS的默认用户都无法通过ssh登录到服务器了。这就导致了一个致命的问题,我没法登录服务器,也无法修复sshd_config文件了。修复的方法很麻烦,不算搜索过程,花了1小时多才算搞定。修复方法后面再说。</p><h6 id="安装S3FS"><a href="#安装S3FS" class="headerlink" title="安装S3FS"></a>安装S3FS</h6><p>S3FS全称是<code>s3fs-fuse</code>。该工具基于FUSE,可以把AWS的S3(对象存储)目录映射到Linux,MacOS或FreeBCD上。这里我的EC2是用的Amazon Linux,所以按这个环境进行安装,其他环境略有不同,可以查看Github上S3FS项目的官方文档,这里不做赘述。</p><p>按文档上的命令安装</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo amazon-linux-extras install epel</span><br><span class="line">sudo yum install s3fs-fuse</span><br></pre></td></tr></table></figure><p>然后失败了,因为这个EC2有点老,是Amazon Linux1,无法使用epel。不得已只好手动安装。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo git clone https://github.com/s3fs-fuse/s3fs-fuse.git</span><br><span class="line">cd s3fs-fuse</span><br><span class="line">sudo ./autogen.sh</span><br><span class="line">sudo ./configure</span><br><span class="line">make</span><br></pre></td></tr></table></figure><p>验证一下是否安装成功</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">s3fs --version</span><br></pre></td></tr></table></figure><h6 id="安装FUSE"><a href="#安装FUSE" class="headerlink" title="安装FUSE"></a>安装FUSE</h6><p>然后还需要安装<code>fuse</code>,也同样手动安装。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo wget https://github.com/libfuse/libfuse/releases/download/fuse-2.9.7/fuse-2.9.7.tar.gz</span><br><span class="line">sudo tar -xzvf fuse-2.9.7.tar.gz</span><br><span class="line">sudo rm -f fuse-2.9.7.tar.gz</span><br><span class="line">sudo mv fuse-2.9.7 fuse</span><br><span class="line">cd fuse/</span><br><span class="line">sudo ./configure --prefix=/usr</span><br><span class="line">sudo make</span><br><span class="line">sudo make install</span><br><span class="line">sudo export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/lib64/pkgconfig/</span><br><span class="line">sudo ldconfig</span><br></pre></td></tr></table></figure><p>验证一下是否安装成功</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">pkg-config --modversion fuse</span><br></pre></td></tr></table></figure><h2 id="挂载S3-Bucket"><a href="#挂载S3-Bucket" class="headerlink" title="挂载S3 Bucket"></a>挂载S3 Bucket</h2><h6 id="创建SFTP的AWS配置文件"><a href="#创建SFTP的AWS配置文件" class="headerlink" title="创建SFTP的AWS配置文件"></a>创建SFTP的AWS配置文件</h6><p>S3FS需要创建一个<code>${HOME}/.passwd-s3fs</code>文件,作为保存AWS秘钥的文件。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">echo [ACCESS_KEY_ID]:[SECRET_ACCESS_KEY] > ${HOME}/.passwd-s3fs</span><br><span class="line">chmod 600 ${HOME}/.passwd-s3fs</span><br></pre></td></tr></table></figure><p>把ID和KEY替换成你AWS S3的ID和KEY,写入该文件就行。还有设置正确的权限。</p><p>另外由于我使用默认用户<code>ec2-user</code>登录和安装,和SFTP不是同一个用户,所以需要配置fuse,允许其他用户访问。在<code>/etc</code>下创建<code>fuse.conf</code>文件,然后写入:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># /etc/fuse.conf - Configuration file for Filesystem in Userspace (FUSE)</span><br><span class="line"></span><br><span class="line"># Set the maximum number of FUSE mounts allowed to non-root users.</span><br><span class="line"># The default is 1000.</span><br><span class="line">#mount_max = 1000</span><br><span class="line"></span><br><span class="line"># Allow non-root users to specify the allow_other or allow_root mount options.</span><br><span class="line">user_allow_other</span><br></pre></td></tr></table></figure><p>主要是<code>user_allow_other</code>选项就可以开启。</p><h6 id="挂载S3"><a href="#挂载S3" class="headerlink" title="挂载S3"></a>挂载S3</h6><p>最后在挂载命令中用参数指明AWS的秘钥文件路径就可以了。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">s3fs my-bucket /home/mysftpuser/myfolder -o passwd_file=${HOME}/.passwd-s3fs -o --allow-root -o dbglevel=info -f -o curldbg</span><br></pre></td></tr></table></figure><p>要注意的是不要直接挂载到用户根目录,所以我特别加了一层<code>myfolder</code>,因为可能还需要添加<code>.ssh</code>之类的文件到根目录,避免在SFTP中共享出来。</p><p>进入myfoler中查看一下是否出现S3对应bucket的文件,或者创建一个,看是否同步即可验证挂载是否成功。</p><h6 id="撤销挂载"><a href="#撤销挂载" class="headerlink" title="撤销挂载"></a>撤销挂载</h6><p>如果要撤销挂载,S3FS并不提供特别的命令,直接使用<code>umount</code>命令即可。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">umount /home/mysftpuser/myfolder</span><br></pre></td></tr></table></figure><h2 id="修复EC2的SSH"><a href="#修复EC2的SSH" class="headerlink" title="修复EC2的SSH"></a>修复EC2的SSH</h2><p>现在来说说如何修复SSH受损的EC2。</p><p>在<code>sshd_conf</code>文件受损后,我再次登录时会收到<code>connection refused</code>的错误。由于这是一个普通的EC2,我也没有启用过串行控制台,所以只能选择“救援实例”的方式。</p><p>简单来说,就是创建另外一个EC2实例服务器作为救援实例,然后把受损的EC2的主硬盘卷拿出来当做数据盘挂载到这个救援实例上,再通过救援实例SSH登录上去修改数据盘中的sshd_conf文件。这个方法的唯一缺点是需要停止实例一小段时间,如果有实例存储之类的会在停止后丢失的东西,那就不太适合。</p><h6 id="创建救援实例"><a href="#创建救援实例" class="headerlink" title="创建救援实例"></a>创建救援实例</h6><ol><li>选择与受损EC2实例相同的AMI创建救援实例,或者直接从受损实例创建一个snapshot,再用snapshot创建实例,保证其基本环境一致,而不会对挂载卷产生其他影响。</li><li>停止受损的EC2实例</li><li>在Volumn中分离受损卷(/dev/xvda 或 /dev/sda1)。</li><li>SSH登录到救援实例上</li><li>lsblk查看一下当前的卷</li><li>创建一个文件夹用于挂载受损卷</li></ol><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo mkdir /mnt/rescue</span><br></pre></td></tr></table></figure><ol start="7"><li>挂载受损根卷,xvdf1是刚才查询到的卷名</li></ol><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo mount -t xfs -o nouuid /dev/xvdf1 /mnt/rescue/</span><br></pre></td></tr></table></figure><ol start="8"><li>再次lsblk查询一下</li></ol><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT</span><br><span class="line">xvda 202:0 0 8G 0 disk</span><br><span class="line">└─xvda1 202:1 0 8G 0 part /</span><br><span class="line">xvdf 202:80 0 8G 0 disk</span><br><span class="line">└─xvdf1 202:81 0 8G 0 part /mnt/rescue</span><br></pre></td></tr></table></figure><ol start="9"><li> 编辑修复sshd_conf文档</li></ol><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo vi /mnt/rescue/etc/ssh/sshd_config</span><br></pre></td></tr></table></figure><ol start="10"><li>卸载修复的卷</li></ol><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo umount /mnt/rescue/</span><br></pre></td></tr></table></figure><ol start="11"><li>在AWS控制台上将修复的卷从救援实例上分离,再以根卷形式挂载到原受损实例上。</li><li>重新启动原实例,SSH登录测试是否成功。</li><li>停止救援实例。</li></ol><p>这样就修复好了,以后还是应该谨慎一些,其实每次修改sshd_conf的时候,在退出前另外开一个终端登录一下就可以避免这种情况。DevOps还是需要一颗过度谨慎的心。</p>]]></content>
<summary type="html"><p><img src="/images/terminal.png" alt="terminal"></p>
<p>最近有一个客户想在AWS EC2上安装SFTP服务,其实之前已经给他设置了AWS Transfer Family服务,可以直接为S3开通SFTP服务。不过这个客户觉得太贵,所以还是想在已经存在的EC2服务器上安装SFTP服务,并且连接到S3。本来以为很简单的事情,没想到花了一天时间,还差点把服务器弄坏了。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="aws" scheme="https://blog.tychio.net/tags/aws/"/>
<category term="ec2" scheme="https://blog.tychio.net/tags/ec2/"/>
<category term="sftp" scheme="https://blog.tychio.net/tags/sftp/"/>
<category term="ssh" scheme="https://blog.tychio.net/tags/ssh/"/>
<category term="devops" scheme="https://blog.tychio.net/tags/devops/"/>
<category term="linux" scheme="https://blog.tychio.net/tags/linux/"/>
</entry>
<entry>
<title>Nest.js快速启动API项目</title>
<link href="https://blog.tychio.net/2022/08/15/nestjs/"/>
<id>https://blog.tychio.net/2022/08/15/nestjs/</id>
<published>2022-08-15T12:40:10.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/nestjs.png"></p><p>最近上了一个新项目,这个客户管理一个庞大的任务和团队集群,而不同流程所适用的系统也不太一样,比如salesforce,hubspots之类的。这次的新项目需要在另外两个平台之间做一些事情。目前只需要先封装其中之一的API,因此我们选定使用NodeJS的框架<a href="https://nestjs.com/">Nest.js</a>来实现这套API。</p><span id="more"></span><h1 id="快速启动"><a href="#快速启动" class="headerlink" title="快速启动"></a>快速启动</h1><p>开启nestjs项目有3种便捷的方式。</p><h5 id="使用nest自带的命令行工具"><a href="#使用nest自带的命令行工具" class="headerlink" title="使用nest自带的命令行工具"></a>使用nest自带的命令行工具</h5><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm i -g @nestjs/cli</span><br><span class="line">nest new project-name</span><br></pre></td></tr></table></figure><p>即使不使用这种方式,也建议在node全局安装命令行工具,这样可以方便的生成各种nestjs的模块,比如controller和service之类的。</p><h5 id="直接使用starter项目"><a href="#直接使用starter项目" class="headerlink" title="直接使用starter项目"></a>直接使用starter项目</h5><p>这里还有一个用于启动的样例项目,可以直接使用。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">git clone https://github.com/nestjs/typescript-starter.git my-app</span><br><span class="line">cd my-app</span><br><span class="line">npm install</span><br><span class="line">npm run start</span><br></pre></td></tr></table></figure><p>而且这个项目还附带了Typescript。不过要记得把之前的git信息删掉。</p><h5 id="用npm安装所需的包"><a href="#用npm安装所需的包" class="headerlink" title="用npm安装所需的包"></a>用npm安装所需的包</h5><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata</span><br></pre></td></tr></table></figure><p>直接安装nestjs的core和common就可以,rxjs和reflext-metadata也是必需的。</p><p>这种方式比较干净,目录什么的需要自己创建。不过也可以使用命令行创建controller之类的,目录会自动创建好。</p><p>总的来说,nestjs和其他语言API框架类似,很多东西可以自动生成或者无需编写,所以约定的习惯非常重要,尽量不要创建一些“独特”的结构,避免以后踩坑。</p><h2 id="创建controller"><a href="#创建controller" class="headerlink" title="创建controller"></a>创建controller</h2><p>创建好项目之后,我创建了一个controller,nest.js中controller可以通过修饰器直接提供路由,因此没有一个route之类的文件用于配置。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nest g controller projects</span><br></pre></td></tr></table></figure><p>nest.js的目录约定是按业务模块划分,因为src目录中会出现一个projects的目录,该目录下会生成一个projects.controller,以及附带的单元测试。</p><h2 id="创建service"><a href="#创建service" class="headerlink" title="创建service"></a>创建service</h2><p>接着创建service,用于封装目标任务管理平台关于Projects的API。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nest g service projects</span><br></pre></td></tr></table></figure><p>创建controller和service都会自动加入到module里,可以在每次生成后用git diff查看一下生成了哪些代码,也好心理有数。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">import { Controller, Get, Req } from '@nestjs/common';</span><br><span class="line">import { Request } from 'express';</span><br><span class="line">import { Project } from 'src/interfaces/project.interface';</span><br><span class="line">import { ProjectsService } from './projects.service';</span><br><span class="line"></span><br><span class="line">@Controller('projects')</span><br><span class="line">export class ProjectsController {</span><br><span class="line"> constructor(private projectsService: ProjectsService) {}</span><br><span class="line"></span><br><span class="line"> @Get()</span><br><span class="line"> findAll(@Req() request: Request): Project[] {</span><br><span class="line"> return this.projectsService.findAll();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">import { Injectable } from '@nestjs/common';</span><br><span class="line">import { Project } from 'src/interfaces/project.interface';</span><br><span class="line"></span><br><span class="line">@Injectable()</span><br><span class="line">export class ProjectsService {</span><br><span class="line"> findAll(): Project[] {</span><br><span class="line"> return [];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="结构和命名"><a href="#结构和命名" class="headerlink" title="结构和命名"></a>结构和命名</h1><p>另外我特别想说明一下的是,虽然我为controller和service都使用了相同的名称,但并不是说他们是一一对应的。很多项目只是为了分层而分,controller和service都是一一对应的,其实并不正确。分层是因为它们拥有不同的意义,只有明确语义,才能在思维的过程中更好的掌握代码,也可以更好的复用,层次起到的是一种认知转换的作用。如果只是把底层的对象毫无变化的映射出来,那这个过程是毫无意义的。</p><p>这里的service在nestjs中其实是provider的一种,而provider的意义则是从各种不同的地方提供数据或其他东西。我使用的ProjectsService意义在于封装另一个API,所以这个projects来源于目标任务管理平台的API名称。而controller的名称projects指的是我创建的API所要提供的数据是project,只不过它们在这里确实是同一个东西,所以名称也一样。</p><p>假设,现在的业务逻辑是需要从目标任务管理平台获取projects,之后过滤出两种不同特性的projects,一种叫task任务,需要分配给人员;另一种叫note记录,只是标记一下。它们拥有不同的特性。那么我就会创建2个controller,taskController和noteController,但是它们都调用ProjectsService去使用不同的过滤条件获取数据。</p><h1 id="HTTP请求"><a href="#HTTP请求" class="headerlink" title="HTTP请求"></a>HTTP请求</h1><p>使用nest.js官方的Http模块<code>HttpModule</code>就可以向其他API发出请求。该模块其实也是封装的Axios,所以用起来很方便。先安装相关模块。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm i --save @nestjs/axios axios</span><br></pre></td></tr></table></figure><p>然后在app.module中引入。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">import { HttpModule } from '@nestjs/axios';</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">@Module({</span><br><span class="line"> imports: [HttpModule], // 引入Http模块</span><br><span class="line"> controllers: [ProjectsController],</span><br><span class="line"> providers: [ProjectsService],</span><br><span class="line">})</span><br><span class="line">export class AppModule {}</span><br></pre></td></tr></table></figure><h5 id="处理Axios对象"><a href="#处理Axios对象" class="headerlink" title="处理Axios对象"></a>处理Axios对象</h5><p>在service中可以这样处理http请求的返回值。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">import { HttpService } from '@nestjs/axios';</span><br><span class="line">import { Injectable } from '@nestjs/common';</span><br><span class="line">import { map, Observable } from 'rxjs';</span><br><span class="line">import { Project } from 'src/interfaces/project.interface';</span><br><span class="line">import { AxiosResponse } from 'axios';</span><br><span class="line"></span><br><span class="line">@Injectable()</span><br><span class="line">export class ProjectsService {</span><br><span class="line"> constructor(</span><br><span class="line"> private readonly httpService: HttpService</span><br><span class="line"> ) {}</span><br><span class="line"> findAll(): Observable<Project[]> {</span><br><span class="line"> return this.httpService.get('http://localhost:3000/api-json').pipe(</span><br><span class="line"> map((axiosResponse: AxiosResponse) => {</span><br><span class="line"> return axiosResponse.data;</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>因为Axios会包裹一层,用data作为统一的key,所以需要map出来。这里的pipe和map方法都是来自rxjs。rxjs的核心就是Observable,使用响应式编程的方式,封装了回调和异步方式的代码。因此这里你看不到promise或者await/async之类的关键字。</p><h1 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h1><p>上面的请求,我只是使用了一个本地的json接口用于测试。要真正的调用目标平台的API,还需要配置项,因为包括API token这种重要字符串的一些值,需要放在环境变量中,不能直接放在代码被git提交。</p><p>所以我需要加上config的配置,安装nest.js的config包,它封装的其实是dotenv包,经常使用nodejs的话,应该会很熟悉这个包。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm i --save @nestjs/config</span><br></pre></td></tr></table></figure><p>同样在app.module中引入config模块。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">import { ConfigModule } from '@nestjs/config';</span><br><span class="line">import configuration from './config/configuration';</span><br><span class="line">...</span><br><span class="line"> imports: [</span><br><span class="line"> HttpModule,</span><br><span class="line"> ConfigModule.forRoot({</span><br><span class="line"> load: [configuration],</span><br><span class="line"> })</span><br><span class="line"> ],</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这里使用forRoot是因为该模块是单例模式的。而传入的参数<code>load</code>可以把config对象载入。</p><p>引入的<code>config/configuration</code>文件是新创建的配置对象。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">export default () => ({</span><br><span class="line"> port: parseInt(process.env.PORT, 10) || 3000,</span><br><span class="line"> runn: {</span><br><span class="line"> url: process.env.RUNN_API_URL,</span><br><span class="line"> token: process.env.RUNN_API_TOKEN</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>我配置了端口,以及API的URL和Token。</p><p>然后可能需要用到Typescript的接口,可以使用nest生成文件。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nest g interface runn-config</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">export interface RunnConfig {</span><br><span class="line"> url: string</span><br><span class="line"> token: string</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在service中就可以获取这些配置项。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">import { ConfigService } from '@nestjs/config';</span><br><span class="line">import { RunnConfig } from 'src/interfaces/runn-config.interface';</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line">constructor(</span><br><span class="line">private readonly httpService: HttpService,</span><br><span class="line">private configService: ConfigService</span><br><span class="line">) {}</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line">const config = this.configService.get<RunnConfig>('runn');</span><br></pre></td></tr></table></figure><p>不要忘记在根目录下创建<code>.env</code>文件填入配置的值。另外按习惯,可以创建.env.sample文件,只包含key,没有值,类似模板,被git提交管理。而.env文件则被gitignore。只在本地保留。在服务器上需要另外生成一份。</p><h3 id="全局添加headers"><a href="#全局添加headers" class="headerlink" title="全局添加headers"></a>全局添加headers</h3><p>URL使用了,但Token需要在headers中添加。在app.module中使用HttpModule的register方法就可以配置其中封装的Axios。不过由于token来自config,所以需要用比较麻烦的registerAsync,注入config后,在useFactory中使用。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">...</span><br><span class="line"> imports: [</span><br><span class="line"> HttpModule.registerAsync({</span><br><span class="line"> imports: [ConfigModule],</span><br><span class="line"> inject: [ConfigService],</span><br><span class="line"> useFactory: (configService: ConfigService) => ({</span><br><span class="line"> headers: {</span><br><span class="line"> 'Content-Type': 'application/json',</span><br><span class="line"> Authorization: 'Bearer ' + configService.get('runn.token')</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }),</span><br><span class="line"> ...</span><br><span class="line"> ],</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这样我就创建了一个基本API框架,并请求了一个简单的目标任务管理系统的API获取数据。</p><h1 id="API文档"><a href="#API文档" class="headerlink" title="API文档"></a>API文档</h1><p>另外,由于客户需要了解和测试我们的API,所以需要一个postman的api集合。我准备使用Swagger,这是一个API文档的自动生成工具,它会根据框架中定义的API和参数,自动生成一个页面,包含所有的API和参数说明,以及可以直接请求该API。当然这需要修饰器的辅助。先安装nestjs的swagger包。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm i --save @nestjs/swagger</span><br></pre></td></tr></table></figure><p>然后在<code>main.ts</code>中引入并配置。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">import { NestFactory } from '@nestjs/core';</span><br><span class="line">import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';</span><br><span class="line">import { AppModule } from './app.module';</span><br><span class="line"></span><br><span class="line">async function bootstrap() {</span><br><span class="line"> const app = await NestFactory.create(AppModule);</span><br><span class="line"></span><br><span class="line"> // swagger</span><br><span class="line"> const config = new DocumentBuilder()</span><br><span class="line"> .setTitle('My APIs')</span><br><span class="line"> .setDescription('My APIs documents')</span><br><span class="line"> .setVersion('1.0')</span><br><span class="line"> .build();</span><br><span class="line"> const document = SwaggerModule.createDocument(app, config);</span><br><span class="line"> SwaggerModule.setup('api', app, document);</span><br><span class="line"> // swagger end</span><br><span class="line"></span><br><span class="line"> await app.listen(3000);</span><br><span class="line">}</span><br><span class="line">bootstrap();</span><br></pre></td></tr></table></figure><p>然后就可以通过<code>http://localhost:3000/api</code>访问该API文档页面。不过目前所有API都会出现在default默认标签下。官方示例中使用的addTag方法虽然可以添加一个标签,但并不能指定某些API放入该标签。我需要通过修饰器实现。</p><p>在controller的方法上使用<code>@ApiTags('Project')</code>即可,该方法会被放置在Project标签下。</p><p>除了API文档的页面形式。<code>http://localhost:3000/api-json</code>的JSON格式才是最重要的,可以使用它在Postman中直接导入。</p>]]></content>
<summary type="html"><p><img src="/images/nestjs.png"></p>
<p>最近上了一个新项目,这个客户管理一个庞大的任务和团队集群,而不同流程所适用的系统也不太一样,比如salesforce,hubspots之类的。这次的新项目需要在另外两个平台之间做一些事情。目前只需要先封装其中之一的API,因此我们选定使用NodeJS的框架<a href="https://nestjs.com/">Nest.js</a>来实现这套API。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="javascript" scheme="https://blog.tychio.net/tags/javascript/"/>
<category term="framework" scheme="https://blog.tychio.net/tags/framework/"/>
<category term="nestjs" scheme="https://blog.tychio.net/tags/nestjs/"/>
<category term="node" scheme="https://blog.tychio.net/tags/node/"/>
<category term="api" scheme="https://blog.tychio.net/tags/api/"/>
</entry>
<entry>
<title>Remix.run新手教程</title>
<link href="https://blog.tychio.net/2021/11/28/remix-guide/"/>
<id>https://blog.tychio.net/2021/11/28/remix-guide/</id>
<published>2021-11-28T12:17:24.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/remix.jpg"></p><p>最近刚在JavaScript Weekly看到了Remix.run(以下简称Remix)准备开源发布1.0版本。没过两天就看到Remix飙到Github Trending趋势榜第一了。感觉是已经火了啊,不知道后续跟Next.js比怎么样。不过看起来不算太复杂,很多东西都封装起来了,语法很轻。所以准备学习一下,顺便做点学习笔记。</p><span id="more"></span><h2 id="创建项目"><a href="#创建项目" class="headerlink" title="创建项目"></a>创建项目</h2><p>从npm下载remix最新版并生成项目。</p><p><code>npx create-remix@latest</code></p><blockquote><p><code>? Where would you like to create your app?</code> my-cv</p></blockquote><p>我准备做个展示简历的Web应用,动态显示项目经历,这样还算有点需求,不然单纯只是个Demo,毕竟没有需求就没法好好深入学习。而且SSR(Server Side Render服务端渲染)的特色就是方便SEO,做个展示动态数据类的应用是最适合的。命名<code>my-cv</code>。</p><blockquote><p><code>? Where do you want to deploy? Choose Remix if you're unsure, it's easy to change deployment targets.</code></p></blockquote><ul><li>Remix App Server</li><li>Express Server</li><li>Architect (AWS Lambda)</li><li>Fly.io</li><li>Netlify</li><li>Vercel</li><li>Cloudflare Workers<br>当然选择原汁原味的Remix App Server了</li></ul><blockquote><p><code>? TypeScript or JavaScript?</code><br>TypeScript</p></blockquote><ul><li>JavaScript<br>我选择了TypeScript。其实我个人的项目一般不选择TypeScript,不过这次还需要学习以下,就尽量模拟的更正式一些。对于个人开发者来说TypeScript弊大于利,虽然确实可以有效减少错误,和找错误的时间成本,但是定义类型等启动项目时准备的时间更多,还有好不容易找到的冷门库不支持TS的风险。</li></ul><blockquote><p>? Do you want me to run <code>npm install</code>? <code>Y</code><br>最后安装就行了。</p></blockquote><h2 id="启动项目"><a href="#启动项目" class="headerlink" title="启动项目"></a>启动项目</h2><p>进入项目目录 </p><p><code>cd my-cv</code></p><p>启动起来看看</p><p><code>npm run dev</code></p><p>这里 Node版本12会报错,16没问题。</p><blockquote><p>“Could not locate @remix-run/serve. Please verify you have it installed to use the dev command.”</p></blockquote><p>可以访问<code>http://localhost:3000</code>了。<br>直接带有一个Demo,展示了路由的各种状态,404,401之类的,还有带参数的路由。可以留着参考,也可以删掉。</p><h2 id="创建页面"><a href="#创建页面" class="headerlink" title="创建页面"></a>创建页面</h2><p>这个Demo样式还行,就留着了,反正自己写样式对于学习Remix没有太大意义。所以我把导航改成中文,然后第二个页面改成一个新路由,一会可以创建它,用来展示简历。然后第三个Github的连接改成自己的了。</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">ul</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">li</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">Link</span> <span class="attr">to</span>=<span class="string">"/"</span>></span>首页<span class="tag"></<span class="name">Link</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">li</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">li</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">Link</span> <span class="attr">to</span>=<span class="string">"/resume"</span>></span>简历<span class="tag"></<span class="name">Link</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">li</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">li</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">a</span> <span class="attr">href</span>=<span class="string">"https://github.com/tychio"</span>></span>GitHub<span class="tag"></<span class="name">a</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">li</span>></span></span><br><span class="line"><span class="tag"></<span class="name">ul</span>></span></span><br></pre></td></tr></table></figure><p>然后,创建对应的页面。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">mkdir app/routes/resume</span><br><span class="line">touch app/routes/resume/index.tsx</span><br></pre></td></tr></table></figure><p>然后填入一些静态文本,名字和介绍。还有技能,这个可以用载入动态数据来做,先做前端部分,直接从字面量返回。利用<code>useLoaderData</code>返回数据。</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { LoaderFunction } <span class="keyword">from</span> <span class="string">"remix"</span>;</span><br><span class="line"><span class="keyword">import</span> { useLoaderData, json } <span class="keyword">from</span> <span class="string">"remix"</span>;</span><br><span class="line"><span class="keyword">type</span> ResumeData = {</span><br><span class="line"> <span class="attr">skills</span>: <span class="built_in">Array</span><{ <span class="attr">name</span>: <span class="built_in">string</span> }>;</span><br><span class="line">};</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> loader: LoaderFunction = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> data: ResumeData = {</span><br><span class="line"> <span class="attr">skills</span>: [</span><br><span class="line"> <span class="string">'JavaScript'</span>, <span class="string">'CSS/HTML'</span>, <span class="string">'React'</span>, <span class="string">'Remix'</span></span><br><span class="line"> ]</span><br><span class="line"> };</span><br><span class="line"> <span class="keyword">return</span> json(data);</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> <span class="function"><span class="keyword">function</span> <span class="title">ResumeIndex</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> resume = useLoaderData<ResumeData>();</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="xml"><span class="tag"><<span class="name">div</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">h1</span>></span>Zhang Zhengzheng<span class="tag"></<span class="name">h1</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">p</span>></span></span></span><br><span class="line"><span class="xml"> A full-stack developer, Senior consultant, Freelancer.</span></span><br><span class="line"><span class="xml"> <span class="tag"></<span class="name">p</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">p</span>></span></span></span><br><span class="line"><span class="xml"> {resume.skills.map((skill, index) => (</span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">span</span>></span>{index !== 0 ? ', ' : ''}{skill.name}<span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line"><span class="xml"> ))}</span></span><br><span class="line"><span class="xml"> <span class="tag"></<span class="name">p</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>注意:这里的<code>loader</code>是被后端API钩子<code>useLoaderData</code>调用的,所以看不到使用</p></blockquote><p>我还定义了<code>ResumeData</code>类型用于该页面的动态数据,它包含了<code>skills</code>。</p><h2 id="使用数据库ORM"><a href="#使用数据库ORM" class="headerlink" title="使用数据库ORM"></a>使用数据库ORM</h2><p>下一步,找一个ORM,把数据彻底放在数据库里。我选择了Prisma,</p><p><code>npm install --save-dev prisma</code></p><p><code>npm install @prisma/client</code></p><h4 id="初始化ORM"><a href="#初始化ORM" class="headerlink" title="初始化ORM"></a>初始化ORM</h4><p><code>npx prisma init --datasource-provider mysql</code></p><p>我选择了mysql,你可以直接使用SQL Lite <code>npx prisma init --datasource-provider sqlite</code>。</p><p>然后在添加的<code>.env</code>文件里设置<code>DATABASE_URL</code></p><p><code>mysql://<username>:<password>@<host | localhost>:<port>/<database_name></code></p><p>然后执行 <code>npx prisma db pull</code> 读取数据库并自动生成schema。</p><p>再执行<code>npx prisma generate</code>生成客户端。<br>这样就可以通过以下代码使用ORM。</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> { PrismaClient } <span class="keyword">from</span> <span class="string">'@prisma/client'</span></span><br><span class="line"><span class="keyword">const</span> prisma = <span class="keyword">new</span> PrismaClient()</span><br></pre></td></tr></table></figure><h4 id="创建表"><a href="#创建表" class="headerlink" title="创建表"></a>创建表</h4><p>我提前创建了skills表,所以刚才pull的时候,在<code>prisma/schema.prisma</code>文件里就有了model。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">model skills {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String? @db.VarChar(30)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果数据库中没有的表,先在这里写上model schema,再执行<code>npx prisma db push</code>就可以在数据库中创建对应的表。</p><blockquote><p>记得在<code>.gitignore</code>里添加<code>.env</code>。如果使用的SQLite,还有<code>/prisma/xxx.db</code>。</p></blockquote><h4 id="插入数据"><a href="#插入数据" class="headerlink" title="插入数据"></a>插入数据</h4><p>创建<code>prisma/seed.ts</code>文件,用于最初的数据。</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> { PrismaClient } <span class="keyword">from</span> <span class="string">"@prisma/client"</span>;</span><br><span class="line"><span class="keyword">let</span> db = <span class="keyword">new</span> PrismaClient();</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">seed</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">await</span> <span class="built_in">Promise</span>.all(</span><br><span class="line"> getSkills().map(<span class="function"><span class="params">joke</span> =></span> {</span><br><span class="line"> <span class="keyword">return</span> db.skills.create({ <span class="attr">data</span>: joke });</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">seed();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getSkills</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> [</span><br><span class="line"> { </span><br><span class="line"> <span class="attr">name</span>: <span class="string">'JavaScript'</span></span><br><span class="line"> },</span><br><span class="line"> ...</span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>安装<code>ts-node</code>包执行seed。</p><p><code>npm install --save-dev ts-node</code></p><p>方便起见,在<code>package.json</code>中加入</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="string">"prisma"</span>: {</span><br><span class="line"> <span class="attr">"seed"</span>: <span class="string">"ts-node prisma/seed.ts"</span></span><br><span class="line">},</span><br></pre></td></tr></table></figure><p>然后执行seed</p><p><code>npx prisma db seed</code></p><h4 id="使用数据"><a href="#使用数据" class="headerlink" title="使用数据"></a>使用数据</h4><p>在app目录下创建utils目录,以及文件<code>utils/db.server.ts</code></p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> { PrismaClient } <span class="keyword">from</span> <span class="string">"@prisma/client"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> db: PrismaClient;</span><br><span class="line"></span><br><span class="line"><span class="keyword">declare</span> <span class="built_in">global</span> {</span><br><span class="line"> <span class="keyword">var</span> __db: PrismaClient | <span class="literal">undefined</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (process.env.NODE_ENV === <span class="string">"production"</span>) {</span><br><span class="line"> db = <span class="keyword">new</span> PrismaClient();</span><br><span class="line"> db.$connect();</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="built_in">global</span>.__db) {</span><br><span class="line"> <span class="built_in">global</span>.__db = <span class="keyword">new</span> PrismaClient();</span><br><span class="line"> <span class="built_in">global</span>.__db.$connect();</span><br><span class="line"> }</span><br><span class="line"> db = <span class="built_in">global</span>.__db;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> { db };</span><br></pre></td></tr></table></figure><blockquote><p><code>development</code>环境的区别是缓存了连接实例,这样不会每次重启</p></blockquote><p>在之前的<code>resume/index.tsx</code>页面使用它。</p><p><code>import { db } from "~/utils/db.server";</code></p><blockquote><p><code>~</code>是默认在Remix的模板中tsconfig.json配置的,代表app目录。</p></blockquote><p>更新loader方法</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> loader: LoaderFunction = <span class="keyword">async</span> () => {</span><br><span class="line"> <span class="keyword">const</span> data: ResumeData = {</span><br><span class="line"> <span class="attr">skills</span>: <span class="keyword">await</span> db.skills.findMany()</span><br><span class="line"> };</span><br><span class="line"> <span class="keyword">return</span> data;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样基本的Remix流程就走通了。从数据库到页面。</p><p>另外我还替换了原来的logo。<a href="https://docs.google.com/drawings/">谷歌绘图</a> 制作svg还挺好用。</p><p><img src="/images/my_cv.png"></p><p><a href="https://github.com/tychio/my-cv">代码</a>放到Github上了,后面还要继续,和文中可能会有差异。</p>]]></content>
<summary type="html"><p><img src="/images/remix.jpg"></p>
<p>最近刚在JavaScript Weekly看到了Remix.run(以下简称Remix)准备开源发布1.0版本。没过两天就看到Remix飙到Github Trending趋势榜第一了。感觉是已经火了啊,不知道后续跟Next.js比怎么样。不过看起来不算太复杂,很多东西都封装起来了,语法很轻。所以准备学习一下,顺便做点学习笔记。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="javascript" scheme="https://blog.tychio.net/tags/javascript/"/>
<category term="react" scheme="https://blog.tychio.net/tags/react/"/>
<category term="framework" scheme="https://blog.tychio.net/tags/framework/"/>
<category term="ssr" scheme="https://blog.tychio.net/tags/ssr/"/>
<category term="next.js" scheme="https://blog.tychio.net/tags/next-js/"/>
<category term="guide" scheme="https://blog.tychio.net/tags/guide/"/>
</entry>
<entry>
<title>远程工作的这几年</title>
<link href="https://blog.tychio.net/2021/11/23/remote-working/"/>
<id>https://blog.tychio.net/2021/11/23/remote-working/</id>
<published>2021-11-23T09:32:10.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/remote.jpg"></p><p>这几年一直在做远程开发工作,一个原因是陪读,老婆留学的地方一半说法语一半说荷兰语,基本不说英语,所以英语都半吊子的我不好找工作,只能去陪读;另一方面也是想尝试一下这种新鲜的工作方式,毕竟新奇的事物对我总是充满吸引力。当然,远程工作方式有艰辛之处,也有惬意之时。总之,我会试着把这些都总结分享出来。</p><blockquote><p>另外需要说明的是,我是以类似工作室的方式进行远程开发工作的,所以会有一些某种程度上的同事,偶尔在一个项目上协作。不过更多的是和PM,客户以及客户所雇佣的其他开发团队人员沟通。</p></blockquote><span id="more"></span><h2 id="面具下的同事、客户和我"><a href="#面具下的同事、客户和我" class="headerlink" title="面具下的同事、客户和我"></a>面具下的同事、客户和我</h2><p>远程工作意味着我和所有能接触到的人,所见到的仅仅只是对方的昵称和头像。一般而言我们不会看到彼此,工作环境也都是个迷。至少我就在被窝里参加过几次早会。最多只是一些语音交流,甚至于纯粹的文字,还不是即时沟通,而是在敏捷面板的故事卡上。这样还算好的,只是彼此比较陌生,即使很影响团队协作和信任。但更糟的是,也许哪天对方就换人了。</p><p>在加入一个项目时,一般需要添加一系列账号,这个过程很繁琐,注册或邀请,添加权限,陌生的ID需要给现有成员介绍其角色职能。比如每个项目都有Github或其他托管服务,虽然可以直接加入Github账号到团队,但是频繁换人有时也会导致客户的不信任,但又不得不因为一些新项目或紧急任务调整人员分配。所以我们一般会使用团队共享的Github账号,以统一名称和团队logo进行工作。</p><p>有一天我发现客户所雇佣的QA也是这样的,他的账号用的是一个人名,可是提到这个名字的时候用的是说别人的口吻,后来我猜这名字应该是他们团队最开始担任QA的人。一时之间,我分不清前面那个和我对接工作的人还是不是现在这个,或者前面已经换了好几个了。怪不得经常觉得他有点健忘,昨天说好的,今天又忘了,原来是换人了,有些细思极恐。</p><p>还有一个项目,就我和客户另外雇佣的一个印度程序员2个人开发。以前我总觉得这个人很死板,机械化,说话没有半点人性的感觉。直到有一次语音会议时,他突然感兴趣的问了问我所在的城市,可能是在Sentry上看到我的登录ip地址,所以好奇问问。我觉得很高兴,团队有了更多的交流。结果第二天同样的账号,告知我,他们不允许在工作中聊天。我不知道是他自己告诉我的,然后再也不敢聊天了,还是他已经被开除了。这种感觉很不好,被迫地,你无法把他当做一个人对待。</p><p>就连一些小型公司的客户也会这样,可能因为离职之类的,接替者就直接使用了之前的账号,名称和头像。总之,远程工作场景中的我们都是带着面具的。有时候作为第一个开启项目的开发,也许可以拥有自己名称的账号,不过想到以后又会有别人冒名顶替自己,顿时感觉也挺不爽的。</p><h2 id="不在场证明的恐惧"><a href="#不在场证明的恐惧" class="headerlink" title="不在场证明的恐惧"></a>不在场证明的恐惧</h2><p>刚开始远程工作的时候,总会不自觉的担心客户或同事找我。因为,一旦没有第一时间回复,就会感觉自己没法证明有在电脑前认真工作。可能是在公司上班的惯性思维,一旦真的拥有上班划水的大把时间,反而有点瓜田李下的心虚不自信。</p><p>有一段时间真的很累,主要是心累。总是把自己工作的时间最大化,好像有种无形的手把我按在键盘上工作。担心去厕所太久,下楼拿快递用跑的,中午也不能安心的休息一下,连吃饭都坐在电脑前守着什么。我意识到这样下去不行,所以开始思考为什么会出现这样的感受。</p><p>可能我们早就有这种心态了,只是在公司并不明显。这是一种对工作和生活边界感的认知能力。即使是在公司上班,很多人也会遇到下班微信老板找的情况。可能有人果断的无视了,或者有人立马答应了,但内心肯定都是摇摆过的。我觉得远程工作时也是这样,而且是无时无刻不在考验着自己的内心。我明白我必须更清晰的界定工作和生活,因为我没办法靠环境或者其他什么去区分它们。无论工作还是休息,我都是在家,都会使用电脑,甚至都会写代码。</p><p>后来我会做一些有仪式感的事情来区分工作和生活。比如我会用把即时消息软件的状态改成离开,大大方方的让别人知道我离开了,再去做别的事情。还有利用f.lux让屏幕模拟日照,提醒自己下班的时间。使用两套Google账户和Chrome。有了这些直白的切换,感觉工作和休息都更加专注了,可能有点像番茄钟的原理。</p><p>总之,心态变好了,每天也更轻松,同时工作完成的也不错,客户更加的信任,无论我在或不在,这是一个良性循环。</p><h2 id="被迫工具软件横向测评"><a href="#被迫工具软件横向测评" class="headerlink" title="被迫工具软件横向测评"></a>被迫工具软件横向测评</h2><p>由于小项目比较多,不可能靠一个项目填满每周40小时,所以需要在很多项目中反复横跳,这就导致同一时期会为多个客户工作。他们可不会恰好使用同一工具,本来同类竞品层出不穷,特点也各不相同。我只能挨个安装一遍,被迫把各种即时通讯,视频会议,敏捷看板,任务管理之类的应用都用了一个遍。</p><p>即时通讯常用的是Skype和Slack,不过有时也会用Google Chat,还有Discord和Microsoft的Teams。而且经常附带的视频功能很不好用,还得专门用一个App开视频会议和共享屏幕。以前经常用Zoom,后来Slack付费版带的Huddle挺好用也方便,还用过GotoMeeting,Teamviewer之类的。这些提到的大部分都还在我电脑上,除非一些小众的,在客户翻脸之后就可以安心卸载了。</p><p>敏捷面板就更多了,毕竟每个项目的团队情况都不一样,本身就有很多规模和业务需求的差异。最常见的是Trello和JIRA,虽然它们现在都属于Atlassian公司了,不过仍然是高富帅和屌丝的区别。还有Asana也常有客户喜欢用,五颜六色的,最近降低了整个色块的饱和度和明度,感觉有点丧。可能是为了和连Logo都是彩色的Monday区分。最可怕的是有客户会拿Google Doc列几条当任务,感觉就是一锤子买卖,随时准备跑路。</p><h2 id="全球化"><a href="#全球化" class="headerlink" title="全球化"></a>全球化</h2><p>一开始我并没有意识到时差是一个问题,因为只要记住差几个小时,就可以保持同步。就像国内最东边和最西边差了3,4个小时,但是用一样的时钟,也没有太大问题。后来,只能说我还是太天真了。</p><p>在我想来,客户全在北美,那么就是一个时区吧。不是的,美国分了3个时区,有的客户在洛杉矶,太平洋时区(西部),有的在芝加哥属于中部时区,在纽约的就是大西洋时区(东部)。而我是东1区,还有国内的同事东8区。有时候客户同时会找印度团队,结果印度的时区是+5:30,这个半小时就很迷,不能将就一个往整数靠一靠么。</p><p>这我就忍了,结果还没完,还存在冬令时和夏令时这种东西,也就是冬天天黑的早,要把时间往前挪一小时。后来我查了为什么中国没有划分呢,发现以前确实是有的,86年-92年实施了几年,但是因为各种原因吧,也是因为有一部分南方地区维度很低,冬夏白昼差异不大,所以没必要,就取消了。不过欧洲和美洲都还有,但是!欧洲是3月第1周,美洲是3月第二周。在这一周内时间又多错位了1小时,简直令人抓狂。</p><p>终于知道桌面应用里世界时钟是做什么的了,现在总得摆上4,5个时钟放在那。不然大半夜吵醒客户就搞笑了。经常约下次会议时间,还得带上物主代词,“你的早上”,“我的中午”之类的。不然都不知道说的是什么时间。在那错位的一周就更尴尬了,还得不时的互相提醒,害怕对方弄错。或者发现对方直接没想到这一点,就可以默默的自己调整时间了。</p><h2 id="忙碌的猎头们"><a href="#忙碌的猎头们" class="headerlink" title="忙碌的猎头们"></a>忙碌的猎头们</h2><p>从离职开始远程工作的时候,我就把领英的信息按照实事更新了一下,地点改成了海外居住的地方。可能是发现我离职了,海外的猎头们就会跑来接触,即使设置了off状态也没用,很热情,但我说我没有工作许可,就沉默了。偶尔可能有愿意提供工作签的,我说不会说法语,荷兰语?学都不想学,然后就没有然后了。</p><p>好几次这样下来,感觉有点尴尬,我就把地点改回国内了,结果国内的猎头们又纷纷询问,我只好说我只能远程,人不在国内,感觉他们的回复总透着一种“呵呵,你在逗老子(娘)”的感觉。后来想想,以后还是要回国的,不好弄得这些猎头们觉得我再耍他们吧,还是指着外国人坑吧。我又把地点改回国外了,然后任他们叫破喉咙,我也无视之。添加一个自动filter,领英邮件归入垃圾箱,搞定。后来,竟然还有好心的猎头大爷穷追不舍,开头就来一句”希望你一切都好“,末尾补一句“这个机会是100%远程的”,感觉他是怀疑我中招后躺了。毕竟最近欧洲有点爆发的趋势。</p><h2 id="大饼的可行性分析"><a href="#大饼的可行性分析" class="headerlink" title="大饼的可行性分析"></a>大饼的可行性分析</h2><p>说点开心的吧。往往我们看到的远程工作招聘配图,是在一个沙滩上抱着笔记本惬意的工作。边工作边度假成了每一个自由职业者向往的大饼。所以我稍微地实践了一下,虽然没有去海滩,但基本上一次行程就贯穿了阿尔卑斯。</p><p>总的来说,我的策略是这样的。周末出发旅行,一路游玩,周日晚上到一个小镇预定好的短租,可以很便宜的住5天,然后工作日工作,中午可以在小镇附近转转,晚上可以把工作时长补回来,反正6点以后不管什么店都关门了。到了周末再继续各种交通工具开始转移,不过大部分铁路就够了。</p><p>就这样,我在不请假不耽误工作的情况下,多了一些奇奇怪怪的打卡,在布拉格广场修bug,天鹅堡旁早会,雪山峰顶写代码,还参加了一次JSConf EU。</p><p>听起来挺酷的,不过也是真的累,一个月都在外面旅行,回来之后彻底不想出门了。说起来也是幸运,2019年秋天我总感觉不想继续疯狂的旅行了,就想待在家,后来连日常购物都找了在线的商家送货上门。后面发生了什么大家都知道了。那会儿外国可能已经开始流行了,只是没人意识到,包括我也是。没什么预感之类的,只是恰好不想出去旅行了。远程工作方式让我可以轻易地选择躲在家里,在回国之前的2年时间里,除了去市政厅续身份和打疫苗,几乎没出过公寓大门,每周只下楼拿一次食品快递。</p><h2 id="没有请假的借口"><a href="#没有请假的借口" class="headerlink" title="没有请假的借口"></a>没有请假的借口</h2><p>就像我之前说的,远程方式很难区分工作和生活。我可以工作的时候旅行,与之对应的,我无论干什么也还是可以工作。所以原先很多可以在公司请假的借口,在远程这里似乎用不上了。</p><p>感冒了,没事,可以在被窝工作;<br>暴雨了,没事,不用出门,在家工作;<br>网断了,没事,手机流量报销;<br>停电了,没事,出来找个咖啡馆继续;<br>要出门办事,没问题,工作时间可以往后挪一挪;<br>要照看孩子,没问题,边工作边照看,不耽误;</p><p>总之,很难找到说服自己的理由去休息一整天,最多只是从8小时变成4小时。即使现在我在隔离酒店,还在努力工作,因为没有给自己放假的借口。有时候工作时间自由的像休假一样,也意味着休假也可以保持着工作,全在一念之间。</p>]]></content>
<summary type="html"><p><img src="/images/remote.jpg"></p>
<p>这几年一直在做远程开发工作,一个原因是陪读,老婆留学的地方一半说法语一半说荷兰语,基本不说英语,所以英语都半吊子的我不好找工作,只能去陪读;另一方面也是想尝试一下这种新鲜的工作方式,毕竟新奇的事物对我总是充满吸引力。当然,远程工作方式有艰辛之处,也有惬意之时。总之,我会试着把这些都总结分享出来。</p>
<blockquote>
<p>另外需要说明的是,我是以类似工作室的方式进行远程开发工作的,所以会有一些某种程度上的同事,偶尔在一个项目上协作。不过更多的是和PM,客户以及客户所雇佣的其他开发团队人员沟通。</p>
</blockquote></summary>
<category term="Work" scheme="https://blog.tychio.net/categories/Work/"/>
<category term="remote" scheme="https://blog.tychio.net/tags/remote/"/>
<category term="developer" scheme="https://blog.tychio.net/tags/developer/"/>
<category term="team" scheme="https://blog.tychio.net/tags/team/"/>
</entry>
<entry>
<title>后MVC时代的前端架构</title>
<link href="https://blog.tychio.net/2017/02/16/frontend-architecture/"/>
<id>https://blog.tychio.net/2017/02/16/frontend-architecture/</id>
<published>2017-02-16T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/component.jpg"></p><p>很多人觉得,前后端的差异主要是分别承载了数据和样式,功能和皮肤。前端就是视觉方面的,后端是实质性的。追溯到很多年前,确实是这样的,所谓的前端只是由于后端MVC中的View过于复杂,为了提升用户体验,提高加载速度,以及降低服务器压力,所衍生出的一些优化技术。</p><span id="more"></span><h2 id="前端框架演进"><a href="#前端框架演进" class="headerlink" title="前端框架演进"></a>前端框架演进</h2><p>最初前端没有架构,也不需要。但随着UI交互的复杂度激增,我们发现API提供的数据仍然需要进行处理,再进行渲染,而分离这些需要处理的数据和视觉渲染部分后又需要一层进行控制,自然而然,将后端的MVC照搬了过来。但其实人们并没有发现,并不是我们把MVC搬到了前端,而是把后端的V搬到前端之后又分成了MVC,这个问题暂且搁下,后面会解释。</p><p>接着前端又再一次的变得复杂,尤其是不同交互对于同一资源的操作,导致过程化的控制器过于臃肿,而不堪重负,MVVM应运而生。人们都以为,MVVM就是把C变成了ViewModel,但是ViewModel是一个实体,如何控制呢?真正用会MVVM的开发者都会发现,MVVM提倡的是定义而不是控制,定义M和V的关系,什么样的Model应该呈现什么样的View,然后一切自然而然的随着用户的行为去改变。</p><p>其实原先的C被更高度的抽象了,变成了框架的一部分,读取M和V的关系,并监听他们,在一方改变的时候根据关系修改另一方,达到双向绑定。而ViewModel其实是为了描述这一关系而抽象出来的Model,因为它是相对于Model更偏向View的,所以叫ViewModel。接着出现的一系列MVWhatever很好的说明了ViewModel并不是C变的,人们以为既然C可以变成VM,那也可以变成别的。殊不知,ViewModel也是一种Model,它是由View分裂出来的,而View只能分裂出Model和View,不会出现别的。</p><h2 id="样式和数据的区别"><a href="#样式和数据的区别" class="headerlink" title="样式和数据的区别"></a>样式和数据的区别</h2><p>React横空出世,却没有人能说清楚它到底是MV什么,甚至许多人搞不清M在哪,最后干脆说React就是个View。那么React到底是什么架构呢?先别急,我想先讲一个故事:</p><blockquote><p>有一个语言学者提出一个观点 - 法国人的数学很烂,为什么呢?因为法语中的quatre-vingt(80)的意思是 4个20,居然都不会把4乘以20计算出来,是不是数学很差?</p></blockquote><p>第一次听到这个故事,我感到很好笑,难道汉语就不是这样了吗?我们叫做“八十”,其实还是8个10,和4个20有什么区别,还不是都没有计算。后来再一想,不仅不是,而且法国人数学应该很好,因为汉语遵循了现代通用的10进制,而法语是20进制和10进制混合的,所以他们大脑可以自然的映射10进制和20进制,就好比是左撇子为了迎合右撇子的世界而变得思维敏捷一样的道理。</p><p>讲到这里,可能你会觉得莫名其妙,这跟前端有什么关系。其实我只是想说明一个道理,我们经常会被一瞬间的思考误导,比如这个故事中,人们会把习惯的“八十”看做是一个独立的东西,而嘲笑法国人他们的“quatre-vingt”,其实我们只是习惯了这种计算,或者说在大脑中建立了映射,就忽略了计算,而将其看做一个整体。由此说明,我们在看到一个信息的时候是会进行无意识计算的,从而你会认为你看到的就是那个东西本身,并不需要计算。我们认为“八十”是一个对象,而不是表达式,但事实是没有符号可以表达80这个数,所以用‘八个十’的表达式来表示,只不过我们太熟练所以自动计算了。</p><p>抛弃固有思维,现在假设@为81进制数的最大数,既80,再假设有一种操作符可以把十进制变成81进制数,这一过程是不是数据的计算?但如果有一种字体格式可以把连在一起的8和0两个数字变成@的样子,这算不算样式的改变?也许你会觉得通过操作符是在计算机中当做了@,而字体只是长得像,但计算机真正认识的只有可能是二进制binary,所以整个过程是不是也可以看做都是样式?还有人会说,数据无论变成什么样式它本身都不会变,而样式会变。但如果显示器有色差呢,你看到的蓝色就真的是蓝色吗?样式没变,你看到的仍然会变。</p><p>其实数据和样式本质没有区别,区别只在于程度,从计算机到人类的理解程度。差异来自于我们自己,我们把难以理解的叫数据,容易理解的叫样式,计算时间长的叫数据,计算时间短的叫样式。一个矩形你知道是样式,一个长xx,宽xx,边框为xx的东西你会以为是数据,但对于程序来说其实是一样的。处理一下,得到另一种形式的等同的东西,这就是一个不断翻译的过程,从二进制翻译到人类语言,甚至到非语言的一种印象,比如视觉,语音,甚至意识。无论是什么,都是让人类更容易理解。</p><h2 id="Component架构"><a href="#Component架构" class="headerlink" title="Component架构"></a>Component架构</h2><p>耗费了这么大的篇幅说明样式和数据没有区别的目的其实是为了解释React的架构。前面说到的MVC和MVVM其实都是对于View的一种演进,因为所谓View才是最复杂的,为什么说所谓呢,基于前面的结论,View就是Model,但它是人们难以理解的部分,所以没有被抽象为Model,而是直接显示出来让用户自己去读。为了便于理解,想象一种极端的情况,Model只有二进制形式,直接显示给了用户,理论上来说,用户是可以看懂的,只是难度高了一点点。而从View抽象成Model的过程,其实就是程序将二进制翻译出来的过程。而框架的所做的改进就是翻译程度的提升,让用户更容易的读取。</p><p>回到React身上,它的架构就十分清晰了,前面的MVC是把后端的View分离出了一部分Model,而MVVM,是把MVC的V又分离了一部分Model出来。React所做的犹如它的版本号一般,直接起飞,每个Component其实都是View分离出来的Model,理论上来说,你能抽象出无限层Component,这个极限上,View已经简单的没有意义了。而实际来说,你可以视项目情况而定,把View抽象到某个程度后扔给用户自己阅读。而由于分成无数层,M到V的过程也变得简单,不再需要控制,因为复杂的计算已经被分解成了极简单的计算分散到每一层中了,甚至有时候仅仅是Component为传入的props加上一些字面量,然后传入另一个Component,或者是分解或组合成Object再向下传递。一旦真的理解了View即是Model的思想,你就会发现,React似乎什么也没做,其实却把什么都做了,而且非常简单。</p><p>但Component这种架构也有其问题所在,那就是太过于松散,对架构设计的要求比较高。一旦你并非基于由机器到人类的理解程度来抽象分层你的Component,其复用性和扩展性就会大大降低。</p><h2 id="前端面向对象"><a href="#前端面向对象" class="headerlink" title="前端面向对象"></a>前端面向对象</h2><p>前面说到了View和Model没有本质上的区别,那么前端架构和后端架构为什么会有区别呢?原因很简单,后端可以把未翻译完的数据丢给前端,但前端不能随随便便丢给用户,所以前端变成了多层MV,而不是后端简单的分为了一层MV。前端面向对象的设计也更加的困难,尤其是拥有多年后端开发经验的开发人员,更容易误导自己,因为在后端翻译完成的东西,在前端就变成了最原始的东西。</p><p>举个例子,就拿最常见的电商说事吧,设计一个商品页面的Component架构。在拥有所谓数据的时候,它是某一个商品的页面,比如一件印着国旗的T恤。但显然我们的代码库在运行之前是没有这个从数据库传过来的数据的,所以我们没法把它抽象成一个叫做NationalFlagTShirtPage的Component的。退而求其次,在失去后端数据之后,它应该是一个衣服类的商品页面,比如有一些尺码对照和试衣功能,所以可以有一个叫做ClothesShowcase的Component。说到后端数据的这一层,并不是为了搞笑,它反映出了一个问题。其实后端对于前端,就是上一层的Component。同理,在下一层的Component中,我们也应该忽视这一层传入到下一层的数据,因为它不应该有这个数据。</p><p>比如ClothesPage还应该包含一个显示图片的区域,和一个显示尺码信息的区域。这时,很多人会在ClothesShowcase的里面再放一个ClothesImage和ClothesInformation,但是在上一层作为代码一部分的’Clothes’,在这一层应该已经被忽略,我们应该放入ProductImage和SizeInformation。只有当ClothesPage调用SizeInformation并传入’S’,’M’,’L’之类衣服尺码作为参数的时候,它才是衣服尺寸,而它自身应该仅仅是尺寸信息的Component。我们会发现,上一层的代码(字面量’L’等),变成了下一层的数据,如此一直下去,所有的特性都会变成数据,而代码,可能仅仅是一些最基础的元素,比如按钮,方框之类的,甚至是HTML本身。</p><p>如果一直使用对于后端来说的数据层面的’clothes’的话,就只能一次把ClothesPage这个Component写好,而没法继续抽象了。而其子Component可能仅仅是由于过于臃肿而强行分割的patial了。这样做除了代码短一些之外,完全不具有复用性和扩展性,因为ClothesPage下的所有Component都只能为Clothes服务了。而其他每个概念都必须把这一切重复一遍。</p><p>如果走入另一种极端呢,抽象出一种万用Component,只要传入一个大而全的object,就可以渲染出任意的页面。这个时候虽然复用性有了,但是你会发现你什么也没做,因为这个Component就是HTML的另一层封装,那些传入的参数包含了所有数据,你需要把这些不同的数据同化。</p><p>归根结底,Component架构的精髓在于多层,按照人类理解程度分层。否则永远无法分清楚什么是数据,什么是样式,因为它们只会在某一层中有划分。混乱的分层只会导致架构回归到传统的MVC两层结构中去。也因此,前端面向对象必须基于一层,脱离了某一层而论对象或类,都是可笑的,一个类,到了下一层可能就是一个实例。</p>]]></content>
<summary type="html"><p><img src="/images/component.jpg"></p>
<p>很多人觉得,前后端的差异主要是分别承载了数据和样式,功能和皮肤。前端就是视觉方面的,后端是实质性的。追溯到很多年前,确实是这样的,所谓的前端只是由于后端MVC中的View过于复杂,为了提升用户体验,提高加载速度,以及降低服务器压力,所衍生出的一些优化技术。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="mvc" scheme="https://blog.tychio.net/tags/mvc/"/>
<category term="mvvm" scheme="https://blog.tychio.net/tags/mvvm/"/>
<category term="react" scheme="https://blog.tychio.net/tags/react/"/>
<category term="architecture" scheme="https://blog.tychio.net/tags/architecture/"/>
<category term="frontend" scheme="https://blog.tychio.net/tags/frontend/"/>
</entry>
<entry>
<title>碎片化结对编程</title>
<link href="https://blog.tychio.net/2016/05/25/clastic-pair-programing/"/>
<id>https://blog.tychio.net/2016/05/25/clastic-pair-programing/</id>
<published>2016-05-25T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/clastic-pair.jpg"></p><p>真的很久没写博客了,一直提不起兴趣,总觉得写一些代码如何写,工具如何用,过一阵子就不是很有用了,所以想写一些自己的心得体会,但又很难总结成文章。这几天突然间想通了一些,也许我是时候抛开前端这个枷锁了,今天我们来谈谈敏捷开发的结对编程。</p><p>想当年(然而并没有几年)刚来到ThoughtWorks的时候,除了英语,我最不适应的就是pair,即<em>结对编程</em>。因为刚上项目的我只能跟着结对对象的思维走,即使我思路正确了也无非是在我的结对对象写的代码上印证了一下,少有的贡献就是不时的提醒他一下typo之类无关紧要的错误。然后当我拿到键盘时,还是因为信息的不对等,我无法在全局层面上做出贡献,因为我必须非常熟悉整个项目才能说服我的pair,修改一些架构上的代码,否则只能改进一些细节上的代码片段。这种毫无创造性的工作方式让我昏昏欲睡,说好的挑战,困难,压力呢?我感到了一种可有可无的迷茫。</p><span id="more"></span><h3 id="不同的Pair类型"><a href="#不同的Pair类型" class="headerlink" title="不同的Pair类型"></a>不同的Pair类型</h3><p>接着我开始了没有什么意义的强势逆袭之路,每天回家之后熟悉代码,有时半夜还在写代码,当然没有结对编程的我只能写完checkout,仅仅是为了熟悉代码库。因为我感受到对代码库的熟悉程度某种程度上决定了结对编程时的话语权。</p><p>后来终于在第四个项目上,我始终掌握了代码库最全的信息,至少在团队内是这样的。虽然进度压力仍然不小,但我们还是装模作样的进行了结对编程,这次是从项目开始我就加入了,如此,我在结对时的角色发生了变化,大部分是在给我的结对对象传授知识。慢慢的我发现我总是在写代码,我的结对对象只负责看,这样好像不太对,为什么不对呢?那时我并不知道原因,但我试着改变了风格,让对方来写,我提建议。当然有时候遇到复杂的问题,我也会急不可耐的把键盘抢过来快速搞定它,再讲解一番,接着在code review的时候发现我的结对对象并没有听懂我为什么要这么做,然而这是另外一个问题了,暂且不谈。</p><p>其实上面提到的就是几种不同类型的结对方式:</p><ol><li><p><strong>Backseat Driver</strong></p><p> 就是改变风格的我,让对方写,但还是我drive,这样至少一个在写,一个在思考,交流还是比较多的。</p></li><li><p><strong>Keyboard Grabber</strong></p><p> 就是急不可闹抢过键盘飞快写起来的我,通常是因为双方技术和信息量差距较大,而较高一方没有耐心了。</p></li><li><p><strong>Silent Sleeper</strong></p><p> 也就是上面提到的昏昏欲睡的我,这一般是另一方太快,而且没有说清楚自己的逻辑导致的,当然也有可能是你思维太慢,比如代码库的信息量太少。</p></li><li><p><strong>Sprinter</strong></p><p> 在第一个项目我刚刚上手写的时候,大概我的结对对象眼中我就是个sprinter吧,总是想从架构上做一些重构,但又不会TDD,所以刚有个念头就被喊stop了。</p></li></ol><h3 id="结对编程究竟为了什么"><a href="#结对编程究竟为了什么" class="headerlink" title="结对编程究竟为了什么"></a>结对编程究竟为了什么</h3><p>简单来说,结对编程是为了交流并传播知识,还有避免陷入思维盲区,所以要让两个人一起写代码,沟通相互的不同,在从思维差异到相通的过程中找到更棒的解决方案,最终使团队中每个人都能达到共同智慧所能达到的最高境界——思维的并集。然而从我们决定结对编程开始,一直到最高境界是有很长一段路要走的。在磨合的过程中,这恰好又是四个类型:</p><ol><li>高手写,新手看。</li><li>新手写,高手看。</li><li>两个高手相争。</li><li>两个新手入坑。</li></ol><p>注意,这里的高手和新手只是指的相对而言,可能是对代码的信息量不同,也可能是技术方向不同导致的。</p><p>前面两种其实都是在传授知识,只不过一种是直接传授,一种是通过反馈来传授。这两种情况都是在团队成员所掌握的信息不平衡时所出现的,它们的目标都是最终达到每个人的知识都是一致的。</p><p>而后面两种才是项目中的常态,先让我们抛弃高手与新手这样的概念,只是两个水平和信息量相近的程序员进行结对编程。如果两人的思路差异比较大就会产生第三种情况,在某些技术点上产生分歧,然后挣的面红耳赤。最终可能是某人赢得了胜利,这样就失去了结对编程得意义,也浪费了另一个人的思维。也可能是在两边的妥协中产生了一个微妙的结果,这显然没有达到理想的状态,仅仅是思维的交集。如果两人的沟通渐入佳境,找到了和谐的思维交流方式,仍然要小心陷入第四种情况。</p><p>我们知道结对编程的好处之一是防止陷入思维盲区,因为一个人的思维是有限的,很容易忽略掉一些东西而不自知,俗话说得好,旁观者清,也是这个道理。但当两人的思维渐渐一致,有了默契之后还算两个思维吗?是不是也会存在思维盲区。如此看来结对编程的目标是让两个人思维一致的理想状态,而这种理想状态又会导致没有结对时思维盲区的问题,这就像是一个悖论,难道我们只能在即将到达理想状态的时候体验一下结对编程的好处?我渐渐的发现,肯定有哪里不对。</p><h3 id="结对编程状态模型"><a href="#结对编程状态模型" class="headerlink" title="结对编程状态模型"></a>结对编程状态模型</h3><p>既然两个人总在结对编程时会产生这样的问题,那我们就轮换着来,实际上很多项目就是这样做的,但通常我们会用一个小程序随意选择结对对象。看起来很科学,我们终于可以比较均匀的和其他人结对了,这样就不会和某个人变成同一个思维了。</p><p>然而问题就和bug一样总是层出不穷。如果在任务粒度比较粗,而人数又不多的时候,交换结对又变得很艰难,因为你做完了这个用户故事,另一对人可能才刚发现并开始着手解决问题,此时显然不适合更换,而你和你的结对对象也不可能干等着,依赖于另一对人当前用户故事的用户故事也没法做,于是经常出现某对结对一直在做某个部分的用户故事,然而我们只能祈祷两对结对刚好差不多同时做完。这某种程度上就是所谓的理想状态,两个人变成了一个人。</p><p>必须继续改进,所以我们让一个人待在用户故事上,另一个人交换。尝试了一阵子后,又有人觉得更换结对对象太频繁了,刚加入项目的新人还没来得及熟悉一个用户故事,又被换到另一个用户故事上了,最后变成了一个人在主导用户故事,另一个人始终旁观。这又变成了最初的状态,一个人做,另一个看,还是一个人的思维。</p><p>虽然问题很多,但我忽然间想到了一个模型,也许能把问题简化一下,如图:<br><img src="/images/pair-model.jpg" alt="pair model"></p><p>这条轴代表的是沟通交流的程度,可以称为结对编程的状态轴,而两个端点就是上面提及的问题的状态,没有沟通和彻底沟通都会形成单一思维,失去了结对编程的意义。在结对编程的过程中双方就会变得一致,往右边移动,如果有什么因素使得双方信息不对等或者知识不一致,就会产生分歧,往左边移动。中间的红线则代表了真正最佳的状态,有一定的沟通,但又不是一种思维,可以避免思维盲区。</p><p>现在问题就简单了,我们如何驱动一对结对对象向左或者向右,将状态维持在红线附近呢?</p><h3 id="打碎你的结对"><a href="#打碎你的结对" class="headerlink" title="打碎你的结对"></a>打碎你的结对</h3><p>我认为必须改变我们的目标,否则两个人的目标一致总会达成彻底的沟通。而我们的目标就是完成用户故事解决问题,所以我想到了改变两人所分配的用户故事,也就是说你和你的结对对象将不再工作在同一张卡上。这听起来有点天方夜谭,如果两个人不工作在同一张卡上还算是结对编程吗?我认为只要两个人坐在一起有交流就算是结对编程,无所谓你们的目标是什么,至少你们的共同目标还是做好项目。</p><p>这样一来我们可以更容易的做一些改变,因为结对被打碎后变得更灵活了,尤其是对任务粒度粗而人数少的团队而言。</p><h6 id="基于更细的粒度"><a href="#基于更细的粒度" class="headerlink" title="基于更细的粒度"></a>基于更细的粒度</h6><p>现在,我们可以更容易的交换了,你只需要找到跟你目前做的部分有可能有重叠的其他人,或者更容易实践的做法是,找到可能跟你产生代码冲突的人,跟他结对去解决可能冲突的地方。可能是一个模块,也可能是一个方法,甚至是几行公用代码。</p><p>你肯定有个疑问,如果自己领的任务和其他人都没有关系怎么办。我想项目中总会有人擅长这个,或者做过类似的东西,找到他打断他目前的工作,和你一起搞定关键的部分,至少要让你明白如何做才能解决问题。如果没有这样的人,那说明也许你就是这个人,相信自己完成这个任务。或者可以找BA(业务分析师)一起结对完成,这样可以保证方向正确性。无论是什么情况,总会有办法找到你的结对对象,因为这是从你的意图出发找到的,它基于每个可能的冲突,而不是任务,所以你慢慢会发现,重要的是明确你的意图,找到方向,而不是和谁结对。</p><h6 id="学会分解任务"><a href="#学会分解任务" class="headerlink" title="学会分解任务"></a>学会分解任务</h6><p>要明确意图其实很容易,先弄清楚你的需求,也就是用户故事的内容,然后细分它们。但往往我们划分的任务最后看起来并不完全正确,大部分人并不明白分解任务是为了什么,按什么维度划分,要划分成多细的粒度,因为没有驱动力,只能完全凭借经验去分解任务。</p><p>现在当你一个人拿到一个用户故事后,你需要找一个结对的对象,所以你先得明确自己的任务意图,分解任务会变得很有动力。你需要考虑的是,分解出来的任务粒度是否和别人相近,否则你和其他人都会难以确定你们是否有冲突。这时你自然会明白需要具体到什么程度,以及包含哪些信息。</p><p>举个例子,你拿到的用户故事需要一个可编辑的列表,你首先肯定会考虑把它存到数据库。当你分解的任务粒度比较大时,可能是“保存A列表中的元素”,这样当另外一个人也需要操作另一个类似的B列表的时候,他可能不会想到你。而任务粒度太小,比如“创建数据库表xxx_a”,也是同样的情况。所以现在你分解任务时的问题会变成,如何更广泛的匹配其他人的任务呢?这样该任务也许会变成“通过数据库存储a,并显示到A列表中,它与b业务相关”。这不需要你写出来让别人看到,只要心中明白了这些任务就可以了,最重要的是认知。</p><h6 id="迫切需要站会"><a href="#迫切需要站会" class="headerlink" title="迫切需要站会"></a>迫切需要站会</h6><p>刚刚你理解了自己的用户故事,但要找到你的结对对象,还需要了解其他团队成员的用户故事,或者当前正在做的任务。这时你会想到站会,是的,在敏捷中站会是最容易实现但最难实践的一环。说最容易是因为我们只要每天早上站一起就可以像模像样了,说最难是因为很难真的被利用到,因为这是一个全员的实践,需要每个人深刻的理解其意义。</p><p>我曾见过的最离谱的站会大致分为两种,一种是汇报式的,团队中层级最高的人会不自觉的站在中间,或者其他人会慢慢转向并围拢他,然后每个人向他报告自己昨天完成了什么,他们会忽略掉其他人,因为他们自己也没有向其他人讲述。另一种是结果式的,只说现在和某某正在结对,昨天完成了xx用户故事,或者正在做xx用户故事,明天准备做哪个xx用户故事,但没有任何细节和具体描述,这些信息其实看一眼物理墙就可以知道了。</p><p>然而现在不一样了,每个人心中都带着一个疑问,我昨天做的会不会影响其他人,今天准备做的会不会谁能帮到我。站会将不再难以捉摸,你会迫切的想表达自己之前做的,准备做的,遇到的问题,还有更重要的是对其他人的这三个方面异常的感兴趣,他们到底在做什么。</p><h6 id="团队思维复杂度"><a href="#团队思维复杂度" class="headerlink" title="团队思维复杂度"></a>团队思维复杂度</h6><p>渐渐的大家都会独立思考,不仅是思考自己做的事,还思考别人的事,因为除了你没有其他人会为了你的用户故事而思考。在一次简短的结对编程之后,你的结对对象只能确保你们之间没有冲突了,他所要的功能已经完成了,而你必须自己确保这个用户故事切实的完成了。这样,不会再有人所思考的东西是别人的子集,或者与其他人相似,每个人都有自己所要考虑的东西和目标,而其中又有许多交集。</p><p>这种看起来似乎是打乱团队一致性的效果,产生了一种很有趣的现象。不过在说清楚它的优劣之前,我必须先阐述清楚一个概念。团队中所有成员的思维之间有着许多相似和矛盾,这就是<em>团队思维复杂度</em>。</p><p>往往我们会不断增加团队的一致性,力求全员达成共识,这其实是在降低团队思维复杂度,这样不仅可以降低管理成本,还可以在有问题的时候及时调整,因为每个人得思想都是较为一致的,所以解决问题的能力较强,这叫做团队的<em>恢复稳定性</em>。但同时团队的思维和解决办法过于单一,容易陷入各种问题中,也就是所谓的一条腿走路的弊端。而如果团队过于复杂,每个人都有自己的想法,矛盾和制约无处不在,此时的团队很难被改变,却避免了前面所说的思维单一的问题,各种不同的思维会更大范围的覆盖各种解决方案,更多会去避免遇到问题,这是团队的<em>规避稳定性</em>。</p><p>也就是说当团队过于单一的时候,规避稳定性低,容易遇到问题,但恢复稳定性高,可以更好的解决问题。而复杂度较高的时候,规避稳定性高,可以提早发现并避免问题,但恢复稳定性低,一旦碰到严重问题,风险会非常大。所以在项目管理中,为了降低风险,我们应该找到一个更平衡的点去维持住团队思维复杂度,而不应该一味的增加或降低团队思维复杂度。</p><p>碎片化的结对编程恰恰是为了维持这一平衡的实践,在独立思考的团队中,每个人都将保有自己的思路,而结对的时候又会达到沟通思维的目的,使团队思维复杂度维持在一个中间水平。即能降低风险,又可以防止问题的发生。</p><h6 id="广泛深入风险"><a href="#广泛深入风险" class="headerlink" title="广泛深入风险"></a>广泛深入风险</h6><p>说到风险,在敏捷中我们还提倡先做风险大的事,因为尽早发现问题暴露风险,才有更多的时间去消化风险解决问题。但往往我们很难识别风险,所以总会在低风险的用户故事中潜伏者一两个比较大的问题。</p><p>在我们的项目中总是会由相关技术的专家级成员来进行提前的分析,但一个人的精力是有限的,而且我们有时候很难判断一个用户故事所涉及的技术范围具体是什么,所以不如由做的人来提前分析。然而这样又产生了一个问题,在人员较少用户故事较多的团队中(比如我们团队),第一时间我们能接触到的用户故事是有限的,碎片化的结对编程可以很好的解决这个问题。当每个人都领取一个用户故事的时候,我们可以更大范围的接触到这个迭代的用户故事,尽早的找出或碰到有风险的问题,而且在实践中,我发现这些问题往往是和好几个用户故事相关的,如此一来,相当于多人分别识别风险,然后共同解决问题,开发效率和质量也会有一定提升。</p><h6 id="驱动"><a href="#驱动" class="headerlink" title="驱动"></a>驱动</h6><p>在碎片化结对编程的实践中,我们也遇到过一些问题。比如当有几个用户故事是连续依赖的时候,我们还可以分别领取这些用户故事吗?即使分别领了卡,两个人还是会工作到被依赖的那个用户故事上。但你会发现这样其实并没有回到普通的结对编程状态,一个重要区别是,两个人仍然是抱有不同目标进行开发的。一个人的目标是完成这个用户故事,而另一个人的目标是让这个用户故事为其将要做的那个用户故事服务。所以做被依赖用户故事的人会被结对对象驱动。</p><p>有人会发现,这样不就是考虑太多了吗,用户故事不应该考虑以后的实现。其实这是一个平衡的过程,在成本相同的情况下,选择更好的方案肯定是没错的,关键在于不能将精力浪费在没用的东西上。而结对对象所关注的正是他挑的用户故事,他所关注的问题恰恰是除了当前的用户故事最应该被考虑的。在这种驱动和制衡下,你们会选择既能完成当前用户故事,又能让下一个依赖的用户故事更顺利的解决方案,避免一些风险。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>简而言之,碎片化的结对编程的主要思想是将一对结对对应一个用户故事的死板搭配更加合理的分散成多个结对对象对应多个特性的灵活搭配,以减少浪费。重要的是,每个人一定要形成自己独立的思维习惯,在这种实践下每个人都会自然而然的寻找自己的结对对象。</p><p>当然碎片化结对编程也有其局限,在人员对敏捷实践不熟悉的情况下,贸然使用很容易导致各自独立编程不结对的情况出现,它仅是对结对编程的一种适应性改进,适合于用户故事粒度较大,人员数量较少的团队。</p>]]></content>
<summary type="html"><p><img src="/images/clastic-pair.jpg"></p>
<p>真的很久没写博客了,一直提不起兴趣,总觉得写一些代码如何写,工具如何用,过一阵子就不是很有用了,所以想写一些自己的心得体会,但又很难总结成文章。这几天突然间想通了一些,也许我是时候抛开前端这个枷锁了,今天我们来谈谈敏捷开发的结对编程。</p>
<p>想当年(然而并没有几年)刚来到ThoughtWorks的时候,除了英语,我最不适应的就是pair,即<em>结对编程</em>。因为刚上项目的我只能跟着结对对象的思维走,即使我思路正确了也无非是在我的结对对象写的代码上印证了一下,少有的贡献就是不时的提醒他一下typo之类无关紧要的错误。然后当我拿到键盘时,还是因为信息的不对等,我无法在全局层面上做出贡献,因为我必须非常熟悉整个项目才能说服我的pair,修改一些架构上的代码,否则只能改进一些细节上的代码片段。这种毫无创造性的工作方式让我昏昏欲睡,说好的挑战,困难,压力呢?我感到了一种可有可无的迷茫。</p></summary>
<category term="Work" scheme="https://blog.tychio.net/categories/Work/"/>
<category term="agile" scheme="https://blog.tychio.net/tags/agile/"/>
<category term="development" scheme="https://blog.tychio.net/tags/development/"/>
<category term="thinking" scheme="https://blog.tychio.net/tags/thinking/"/>
<category term="clastic" scheme="https://blog.tychio.net/tags/clastic/"/>
</entry>
<entry>
<title>翻译《Pro AngularJS》</title>
<link href="https://blog.tychio.net/2015/08/31/translate-pro-angularjs/"/>
<id>https://blog.tychio.net/2015/08/31/translate-pro-angularjs/</id>
<published>2015-08-31T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/pro-angularjs.png" alt="Pro AngularJS"></p><p>从去年春天就开始翻译这本《Pro AngularJS》,前前后后将近1年半总算是正式出版。从最初的兴奋,到期间的苦逼,最后拿到样书,还是很满足的。这本书由浅入深的详细介绍了AngularJS的各种功能和原理,以及大量示例贯穿全书,开头甚至还有一些JavaScript的基础。原书一共600多页,我和同事各翻译了300多页,我主要是翻译的关于Services的第三部分以及第一部分的后几章。</p><p>总的来说收获很多,对AngularJS有了更深入的理解,虽然书中使用的AngularJS版本已经比较旧了,但是对很多方法的使用以及原理的解读还是非常不错的。并且英语阅读能力也感到有明显提升,许多长句子一开始完全不知所云,花了好几个小时通过上下文和代码才慢慢理解其中深意。当然就翻译本来说,也是有许多感想,所以才写了这篇文章总结一下翻译一本书所需要注意的地方。</p><span id="more"></span><h3 id="格式"><a href="#格式" class="headerlink" title="格式"></a>格式</h3><p>其实Markdown在今天已经十分的成熟,就像本文一样,直接使用编辑器就可以书写,还可以添加各种CSS样式。但是翻译时我还是选择了Word来编辑文本,而且由于Mac上没有购买正版,我只能用虚拟机在Windows中使用WPS免费版来编辑。费这么多功夫是为什么呢,原因很简单,出版社的编辑用的是Word,你没法指望他们使用CSS去编辑样式出版书籍。所以痛苦的格式修改开始了。</p><p>在最初开始翻译的时候,几乎一半的时间都花在了格式上。其实最后是有编辑来调整格式的,作为译者不需要做什么特别修改。但有一个小问题,如果你全部都使用正文的话,哪些标题是同级的,哪些标题是不同级的,完全就搞不清楚了,就算加个粗什么也会由于前后记忆偏差而导致乱掉,甚至目录都无法生成。所以首先要做的不是把这些文本调整成什么样式,而是把他们调整成不同的区分开来。</p><p>比如我后来设置了标题1、标题2、标题3、标题4、正文、代码、特别、警告等类型的文本,然后需要用什么就点一下。之后翻译的进度就大大提升了,并不需要特别在意当前的样式,只是区分,如此以后只要修改对应类型的格式就可以把全文的格式都修改了。</p><h3 id="翻译"><a href="#翻译" class="headerlink" title="翻译"></a>翻译</h3><p>说到翻译,最难的其实不是英语水平问题,而是技术水平,这就是为什么技术书籍的翻译要找技术人员来做,而不是专业翻译人员。如果只是按照字面来翻,看上去可能和作者的话相一致,但技术层面可能会出现误解之类的地方,所以一定要作为技术人员去解读,把代码也看一遍,弄懂作者的意图再来翻译文本就会好很多。比如原书中这一句话:</p><blockquote><p>These are an artifact of<br>AngularJS processing the elements and resolving data bindings.</p></blockquote><p>其中的<code>artifact</code>的字面意思是人工制品,但是这句话要是翻成“AngularJS有人工制品处理元素并解决数据绑定”,感觉太奇怪了。所以我查了许多字典,发现这个词不仅有人工制品的意思,还有古文物的意思,所以它真正的意思是器具。然后我又看了看上下文和相关代码,这里作者要表达的意思是AngularJS已经有了能处理那些的东西,开发者不需要管这些,所以它们应该是特别好用的一个“器”。最终我把Artifact翻译成了“神器”,自认为还是比较贴切的。</p><p>通过上面的例子,说明翻译不仅需要英语水平,还需要技术的理解。所以我的意思是说,还是需要英语水平的,确实两者缺一不可。所以再来看一个长句子:</p><blockquote><p>The benefit of using this technique is that it allows you to<br>limit the scope of controller functionality to the part of the application where it will be used, which makes it easier to<br>perform good unit tests (as described in Chapter 25) and prevents unexpected dependencies between components in<br>the application.</p></blockquote><p>里面包含了that、where、which各种从句,所以断句是很重要的,把逗号和which后面的部分断开,来看看前面的这句。首先有一个benefit,意思是这个好处就是(is that)后面这句话,而后面这句话断开后剩下“limit the scope of controller functionality to the part of the application”,再加上前面的部分,和where所带的部分,翻译后就是“使用该技术的好处是|它让你可以|将控制器的作用域的功能限制在应用程序中|使用它的那部分上”。</p><p>接着应该调整一下后面那部分的顺序,按中文习惯断句,就成了“使用该技术的好处是,它可以让你将控制器作用域的功能限制在应用程序的一部分中,就是使用该功能的那部分。”</p><p>最后,我觉得翻译最重要的就是,说人话。如果生搬硬套的把英文翻译成中文,但失去了原有的意思,那将是致命的,所以一定要语法通畅和语义明确。</p><h3 id="注解"><a href="#注解" class="headerlink" title="注解"></a>注解</h3><p>有一种东西叫译者注,是翻译者自己的话。所以很多时候我们看到书中的译者注成了译者的吐槽弹幕,有翻译不了的注一下,有专有名词注一下,甚至原文有错也注一下,这都是或多或少的滥用了译者注。最开始我也是这样使用,但后面校对的时候,我以一个读者的思维看了一遍,感觉译者注基本都是废话,大大的破坏了原书的感觉。</p><p>比如翻译方面的解释,其实这说明译文有问题,应该优化译文,让其表达的更清楚,而无需注解。</p><p>专有名词也不需要解释,读者不是小白,书籍所面向的人群是否可以理解该专有名词,不是译者该考虑的问题,如果作者认为有必要解释,原文就会有解释,译者只需要翻译就行了,如果没有,说明作者认为读者应该理解,就不该解释。</p><p>原文有错与否也不是译者该操心的,如果是明显的印刷错误,可以翻译为正确的,然后注解一下或不注解。如果是其他层面的错误,就应该翻译成错的,况且我也不认为译者可以100%保证那就是错误的。</p><p>但究竟什么时候应该使用译者注呢?我认为只有在翻译之后会产生问题的地方才该使用译者注。举个典型的例子,在译文中有这样一段话:</p><blockquote><p>迷惑的是,AngularJS所提供的内置过滤器叫作filter。</p></blockquote><p>读者真的迷惑了,迷惑的到底是什么?从中文来看没什么迷惑的地方。所以我们再看看原文:</p><blockquote><p>One of the built-in filters that AngularJS provides is called, confusingly, filter.</p></blockquote><p>明显看出了问题,过滤器的英文就是filter,所以在英文中这句话看着很奇怪,好比在中文里说,AngularJS所提供的过滤器叫做过滤器。所以直接翻译过来时,中文读者反而会迷惑,此时译者注就可以光明正大的出来解释一下了。</p><h3 id="协作"><a href="#协作" class="headerlink" title="协作"></a>协作</h3><p>这次的翻译过程最有意思的就是合译了,因为有协作的部分。虽然我们是分开翻译,但是还是有很多地方需要协作。比如最常见的就是专有名词翻译的统一性问题,就算是自己一个人翻都有可能前后出现偏差,何况是两人呢。</p><p>由于我在HTML中文兴趣组参与过几次翻译工作,所以知道这个问题,一开始我就在Github建立了一个单词对照表(<a href="https://github.com/tychio/glossary/blob/master/JavaScript/AngularJS.md">glossary</a>)用来维护专有名词对照表。要特别注意的是,每个对照单位都应该是专有的,特别的,可能是词组也可能是单词,或者也可能是语团,但绝不能用其中的任意两项来组合使用,它们都应该是独立的。</p><p>无论如何,第一次翻译书籍,肯定有很多不足之处,但也收获了大量的经验,如果还有下一次的机会,希望能做的更好。总结这些,也希望能帮到其他人,无论是翻译书籍还是文档,以一个读者的心就做应该会有所收获。</p>]]></content>
<summary type="html"><p><img src="/images/pro-angularjs.png" alt="Pro AngularJS"></p>
<p>从去年春天就开始翻译这本《Pro AngularJS》,前前后后将近1年半总算是正式出版。从最初的兴奋,到期间的苦逼,最后拿到样书,还是很满足的。这本书由浅入深的详细介绍了AngularJS的各种功能和原理,以及大量示例贯穿全书,开头甚至还有一些JavaScript的基础。原书一共600多页,我和同事各翻译了300多页,我主要是翻译的关于Services的第三部分以及第一部分的后几章。</p>
<p>总的来说收获很多,对AngularJS有了更深入的理解,虽然书中使用的AngularJS版本已经比较旧了,但是对很多方法的使用以及原理的解读还是非常不错的。并且英语阅读能力也感到有明显提升,许多长句子一开始完全不知所云,花了好几个小时通过上下文和代码才慢慢理解其中深意。当然就翻译本来说,也是有许多感想,所以才写了这篇文章总结一下翻译一本书所需要注意的地方。</p></summary>
<category term="Work" scheme="https://blog.tychio.net/categories/Work/"/>
<category term="javascript" scheme="https://blog.tychio.net/tags/javascript/"/>
<category term="angularjs" scheme="https://blog.tychio.net/tags/angularjs/"/>
<category term="translate" scheme="https://blog.tychio.net/tags/translate/"/>
</entry>
<entry>
<title>用“五个为什么”写CSS</title>
<link href="https://blog.tychio.net/2015/06/19/five-whys-for-css/"/>
<id>https://blog.tychio.net/2015/06/19/five-whys-for-css/</id>
<published>2015-06-19T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p>相信大多数人都有过关于CSS的痛苦经历,从我加入公司到现在,不到两年的时间里,听到最多CSS相关的讨论就是‘很难调’。所以我也一直在探究这其中有怎样的问题,为什么很多人觉得CSS很难写,如何才能让其他人更优雅的写CSS。在Code Review的时候,我渐渐的发现了问题所在,其实很多人已经掌握了丰富的CSS知识,但却不知道如何分组属性写成class。最后只好在需要改变样式的元素上随意起个名字做class然后把所有要写的属性丢进这个class里,如果优先级不够,再把前面的选择器都加上。结果就是CSS代码不断堆积,重复和冗余不断增多,维护也变得举步维艰。</p><span id="more"></span><p>问题找到了,但如何解决呢,虽然我在项目组内做了几次分享,还经常在Code Review的时候提出一些问题,却还是收效甚微。有时候知道什么是正确的很容易,但知道如何才能做到正确却很难。直到最近,看了几本书之后,发现了一个很适合指导设计CSS的方法,那就是五个为什么或者叫五问法。五问法来自丰田的精益生产,后来自然衍生到了精益创业中,在DDD以及UX相关书籍中都会见到这个方法,其主旨是深入发觉大量现象的背后所隐藏的真正原因。乍一看它是一个管理方法,其实我觉得它是一种思维方式,即刨根问底的找到问题的根本原因并解决。所以被应用于各个领域,自然对于CSS所面临的问题也正恰如其分。</p><h2 id="场景示例"><a href="#场景示例" class="headerlink" title="场景示例"></a>场景示例</h2><p>先来举个例子吧,某天Code Review发现了一条CSS代码是这样写的:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.max-width {</span><br><span class="line">max-width: 300px;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由此产生了以下对话(纯属虚构):</p><blockquote><p>UI Dev:“不应该这样写,这和直接写内联样式有什么区别呢?”</p></blockquote><blockquote><p>Dev:“如果我不加最大宽度,页面上那个元素左边就会多出一部分,不然加个margin外边距可以吗?”</p></blockquote><blockquote><p>UI Dev:“这个…我也不确定,我从没遇到过这样的问题,一定是哪里有问题。”</p></blockquote><blockquote><p>Dev:“确实这样写也挺不好的,过一段时间就不知道这行代码什么意思了,也不敢修改它。但究竟应该如何写呢?”</p></blockquote><blockquote><p>UI Dev:“呃,这样吧,我们来试试五个为什么,找找问题的根本原因。”</p></blockquote><blockquote><p>Dev:“好啊,CSS的问题也困扰我好久了,能解决就最好了。”</p></blockquote><blockquote><p>UI Dev:“首先问问,为什么要给元素加最大宽度呢?”</p></blockquote><blockquote><p>Dev:“因为不加就就会多出一部分呀。”</p></blockquote><blockquote><p>UI Dev:“那为什么这个元素会多一部分呢?”</p></blockquote><blockquote><p>Dev:“因为没加最大宽度,开个玩笑,别生气,其实我也不确定,不过用DevTools看了一下,好像它的父元素的宽度也不对。”</p></blockquote><blockquote><p>UI Dev:“已经接近了,为什么父元素的宽度不对?”</p></blockquote><blockquote><p>Dev:“因为父元素的内边距两边不一样。”</p></blockquote><blockquote><p>UI Dev:“为什么父元素的内边距不一致?”</p></blockquote><blockquote><p>Dev:“啊,我知道了,原来为父元素的父元素写了一个last的伪选择器,它是用来把padding-right设为0的,因为父元素现在正好是最后一个,所以被影响了。”</p></blockquote><blockquote><p>UI Dev:“别急,为什么要把最后一个元素的padding-right设为0?”</p></blockquote><blockquote><p>Dev:“因为原先最后面的那个元素里面是一个无法修改样式的控件,需要把padding-right设为0才能放得下。”</p></blockquote><blockquote><p>UI Dev:“所以这才是问题所在,我们的意图是给空间的容器加上padding-right为0的属性对吗?而不是给最后一个元素加,所以应该写一个class,也许叫做‘widget-container’之类的,放在那个容器上,然后把last伪选择器删掉,如此一切就正常了。原先出问题的地方其实是没问题的。”</p></blockquote><blockquote><p>Dev:“原来是这样,太好了,我学到了,样式出问题的地方不一定是代码有问题的地方,五个为什么太有用了。”</p></blockquote><p>这样反复问多次“为什么”可以让我们找到问题的根本所在,如果仅仅从表面现象去解决问题很可能导致南辕北辙的后果。而且在例子中的last伪选择器就是因为没有找到根本原因而简单粗暴的写了这样一行代码而导致的。这个例子还很好的展现了五个为什么对于CSS的益处,不仅是找到问题的根本原因,还使得我们在写CSS的时候意图更加明确。如此一来,class命名难的问题也迎刃而解,padding-right应该为的0的元素是那个控件的容器,所以很容易想出“widget-container”这样的名字,因为通过五个为什么的方法找到了真正的意图,此时,class叫什么和应该放在哪都是水到渠成了。</p><h2 id="按比例投入"><a href="#按比例投入" class="headerlink" title="按比例投入"></a>按比例投入</h2><p>但有时候我们所面对的项目不会这么善良,“为什么”的层级越多,说明CSS的关系也越复杂,所以现在我们来谈谈五个为什么中的一个重要原则,按比例投入。其主旨是小问题小投入,大问题大投入,问题等级越高,投入也应该越大。在CSS中来讲,就是当发现样式异常时,使用五个为什么深入找到的根本原因所在之处的重复次数越多,说明问题越严重,对问题的解决方案也应投入的更多。</p><p>再回到上面的例子中,通过一个元素位置异常的问题,找到根本原因来自一个控件需要内边距为0的容器元素,由于第一次发现,所以选择投入较小的解决方案,针对该控件加一个class用来去掉内边距。目前看来是很正确的,但如果接二连三的从不同的问题上深入找到这个控件上,那就说明问题等级提升了,不应该仅仅是在每个调用控件的容器上添加该class。此时我们可以考虑其他方式,比如把所有容器内边距都设为0,而有针对性的对内部元素添加外边距,如果问题等级继续提升,还可以修改甚至替换控件,或者重构其他部分来适应该控件。总之就是要按问题等级选择解决问题的手段,这样的好处不仅仅是原先在精益中那样可以自动调节效率,还可以等样式需求更明确的时候作出相应的重构。</p><p>由于CSS的描述性,使得它很自由,所以同一个需求,往往一百个开发者有一百种实现。在第一次碰到一个需求时,更是很难写出最佳实现,只能有针对性的写一个专属class把需要的属性扔进去。其实问题不在于此,而在于之后是否能在相同问题出现时重构原先的代码,根据所有相关问题写出更具普适性的class。有经验的UI Dev有时会通过经验来判断,直接写出这种class,Bootstrap这类框架就是这样的,但没有或较少经验的开发者就会产生疑惑。五个为什么的按比例投入原则可以很好的驱动CSS的开发,用深入的根本原因连接不同元素甚至不同页面上出现的问题,这样使我们能够安心的以目前的问题等级来组织代码,等到再次碰到问题并找到这里,才再次重构以解决问题。</p>]]></content>
<summary type="html"><p>相信大多数人都有过关于CSS的痛苦经历,从我加入公司到现在,不到两年的时间里,听到最多CSS相关的讨论就是‘很难调’。所以我也一直在探究这其中有怎样的问题,为什么很多人觉得CSS很难写,如何才能让其他人更优雅的写CSS。在Code Review的时候,我渐渐的发现了问题所在,其实很多人已经掌握了丰富的CSS知识,但却不知道如何分组属性写成class。最后只好在需要改变样式的元素上随意起个名字做class然后把所有要写的属性丢进这个class里,如果优先级不够,再把前面的选择器都加上。结果就是CSS代码不断堆积,重复和冗余不断增多,维护也变得举步维艰。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="css" scheme="https://blog.tychio.net/tags/css/"/>
<category term="style" scheme="https://blog.tychio.net/tags/style/"/>
<category term="front-end" scheme="https://blog.tychio.net/tags/front-end/"/>
<category term="fivewhys" scheme="https://blog.tychio.net/tags/fivewhys/"/>
</entry>
<entry>
<title>CSS语义思维</title>
<link href="https://blog.tychio.net/2015/03/13/thinking-in-semantic-css/"/>
<id>https://blog.tychio.net/2015/03/13/thinking-in-semantic-css/</id>
<published>2015-03-13T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/linguistic.png"></p><p>前一阵子在项目组中讲了一个关于CSS的Session,在讲之前我曾收到了许多意见,大部分是希望能讲讲CSS实用性的技术,比如盒模型,CSS3之类的。干货人人都喜欢,因为看得见摸得着,拿来就有用,但我最后还是决定讲一些”湿货“。因为在Code Diff的时候我发现了许多样式的问题不是由于不会写CSS导致的,而是由于在错误的地方使用了写在错误地方的样式。</p><p>其实CSS很简单,没有计算没有流程,只是一直描述,无论什么复杂的效果,你只要Google一下就知道怎么写了,甚至可以直接copy。但CSS又很复杂,一个元素的表现会受到它旁边的兄弟元素,也会受到内部的子元素影响,还会受到父元素影响,在这种多重影响下,一个元素的显示逻辑会变得错综复杂。有没有面对塌陷的块级元素而束手无策?无论怎么改它的属性就是得不到自己想要的,但看看似乎一模一样的示例程序却安然无恙,是不是恨得咬牙切齿?我想这就是本文所要解决的主要问题,让你学会如何优雅的写CSS。</p><span id="more"></span><h3 id="泾渭分明-明确书写意图和表现语义"><a href="#泾渭分明-明确书写意图和表现语义" class="headerlink" title="泾渭分明 - 明确书写意图和表现语义"></a>泾渭分明 - 明确书写意图和表现语义</h3><p>其实我们只要稍微接触过CSS,一定都学过盒模型,我假设你已经对margin、padding和border已经很熟悉。好了,我们经常会遇到一种情况,想让父元素和子元素之间有一些空间,比如布局的时候,container和content之间的留白。一般来说有两种方式,一种是给父元素加padding内边距,另一种是给子元素加margin外边距。现在想想,你是否慎重考虑过用哪一种方式?你是否明白两者的区别?如果你从未考虑过这些,现在考虑也不迟,这真的很重要,如果你真的想优雅的运用CSS,而不是被它耍的团团转,如果你真的想把CSS写出规模,那你就要认真的思考这里面的逻辑。</p><p>先说说父元素的container加上padding的方式,子元素的背景将不会延伸到空白区域,另一种给子元素加margin的方式则反之。但我想先抛下这个表象,看看此时加上padding的父元素的逻辑。它意味着,我创建了一个容器,并且该容器的内部有一些区域是被看不见的东西填充的,无论放入什么都不应该占据这个区域。在你的脑海中想象一个厚厚的玻璃瓶,它就好像是在说“虽然我是透明的,但是在这厚厚的玻璃之内才是你应该呆的地方,无论是什么只要你想放在瓶子里”。现在来看看子元素,是的,其背景也应该并且确实在内部,而不该占据那段空白的区域。如果是为子元素加上margin,这意味着我在容器中创建了某个东西,而这个东西有一个自己的地盘,容器内的其他东西都不能进入这里。</p><p>所以他们的本质区别在于其所表达的意义,也就是语义,一个是容器上的语义,另一个是容器内元素的语义,主语也不同,谓语意义也不同。那么这能说明什么呢,不得不提一下高级语言中的高级,这代表某个语言将机器语言封装的更好,接口更接近自然语言,其强大不言而喻。其实编程语言的发展轨迹就是不断把机器语言往自然语言翻译,让人们可以更容易的跟机器沟通,终极目标自然是人工智能,和机器用自然语言自由沟通。所以虽然代码或者说CSS的语法并没有什么变化,但我们应该在思维上清楚的区分代码片段的意图和语义来写CSS。</p><p>语言学家乔姆斯基曾构造过一句符合语法的话:</p><blockquote><p>“Colorless green ideas sleep furiously”</p></blockquote><p>意思是无色的绿色想法愤怒的睡觉。想法睡觉是违反常识的,无色的绿色是矛盾的,愤怒的睡觉是不合常理的。仅仅符合语法是不够的,仅仅不报错也是不够的,尤其是CSS这种描述性的语言很少会报错,所以我想说的是,其描述性质让我们有更多选择来实现我们所期望的效果,但我们却应该慎重,要让语义分明,而不是在有限的语法规则下任意妄为,那样写不出好的CSS代码。除了其本身的语法规则,我们必须自我约束,引入语义规则,不仅仅是为了提高可读性这样的理由,而是拉近人与机器的距离,让我们在思维上更加和代码契合,这样才能写出优秀的程序。</p><p>言归正传,究竟如何语义化CSS呢。之前有讲过OOCSS,其实面向对象的思想就是一种语义化,将虚拟的元素看作实际的对象,用常识来构造代码,就像是用户体验中的用户习惯一样,这是一种遍及全人类的用户习惯,让代码更友好,即所谓的优雅。</p><p>比如最常见的布局,我们一般都会有header、body和footer,这样无需任何说明和规则,谁都会理解,header在前,footer在后,中间是body。有趣的是table中的thead、tbody和tfoot,由于加载优化,我们即使把tfoot放在tbody前面,让其先加载,但显示的时候tfoot仍然会在tbody下面,由此可见语义化的重要,否则你一定会以为不是W3C的人弄错了标准,就是浏览器厂商搞出了Bug。虽然这是HTML标签不是CSS,但我觉得这个例子很恰当的说明了一点,显示逻辑是独立的,不应该和其他逻辑混为一谈,它应该只关心如何显示。</p><p>再比如,一个导航条一般分为左右两边,里面各有若干链接。你会如何命名?navbar-left和navbar-right是Bootstrap所使用的class,但当宽度减少,Responsive响应式的Bootstrap会让这些链接都收进下拉菜单中,左和右又从何谈起呢?同样是Bootstrap中的颜色命名就做的十分成熟,不是green、blue、red这些词汇,而是success、primary、warning这些词,它们的区别是表现化与语义化的区别,我喜欢把这个叫做<code>显示逻辑</code>。也就是说在class层面我们应该只关注元素(或者说对象)是用来做什么的(意图)以及它应该表示什么(语义)。这里三种颜色代表成功、重要和警告,至于我们是用绿、蓝和红来表示还是通过其他什么颜色甚至形状,应该在style层面写CSS属性表示。如此我们使用class时无需迷茫于表示成功应该用绿色还是红色,意图明确无误。回到导航栏的例子,我们应该使用main和sub之类的词语标识导航栏中重要的和次要的链接,即使在某些实现下,主要和次要没有区别也不用担心,那是该实现的显示逻辑,我们不应该横加干涉。</p><h3 id="狡兔三窟-善用class与style多对多的复杂关系"><a href="#狡兔三窟-善用class与style多对多的复杂关系" class="headerlink" title="狡兔三窟 - 善用class与style多对多的复杂关系"></a>狡兔三窟 - 善用class与style多对多的复杂关系</h3><p>说到class和style(样式属性),这又是一段混乱不清的关系。其实每当我们要实现一个样式,自觉或不自觉的都会考虑,是将元素和class一一对应,然后为元素上的class写style,还是用class和几条style对应之后在元素上搭配组合。前者的好处是每个class中的style不会影响其他class,但可用性十分的低,极端点说这已经不算是在编程了,而是在画页面;后者的好处可以灵活使用不同的样式搭配给元素使用,组合出元素需要的效果,但很难维护,牵一发动全身,样式很难测试,无法保证在你动了一条style之后,没有影响其他的地方。甚至最要命的是大部分CSS代码都是无意识的游走在这两种情况之间,所以我们要做的不是选择一种极端,而是找到一个平衡点,并使用一些方法使得我们的CSS既能灵活复用在各种元素上,又能易于维护,不致修了这破坏那的情况。</p><h6 id="分组"><a href="#分组" class="headerlink" title="分组"></a>分组</h6><p>首先,对于一个或一套元素的样式,我们应该有自己的创建原则,而不是想到哪写到哪。比如,我一般使用这样的步骤来创建元素:</p><ol><li>结构</li></ol><p>首先仅关注结构布局,以站点整体为基准,将元素抽象为一个或多个结构。</p><p>比如制作一个按钮,你也许会发现它的尺寸和导航上的链接一致,而内外边距以及display和overflow之类的属性又和页面容器一样,这时你就可以把按钮的结构拆成两个结构,一个是尺寸,一个是边界逻辑,这种情况时常发生,所以有的CSS理论认为应该把width和height这对尺寸属性和padding以及margin这对边距属性彻底的分开也是这个道理。其实并不是说它们4个放一起就决定不行,只是一般它们都有各自复用的价值,所以常常被拆开使用,归根揭底这是由显示逻辑决定的。</p><ol><li>皮肤</li></ol><p>一般由于设计统一性,我们可以借助设计指导,轻松的制作出标准皮肤。但要注意的是,皮肤指的不仅仅甚至不一定是颜色之类的。继续用Bootstrap举例,有5种基础颜色,但这并不是皮肤,5种颜色的语义不同,它们只是默认用了5种颜色,勉强可以看作默认皮肤,而真正的皮肤是theme,让按钮变得有立体感。还有一个误区就是认为哪几种属性是皮肤,哪几种不是。就像前面所说的,我们要在语义上进行划分。一个属性在某些意图下可能是结构,在另外的意图中可能是皮肤。<img src="https://raw.githubusercontent.com/UIWorkshop/onlyjs/master/source/Logo.jpg">举个极端的例子,在一个Workshop中我用CSS和HTML制作了左边这样的Logo,Logo本身应该完全算是皮肤,因为一般来说就是一张图片,所以制作它的所有CSS属性都应该算作皮肤。下面的CSS代码中,无论是定位还是文字的处理,以及尺寸等,都是为了构造Logo,所以这些属性都是皮肤。</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-id">#logo</span> <span class="selector-tag">i</span>, <span class="selector-id">#logo</span> <span class="selector-tag">b</span> {</span><br><span class="line"> <span class="attribute">position</span>: relative;</span><br><span class="line">}</span><br><span class="line"><span class="selector-id">#logo</span> <span class="selector-tag">i</span> {</span><br><span class="line"> <span class="attribute">left</span>: -<span class="number">30px</span>;</span><br><span class="line"> <span class="attribute">transform</span>: <span class="built_in">rotate</span>(-<span class="number">30deg</span>);</span><br><span class="line"> <span class="attribute">font-size</span>: <span class="number">60px</span>;</span><br><span class="line"> <span class="attribute">letter-spacing</span>: -<span class="number">11px</span>;</span><br><span class="line"> <span class="attribute">opacity</span>: <span class="number">0.3</span>;</span><br><span class="line">}</span><br><span class="line"><span class="selector-id">#logo</span> <span class="selector-tag">b</span> {</span><br><span class="line"> <span class="attribute">top</span>: -<span class="number">43px</span>;</span><br><span class="line"> <span class="attribute">left</span>: <span class="number">40px</span>;</span><br><span class="line"> <span class="attribute">font-size</span>: <span class="number">68px</span>;</span><br><span class="line"> <span class="attribute">word-break</span>: break-all;</span><br><span class="line"> <span class="attribute">width</span>: <span class="number">3px</span>;</span><br><span class="line"> <span class="attribute">line-height</span>: <span class="number">10px</span>;</span><br><span class="line"> <span class="attribute">text-indent</span>: <span class="number">6px</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>状态</li></ol><p>除了结构和皮肤,其实还有一类很容易被人忽视的样式,即使是设计人员也经常忘了它,那就是状态。最常见的是hover,鼠标触碰元素与否的状态,以及active,当前选中的标签,当前所在的分页等等。通常人们会把它划分到皮肤中,但你要明白我们划分的依据是意图,很显然,状态不是皮肤,仅仅是通常为表达某些状态时会使用一些颜色,毕竟颜色是最好的表示方法。它还与各种逻辑有着千丝万缕的联系,经常会使用JavaScript来加以控制,所以将状态单独分出来是极有必要的,JavaScript只需要更改一个class就可以实现状态的切换,而不是在执行的时候才想起来应该把哪里隐藏把哪里显示,或者是变个颜色出个动画之类的。</p><h6 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h6><p>接着,无论我们是已经把各种写好的CSS属性分好了组,还是正准备以这种方式开始写CSS,我先引入一个语言学的术语 <code>语块(Lexical chunk)</code>,创造这一术语的 Michael Lewis 认为语言并非由传统语法和词汇组成,而是由多个词汇的预制语块组成。我们现在分好的组其实就是一个个CSS的语块,CSS本身就是这种预制功能,我们要做的就是把style语块化,然后在HTML中写上代表它的class名称,当HTML元素上的多个class组合在一起时就组成完整的语义。</p><p>比如表示步骤的元素,这是一个形状类似标签,一个挨一个的排列的元素。我将它的尺寸等属性抽出来命名为label,然后把布局的属性组命名为float-left,如果我的设计风格是扁平化的,我可以把相关的皮肤属性命名为flat,你知道虽然扁平化一般用不着特别的属性,但我也可以写一些强制去掉圆角和渐变背景的属性。作为步骤,会分为当前的步骤,之前的步骤,以及还没到的步骤,当前的步骤可以命名为current,或者is—active,后面的步骤不能点所以可以命名为is-disable,这些都是状态。而现在我们来看看当前步骤元素是什么样的</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"><div class="flat label float-left is-active"></div></span><br></pre></td></tr></table></figure><p>总结成一句话就是“The flat label which float left is active”。这就是语义化的CSS。把组织好的语块像说话一样作用于元素上,对元素发出指令,让其变成想要的样式。而且你应该会发现,一组该元素所独有的属性,或者是尺寸之类的一般是主语,布局是动宾短语,而皮肤多是一些形容词,最后状态可以用表语。</p><p>总之,CSS作为一个描述性的语言,有很多人觉得不能算一种语言,但我反而觉得这是一种高级语言,因为更接近自然语言。当然其主要作用是在视觉渲染上,所以应该是一种受限的语言,可以看作是接近自然语言的子集。所以它的性质觉得了其重语义轻语法的特点,写CSS不能仅靠语法规则,一定要用上语义规则,最好是能和项目中所有人达成共识,CSS框架其实就是一种通用共识。</p>]]></content>
<summary type="html"><p><img src="/images/linguistic.png"></p>
<p>前一阵子在项目组中讲了一个关于CSS的Session,在讲之前我曾收到了许多意见,大部分是希望能讲讲CSS实用性的技术,比如盒模型,CSS3之类的。干货人人都喜欢,因为看得见摸得着,拿来就有用,但我最后还是决定讲一些”湿货“。因为在Code Diff的时候我发现了许多样式的问题不是由于不会写CSS导致的,而是由于在错误的地方使用了写在错误地方的样式。</p>
<p>其实CSS很简单,没有计算没有流程,只是一直描述,无论什么复杂的效果,你只要Google一下就知道怎么写了,甚至可以直接copy。但CSS又很复杂,一个元素的表现会受到它旁边的兄弟元素,也会受到内部的子元素影响,还会受到父元素影响,在这种多重影响下,一个元素的显示逻辑会变得错综复杂。有没有面对塌陷的块级元素而束手无策?无论怎么改它的属性就是得不到自己想要的,但看看似乎一模一样的示例程序却安然无恙,是不是恨得咬牙切齿?我想这就是本文所要解决的主要问题,让你学会如何优雅的写CSS。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="css" scheme="https://blog.tychio.net/tags/css/"/>
<category term="style" scheme="https://blog.tychio.net/tags/style/"/>
<category term="front-end" scheme="https://blog.tychio.net/tags/front-end/"/>
</entry>
<entry>
<title>浅谈AngularJS模板</title>
<link href="https://blog.tychio.net/2014/07/20/template-of-angularjs/"/>
<id>https://blog.tychio.net/2014/07/20/template-of-angularjs/</id>
<published>2014-07-20T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/angular.jpg" alt="AngularJS Template"></p><p>作为最流行的MVVM(Model-View-View-Model)框架之一,相信大部分前端对AngularJS都不会陌生,我也一样久仰大名。不得不说,AngularJS所带来的改变是巨大的,被称为未来浏览器的模式一点也不为过,尤其是思维上的转变。</p><p>作为一个常年挥舞着jQuery去指挥无穷无尽的DOM的前端,初次接触AngularJS是有困难的,许多先贤警告我们不要在AngularJS中使用jQuery,不是没有道理的。即使AngularJS中带有jQlite对象,也仅仅是为了弥补一些地方AngularJS的局限性。AngularJS操作UI的方式与jQuery有着极大区别,在深入学习之后,我渐渐的发现了这点。过去使用jQuery的前端就像一个操纵提线木偶的傀儡师,而手握AngularJS的前端简直是不折不扣的魔法师。前端开发者不再需要根据数据去改变DOM,然后填入数据,我们所要做的仅仅是决定数据的表现形式后等待数据的注入。文档流中的元素就像活过来了一样,根据数据表现出了对应的样子。</p><p>这一切的核心除了匪夷所思的DOM监听机制,还有就是AngularJS的模板(template)以及其中多不胜数的内置指令(directive)了。因此,我将在本文中谈谈AngularJS的模板以及其思维模式。</p><span id="more"></span><h3 id="模板中的内置指令"><a href="#模板中的内置指令" class="headerlink" title="模板中的内置指令"></a>模板中的内置指令</h3><p>AngularJS模板和EmberJS的模板相比更为普通一些,仍然是HTML格式的文件。这使得许多人并未真正了解AngularJS的模板,而认为AngularJS只是提供了一堆内置指令并可用于HTML文件。不过就先来说说这些内置指令吧,模板后面再详细讨论。</p><ol><li>ngIf是个对于模板很重要的指令,它是基本的条件表达,满足条件时则存在,不满足则不存在。通过它可以轻松的让模板基于数据呈现不同结构。另外它会形成独立Scope,这也是其与ngShow/ngHide的区别之一。如果ngIf出现太多可能会导致页面渲染速度降低,此时可以选择ngSwitch来代替它,不过此时最好先一下检查你的逻辑。</li></ol><ol><li><p>ngRepeat则是另一重要指令,能循环创建DOM。可以说只要数据中有数组等结构,这一指令就必不可少。配合$index等索引变量,ngRepeat可以创造出多种形式的列表。还有ngRepeatStart/ngRepeatEnd可以将2个元素之间的内容循环创建,但我很少使用它们,因为这种混合多种元素的HTML结构不太好。</p></li><li><p>ngClass是样式层面上的主要指令,它的值可以是存放class名的变量,也可以是带有条件的对象。如此可以通过表达式来选择需要的class,以呈现不同的样式。ngHide与ngShow其实就是特殊的ngClass,ng-hide=”[expression]”相当于ng-class=”{‘hide’: [expression]}”。</p></li></ol><p>这些基本的指令构成了一套很有效的模板逻辑,我们可以消除掉各种HTML的重复性代码,还能在单个模板中呈现出无数的形式。但我不赞成将大段的HTML做成partial或directive并通过switch或if来选择性呈现,因为模板应该是可复用的组件,而不是带有逻辑的路由。</p><h3 id="真正的页面只有一个"><a href="#真正的页面只有一个" class="headerlink" title="真正的页面只有一个"></a>真正的页面只有一个</h3><p>AngularJS的主旨即快速创建单页面应用,所谓单页面就是说真正的页面只有一个,其中变化的只是模板和数据。但许多习惯于传统Web的开发人员往往找不到单页面的感觉,自然而然的将模板当作了以往的HTML页面,然后根据AngularJS的路由一一对应到模板。这是时常发生的情况,即使是在开发的中途也可能转入这个误区,因为模板的逻辑难以划分,还有被控制器牵连进去的,最后只能让每个路由单独使用对应模板。</p><p>模板不是页面,不应该和路由有任何逻辑关系,它关心的应该仅仅是数据呈现结构。所谓的数据呈现结构是说数据需要以何种方式呈现,比如列表、统计图、详情或分页等,而不是数据结构。它往往依赖于HTML结构,所以当HTML的结构不够表意时,模板的划分也会跟着变得困难。一个模板就是一个对象,不属于它的东西,无论多麻烦,即使HTML可以放在一起,也一定要排除在外,无论封装成directive还是partial,或者分离出去与之变成平级关系。</p><p>模板也不应该关心数据的内容,即使两个路由中显示的数据内容完全不一样,但显示结构一样或类似,模板就应该利用自身的逻辑在注入数据后呈现对应的内容。但值得注意的是,其自身逻辑应该判断数据本身而不是引用控制器的逻辑,也就是说模板要有自身的逻辑,不应该将页面逻辑混合散落在模板和控制器中。那么如何做才好呢,简而言之就是UI逻辑和业务逻辑的分离,模板中应该只存在UI逻辑。我们应该封装模板,像黑盒一样,无论控制器有什么业务逻辑,都不应该干涉UI逻辑。</p><p>比如ngIf指令可以使用表达式,我们应该利用数据本身达到逻辑表达,而不是依赖于控制器中依赖于业务逻辑的变量,比如使用data.length > 0而不是hasData。这看起来是一样的,但其实区别很大,前者将具体逻辑放到了模板中,当数据为空时不显示,而后者则把逻辑抛给了控制器。这样就不仅仅是逻辑放置在哪的问题了,这是违背MVVM框架初衷的。因为当data为空后,控制器必须之前在监听data,并将hasData设为false,而这些事应该交由AngularJS在数据绑定时自动完成。所以如果实在分不清什么逻辑是UI逻辑要放在模板中,什么逻辑是业务逻辑该放在控制器中,那就遵循一个原则,能利用Angular性能时就利用,不要做AngularJS已经做过的事。</p><h3 id="Scope属于谁?"><a href="#Scope属于谁?" class="headerlink" title="Scope属于谁?"></a>Scope属于谁?</h3><p>其实这是思维方式的转变,所以误入歧途是不可避免的,而其中最多的误解在于Scope。Scope本身是作为在控制器中的模板作用域,实际效果就是Scope上的属性在模板中可以直接使用,无论是在花括号还是Angular的表达式中,比如指令中。但开发人员很容易被其表面所迷惑,将其当作传统后端模板的变量来使用,更有甚者发明了$scope.vm作为ViewModule来使用,但其实Scope本身就应该是ViewModule。其原因还是在于思维,不同于传统后端的模板变量,Scope是依赖注入到控制器的,也就是说Scope不属于控制器,而属于模板。很明显在模板中将不需要Scope这一名称来指明,因为所有变量都是Scope的。</p><p>Scope属于控制器或模板有区别吗?有而且非常大。当Scope一片空白的来到控制器时,我们自然而然的认定这样一个原先空空的变量肯定是被控制器创造出的,然后在模板中被使用。但我们换个角度想想,如果模板先定义了它需要的Scope的结构,而后控制器仅仅只是按照预先的定义插入对应的数据,Scope的结构是不是明显属于模板。仔细想想其实很好理解,模板和任何控制器组合都是这套Scope结构,而控制器脱离了该模板Scope就变了,而且模板不用的东西,控制器放到Scope中也等于没有。也就是说,往往我们先定义了控制器才开始编辑模板,自然认为Scope是控制器创建好给模板用的,但如果我们先创建模板,控制器其实只能按照模板中规定的结构来填入数据。而这就像是Java中的接口一样,模板定义好接口,然后控制器只要满足这些填入自己的数据,就能在页面上获得需要的东西,而且同样它们都是一对多的关系。也许有人会说,那也存在同样的逻辑和结构却需要不同显示方式的情况。其实,这仅仅是表现形式的改变,数据呈现结构却没有发生本质上的改变,一个列表,无论是列、行还是格子都仍然是一个表格,模板应该用ngRepeat将其呈现,然后就是CSS的事情了。当你遇到无法使用CSS转变呈现方式的时候,首当其冲的应该考虑一下,是否HTML写的不好,不够灵活,没有语义化呢。</p><p>当我们完成这样的思维转变,模板将不再依赖于控制器,它也可以完整的自我封装起来。这样做的好处显而易见,HTML的代码将有巨大的精简,重复的代码消失的无影无踪,因为当它们重复时就可以形成一个独立的模板,模板可以套模板,它们还可以互相引用。但要注意循环引用,比如a引用了b而b又引用了a。如此CSS的重构也将变得更加简单,此时考虑使用OOCSS等提高CSS复用性将变得易如反掌。另外如果某个模板具有关联十分紧密的复杂逻辑还可以打包做成指令,这样模板的逻辑将更加的强大。</p><h3 id="命名转变思维"><a href="#命名转变思维" class="headerlink" title="命名转变思维"></a>命名转变思维</h3><p>要做到这样的思维转变其实是很难的,不过有一个小技巧可以借以完成这样的转变,那就是命名。当设计模板Scope结构时,我们可以更多的考虑这样的一个结构中某数据是什么。在控制器中,也许是一组系统管理员的数据,我们可以命名为admins,也许是一组用户数据users,又或者是一组读者数据reader,还可能是采购员buyer。不过等等,一旦使用了这之中的某个命名,模板就会和控制器绑在一起,你无法为其他的数据使用该模板,而恰恰对于设计统一性来说,这些数据又很可能会放在同种结构之中,也许仅仅是颜色或标题的差异。</p><p>此时还能做什么,复制一份相同的模板然后选择另外的命名?完全错误,我们应该抽象它们,它们都是一类数据,列表list。就像之前我说的,设计模板时仅仅应该关心其数据表现结构,它就是一个列表,列表的数据内容完全不会影响到结构。无论是users还是buyer,都可以放入一个list模板中。这是非常重要的,一个长期和数据库打交道的后端,惯性思维会将user和users归为一类,并认为admins和users是完全不同的。但是到了UI层面,这些显而易见的规则已经不适用了,users和admins都是list,而admin和user都是object。如果你担心不同数据类型需要的UI表现不一样,那就应该先考虑CSS实现兼容各种形式,其次是考虑嵌入一些不同的模板或指令来实现差异化,而不是直接复制两个模板。</p><p>总之,使用AngularJS一定不要过多考虑流程,而要考虑UI本身,我觉得这有点像面向对象的思维方式,UI既对象。利用AngularJS的监听机制让数据驱动模板产生交互。甚至我还经常利用JavaScript中Object的引用类型特性,使一些关联的数据之间也能产生数据与视图之间的关联,这样连通UI与数据和数据与数据之后就不用再管UI了,只需要操作数据即可,UI自然会表现出应有的状态。</p>]]></content>
<summary type="html"><p><img src="/images/angular.jpg" alt="AngularJS Template"></p>
<p>作为最流行的MVVM(Model-View-View-Model)框架之一,相信大部分前端对AngularJS都不会陌生,我也一样久仰大名。不得不说,AngularJS所带来的改变是巨大的,被称为未来浏览器的模式一点也不为过,尤其是思维上的转变。</p>
<p>作为一个常年挥舞着jQuery去指挥无穷无尽的DOM的前端,初次接触AngularJS是有困难的,许多先贤警告我们不要在AngularJS中使用jQuery,不是没有道理的。即使AngularJS中带有jQlite对象,也仅仅是为了弥补一些地方AngularJS的局限性。AngularJS操作UI的方式与jQuery有着极大区别,在深入学习之后,我渐渐的发现了这点。过去使用jQuery的前端就像一个操纵提线木偶的傀儡师,而手握AngularJS的前端简直是不折不扣的魔法师。前端开发者不再需要根据数据去改变DOM,然后填入数据,我们所要做的仅仅是决定数据的表现形式后等待数据的注入。文档流中的元素就像活过来了一样,根据数据表现出了对应的样子。</p>
<p>这一切的核心除了匪夷所思的DOM监听机制,还有就是AngularJS的模板(template)以及其中多不胜数的内置指令(directive)了。因此,我将在本文中谈谈AngularJS的模板以及其思维模式。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="javascript" scheme="https://blog.tychio.net/tags/javascript/"/>
<category term="angular" scheme="https://blog.tychio.net/tags/angular/"/>
<category term="angularjs" scheme="https://blog.tychio.net/tags/angularjs/"/>
<category term="template" scheme="https://blog.tychio.net/tags/template/"/>
</entry>
<entry>
<title>如何写好CSS?(OOCSS\DRY\SMACSS)</title>
<link href="https://blog.tychio.net/2014/03/15/css-principle/"/>
<id>https://blog.tychio.net/2014/03/15/css-principle/</id>
<published>2014-03-15T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p>很久没有写博客了,一是刚入职比较忙,二是因为总有学到新的有趣的东西,停不下脚步来总结一下。最近出差到了帝都,反而能挤出些时间来写点什么了,也正好趁着出差做的这个项目讨论一下CSS理论。</p><p>我现在面对的CSS基本上就是一个三头六臂的怪物,一点不夸张,因为真的是三头六臂,同一个样式在同一个element上作用了好几遍,而同一个样式又分散在4,5个class上,优先级有很多层。可以看得出这个怪物不是一个人造就的,早期的开发者选择了SCSS技术,但混乱的import导致了一些基本的样式被多次调用,而后面的开发者又为了摆脱之前的混乱引入了其他共用样式,但无济于事。原因出在HTML上,CSS依托于HTML没有被正确的抽象,而HTML又完全的依赖业务,所有class以业务取名,HTML和CSS基本没有复用,最终抽出的共用样式也仅仅是又一次的重复。CSS重构最难的地方在于没有脚手架,即测试。虽然有一些方法来测试,比如reftest,但还不够成熟。抱着有总比没好的心态,CSS被一层又一层的覆盖了上去。</p><span id="more"></span><h2 id="真正的问题是什么?"><a href="#真正的问题是什么?" class="headerlink" title="真正的问题是什么?"></a>真正的问题是什么?</h2><p>CSS即层叠样式表,所以一层一层覆盖其实是其本质特征。真正的问题在于维护,许多人认为CSS仅是样式,不是代码,无需维护,所以任意书写,只要将自己需要的样式的优先级设为最高即可,才导致了深层级CSS的出现,因为每次添加一个样式就必须比以前的优先级高才能在页面看到。深层级不仅造成维护性降低,可读性也是一个问题,人不是机器,无法很优雅的按优先级阅读,所以很难确认一个样式用于哪里,其实还存在许多的冗余样式,在任何地方都被覆盖的样式。这样的代码在扩展性上,一开始反而是有优势的,因为添加一个新class,无需担心影响其他地方,但慢慢随着项目规模的增大,页面增多,需要复制样式的地方也越来越多,它们之间又存在微小的差异,设计的更改,需求的变化,这一切都会将这种快餐式的CSS推进柏油坑。因为难以维护,所以无法响应需求,所以无法复用,只能复制,恶性循环。</p><p>正如上面所说的,问题在于可读性、维护性、扩展性、复用性这几个方面。所以只要提高它们就能解决问题, 虽然这么说,也不是如此简单的。先来谈谈在CSS中,这些概念都有着怎样的意义。</p><h4 id="可读性"><a href="#可读性" class="headerlink" title="可读性"></a>可读性</h4><p>有人认为CSS不是程序,不需要可读性,有人认为CSS只要写出来就有可读性,因为很简单。抛开各种预处理器不说,原生CSS结构确实简单,没有需要编程的部分,但仍然可能导致混乱。原因有二,一是CSS可以层叠,其中涉及到了优先级和作用范围,如果写的不好,人很难读出其中的意义,二是CSS属性众多,加上CSS3引入了很多用法独特的属性,一个选择器可能包含几十个属性。比如下面这段我随便写的CSS代码:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">span {</span><br><span class="line">-webkit-box-shadow: 6px 4px 4px red;</span><br><span class="line">-moz-box-shadow: 6px 4px 4px red;</span><br><span class="line">box-shadow: 6px 4px 4px red;</span><br><span class="line">}</span><br><span class="line">div span {</span><br><span class="line">border-width: 4px;</span><br><span class="line">border-style: dotted;</span><br><span class="line">border-color: blue;</span><br><span class="line">}</span><br><span class="line">#box {</span><br><span class="line">border-left: 2px solid red;</span><br><span class="line">border-bottom: 2px solid red; </span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>乍一看也没什么,都是border,大致能看出来这段CSS只是为了添加一个红色的阴影让box看起来比较立体。但中间的部分似乎是捣乱的,你可能会说这太傻了,看不到吗。是的,当这3部分散落在上万行的CSS中时,肯定看不到。于是有人很自然的想起了我们可爱的浏览器,没错,在浏览器中可以快速找到作用于目标的CSS样式,但这也是万恶之源。首先我假设你不知道中间那部分东西是为了什么而写的,因为你是靠浏览器找到它的。然后剩下两种可能,不管三七二十一改了再说和看看它为什么存在。前者悲剧的可能性是100%,后者悲剧的可能性是90%,因为你已经掉坑里了,很快我们会发现要修改它还牵扯到了另外的地方,接着在浏览器中探索到另一个莫名其妙的样式,当你弄懂全部的时候,你应该已经把上万行的代码弄了个一清二楚了,也许最幸运的是,浪费了几个小时的时间发现只需要修改一行就能达到目的。</p><p>当然,我们可以天真的认为,只要把他们写在一起就可以了,这样找起来很简单。而我将继续顺着这样的思路来尝试曝露问题。</p><h4 id="维护性"><a href="#维护性" class="headerlink" title="维护性"></a>维护性</h4><p>所谓物以类聚是很有道理的,人们习惯将事物归类,但问题是分类标准,样式并不关心业务,无论是什么文字内容,还是功能有何不同,它在乎的只是样式,比如文字的尺寸,间距和宽高,颜色等等。如果简单的将一个组件的样式放在一起,势必带来的就是小段代码的重复书写。不觉得有多严重?我来举个栗子。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">aside {</span><br><span class="line">box-shadow: 6px 4px 4px #AA3343;</span><br><span class="line">}</span><br><span class="line">nav {</span><br><span class="line">box-shadow: 6px 4px 4px #AB3633;</span><br><span class="line">}</span><br><span class="line">.item {</span><br><span class="line">box-shadow: 6px 4px 4px #AA3732;</span><br><span class="line">}</span><br><span class="line">.item.otherStatus {</span><br><span class="line">box-shadow: 6px 4px 4px #AA3132;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>继续说上面的例子,box需要阴影,但如果这个项目的UI统一风格,包括sidebar,navigator以及item都需要这样的阴影呢?再如果,明天客户或者UX一拍脑袋,这个阴影应该是灰色的不该是红色的呢?不要继续天真的认为全局替换是救命稻草。首先,没有几个网站会用red,blur做色调的,你用的应该是#AA3333,这样的代码,然后你发现sidebar用了#A43433,而navigator是#AB3633,等等,item有两个状态,而两个状态对应的颜色是不一样的。这怎么可能?但当你打开浏览器的时候你会发现本来就相差无几的颜色,在阴影中变得一模一样了,谁看的出来呢,当初使用的时候可能也不过是随意的在mockup中取的一个颜色。</p><p>大量的重复带来的不仅仅是代码的冗余,我们必须靠人力去同步它们,而人很难保证它们的修改是完全一致的,尤其是当它们中引入了一些不一致的独特的东西时。不要小看CSS,其后果就是进度和人力的压力,后面就是PM有没有读过《人月神话》的事了。</p><p>肯定有人在想,谁让你当初要写成这样呢。我们在读代码的时候最喜欢问,当初为什么要这么写?但慢慢的你会读出它的历史,有时候它是身不由己的。这就涉及到了下一个要讨论的内容。</p><h4 id="扩展性"><a href="#扩展性" class="headerlink" title="扩展性"></a>扩展性</h4><p>扩展性是一个具有欺骗性的东西,所谓的扩展性其实就是在现有基础上再次开发新东西的性能,但我认为它还必须有前提条件,那就是保持可读性与维护性。</p><p>简单的追求可维护性是自取灭亡,原因很简单,将新旧代码完全分离的时候扩展性最高,因为不必担心对以前的部分有影响,新的样式可以随意发挥。是不是很神奇,这样想的我们写下的代码,肯定就是前面我们追问的代码。所以自己回答自己吧,当初没考虑可读性和维护性,只想着快点增加新的样式,就这么写了。</p><p>那什么才是一个好的扩展性呢,简单来说,就是多功能产品。比如一个box,也许它的样式就时</p><h4 id="复用性"><a href="#复用性" class="headerlink" title="复用性"></a>复用性</h4><p>似乎我一直在说的就是重复,那我们就来说说复用性,如何才能复用CSS代码是一个很大的问题,比如粒度,是一两个属性进行复用还是一大组选择器进行复用呢,再比如对象,是为了class复用属性,还是为了html复用class呢。这些选择不算太重要,但是带来的影响却很重大,可以说是整个CSS结构的改变。下面继续用box的阴影来讨论复用。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.shadow {</span><br><span class="line">-webkit-box-shadow: 6px 4px 4px #A93334;</span><br><span class="line">-moz-box-shadow: 6px 4px 4px #A93334;</span><br><span class="line">box-shadow: 6px 4px 4px #A93334;</span><br><span class="line">border-left: 2px solid #A93334\9;</span><br><span class="line">border-bottom: 2px solid #A93334\9; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样看起来我有了一个shadow的class可以给任意的目标加上这个阴影了,但这导致了一个复用的问题,和上面那段捣乱的CSS样式一样,如果item已有另外2个border了,那这个class是无法去除的。所以复用时不仅要考虑需要什么,还要考虑不需要什么。另外一些必须的属性比如display还有overflow等也是要考虑的,因为user agent的原因,很多属性是隐藏在element中的。</p><h2 id="如何解决问题?"><a href="#如何解决问题?" class="headerlink" title="如何解决问题?"></a>如何解决问题?</h2><p>主流的CSS原则有OOCSS,DRY,SMACSS以及BEM,他们皆是为解决CSS的各种问题而生。</p><h4 id="OOCSS"><a href="#OOCSS" class="headerlink" title="OOCSS"></a>OOCSS</h4><p>OOCSS即面向对象的CSS,这里对象指的是页面中的元素对象,与传统编程中的面向对象不太相同,比如不存在方法这种东西,硬要说的话,附加的一些class可以看作是继承或者接口之类的东西来实现对象的差异化。比如电商网站中的商品就是一个典型的对象,它们既有许多相同的部分,又有许多差异,宽高、按钮、图片、标题等基本布局都是相同的,而边距、线框、背景颜色、字号等都是差异化的。由此按照OOCSS的指导原则,我们应该写一个product class,然后为其添加一些border、theme之类的class来差异化它:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.product {</span><br><span class="line">display: block;</span><br><span class="line">overflow: hidden;</span><br><span class="line">float: left;</span><br><span class="line">width: 200px;</span><br><span class="line">height: auto;</span><br><span class="line">}</span><br><span class="line">.product-head{...}</span><br><span class="line">.product-body{...}</span><br><span class="line">.product-foot{...}</span><br><span class="line"></span><br><span class="line">.product-theme-black {</span><br><span class="line">background: black;</span><br><span class="line">color: white;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.product-border {</span><br><span class="line">border: 1px solid #333;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样在以上两种附加class的作用下,我们在html中就可以获得4种不同的product样式,随着附加class增加,product的样式也会呈指数增加,千变万化。这仅仅是一个简单的例子,意在点出OOCSS的理念,但并没有突出它的意义所在。别着急,先来看看OOCSS的两大原则。</p><p><em><strong>1. 分离容器与内容</strong></em></p><p>所谓的容器即包裹对象的元素,比如一个div,我们经常会命名为wrap、container、body等。那么如何才算是分离容器与内容呢?很简单,一句话,内容在哪都可用。也就是说不应该出现这样的情况:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.container .product {</span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样干的结果就是复用性大大降低,因为只能在这个容器内使用它了。但这并不代表我们应该将所需的样式全部一股脑的扔进单独的class中,对于差异化应该单独放在一个class中,这才是OOCSS的精髓。</p><p>举个例子,当我们既不想牺牲太多性能,又想来个瀑布流显摆显摆的时候,大部分前端都会使用column,类似泳道的设计。你想说哦不,这是伪pinterest,但是谁在乎呢,用户是不会有闲工夫拖拽浏览器的宽度来鉴别它的,在IE下商品多的时候至少不会太卡。哈,别较真,首先分为几个column,然后按照高度往里填放商品,先来看看下面的代码吧,我有省略一些样式避免误导:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.column {</span><br><span class="line">height: auto;</span><br><span class="line">width: 200px;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.product {</span><br><span class="line">width: 180px;</span><br><span class="line">margin-right: 20px;</span><br><span class="line">margin-bottom: 10px;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看起来不错,每列200px宽,商品放入其中,水平间距要大,垂直间距要小些才像column。但是等等,我们总还是需要整齐摆放的商品列表的对不对。也许margin并不是product的必要属性,至少它应该是可变的。我们抽出它来:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.product {</span><br><span class="line">width: 180px;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.vertical-product {</span><br><span class="line">height: 400px;</span><br><span class="line">margin-right: 10px;</span><br><span class="line">margin-bottom: 10px;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.horizontal-product {</span><br><span class="line">height: auto;</span><br><span class="line">margin-right: 20px;</span><br><span class="line">margin-bottom: 10px;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样便将column或list之类的容器与product分开来毫无关系了,即使以后出现了其他组织形式,只要product的基本结构没有变都可以直接复用,无非是添加一些附属样式到新的xxx-product的class中。另外这样做还有一个好处,设计逻辑放在了HTML中,CSS更加强大。</p><p>什么是样式逻辑?商品在瀑布流中不定高,在列表中定高,这就是一种样式的逻辑,如果用父子选择器的形式写在CSS中,那它就失去了自由。而放在HTML通过选择添加何种附属class来展现不同形式的product,则非常的自由与灵活。另外值得一说的是,margin-bottom是一样的,但我们应该各自放在各自的class里面,原因很简单,它们仅仅是一不小心恰好一样,在设计逻辑中它们并不是一样的bottom,这里并不是重复,而是看起来一样。如果以后需要改变其中的一个bottom,共用则显得非常别扭。</p><p><em><strong>2. 分离皮肤与结构</strong></em></p><p>第二点很容易理解,皮肤(theme)就是视觉效果,即使被剔除网页也没有什么影响的就是皮肤;而结构指地并不是像HTML这样抽象的结构,因为CSS毕竟还是样式,所以结构只是相对的页面结构。</p><p>先来看看我们的product吧,添加一些背景色和边框:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.product {</span><br><span class="line">width: 200px;</span><br><span class="line">background: #F6F2F2;</span><br><span class="line">border: 1px solid #C4A0A0;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看起来还不错,不过设计师都是自大狂,精心的调色,完美的搭配,绝对不会让你仅仅使用这么一次的,页面的其他模块、sidebar甚至是header都可能采用相同的背景颜色与边框,它们甚至可能互相嵌套。好吧,这其实在设计上是为了视觉统一,毕竟没有几个设计大师能hold住3,4种以上的颜色。所以我们能做的并不是在每个class中添加这样的样式,而是把它提出来成为独立的class,原因就像我开篇说的那样,颜色为混沌之源。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.main-bg {</span><br><span class="line">background: #F6F2F2;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.main-border {</span><br><span class="line">border: 1px solid #C4A0A0;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样就可以在页面中随时使用主要的设计元素了,而且需要修改时也非常的简单,不用担心有什么地方漏掉。另外我将背景与边框分为了两个class,原因还是设计逻辑应该放在HTML,背景与边框并不是一定同时出现的,两者的关系应该由HTML决定,即使设计上逻辑决定了两者的绑定,在实现时也有可能因为HTML结构而放在两个不同的元素上。</p><p>OOCSS强调class,将每组样式写成一个class方便HTML中使用,众多class组合起来能千变万化组成一个对象。所以如果是想一劳永逸的写一套UI作为开发时使用的样式,我建议使用OOCSS来进行开发。但它也有缺点,过多的将设计逻辑放在HTML中,极大的自由化了页面开发时的选择,如果写HTML的开发者不能很好的理解整套CSS的结构,较易在HTML中造成class混乱。</p><h4 id="DRY-CSS"><a href="#DRY-CSS" class="headerlink" title="DRY CSS"></a>DRY CSS</h4><p>DRY就是Donot repeat youself 不要重复。但其实这个名字有点无趣,哪个理论不是消除重复呢,但如何消除才是意义所在。总的来说我认为DRYCSS与OOCSS是两个极端,所以我将会以对比的方式来讲讲DRYCSS的内容。使用DRYCSS很简单,三步。</p><p><em><strong>1. 分组可复用属性</strong></em></p><p>DRYCSS跟OOCSS有点像,第一步都是分组样式,消除重复,但就像我说的,关键在于如何。OOCSS将样式集合看作对象,所以分组的逻辑是,某个元素本身应该是什么样的,而DRYCSS则关注重复,无论什么逻辑,只要是一样的就应该只有一个。其中粒度是值得思考的问题,如果太细,那只会成为一行样式一组这样无意义的情况,如果太粗,又会变成毫无复用性的庞然大物。我认为可以将一些有关联的缺了A时B就没作用的样式分为一组,还可以将某些惯用搭配分为一组。下面举个例子:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line">float: left;</span><br><span class="line">position: absolute;</span><br><span class="line">display: inline-block;</span><br><span class="line">overflow: hidden;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这是一组样式,可用来触发<a href="http://kayosite.com/block-formatting-contexts-in-detail.html">Block formatting Contexts(块级格式化上下文)</a>,如此就完成了一组样式。接着再写2组关于尺寸的样式吧。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line">width: 960px;</span><br><span class="line">height: auto;</span><br><span class="line">}</span><br><span class="line">{</span><br><span class="line">width: 720px;</span><br><span class="line">height: 600px;</span><br><span class="line">}</span><br><span class="line">{</span><br><span class="line">width: 220px;</span><br><span class="line">height: 600px;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这是三组样式用来布局,将页面分为左右两部分。</p><p><em><strong>2. 按逻辑为分组命名</strong></em></p><p>接着我们来为其命名,其实就是添加一个ID选择器,但是我们并不真的使用它,而是用来标示该组样式。下面就来命名上面所分组的样式。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">#BLOCK_FORMATTING_CONTEXTS</span><br><span class="line">{</span><br><span class="line">float: left;</span><br><span class="line">position: absolute;</span><br><span class="line">display: inline-block;</span><br><span class="line">overflow: hidden;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#LAYOUT_FULL</span><br><span class="line">{</span><br><span class="line">width: 960px;</span><br><span class="line">height: auto;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#LAYOUT_CONTENT</span><br><span class="line">{</span><br><span class="line">width: 720px;</span><br><span class="line">height: 600px;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#LAYOUT_SIDEBAR</span><br><span class="line">{</span><br><span class="line">width: 220px;</span><br><span class="line">height: 600px;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这一步类似OOCSS的class,它决定了每组样式所代表的逻辑或用途,然而DRYCSS多了最关键的下一步,也是与OOCSS本质区别。</p><p><em><strong>3. 为各个分组添加选择器</strong></em></p><p>DRYCSS在使用时和OOCSS有着巨大的差异,在CSS文件中写入HTML中的class选择器来使用这些分组后的样式,而不是直接在HTML中使用CSS文件中写好的class。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.header,</span><br><span class="line">.container,</span><br><span class="line">.content-right,</span><br><span class="line">.content-left,</span><br><span class="line">#BLOCK_FORMATTING_CONTEXTS</span><br><span class="line">{</span><br><span class="line">float: left;</span><br><span class="line">position: absolute;</span><br><span class="line">display: inline-block;</span><br><span class="line">overflow: hidden;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.header,</span><br><span class="line">.navigator,</span><br><span class="line">.container,</span><br><span class="line">#LAYOUT_FULL</span><br><span class="line">{</span><br><span class="line">width: 960px;</span><br><span class="line">height: auto;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">.content-right,</span><br><span class="line">.section,</span><br><span class="line">#LAYOUT_CONTENT</span><br><span class="line">{</span><br><span class="line">width: 720px;</span><br><span class="line">height: 600px;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">.content-right,</span><br><span class="line">.sidebar,</span><br><span class="line">.profile,</span><br><span class="line">#LAYOUT_SIDEBAR</span><br><span class="line">{</span><br><span class="line">width: 220px;</span><br><span class="line">height: 600px;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,使用DRYCSS时,在HTML中所写的class将会非常表意,元素本身是什么用来做什么,就使用其意义的class命名,而且基本上是一个元素对应一个class,HTML将变的简单明了。另外DRYCSS也是相对于OOCSS的一种逆向思维,这才是最有趣的地方。在开发中,不应该像OOCSS那样思考如何应对未来假象的HTML,而是仅仅思考CSS本身。</p><p>总的来说,OOCSS适合开发CSS框架或整套UI模版,是自外向内的UI开发方式;而DRYCSS则适合拯救混沌的HTML,或者加强HTML的结构性和表意性,是自内向外的UI开发方式。这里的内指地是HTML结构,外指地是CSS样式。</p><h4 id="SMACSS"><a href="#SMACSS" class="headerlink" title="SMACSS"></a>SMACSS</h4><p>这是一个相对繁杂的CSS理论,分为Base、Layout、Module、Status和Theme共五个部分。不过它的核心思想仍然和OOCSS类似,鼓励使用class。</p><p><em><strong>1. Base 基本属性</strong></em></p><p>基础属性很容易理解,就是最基本的东西,很多样式简单的网站都采用一个简单的二级CSS文件模式,一个base.css通用于所有页面,而每个页面有一个特定的CSS文件,我想这就是Base的雏形。要说具体是什么,比如reset文件,再比如放置clearfix或BFC的一些类似工具集的文件。</p><p>其实最终会发现,在Base中的CSS属性将会是几乎全站都要用到的属性,但我不想这么描述Base,因为这会误导人。大多数情况下,在一个网站建立之初也只会有几个简单的页面,于是这几个页面都要用到的属性就变成了通用属性,但并不是这么简单的。随着网站规模的扩大,需求的增加,设计师们灵感的迸发,所谓的通用和统一也在发生着潜移默化。所以在编写Base时,应该遵循的基准是,哪些样式是你做下一个网站时也会想用的,哪些样式即使设计改变了也只需要改变一些数值和颜色,哪些样式是一些基本原则;而不应该将目前大部分页面都在使用的样式放在Base中,还是那个道理,它们也许仅仅是恰好相同,而非逻辑一致。</p><p><em><strong>2. Layout 布局</strong></em></p><p>布局是一个网站的基本,无论是左右还是居中,甚至其他什么布局,要实现页面的基本浏览功能,布局必不可少。SMACSS将这一功能单独提出也是非常正确的,另外还约定了一个前缀<code>l-/layout-</code>来标识布局的class。举个最普遍的例子。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.l-header {}</span><br><span class="line">.l-brand {}</span><br><span class="line">.l-navigator {}</span><br><span class="line">.l-container {}</span><br><span class="line">.l-sidebar {}</span><br><span class="line">.l-content {}</span><br><span class="line">.l-footer {}</span><br></pre></td></tr></table></figure><p>这就是一个简单的左右布局,导航和Logo中规中矩在最顶部。</p><p><em><strong>3. Module 模块</strong></em></p><p>模块是SMACSS最基本的思想,同时也是大部分CSS理论的基本,将样式模块化就能达到复用和可维护的目的,但是SMACSS提出了更具体的模块化方案。首先表象上来看,SMACSS中的模块应该拥有一个名字,并且为其class名,而模块其他class皆以为前缀。比如:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">.product {}</span><br><span class="line">.product-title {}</span><br><span class="line">.product-image {}</span><br><span class="line">.product-border {}</span><br><span class="line">.product-shadow {}</span><br></pre></td></tr></table></figure><p>可以看到例子中product是一个模块,title和image是包含在模块内的组件,可用可不用;border和shadow是类似OOCSS的附加class用来改变模块本身。总之,在模块内可以使用其名称做前缀任意组织模块结构,但目前是让其变得更易用,提高可扩展性和灵活度,如果仅仅为了某些功能而特意写一些class就有点有形无实的感觉了。</p><p><em><strong>4. State 状态</strong></em></p><p>状态经常和JavaScript放在一起使用,它是一种用来标识页面状态的class,无论是为用户标识还是用程序标识。还是一个常见的例子,马上就明白。active经常用来表示当前的tab,或者当前选中的目标,这就是一种状态,无论是样式还是程序都需要知道它。</p><p>SMACSS仍然有一个对应的前缀用于标示状态class,<code>is-</code>是一个合适的词,指明某一元素是什么状态。</p><p><em><strong>5. Theme 主题</strong></em></p><p>主题就是皮肤,和OOCSS的分离皮肤与结构不谋而合。更重要的是对于可更换皮肤的站点来说,这样的分离是非常必要的,只需要更换加载的theme文件即可将皮肤更换。</p><p>总的来说,SMACSS是一个较为注意细节与实现的CSS理论,非常适合初涉CSS的人,它可以让你的CSS跑在轨道上而不至于脱轨。其思想也与OOCSS有很多相通之处,如果没有适合的方案,我建议新手可以适当的融入OOCSS的思想而使用SMACSS的结构,这样写出来的网站样式至少不会马上陷入泥沼。</p><h2 id="哪一个好用呢?"><a href="#哪一个好用呢?" class="headerlink" title="哪一个好用呢?"></a>哪一个好用呢?</h2><p>谈了许多的CSS原理,已经有点眼花缭乱,到底哪个好呢?这个问题又归结到了最佳实践上,虽然我并不认为有这样的实践,但我认为一个项目一定会有适合的实践,比如前面说的,如果你想做一个CSS框架然后再写HTML,那就用OOCSS;如果你想先写HTML或者已经有一个旧的页面,那DRYCSS应该很适合你;如果新手不知如何下手,那SMACSS可以指导你入门。</p><p>无论如何,在我过去很长一段时间的独立UI开发“生涯”中,这些情况我都遇到过,也有一些自己的想法,我想我将在下一篇博客中谈谈自己对CSS的理解,并尝试整理出一些类似理论的东西来。</p>]]></content>
<summary type="html"><p>很久没有写博客了,一是刚入职比较忙,二是因为总有学到新的有趣的东西,停不下脚步来总结一下。最近出差到了帝都,反而能挤出些时间来写点什么了,也正好趁着出差做的这个项目讨论一下CSS理论。</p>
<p>我现在面对的CSS基本上就是一个三头六臂的怪物,一点不夸张,因为真的是三头六臂,同一个样式在同一个element上作用了好几遍,而同一个样式又分散在4,5个class上,优先级有很多层。可以看得出这个怪物不是一个人造就的,早期的开发者选择了SCSS技术,但混乱的import导致了一些基本的样式被多次调用,而后面的开发者又为了摆脱之前的混乱引入了其他共用样式,但无济于事。原因出在HTML上,CSS依托于HTML没有被正确的抽象,而HTML又完全的依赖业务,所有class以业务取名,HTML和CSS基本没有复用,最终抽出的共用样式也仅仅是又一次的重复。CSS重构最难的地方在于没有脚手架,即测试。虽然有一些方法来测试,比如reftest,但还不够成熟。抱着有总比没好的心态,CSS被一层又一层的覆盖了上去。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="css" scheme="https://blog.tychio.net/tags/css/"/>
<category term="oocss" scheme="https://blog.tychio.net/tags/oocss/"/>
<category term="drycss" scheme="https://blog.tychio.net/tags/drycss/"/>
<category term="smacss" scheme="https://blog.tychio.net/tags/smacss/"/>
</entry>
<entry>
<title>《代码整洁之道》Clean Code</title>
<link href="https://blog.tychio.net/2013/11/25/clean-code/"/>
<id>https://blog.tychio.net/2013/11/25/clean-code/</id>
<published>2013-11-25T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/cleancode.jpg" alt="Clean Code">这本书读了许久才读了一遍,不同于之前读到的一些方法性的书籍,该书有更多的细节,但其中又存在许多理论性的东西,要消化它们可不是很容易。读完一遍后又快速回顾了一遍,马上又会发现很多新的东西并有一些新的理解,甚至让我觉得写这篇文章有点太急了,不过我还是应该尝试收集一些有营养的东西,至少记录现在的想法,相信对之后的第二遍阅读会更有益处。</p><p>书名其实有点误导我,或者说它不仅仅是其字面意思那样简单,‘整洁’不是简单的清洁,其主旨在于一种设计上的精巧。书的开篇就引用了几位大牛对代码整洁的解释来描述其意义。从抽象的描述中说,整洁代表着优雅、愉悦、精巧、简单,具体来说我觉得有以下几点:</p><ul><li>易于维护,依赖简单。</li><li>设计精巧,没有改进的余地。</li><li>高可读性,意图一目了然。</li><li>完善的测试与错误处理。</li><li>没有重复,最精简的实体。</li></ul><p>最后作者总结为深合已意,其实说起来简单,可做起来却不是这么容易的。代码可不是一块光秃秃的地板,只要使劲擦总能变整洁的。</p><span id="more"></span><p>《代码整洁之道》占了很大篇幅的前半部分其实是在讲编码规范,但又不完全是,似乎是一种指导原则。</p><p>先说说命名,在Refactoring中也有<code>rename method</code>这个方法,最开始看到它的时候其实还觉得挺好笑,觉得这也能算一个重构方法吗,但慢慢项目经验增加后,感到这是最基础也是最有效的一种重构手段。在我毕业后的第一个项目中我就遭遇了麻烦,大量的逻辑使我本来认为不错的编程习惯彻底崩溃,重复的变量名充斥在各个作用域中,有时甚至互相交替使用导致了各种bug。所以那时我就为公司制定了一系列的命名规范,比如统一使用驼峰命名、常量使用大写、用动词+名词的方式命名函数等等。大部分的规范已经变成了我的习惯而沿用到现在,但我仍能感到在为函数或方法命名时的纠结。</p><p>命名需要有意义,好像一句话,但同时又应该避免含糊的词,比如the、a、data之类的词。另外,作者建议不要使用匈牙利命名添加一些编码的前缀,但我觉得在JavaScript这样的弱类型语言中有时还是有点必要的,我还喜欢为jQuery对象添加<code>$</code>前缀来标识它。还有一点很重要,统一用词,有时我会怀疑自己的词汇量是否比外国小学生还烂,不过我发现这也是一个优点。因为对应一个意思,我第一个想到的总会是那个词,而看到那个词我也总会明白其意思,绝不会在get与fetch之间犹豫。</p><p>可能许多人会遇到一个问题,函数的功能太多了,导致名称很长仍无法完全描述其功能,此时解决问题的不是命名,而是重构,函数不该是如此多功能的东西,它需要被拆分。看到第三章,我突然想到,这是不是某种驱动关系。为函数命名驱使我们要不断的理清函数的功能和意义,并保持其单一功能性。</p><p>另外在第八章中讲到的边界也很有意思,说到第三方库,JavaScript中可以说是没有人不用第三方库的,尤其是我现在的公司,依赖于requirejs这样的模块工具使用了许多第三方库。起初每个功能都很快的被开发出来,感觉不错,但慢慢的随着开发的深入,各种插件与库的冲突不断,想替换又发现有很多地方都在调用其方法,代码变成了脏乱差。如果开始可以封装第三方库的方法,此时只需要重写该方法就可以轻松替换。</p><p>在十三章中提到了并发编程,这有点像JavaScript中的异步特性。比如使用<code>jQuery.animation</code>完成一些复杂动画时,需要一个动画队列,此时动画的标签就是限定资源,这是一个生产者和消费者模型。当然真正的并行要比这复杂多了,但仍然可以从中借鉴到一些思想。</p>]]></content>
<summary type="html"><p><img src="/images/cleancode.jpg" alt="Clean Code">这本书读了许久才读了一遍,不同于之前读到的一些方法性的书籍,该书有更多的细节,但其中又存在许多理论性的东西,要消化它们可不是很容易。读完一遍后又快速回顾了一遍,马上又会发现很多新的东西并有一些新的理解,甚至让我觉得写这篇文章有点太急了,不过我还是应该尝试收集一些有营养的东西,至少记录现在的想法,相信对之后的第二遍阅读会更有益处。</p>
<p>书名其实有点误导我,或者说它不仅仅是其字面意思那样简单,‘整洁’不是简单的清洁,其主旨在于一种设计上的精巧。书的开篇就引用了几位大牛对代码整洁的解释来描述其意义。从抽象的描述中说,整洁代表着优雅、愉悦、精巧、简单,具体来说我觉得有以下几点:</p>
<ul>
<li>易于维护,依赖简单。</li>
<li>设计精巧,没有改进的余地。</li>
<li>高可读性,意图一目了然。</li>
<li>完善的测试与错误处理。</li>
<li>没有重复,最精简的实体。</li>
</ul>
<p>最后作者总结为深合已意,其实说起来简单,可做起来却不是这么容易的。代码可不是一块光秃秃的地板,只要使劲擦总能变整洁的。</p></summary>
<category term="Read" scheme="https://blog.tychio.net/categories/Read/"/>
<category term="software" scheme="https://blog.tychio.net/tags/software/"/>
<category term="SE" scheme="https://blog.tychio.net/tags/SE/"/>
<category term="软件工程" scheme="https://blog.tychio.net/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
<category term="代码整洁" scheme="https://blog.tychio.net/tags/%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81/"/>
</entry>
<entry>
<title>Stylus使用指南</title>
<link href="https://blog.tychio.net/2013/11/15/stylus-guide/"/>
<id>https://blog.tychio.net/2013/11/15/stylus-guide/</id>
<published>2013-11-15T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/stylus.jpg" alt="Stylus Logo"><br>Stylus似乎并不是很有名,以至于很多人不知道它是做什么的,但提到SASS相信有不少人听说过甚至使用过很长时间。其实无论是LESS、SASS还是Stylus甚至是Absurd这些预处理工具,都是对CSS的一种延伸和强化。出现这些工具的原因很简单,CSS本身只是一种描述性质的东西,甚至它不能算是语言而是样式表,所以我们需要一个有条件语句和变量甚至是函数的东西去动态生成CSS代码来达到提高效率和增强可维护性的目的。</p><p>本文主要以Stylus语法本身和简单的使用为主要内容,它的目的是介绍和简单指南。将不会过多涉及Javascript的API调用等问题。</p><h3 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h3><p>官方的介绍非常简短而精炼:</p><blockquote><p>Expressive, dynamic, robust CSS</p></blockquote><span id="more"></span><p>富有表现力的动态的强壮的CSS,它反应了一些主要特点。</p><p>首先Stylus相较于SASS更加简洁,甚至冒号也都可以省略,初学Stylus时感到它太神奇了,仅仅以空格分隔属性名和多个属性值就可以生成想要的CSS,而且还可以拼接字符串等等。与此同时,类似Ruby或Python完善的缩进语法,Stylus在简约自由中有效的防止了语法歧义。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">body</span><br><span class="line"> border 10px*.1 soli+'d' darken(red,10%)</span><br><span class="line"></span><br><span class="line">// =></span><br><span class="line">body {</span><br><span class="line"> border: 1px solid #e60000;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其次是动态,这正是其精髓所在,Stylus由Javascript编译,其结构语句也和Javascript相差不多,前端人员可以很轻松的上手。虽然这方面Absurd是一个极端,但Stylus较之LESS则要优越不少,不仅仅是可定义变量,如Javascript般的条件语句和循环语句也为Stylus带来各种可能,加上丰富的内置函数,可以轻松判断和操作各种变量。而利用这样的动态性,就可以写出非常强壮的CSS以满足不同环境和条件下的需要。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">pos(type, args)</span><br><span class="line"> i = 0</span><br><span class="line"> position unquote(type)</span><br><span class="line"> {args[i]} args[i + 1] is a 'unit' ? args[i += 1] : 0</span><br><span class="line"> {args[i += 1]} args[i + 1] is a 'unit' ? args[i += 1] : 0</span><br><span class="line"></span><br><span class="line">absolute()</span><br><span class="line"> pos('absolute', arguments)</span><br><span class="line">fixed()</span><br><span class="line"> pos('fixed', arguments)</span><br><span class="line"></span><br><span class="line">#prompt</span><br><span class="line"> absolute top 150px left 5px</span><br><span class="line"> width 200px</span><br><span class="line"> margin-left -(@width / 2)</span><br><span class="line">#logo</span><br><span class="line"> fixed top left</span><br><span class="line"></span><br><span class="line">// =></span><br><span class="line">#prompt {</span><br><span class="line"> position: absolute;</span><br><span class="line"> top: 150px;</span><br><span class="line"> left: 5px;</span><br><span class="line"> width: 200px;</span><br><span class="line"> margin-left: -100px;</span><br><span class="line">}</span><br><span class="line">#logo {</span><br><span class="line"> position: fixed;</span><br><span class="line"> top: 0;</span><br><span class="line"> left: 0;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="简单指南"><a href="#简单指南" class="headerlink" title="简单指南"></a>简单指南</h3><p>可以看到上面的代码中使用了Mixin(混合)还有三目运算符等手段构建了一个针对position的方法,用来快速生成一个定位代码片段。有底向上来看这段代码,#prompt和#logo是2个ID选择器,在其中调用了一些Mixin,其实Mixin与Function的区别在于,Mixin的内容是一段CSS代码,而Function应该是一个值并自动返回,所以调用它们的时候,前者将会替换为一段CSS,而后者将返回一个Boolean或者像素或者颜色之类的东西,也许用于判断也许直接放入CSS。然后其中的absolute和fixed分别调用了pos这个Mixin。</p><h5 id="前缀"><a href="#前缀" class="headerlink" title="前缀"></a>前缀</h5><p>而且在调用时,也不一定要使用括号的形式,可以使用CSS的形式,直接Mixin名加空格然后写参数。所以有时候可以直接写一个Mixin来修改CSS属性的功能,比如看看下面这个兼容所有标准浏览器阴影的写法,可以很方便的为标准调用加上各标准浏览器的前缀:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">box-shadow()</span><br><span class="line"> -webkit-box-shadow arguments</span><br><span class="line"> -moz-box-shadow arguments</span><br><span class="line"> -ms-box-shadow arguments</span><br><span class="line"> -o-box-shadow arguments</span><br><span class="line"> box-shadow arguments</span><br><span class="line">box-shadow 2px 1px 10px red</span><br><span class="line"></span><br><span class="line">// =></span><br><span class="line">-webkit-box-shadow: 2px 1px 10px #f00;</span><br><span class="line">-moz-box-shadow: 2px 1px 10px #f00;</span><br><span class="line">-ms-box-shadow: 2px 1px 10px #f00;</span><br><span class="line">-o-box-shadow: 2px 1px 10px #f00;</span><br><span class="line">box-shadow: 2px 1px 10px #f00;</span><br></pre></td></tr></table></figure><p>可以看到调用时的写法与一般的写法一样,但是因为Mixin的存在,box-shadow不再是一个属性,可以变成5行带有各浏览器前缀的CSS。不仅仅是box-shadow,CSS3的许多属性都需要添加前缀,那是不是可以更近一步呢,来写一个前缀Mixin吧:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// add prefix for attribute</span><br><span class="line">prefix(p_attr, argu...)</span><br><span class="line"> $pfs = webkit moz ms o</span><br><span class="line"> for $pf in $pfs</span><br><span class="line"> -{$pf}-{p_attr} argu</span><br><span class="line"> {p_attr} argu</span><br><span class="line">// box shadow mixin</span><br><span class="line">box-shadow()</span><br><span class="line"> prefix(box-shadow, arguments)</span><br><span class="line">// run</span><br><span class="line">box-shadow 2px 1px 10px red</span><br></pre></td></tr></table></figure><h5 id="颜色"><a href="#颜色" class="headerlink" title="颜色"></a>颜色</h5><p>如同其他CSS预处理工具一样,Stylus在颜色方面也拥有许多内置函数,无论是判断,提取还是修改都十分强大。函数 <code>red</code> , <code>blue</code> , <code>green</code> , <code>alpha</code> 将分别返回颜色对应的rgba值,<code>dark</code> 和 <code>light</code> 用于判断颜色属于亮色还是暗色,<code>hue</code> , <code>saturation</code> , <code>lightness</code> 则分别返回颜色的色相、饱和度以及亮度,其中色相是在色环上的角度,单位是deg。我经常用的是<code>lighten</code> 和 <code>darken</code> 这两个函数,其作用是增加或减少一个颜色的亮度,另外还有饱和度的操作函数 <code>desaturate</code> 和 <code>satucate</code>。</p><p>似乎没有用于修改色相的函数,不过这个需求很容易通过其他办法搞定。首先使用hue等函数将原始色的色相、饱和度、亮度以及透明度取出,然后对色相的角度进行修改,比如加90deg,最后再使用hsla函数,把去除的对应值当作参数传入即可。下面用一组三态按钮来举个栗子:</p><p data-height="300" data-theme-id="1870" data-slug-hash="CKrwL" data-user="tychio" data-default-tab="result" class="codepen">See the Pen <a href="http://codepen.io/tychio/pen/CKrwL">Single Button</a> by Zhang zhengzheng (<a href="http://codepen.io/tychio">@tychio</a>) on <a href="http://codepen.io">CodePen</a></p><script async src="//codepen.io/assets/embed/ei.js"></script><p>可以看到Stylus中的第一行代码 <code>$clr = #99ff22</code> 只要修改这个颜色值就可以改变按钮的整体风格,并无需考虑hover和active状态时对应的颜色。比如边框使用 <code>darken</code> 来加深,阴影泛光可以使用 <code>lighten</code> 来加亮,在触碰时整体使用了 <code>saturate</code> 来改变饱和度,按下的Active状态我使用了 <code>invert</code> 函数,可以翻转颜色,在视觉设计中这个颜色叫做对位色,即色相处于色环的对面的两种颜色,比如绿对红,黄对蓝,例子中使用了黄绿,所以对位色就是紫色。当然也可以使用上面提到的复杂一些的方法来修改色相,达到使用间隔色之类的效果。</p><h5 id="响应式"><a href="#响应式" class="headerlink" title="响应式"></a>响应式</h5><p>对于响应式的支持,Stylus的media也可以省略花括号,但和Sass有一些区别。Stylus在@media的括号中会原样输出,也就是说,我们不能使用变量或混合还有计算等手段来直接写media query。比如一般情况下需要写一个min-width,如果这样写</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">$mobiWidth = 768px</span><br><span class="line">@media screen and (min-width $mobiWidth - 1px)</span><br><span class="line"> body</span><br><span class="line"> margin 0</span><br></pre></td></tr></table></figure><p>产生的CSS代码则仍然是</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">@media screen and (min-width $mobiWidth - 1px) {</span><br><span class="line"> body {</span><br><span class="line"> margin: 0;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这不是一个bug,尽管在Github上有无数的人提出issue或者在其后+1,作者仍然不为所动,原因不明,不过幸运地是有很多人都提出解决办法,下面是一个比较好的方法:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">media()</span><br><span class="line"> join(' and ', arguments)</span><br><span class="line">$mobiWidth = 768px</span><br><span class="line">$media = media('screen', '(min-width: ' + ($mobiWidth - 1px) + ')')</span><br><span class="line">@media $media</span><br><span class="line"> body</span><br><span class="line"> margin 0</span><br><span class="line"></span><br><span class="line">/// =></span><br><span class="line"></span><br><span class="line">@media screen and (min-width: 767px) {</span><br><span class="line"> body {</span><br><span class="line"> margin: 0;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样就可以使用变量来作为media的参数了,只是写起来会比sass麻烦一些,但我觉得这样也许更自由,你可以改进这个方法,比如传一个object来作为query条件,而不是拼接一个字符串。另外这个方法还用到了 <code>join</code> 内置函数,和Javascript中的Array方法join一样,很容易使用,除了它还有 <code>push</code> , <code>unshift</code> 函数。</p><p>关于数组的定义,对于响应式来说有非常好的帮助,因为响应式往往是一系列的尺寸或设备,无论如何,使用数组可以轻松的定义多组对应与索引的配套值。比如我的blog,对于不同宽度的设备中有不同的内容宽度以及边距,来看看简化的代码:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">$screen = 1920px 1280px 1024px 768px 640px 320px</span><br><span class="line">$width = 1600px 1080px 840px 600px 480px 300px</span><br><span class="line">$margin = 180px 100px 80px 40px 20px 0</span><br><span class="line">media()</span><br><span class="line"> join(' and ', arguments)</span><br><span class="line">responsive(p_index)</span><br><span class="line"> body</span><br><span class="line"> width $width[p_index]</span><br><span class="line"> margin-left $margin[p_index]</span><br><span class="line">responsive(0)</span><br><span class="line">for $i in 0 1 2 3 4 5</span><br><span class="line"> $media = media('screen', '(max-width: ' + $screen[$i] + ')')</span><br><span class="line"> @media $media</span><br><span class="line"> responsive($i)</span><br><span class="line"></span><br><span class="line">// =></span><br><span class="line">body {</span><br><span class="line"> width: 1600px;</span><br><span class="line"> margin-left: 180px;</span><br><span class="line">}</span><br><span class="line">@media screen and (max-width: 1920px) {</span><br><span class="line"> body {</span><br><span class="line"> width: 1600px;</span><br><span class="line"> margin-left: 180px;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">// ...</span><br><span class="line">@media screen and (max-width: 320px) {</span><br><span class="line"> body {</span><br><span class="line"> width: 300px;</span><br><span class="line"> margin-left: 0;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>当然响应式不是简单的改变尺寸,如果你需要控制某些内容的显示则可以使用一个Boolean的数组来判断是否显示,控制结构或样式则可以字符串的数组来放置一些预先写好的Mixin名称。</p><h5 id="CSS-Sprite"><a href="#CSS-Sprite" class="headerlink" title="CSS Sprite"></a>CSS Sprite</h5><p>对于CSS Sprite相信是所有切图者的主要工作产出,以前我也推荐过一些在线的制作Sprite的工具,不过现在有了Stylus,也许我们可能更快的完成这一切。之前公司有需要国旗icon,所以做了这个小项目<a href="https://github.com/tychio/national_flag">national_flag</a>用来创建和维护国旗icon的CSS Sprite。由于国家数目众多,每个国家对应一个国家代码,所以我定义了一个二维数组用来表现图片中国旗的位置,然后在数组中填入代码,用来拼接图标的class名称,然后按照数组中的序号和尺寸就可以生成对应的background-position了。主要代码如下:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">iconBuild(id, col, row)</span><br><span class="line"> .country-{id}</span><br><span class="line"> background-position (0px - (row * $size)) (0px - (col * $size))</span><br><span class="line">/*r /c-> 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16*/</span><br><span class="line">$row00 = CN AF AZ BH BD BN KH TL IN ID IR IQ IL JP JO KZ KW</span><br><span class="line">$row01 = KG LA LB MY MV MN MM NP KP OM PK PS PH QA SA SG KR</span><br><span class="line">$row02 = LK SY TJ TH TM AE UZ VN YE DZ AO BJ BW BF BI CM CV</span><br><span class="line">$row03 = CF RO KM CG CI CD DJ EG GQ ER ET GA GH GN GW KE LS</span><br><span class="line">$row04 = LR __ MG MW ML MR MU MA MZ NA NE NG RW ST SN SC SL</span><br><span class="line">$row05 = SO ZA SD __ TZ __ TG TN UG ZM ZW __ __ __ __ __ __</span><br><span class="line">$row06 = JM PR DO KN VC LC TT CR __ SV GT HN NI PA __ DE __</span><br><span class="line">$row07 = MK AT __ __ __ BG CY __ __ DK SK SI ES EE FI FR __</span><br><span class="line">$row08 = GR HU IE IS IT LV LI LT LU MT MD MC ME NO NL PL PT</span><br><span class="line">$row09 = UK CZ __ RU SM __ SE CH TR UA EU __ CA __ MX __ US</span><br><span class="line">$row10 = AR BO BR CL CO EC GY __ __ PE __ UY VE HK LY NZ RS</span><br><span class="line">$row11 = PY AU SR TJ FM AI __ __ __ __ __ __ __ __ __ __ __</span><br><span class="line">$pos = $row00 $row01 $row02 $row03 $row04 $row05 $row06 $row07 $row08 $row09 $row10 $row11</span><br><span class="line">for $rowList, $row in $pos</span><br><span class="line"> for $country, $col in $rowList</span><br><span class="line"> if $country != __</span><br><span class="line"> iconBuild($country, $row, $col)</span><br></pre></td></tr></table></figure><p>其中for不同于Javascript,rowList为数组遍历出的一个元素,而$row为索引,可以这样理解 <code>for [value], [index] in [array]</code> 。所以可以在两个嵌套的for中获取纵横的位置以及国家代码,来生成CSS。</p><h3 id="Stylue应用"><a href="#Stylue应用" class="headerlink" title="Stylue应用"></a>Stylue应用</h3><p>作为预处理工具,Stylus自然也需要预处理器,不过它不像Sass需要Ruby环境,Stylus由Javascript实现,所以有Javascript就可以处理Stylus。</p><h5 id="编译工具"><a href="#编译工具" class="headerlink" title="编译工具"></a>编译工具</h5><ul><li><a href="https://github.com/edmundask/SublimeText2-Stylus2CSS">SublimeText2-Stylus2CSS</a>是一款SublimeText2的Stylus插件。另外我使用<a href="https://github.com/billymoon/Stylus">这个项目</a>的SublimeText2插件来高亮styl文件的代码。</li><li>另外今年8月WebStorm7也才刚刚支持 - <a href="http://blog.jetbrains.com/webstorm/2013/08/webstorm-7-eap-130-1630-stylus-support/">Stylus Support</a>。</li><li><a href="http://codepen.io/">CodePen</a>支持各种CSS预处理,自然包括Stylus,上面的按钮例子就是嵌入的CodePen。</li><li><a href="http://learnboost.github.io/stylus/try.html">Stylus官方在线</a>其实是一些示例,不过它是可编辑的,所以你也可以随便写些什么,即时可以看到结果。不过好像还在使用低版本的Stylus,比如一些内置函数就不可用。</li></ul><h5 id="Grunt插件"><a href="#Grunt插件" class="headerlink" title="Grunt插件"></a>Grunt插件</h5><p>不过说到处理文件,Grunt还是我的最爱,尤其Stylus是由Javascript实现,在Nodejs中自然是得天独厚。npmjs上有许多用来处理Stylus的插件,下面简单介绍一下Grunt的官方Stylus插件<a href="https://npmjs.org/package/grunt-contrib-stylus">grunt-contrib-stylus</a>。先来看看最简单的配置方法:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">stylus: {</span><br><span class="line"> compile: {</span><br><span class="line"> files: {</span><br><span class="line"> 'path/to/result.css': 'path/to/source.styl'</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如此就可以利用Grunt将source.styl文件中的Stylus代码编译为result.css的CSS代码。当然还可以使用数组来进行多个Stylus文件的打包编译。当然不仅于此,先来看看主要的几个配置项:</p><p><code>paths</code> 将自动使用@import来引入一些Stylus文件,比如一些Mixin集合,放在一个Stylus文件中进行维护,写在paths中后,就可以在每个Stylus文件中调用它们。<code>define</code> 可以定义一些全局变量,然后在Stylus中使用,但我不喜欢使用这个配置,而是更喜欢把全局变量放在一个单独的Stylus文件中,然后将这个文件加入paths的数组中。一句话,把所有不会直接产出CSS的Stylus代码分成若干个Stylus文件,然后全部添加到paths中,这样在所有Stylus文件中都可以随时调用了,但要注意这些Stylus文件的调用关系和使用先后顺序。</p><p><code>compress</code> 及 <code>linenos</code> 是两个Boolean值,用来控制是否压缩处理后的CSS代码以及是否在CSS代码中保留注释。</p><p><code>banner</code> 是一个字符串,会被放置在CSS文件的最前面,一般我用来写注释,比如</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">banner: '\/** \n * <%= pkg.name %> - <%= pkg.description %>\n * version <%= pkg.version %> \n * author <%= pkg.author %> \n * date <%= grunt.template.today() %> \n**/\n'</span><br></pre></td></tr></table></figure><p><code>firebug</code> 将控制是否使用一个Firebug的Stylus插件<a href="https://addons.mozilla.org/en-US/firefox/addon/firestylus-for-firebug/">FireStylus for Firebug</a>,可以在Firefox中调试Stylus。</p><p><code>use</code> 可以引入一些Stylus的其他grunt插件。</p><p>配合watch等Grunt插件就可以达到自动化的Stylus开发,写样式将会非常有效率。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>Stylus是一个由Javascript实现的CSS预处理工具,文件后缀为styl,其拥有变量、函数、混合、条件及循环语句等功能,还有丰富的内置函数用于处理颜色、数字、数组等数据。在grunt的辅助下,Stylus将带来极大的开发效率。</p><p>我还在<a href="http://slid.es/">slides</a>上制作了一个<a href="http://slid.es/tychio/stylus">Stylus简介的幻灯片</a>,还没有经过实践,可能内容有些空泛,不过将持续改进。</p>]]></content>
<summary type="html"><p><img src="/images/stylus.jpg" alt="Stylus Logo"><br>Stylus似乎并不是很有名,以至于很多人不知道它是做什么的,但提到SASS相信有不少人听说过甚至使用过很长时间。其实无论是LESS、SASS还是Stylus甚至是Absurd这些预处理工具,都是对CSS的一种延伸和强化。出现这些工具的原因很简单,CSS本身只是一种描述性质的东西,甚至它不能算是语言而是样式表,所以我们需要一个有条件语句和变量甚至是函数的东西去动态生成CSS代码来达到提高效率和增强可维护性的目的。</p>
<p>本文主要以Stylus语法本身和简单的使用为主要内容,它的目的是介绍和简单指南。将不会过多涉及Javascript的API调用等问题。</p>
<h3 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h3><p>官方的介绍非常简短而精炼:</p>
<blockquote>
<p>Expressive, dynamic, robust CSS</p>
</blockquote></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="css" scheme="https://blog.tychio.net/tags/css/"/>
<category term="stylus" scheme="https://blog.tychio.net/tags/stylus/"/>
<category term="sass" scheme="https://blog.tychio.net/tags/sass/"/>
<category term="less" scheme="https://blog.tychio.net/tags/less/"/>
</entry>
<entry>
<title>改进我的Workflow</title>
<link href="https://blog.tychio.net/2013/09/24/improve-workflow/"/>
<id>https://blog.tychio.net/2013/09/24/improve-workflow/</id>
<published>2013-09-24T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p>有人说过程序员和码农的本质区别就是程序员会不断探索提高生产力的方法。思维模式的转变是提高生产力的最好方式,但打磨我们的工具也是十分有意义的事,本文将从开发环境,自动化开发,开发工具等几个方面针对前端开发效率的提升和代码质量的提高来展开讨论。</p><p>每件事都是一个程序,开发也像程序一样,每个步骤都是一段代码,当开发规模随着文档、代码、需求而增加时,重复的步骤变得越来越多。此时,如果可以像抽象代码一样抽象出一些相同操作就可以大大提升开发效率。因此,出现了更多更优质的工具来代替人工做一些不断重复的开发以减少程序员的工作量。</p><span id="more"></span><h3 id="开发环境"><a href="#开发环境" class="headerlink" title="开发环境"></a>开发环境</h3><h5 id="Nodejs"><a href="#Nodejs" class="headerlink" title="Nodejs"></a>Nodejs</h5><p>首先,需要搭建一个自动化高效率的开发环境。以前我们有shell、java、ruby来进行一些自动化脚本的执行。但自从Nodejs将Javascript带入了服务器,Front End开发环境也发生了翻天覆地的变化。Nodejs不仅仅可以让Jser开发服务端,还让Javascript成为了服务器脚本语言之一,可以用于文件的操作。</p><p>安装Nodejs的方法目前来说很简单,<a href="http://nodejs.org/download/">点这里下载安装包</a>,选择对应的平台的安装包即可。不过不得不说的是Source Code包,这是源码需要编译,虽然由C++写成,但gyp进行管理,所以编译时需要Python2.6+和C++编译器一起工作。通过命令 <code>node -v</code> 来检验是否安装成功,成功则返回当前版本号。</p><p>另外Nodejs还有一样必备的工具npm,就像ruby中的gem一样,是一个Nodejs的包管理器,可以为Nodejs添加一些包。npm的安装非常简单[1],可以说不用安装,在Linux下只有一行命令: <code>make install</code><br>, 而Windows和Mac都默认带有npm。当然如果想专门安装npm也是可以的,Linux下仍然是一行代码:</p><pre><code>curl https://npmjs.org/install.sh | sudo sh</code></pre><p>而Windows会稍微麻烦一点,在<a href="https://npmjs.org/dist/">https://npmjs.org/dist/</a>下载源码,然后放到和node.exe一个文件夹下即可。</p><p>使用npm来安装一些包很简单,使用这样的命令 <code>npm install <package_name></code> ,一般来说会默认安装在当前目录中。但如果使用参数 <code>-g</code> 就可以安装在全局。另外通过在项目中添加一个 <code>package.json</code> 文件,就可以定义项目依赖的Nodejs包,然后直接在该目录中执行 <code>npm install</code> 指令就会将package文件指定的包全部安装在当前目录。</p><h5 id="Shell"><a href="#Shell" class="headerlink" title="Shell"></a>Shell</h5><p>不管是Linux还是Mac都天然的拥有Shell环境,但是Windows中的CMD是无法和Shell相提并论的,而且很多开发工具也需要Shell环境。</p><p>还好Windows中有MSYS[2],全称是Minimal GNU(POSIX)system on Windows,它是一个GNU工具集,包括了bash,make,gawk和grep。可以直接下载 </p><p><a href="http://www.mingw.org/wiki/MSYS">http://www.mingw.org/wiki/MSYS</a></p><p>如果需要整个Unix环境和C的库的话,还需要minGW。也可以直接安装Git Bash工具,会附带有这个,这样环境和Git就会一起装好。</p><p><a href="http://git-scm.com/downloads">http://git-scm.com/downloads</a></p><p>另外在Windows中使用Shell时,有几点需要注意。文件路径的根目录为Git Bash的文件根路径,但是可以使用斜杠和盘符代表Windows的磁盘,比如进入D盘下的workspace文件夹就这样:</p><pre><code>$ cd /d/workspace/</code></pre><p>而Windows特有的文件夹名称中带有空格的问题可以通过两个方法解决。</p><pre><code>$ mkdir /c/"work space"$ rmdir /c/work\ space</code></pre><p>用引号括起来带有空格的文件名或者使用反斜杠来转义空格。</p><h5 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h5><p>Git的安装很分散[12],每种平台都不一样,Linux中也分为两种使用yum或者apt-get来安装:</p><pre><code>// as Fedora$ yum install git-core// as Ubuntu$ apt-get install git</code></pre><p>Mac上是最简单的,在这里安装<a href="http://code.google.com/p/git-osx-installer">http://code.google.com/p/git-osx-installer</a></p><p>Windows也很方便,因为有了<a href="http://msysgit.github.com/">Msysgit</a>,也一样直接安装。</p><h5 id="编辑器"><a href="#编辑器" class="headerlink" title="编辑器"></a>编辑器</h5><p>编辑器是每个程序员最常用的工具,它在很大程度上决定了单纯Coding的效率。原来有人将Vim和Emacs奉为上古神器,不过我喜欢新的东西,SublimeText是目前编辑器中的新贵,拥有海量插件,使用Python编写,配置和操作都非常方便。可以到这里下载:</p><ul><li><a href="http://www.sublimetext.com/">SublimeText2</a></li></ul><p>现在第3版正在进行beta测试,但是由于升级为Python3,原来的插件都因为API更新的问题而无法使用了,相信在正式版发布后插件将会陆续升级。这是第三版的下载地址,不会覆盖第二版。</p><ul><li><a href="http://www.sublimetext.com/3">SublimeText3</a></li></ul><p>此外,虽然它是付费软件,不过作者好像从来不怕没有人付费,如果没有注册仅仅会偶尔在保存时弹出Lisence声明,但确认会弹出官方页面,点取消即可。好像还有破解版本的出现,不过作者已经这么大度了,用破解版好像有点说不过去。国内曾经还有人组织过团购,但是作者表示不存在团购一说,只有公司批量购买,最终只有不了了之,售价$70。</p><h5 id="浏览器"><a href="#浏览器" class="headerlink" title="浏览器"></a>浏览器</h5><p>作为前端最基本的环境,浏览器是必不可少的。Chrome是我最喜欢的浏览器,因为它的快速高效以及很棒的开发者工具。虽然Firefox也是一款出色的浏览器,但Firebug作为一款插件,效率总是差那么一点,当然Firefox现在也推出了自己的调试工具。用于测试的IE浏览器也是常备工具之一,此外还有Opera和Safari。</p><p>Chrome和Firefox很强大的一个原因就是,它们对W3C的标准都很快速的支持,许多最新的特性都可以体现在最新版的Chrome以及Firefox中。特别需要一说的是,它们都有一个每日更新的版本,用户可以体验到最新的功能,而浏览器厂商可以获取崩溃信息等反馈来提高品质。Chrome的每日更新版叫<a href="https://www.google.com/intl/zh-CN/chrome/browser/canary.html">Chrome Canary</a>,Firefox的比较直接,<a href="http://nightly.mozilla.org/">Firefox Nightly</a>。</p><p>还有一款很神奇的浏览器,它不会渲染,也没有界面,基于Webkit内核,它叫<a href="http://phantomjs.org/index.html">PlantomJS</a>,图标的幽灵和名字都突出了这一特点。也许看起来没什么用,但在测试或者做研究时,浏览器不厌其烦的弹出来时,它就有大用处了。</p><h3 id="自动化开发"><a href="#自动化开发" class="headerlink" title="自动化开发"></a>自动化开发</h3><h5 id="Yeoman"><a href="#Yeoman" class="headerlink" title="Yeoman"></a>Yeoman</h5><p>Yeoman按照官方说法[3],它不只是一个工具,还是一个工作流。它其实包括了三个部分yo、grunt、bower,分别用于项目的启动、文件操作、包管理。但我并不太认同这是一个工作流的说法,至少目前来看还不够成熟,在真实的生产环境中会遇到许多问题。而未来的可能性大致应该有两条路可走,也许会产生某些工作流的标准来定义前端开发的软件质量,不过我更认为Yeoman应该走向高可定制的工作流工具的方向,而不是自身作为一个工作流来存在。</p><h6 id="Yo"><a href="#Yo" class="headerlink" title="Yo"></a>Yo</h6><p>Yo是一个项目初始化工具,可以生成一套启动某类项目必须的项目文件。可以通过npm安装它到全局:</p><pre><code>npm install -g yo</code></pre><p>然后还需要安装一些generator,这是一个用于创建某个指定类型项目的生成器。比如安装一个最常用的webapp的生成器,然后就可以在项目路径下生成项目启动需要的所有文件,像这样:</p><pre><code>npm install -g generator-webappcd /project_folder/yo webapp</code></pre><p>但是这种机制有一个很严重的问题,generator产生的文件结构是谁制定的?没有一个官方的相应的标准或者说Guide,generator的形式参差不齐,甚至我发现Firefox OS的generator生成的是一个API接口的Demo而不是一个种子,如果要进行开发需要进行很多删减。</p><p>不过产生这些generator的generator[4]却是一个很好的工具,它应该是一个创造性的工具。首先需要安装generator-generator,然后使用它,接着会看到字符拼接的yeoman,像这样:</p><pre><code>npm install -g yo generator-generator$ mkdir ~/dev/generator-blog && cd $_$ yo generator _-----_ | | |--(o)--| .--------------------------. `---------´ | Welcome to Yeoman, | ( _´U`_ ) | ladies and gentlemen! | /___A___\ '__________________________' | ~ | __'.___.'__´ ` |° ´ Y `</code></pre><p>当然使用它之前应该将写好的项目文件放入 <code>app/templates</code> 文件夹中,并在 <code>templates</code> 同级的路径中加入 <code>index.js</code> 进行配置就可以了。这里的index.js是运行在Nodejs中的,也就是说由它将templates中的项目文件放入该放的地方并且填入一些变量去构建整个项目。这里才是体现一个generator是否是一个好的generator的地方,如果仅仅是将一堆写好的项目文件下载下来那什么意义也没有,不存在万用种子。只有在使用generator生成项目时高度定制才是其意义所在,而相关标准才是最难的部分。</p><h5 id="Bower"><a href="#Bower" class="headerlink" title="Bower"></a>Bower</h5><p>Bower是一个类似于npm的包管理器,但不同的是Bower主要针对前端,并且直接从Github查找需要的库下载到本地缓存。使用很简单,用npm安装bower后可以安装Github的项目并指定版本号,还可以重命名。默认会下载到项目中的 <code>bower_components</code> 文件夹中。[5]</p><pre><code>npm install -g bowerbower install jQuerybower install jQuery#1.10.3bower install jQueryOld=jQuery#1.6.4</code></pre><p>还可以通过bower.json文件来配置需要安装的包,使用 <code>bower init</code> 命令就可以生成bower.json文件,然后在其中写入需要的包及其版本即可</p><pre><code>{ "name": "my_project", "version": "0.1.0", "main": [js/js.js, css/css.css], "ignore": [ ".jshintrc", "**/*.txt" ], "dependencies": { "<name>": "<version>", "<name>": "<folder>", "<name>": "<package>" }, "devDependencies": { "<test-framework-name>": "<version>" }}</code></pre><p>当然它也可以搜索包,像这样搜索一下jquery。</p><pre><code>bower search jquery</code></pre><p>如果觉得bower_components的文件夹名太长不好,可以在 <code>.bowerrc</code> 中以json的形式修改它的路径</p><pre><code>{ "directory": "lib"}</code></pre><p>还有许多其他的配置,可以在Bower存放在Google Doc的文档[7]中查看。</p><p>但是Bower还有一个Bug[6],jQuery在Github上的项目文件是分模块的,必须使用项目中的Grunt才能打包成jquery.js文件,而官方的说法是使用小写q的 <code>jquery</code> 来获取components项目中的jquery文件,但是目前Bower是大小写不分的,所以无法获取独立的jQuery文件。如果bower可以指定获取某个项目中的某个或某些指定的文件将会更加犀利。</p><p>甚至Bower可以在Nodejs中运行一个 <code>bower.commands</code> 文件来让你编写安装各种包的node程序,并且可以监听 <code>end</code> 事件在安装结束后进行操作,这是异步的,这样就可以随心所欲的安装包和控制顺序了。</p><pre><code>var bower = require('bower');bower.commands .install(['jquery'], { save: true }, { /* custom config */ }) .on('end', function (installed) { console.log(installed);});</code></pre><h5 id="Grunt"><a href="#Grunt" class="headerlink" title="Grunt"></a>Grunt</h5><p>Grunt目前来说是这三个Yeoman中最成熟最强大的,最关键的是Grunt有各种各样的插件,可以集成大部分能想得到的开发工具来进行自动化开发。另外Grunt的作者还开发了一整套的插件来适应常规的开发,这套插件以 <code>grunt-contrib-</code> 为前缀(下文中如无特殊说明,均指带有该前缀的插件名),除了文件的基本操作,还包括有测试、编译、压缩、代码检查等各种功能的插件,而且不止一个选择。</p><p>安装Grunt和Bower不太一样[8],需要先在全局安装一个Grunt的客户端,然后在每个项目中安装Grunt。</p><pre><code>npm install -g grunt-clicd /project/npm install grunt</code></pre><p>不过和Bower相似的是,可以通过编写配置json文件来使用 <code>npm install</code> 来安装Grunt和所有需要的插件,另外Grunt的插件也都是npm管理的,所以可以直接在 <code>package.json</code> 中直接编写。</p><pre><code>{ "name": "myProject", "version": "0.1.0", "devDependencies": { "grunt": "*", // other plugin... "grunt-contrib-watch": "*" }}</code></pre><p>安装完成后在项目根目录中建立 <code>Gruntfile.js</code> 文件来配置Grunt的工作流程。下面以 <code>copy</code> 插件为例使用Grunt进行开发。在 <code>exports</code> 中Grunt会以参数形式被传入函数,它有3个方法, <code>initConfig</code> 、 <code>loadNpmTasks</code> 、 <code>registerTask</code>,分别用来定义插件操作,载入插件,注册任务。</p><pre><code>module.exports = function (grunt) { grunt.initConfig({ copy: { main: { files: { src: ['path/**'], dest: 'dest/' } } } }); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.registerTask('default', ['copy']);};</code></pre><p>在配置中以插件名为键定义一个Object作为该插件的配置,其中还可以再定义一层以任务名为键,比如 <code>main</code> ,然后是插件的部分,copy插件使用 <code>files</code> 来定义对文件的具体操作, <code>src</code> 是要复制的文件, <code>dest</code> 则是要复制到的路径。</p><p>然后使用 <code>loadNpmTasks</code> 加载插件,需要写全名,包括grunt-contrib前缀。</p><p>最后是注册一个任务,这里的任务即是执行操作时需要调用的东西。比如代码中注册了 <code>default</code> 任务,包括一个数组中的所有任务,这样在执行default任务时就会执行相应的所有任务。另外default是一个特殊的任务名,如果在执行任务时没有指定名称,则执行该任务。当然直接运行copy任务也是可以的,甚至可以指定一个子任务,比如main。所以下面4行代码是相同的效果。</p><pre><code>gruntgrunt defaultgrunt copygrunt copy:main</code></pre><p>不过需要特别注意的是,注册的任务名不能和原有的任务相同,这样会报错,比如这样:</p><pre><code>grunt.registerTask('copy', ['copy']);</code></pre><p>和copy类似的文件基本操作还有 <code>clean</code> 清除, <code>concat</code> 连接, <code>rename</code> 重命名, <code>compress</code> 打包, <code>crypt</code> 编码等等,相关的配置可以在npmjs.org上的对应项目介绍中找到。</p><p>还有四个用于压缩的插件 <code>htmlmin</code> , <code>cssmin</code> , <code>uglify</code> , <code>imagemin</code> 分别对应HTML文件、CSS文件、JS文件和图片文件;以及两个用于检查代码的插件 <code>csslint</code> , <code>jshint</code> 分别检查CSS代码和JS代码。</p><p>当然,最重要的是,Grunt可以编译一些CSS和JS的其他形式代码。<code>coffee</code> 用于编译CoffeeScript,而CSS就更多了,比如<a href="http://sass-lang.com/">SASS</a>可以使用 <code>compass</code> 或者 <code>sass</code>, 还有 <code>less</code> 和 <code>stylus</code>,我最喜欢的是<a href="http://learnboost.github.io/stylus/">Stylus</a>,因为它使用的是Javascript来编译,而不像SASS是Ruby编译的,还需要准备Ruby的环境,非常麻烦。而且在Stylus中还可以写类似JS的条件语句和循环语句。这个国旗icon的项目很好的使用了Stylus以很短的代码完成了上百个国家的图标的CSS Sprite - <a href="https://github.com/tychio/national_flag/blob/master/country.styl">National Flag on Github</a>。还有许多种Javascript模板的预编译插件,<code>haml</code> , <code>jst</code> , <code>jade</code> , <code>hogan</code> 等等。</p><p>除了用于编码的插件,还有许多用于测试的插件,在grunt-contrib中提供了三个测试框架的插件, <code>nodeunit</code> 用于Nodejs,<code>qunit</code> 用于Qunit,是来自jQuery团队的测试框架,还有Junit的后继者 <code>jasmine</code>。另外Mocha也有自己的Grunt插件 <code>grunt-mocha</code> 。用于捕获多个浏览器测试框架karma也有相应的插件 <code>grunt-karma</code> 。</p><p>此外,contrib中还有一些其他插件,比如 <code>connect</code> 用于http等协议的请求,支持https, <code>commands</code> 用于执行shell命令, <code>manifest</code><br> 用于生成离线应用所需的 <code>manifest.appcache</code> 文件,还有用于插件YUI文档的 <code>yuidoc</code> 。</p><p>最最重要的一个插件就是 <code>watch</code> ,它可以随时监听某些指定的文件,当它们发生改变时执行相应的任务。再次使用copy做例子,添加watch任务后可以在原有文件发生改变时,将复制过去的副本也同步改变。</p><pre><code>module.exports = function (grunt) { grunt.initConfig({ watch: { copy: { files: 'path/**', tasks: 'copy' } }, copy: { main: { files: { src: ['path/**'], dest: 'dest/' } } } }); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.registerTask('default', ['copy', 'watch']);};</code></pre><p>由此,项目开发中的大部分工作都交由程序代替了人工,Yo和Bower可以快速的启动一个项目,Grunt在开发中可以自动化的持续完成编码中重复性的工作以及自动化检查和测试代码以提高质量。</p><h3 id="开发工具"><a href="#开发工具" class="headerlink" title="开发工具"></a>开发工具</h3><h5 id="SublimeText"><a href="#SublimeText" class="headerlink" title="SublimeText"></a>SublimeText</h5><p>在自动化开发的前提下,仍然有很多编码工作是需要亲手完成的,此时编辑器的效率决定了剩下的开发效率。SublimeText一款很棒的编辑器,通过配置和插件的选择可以达到几乎所有需求。</p><p>首先从GUI来说,ST的侧边栏可以随意的拖入文件夹并对其进行操作,而文本区则可以选择多种组合方式,包括网格、最多四栏、最多四列的布局。其滚动条也已经不是一个条了,而是一个代码的缩略图,拖动起来非常方便和清晰。每个文件的标签就像Chrome一样可以随意的拖出拖入。此外,代码的颜色样式可以有几十种方案供选择,还可以下载针对每种语言的颜色方案,目前我知道的仅有最新的Stylus的styl文件没有对应的颜色方案。</p><p>在功能方面,ST最大的特色之一就是会自动生成一份正在打开的文件的拷贝,而且会自动保存,也就是说即使是断电关机,重新打开后原本打开的文件也还是存在不会丢失任何代码。其次,多处编辑也是非常的强大,在代码中选择多处后会出现多个光标,可以同时编辑,而选中一个词后,按 <code>Ctrl+D</code> 就可以多选下一个相同的代码。另外通过 ‘Ctrl+P’ 可以搜索文件,配合 <code>@</code> 或者直接按 <code>Ctrl+R</code> 就可以前往指定的方法和函数,配合 <code>:</code> 或者直接按 <code>Ctrl+G</code> 就可以前往指定的行数。按住 <code>Shift+Ctrl+Up/Down</code> 就可以移动选中行的代码上下移动。其他编辑都有的一般的快捷键自然也都有。</p><p>不过最强大的是,这些功能都可以利用插件实现,比如Emmet也就是大名鼎鼎的Zencoding的继任者就可以通过插件指定一个命令并分配一个快捷键来实现。我还喜欢使用Markdown preview,比如现在我就可以通过它预览一下博客的大致效果。还有刚刚提到的针对每种语言的颜色高亮方案也是插件的形式。还有一款老牌版本控制的工具Tortoise,因为公司还在用SVN这种老古董,Tortoise自然成了不二选择。还有很多插件,可以从官方网站搜索。<br><a href="https://sublime.wbond.net/search/">https://sublime.wbond.net/search/</a></p><p>说到插件,自然少不了管理它的工具,SublimeText的管理工具是Package Control,原来的安装十分麻烦,不过现在官方给出了方法。使用 <code>Ctrl+~</code> 打开控制台,然后复制<a href="https://sublime.wbond.net/installation">这里官方给出的代码</a>到控制台并执行,Package Control就安装好了。之后使用 <code>Ctrl+Shift+P</code> 调出命令面板后就会有一组Package Control的命令,主要会用到 <code>install</code> 和 <code>remove</code><br>两个用于安装和卸载插件。</p><p>关于用户配置,有很多内容,可以参考 <code>Settings - Default</code> 。比如这样:</p><pre><code>{ "caret_style": "phase", "font_size": 16.0, "overlay_scroll_bars": "enabled", "save_on_focus_lost": true, "scroll_past_end": false, "tab_size": 4, "translate_tabs_to_spaces": true, "word_wrap": true, "wrap_width": 80}</code></pre><p>这些配置看到名字就基本可以猜出意思了,主要是wrap_width就是每行的字符数,设置到80,这样可以保持代码的简短,避免长语句。而translate_tabs_to_spaces就是用空格代替制表符。</p><h5 id="Chrome"><a href="#Chrome" class="headerlink" title="Chrome"></a>Chrome</h5><p>我始终喜欢Chrome多过Firefox,因为Chrome的启动速度比Firefox快上许多,Firefox原先有点过于臃肿了,不按标准的地方也不少,虽然后来在Google注资之后,不但版本号追了上来,功能也提升很多。不过Chrome仍是我开发的主要环境,Firefox一般仅作为研究和测试之用。</p><p>Chrome的开发者工具界面非常清爽,无论是在Elements中的HTML还是Sources中的Js,代码阅读和编辑都非常方便,而且在Element中可以修改和添加对应元素的CSS代码,而在Sources中可以直接修改CSS文件。Resources中列出了所有加载的文件,还有session、cookie和本地存储之类的缓存信息,可以方便的对其进行操作。而Network则列出了所有请求,以及相关的信息,甚至可以点击下面的圆点按钮 <code>preserve log upon Navigation</code> 进行请求响应时间的监视。在Timeline中还有更详细的时间监视,包括事件、加载以及内存的使用状况,可以方便的对程序的性能进行调试。在Profiles中可以对Js、CSS、DOM进行统计。还有Audits可以对网站性能和网络性能进行统计。</p><p>最重要的是Console[11],在这里可以直接写入Javascript代码进行调试,还可以收集到程序中输出的各种信息和报错。不过最特别的是它是有API的可编程。一般常用到 <code>log</code> 方法,像下面的代码这样来输出一些变量,当然还有不同的类型,比如 <code>error</code> 方法、 <code>warn</code> 方法。它们的参数也很自由,多个参数将会被空格连接输出,还可以在第一个参数中使用占位符来按类型加入后面的参数。</p><pre><code>console.log('hello ' + world);console.error('Error:', 'nothing...');console.warn('Warn: %s < %d', 'age', 18);</code></pre><p>除了上面三个方法以及类似log的 <code>info</code> 和 <code>debug</code> 方法还有一个特别的方法,那就是断言 <code>assert</code> 方法,它可以判断条件,在false时报错,一般用于测试。</p><p>另外还有三个关于时间的方法, <code>time</code> , <code>timeEnd</code> 和 <code>timeStamp</code> 。time和timeEnd配合使用可以记录程序运行的时间并输出,而timeStamp可以在Timeline的统计中标出一个时间点。</p><p>Chrome的插件也非常的多,这里介绍三款和页面密切相关的工具。</p><p><a href="https://chrome.google.com/webstore/detail/visual-event/pbmmieigblcbldgdokdjpioljjninaim">Visual Event</a> 是一个捕获页面事件的插件,它会将页面所有绑定的事件全部以可视化的方式呈现出来,并且可以点击查看某个元素的事件详细信息。我经常用来检查事件是否正确的绑定到了目标元素上。</p><p><a href="https://chrome.google.com/webstore/detail/edit-this-cookie/fngmhnnpilhplaeedifhccceomclgfbg">Edit This Cookie</a> 顾名思义,用来编辑Cookie的,虽然DevTools也带有这样的功能,但是它更加便利,还可以导出导入,随意修改每个Cookie中的任意条目。虽然它很强大,不过好像利用率最高的功能是一键清空Cookie。</p><p><a href="https://chrome.google.com/webstore/detail/code-cola/lomkpheldlbkkfiifcbfifipaofnmnkn">Code Cola</a> 可以用来修改CSS,与DevTools不同的是,它的操作是左右滑动滑块,而且主要针对CSS3的空间样式,可以快速将元素变成各种角度各种尺寸。</p><p>不过Chrome还是有弱点的,当tab开的太多时会非常卡,因为Chrome每个tab都是一个单独的进程。所以还有一个插件也是很有用的,虽然和开发没有太大关系,<a href="https://chrome.google.com/webstore/detail/onetab/chphlpgkkbolifaimnlloiipkdnihall">One Tab</a> 可以把当前的所有tab都集合起来变成一个页面,当需要打开时在点击链接即可,这样有效防止了过多tab造成的内存不足问题。</p><h3 id="代码管理"><a href="#代码管理" class="headerlink" title="代码管理"></a>代码管理</h3><h5 id="Git-1"><a href="#Git-1" class="headerlink" title="Git"></a>Git</h5><p>关于Git Workflow的讨论很多,最著名的当属<a href="http://nvie.com/">Vincent Driessen</a>的那篇博客[13]。Vincent的工作流的结构很棒,首先有2个主要分支,<code>master</code> 和 <code>develop</code>,分别是主分支和开发分支。然后还有3类次分支,它们可能数量很多,并且不会长时间存在,分别是开发新功能用的feature,发布用的release和修复bug用的hotfix。大致的Git操作可以理解为这样:</p><pre><code># create branchgit checkout -b develop mastergit checkout -b feature develop# commit somethinggit add widget.jsgit commit -m "add a function"# merge to developgit checkout developgit merge --no-ff feature# delete branchgit branch -d feature</code></pre><p>首先创建开发分支 <code>develop</code> ,然后再从开发分支创建一个次分支,接着提交代码并注释提交,合并会开发分支 <code>develop</code> ,最后删除这个临时的次分支。–no-ff的意思是不使用快速合并。其他开发过程中也是大同小异,release分支还有hotfix分支可能需要在确认没问题时合并到develop和master两个分支中然后删除。</p><p>不过这个工作流是考虑到团队开发而设计的,很标准简约,但细节不足。而<a href="https://sandofsky.com/">Benjamin Sandofsky</a>的文章[14]则更加趋向于对commit的管理,也许不能算做工作流,至少算是一种理念。他强调一定要保留有一个私人的分支只存在于本地,然后在合并到主分支时清除原本的commit log。这里会用到一个 <code>merge</code> 命令的参数 <code>--squash</code> 这样合并后不会带来任何commit log。</p><pre><code># create brachgit checkout -b private master# commit somethinggit add widget.jsgit commit -m "add a function"# merge brach but don't commitgit checkout mastergit merge --squash private# commit oncegit commit -m "only this commit"</code></pre><p>但我认为Git工作流和其他一切工程过程一样,不存在银弹。不过这种合并的方式可以成为一种很好的操作流来完成属于每个人自己的工作流。另外从这两种不同风格的Git工作流中也许能找出一些有趣的点。以下是我的看法:</p><ul><li><p>主分支数由开发流程复杂度决定,而开发流程复杂度应该由项目主管根据项目规模确定,所以项目规模决定了主分支数,除了develop也许还需要test、build等等。</p></li><li><p>次分支数由人员和实际情况决定,bug数会决定hotfix的数量,也许产品经理会决定feature的数量,多个不同版本的同类产品也可能会增加release的数量。如果项目规模足够大时,几个小组解决一个问题时也会产生多个临时分支。</p></li><li><p>多人协作以及长时间开发都可能导致日志混乱无法管理,使用squash参数配合临时分支可以清理对别人不必要的commit信息。</p></li><li><p>应使用–no-ff可以避免快速合并,使每次合并等于一次提交,记录在log中,保持分支健康。</p></li></ul><p>因此,在实际开发的工作流中应该按照实际情况创建分支,但应按照以上规范合并分支。</p><h5 id="Github"><a href="#Github" class="headerlink" title="Github"></a>Github</h5><p>Github不止是每个Coder的FaceBook,还是一个非常棒的远程Git仓库,甚至有很多小组将生产项目托管在上面。其实Github上和Git没有太多差别,只是多了一个远程仓库Remote的操作,另外相信每个初入Github的新手都为私钥公钥头疼了好久,下文将会讨论Github的仓库创建和日常操作两部分。</p><p>首先需要在本地建立与Github帐户的联系,在shell中安装SSH,然后像这样使用SSH安装SSH密钥:</p><pre><code>ssh-keygen -t rsa -C "[email protected]"# Creates a new ssh key, using the provided email as a label# Generating public/private rsa key pair.# Enter file in which to save the key (/c/Users/you/.ssh/id_rsa): [Press enter]ssh-add id_rsa</code></pre><p>然后会让你输入一个密码,随意输入就可以了,接着就会生成一个公钥一个私钥。在用户文件夹下的 <code>.ssh</code> 文件夹中找到id_rsa.pub,这个文件里就是公钥,复制里面的内容,然后在Github的Account Settings中的SSH Key页面,点击Add SSH Key按钮,输入一个用于说明的title,接着粘贴公钥到Key中就可以了。</p><p>然后必须在Github上点击 <code>Create a new repo</code> 按钮来创建一个空项目。当然如果选择适当的选项就可以自动生成README文件、Git忽略文件和版权分享声明文件。之后该项目会有一个仓库的地址,可以使用HTTPS和SSH,甚至还有SVN地址:</p><pre><code>https://github.com/<username>/<reponame>[email protected]:<username>/<reponame>.githttps://github.com/<username>/<reponame></code></pre><p>以我的一个对话框jQ插件为例,首先在项目中初始化git,然后添加一个远程仓库,然后就可以往上面提交代码了。</p><pre><code>git remote add myGithub https://github.com/tychio/dialog.gitgit push myGithub master</code></pre><p>因为我使用的HTTPS方式提交,之后会需要输入用户名和密码,如果使用SSH方式则用使用公钥而无需额外操作。使用HTTPS纯属为了记住Github的密码,每天都在敲就不会忘记了。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>工作流应该是一个人最习惯和熟悉的流程,而不应该是照猫画虎,邯郸学步。还是那句话,不存在银弹,所以不会有万用的工作流,只能从中汲取有用的实践,完善改进自己的工作流,达到提高工作效率的目的。</p><p>和学习其他技术一样,应用于工作流之中的工具有无数种,但真正需要和适合的只有自己知道,发现问题,带着问题寻找工具才能真的改进工作流。如果仅仅为了使用前沿的工具而使用,只会使自己的工作效率大打折扣。记得两年前我还在疯狂的复制代码,每当我意识到不能再这样下去的时候,工作流就会自己进化,合适的工具近在眼前,工作效率逐渐提升。我发现问题实在是很好的老师,可以让一个人快速的成长,解决它就可以获得一次提升。</p><p>永远有人有跟你相同的问题,永远有能解决你当前问题的工具,善于使用问题来选择它们就能打造更完善的工作流。如果遇到没有工具能解决的问题,那说明造轮子的时机到了。</p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ol><li><p><a href="https://npmjs.org/doc/README.html">NPM Readme</a></p></li><li><p><a href="http://www.mingw.org/wiki/MSYS">MSYS Wiki</a></p></li><li><p><a href="http://yeoman.io/">Yeoman home</a></p></li><li><p><a href="http://yeoman.io/generators.html">Yeoman generators</a></p></li><li><p><a href="http://bower.io/">Bower home</a></p></li><li><p><a href="https://github.com/bower/bower/issues/859">Bower jquery bug</a></p></li><li><p><a href="https://docs.google.com/document/d/1APq7oA9tNao1UYWyOm8dKqlRP2blVkROYLZ2fLIjtWc/edit#heading=h.4pzytc1f9j8k">Bower configuration</a></p></li><li><p><a href="http://gruntjs.com/getting-started">Grunt getting started</a></p></li><li><p><a href="http://www.henriquebarroso.com/my-top-10sublime-2-plugins/">Top 10 SublimeText2 plugins</a></p></li><li><p><a href="https://sublime.wbond.net/installation">Install package control</a></p></li><li><p><a href="https://developers.google.com/chrome-developer-tools/docs/console#using_the_console_api">Using the console API</a></p></li><li><p><a href="http://git-scm.com/book/en/Getting-Started-Installing-Git">Getting Started Installing Git</a></p></li><li><p><a href="http://nvie.com/posts/a-successful-git-branching-model/">A successful Git branching model</a></p></li><li><p><a href="https://sandofsky.com/blog/git-workflow.html">Understanding the Git Workflow</a></p></li><li><p><a href="https://help.github.com/articles/generating-ssh-keys">Generating SSH Keys</a></p></li></ol>]]></content>
<summary type="html"><p>有人说过程序员和码农的本质区别就是程序员会不断探索提高生产力的方法。思维模式的转变是提高生产力的最好方式,但打磨我们的工具也是十分有意义的事,本文将从开发环境,自动化开发,开发工具等几个方面针对前端开发效率的提升和代码质量的提高来展开讨论。</p>
<p>每件事都是一个程序,开发也像程序一样,每个步骤都是一段代码,当开发规模随着文档、代码、需求而增加时,重复的步骤变得越来越多。此时,如果可以像抽象代码一样抽象出一些相同操作就可以大大提升开发效率。因此,出现了更多更优质的工具来代替人工做一些不断重复的开发以减少程序员的工作量。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="github" scheme="https://blog.tychio.net/tags/github/"/>
<category term="node" scheme="https://blog.tychio.net/tags/node/"/>
<category term="workflow" scheme="https://blog.tychio.net/tags/workflow/"/>
<category term="grunt" scheme="https://blog.tychio.net/tags/grunt/"/>
<category term="bower" scheme="https://blog.tychio.net/tags/bower/"/>
<category term="sublime text" scheme="https://blog.tychio.net/tags/sublime-text/"/>
<category term="chrome" scheme="https://blog.tychio.net/tags/chrome/"/>
<category term="git" scheme="https://blog.tychio.net/tags/git/"/>
</entry>
<entry>
<title>HTML5实现图片上传</title>
<link href="https://blog.tychio.net/2013/08/20/html5-upload-file/"/>
<id>https://blog.tychio.net/2013/08/20/html5-upload-file/</id>
<published>2013-08-20T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p>最近公司项目准备更换图片上传的插件,原来的是一个Flash控件,其实用起来还是不错的,还有进度条,浏览器支持情况也不错。不过因为某些页面的图片上传涉及到了跨域的问题,Flash似乎解决不了了,所以准备索性换成HTML5的,高端大气上档赤。然后这个HTML5上传图片功能自然落到了我的手上了。</p><p>一般来说图片上传无非就是文件操作的问题,本来这是服务器对文件流的一个操作问题,前端应该是管不上的,不过HTML5再次赋予了我们前端神圣而伟大的权利,有了HTML5部分后端失业了lol。</p><p>其实以前写过一个上传图片的插件,不过那时还活在IE6年代,只能用iframe搞定,虽然还挺好使的,不过在HTML5面前就是一个战斗力负5的渣渣,不仅需要后端返回各项数据,还必须把保存的临时图片地址再发给后端保存,实际请求是2次,而且还无法告诉用户上传进度与速度。</p><span id="more"></span><h3 id="如何使用-FileReader"><a href="#如何使用-FileReader" class="headerlink" title="如何使用 FileReader ?"></a>如何使用 FileReader ?</h3><p>首先FileReader是一个用于读取文件的类,我们可以用new关键字实例化一个文件读取器,像这样:</p><pre><code>var fr = new FileReader();</code></pre><p>但是还有一个问题,这是一个HTML5的API,只有部分浏览器支持它,所以还得加上判断,另外它的支持情况是这样的:</p><pre><code>var fr = false;if (typeof window.FileReader === 'undefined') { fr = new FileReader();}</code></pre><hr><table style="text-align: center;"> <tr> <th width="20%">IE</th> <th width="20%">Chrome</th> <th width="20%">Firefox</th> <th width="20%">Opera</th> <th width="20%">Safari</th> </tr> <tr> <td>10</td> <td>7</td> <td>3.6</td> <td>12.02</td> <td>6.02</td> </tr></table>---<p>使用FileReader很简单,它提供了四个简单的接口用来读取文件,分别是abort,readAsBinaryString,readAsDataURL,readAsText。</p><h5 id="readAsXXX"><a href="#readAsXXX" class="headerlink" title="readAsXXX"></a>readAsXXX</h5><p>接口名清楚明白的说明了它的作用,以readAs开头的三个接口自然是用来读取文件的。很显然,所谓的文件,在不同的环境中有不同的格式不同的解释方式,这也正是这三个接口的不同之处。</p><p>但在我们弄清楚读取文件获得了什么之前,也许我们更应该关心目标文件是什么,怎么获取。幸运地是DOM中老早就存在一个files方法可以获取我们要的文件,并且它还提供了一些方法和属性。主要的属性有name,size,type,显然这是文件名、文件尺寸和文件类型,虽然它也提供了3个读取文件的方法getAsXXX,但是由于FileReader的存在已经被废弃很久了,同样被废弃的还有fileName和fileSize。</p><p>另外不得不说,Chrome在文件操作方面做的最出色,早在chrome13就已经实现了文件的写入,而其他浏览器至今还没有实现。</p><p>然后说说读取文件吧,这个过程是需要时间的,所以必须异步读取它,还好我们有load方法,像这样:</p><pre><code>var fr = false;if (typeof window.FileReader === 'undefined') { fr = new FileReader(); fr.readAsXXX(document.getElementById('input_file').files[0]); fr.onload = function (p_fr) { console.log(p_fr.target.result); };}</code></pre><h6 id="readAsBinaryString"><a href="#readAsBinaryString" class="headerlink" title="readAsBinaryString"></a>readAsBinaryString</h6><p>readAsBinaryString的result应该是一个二进制流,而log出的结果是一个夹杂着乱码符号的文本,里面还能看到图片是用PS保存的之类的信息。</p><h6 id="readAsDataURL"><a href="#readAsDataURL" class="headerlink" title="readAsDataURL"></a>readAsDataURL</h6><p>readAsDataURL的result则是一个Base64的图片代码,可以直接放入HTML的img标签的属性src上。</p><h6 id="readAsText"><a href="#readAsText" class="headerlink" title="readAsText"></a>readAsText</h6><p>readAsText的result和二进制的显示出来基本是一样的,包括一个信息头,接着大段的乱码应该是图片本身。</p><p>该方法还有一个可选的参数[encoding],即文本的编码方式,默认为urf-8。</p><h5 id="Abort"><a href="#Abort" class="headerlink" title="Abort"></a>Abort</h5><p>abort是一个特别的方法,用来打断读取。当图片上传超时或者其他操作需要打断时就可以调用这个接口打断。另外还可以监听abort事件来处理打断后的情况。</p><h3 id="使用FormData组织表单数据"><a href="#使用FormData组织表单数据" class="headerlink" title="使用FormData组织表单数据"></a>使用FormData组织表单数据</h3><p>解决了预览的问题,现在该解决上传的正事了,如果使用HTML5的上传方式那么就必须使用Ajax请求来与服务器通信,但表单中的文件应该如何以参数的方式通过ajax请求传送呢?</p><p>在DOM API中,Form提供了一个方法FormData,它可以将表单元素的DOM对象直接转换为参数,通过Ajax请求传送。用起来很简单,使用new关键字将DOM对象传入参数即可:</p><pre><code>var _fd = new FormData(document.getElementsByTagName('form')[0]);</code></pre><p>然后只需要在Ajax请求中送出即可:</p><pre><code>xhr.send(_fd);</code></pre><h5 id="append"><a href="#append" class="headerlink" title="append"></a>append</h5><p>当然我们也可以加入不在表单中的额外参数,使用append方法即可:</p><pre><code>var xhr = new XMLHttpRequest(); var formData = new FormData(document.getElementsByTagName('form')[0]);formData.append('param1', 'a parameter');xhr.open('POST', 'uploader.php');xhr.send(formData);</code></pre><p>append方法一般可以传入一对键值组合的参数用来添加到表单数据之中,但它还提供了另外一种用法,传入参数名以及一个Blob或者File,另外还有第三个可选的参数,是该参数的文件名。</p><p>至于Blob,是一个类似于文件的Object,我的理解是它在某些环境中可以解析为文件,但是在浏览器中是无法识别的。</p><h5 id="支持情况"><a href="#支持情况" class="headerlink" title="支持情况"></a>支持情况</h5><p>作为一个HTML5的方法自然也是有浏览器支持的问题的,如下表:</p><hr><table style="text-align: center;"> <tr> <th width="20%">IE</th> <th width="20%">Chrome</th> <th width="20%">Firefox</th> <th width="20%">Opera</th> <th width="20%">Safari</th> </tr> <tr> <td>10</td> <td>7+</td> <td>4.0</td> <td>12+</td> <td>5+</td> </tr></table>---<p>不过append方法的支持情况就有点不尽人意了,只有Chrome完全支持,Firefox在22以后才支持,其他浏览器均不支持。</p><h3 id="上传文件"><a href="#上传文件" class="headerlink" title="上传文件"></a>上传文件</h3><p>一般来说提交form数据到服务器,上传文件即可交由后端完成。但HTML5需要获取上传进度,就会比较特殊,所以还需要为Ajax请求绑定一些事件来处理不同的情况。</p><h5 id="Event"><a href="#Event" class="headerlink" title="Event"></a>Event</h5><p>一般来说,只需要使用XMLHttpRequest的addEventListener方法来绑定事件,像这样</p><pre><code>xhr.addEventListener('load', function (p_event) { // your code...}, false);</code></pre><p>除了load事件以外,还有一下一些事件,可以满足上传过程中遇到的各种问题。</p><p>1.abort 上传中断时触发。<br>2.error 上传出错时触发。<br>3.load 文件成功读取完成时触发。<br>4.loadend 文件读取结束时无论是否成功触发。<br>5.loadstart 文件读取开始时触发。<br>6.progress 文件读取过程中每秒触发一次。</p><h6 id="progress"><a href="#progress" class="headerlink" title="progress"></a>progress</h6><p>progress方法比较特殊,会在上传过程中一直触发,并获取当前上传的量 <code>loaded</code> 和总量等数据 <code>total</code> 。<br>主要需要用到的有2个数据,loaded已上传的部分和total总量,单位都是b,利用它们算出上传进度就可以显示百分比或设置进度条的宽度,甚至记录进度改变时花费的时间就能算出上传速度。</p><p>另外progress的监听比较特殊,像这样:</p><pre><code>xhr.upload.addEventListener('progress', function (p_event) { var _loaded = p_event.loaded; var _total = p_event.total; var _percent = Math.round(_loaded * 100 / _total); // using percent...}, false);</code></pre><p>需要使用xhr.upload的addEventListener方法来监听事件,而不是直接使用xhr。</p><h3 id="INPUT标签"><a href="#INPUT标签" class="headerlink" title="INPUT标签"></a>INPUT标签</h3><p>最后,是一个文件上传的老问题,无论是HTML5还是4,file类型的input标签样式总是无法统一,也无法美化。所以我们只能以暴制暴,不能化妆那就整容,用其他元素把它彻底覆盖掉。众所周知的做法是把input隐藏,然后问题来了,如何触发上传。</p><h6 id="trigger"><a href="#trigger" class="headerlink" title="trigger"></a>trigger</h6><p>一般首先想到的是模拟触发,比如jQuery中的trigger方法,可以让我们点击甚至其他动作时触发input标签。但是IE由于安全性问题不允许模拟触发file类型的input标签事件,所以如果不支持IE的项目可以使用这个方法轻松搞定。</p><h6 id="透明化按钮"><a href="#透明化按钮" class="headerlink" title="透明化按钮"></a>透明化按钮</h6><p>既然不能模拟,真实用户的点击行为自然是没问题了吧,于是另一个方法诞生了,将input标签变成透明的,覆盖在一个按钮样式的标签上,如此用户看到的是一个美化的按钮,点击的却是Input标签。但是有一个问题,file类型的Input标签在各浏览器中的尺寸和位置都是不太一致的,尤其是改变其尺寸后,有的浏览器甚至无法改变。所以如何按钮较大或者直接是一个区域时则会出现问题。</p><h6 id="鼠标跟随"><a href="#鼠标跟随" class="headerlink" title="鼠标跟随"></a>鼠标跟随</h6><p>方法继续进化,虽然点击区域的尺寸可能会很大,但鼠标的点击永远只是一个点,于是只要让Input标签一直跟随鼠标在区域内移动,将可点击部分随时对准鼠标指针,就可以让鼠标在区域内点击到Input标签了。这个方法解决了所有问题,但它的效率很成问题,甚至不能过分使用函数节流,因为移动过快时可能点击不到。</p><h6 id="Label触发"><a href="#Label触发" class="headerlink" title="Label触发"></a>Label触发</h6><p>后来在StackOverflow上看到了一个很不错的方法,就是利用Labal标签的for属性去触发input标签,只要将for的值写成Input的Id即可。但在我的测试中Firefox好像是不能触发的,不知道是否还有其他的属性需要设置。</p><p>这四种方法各有各的优劣,只能根据具体情况选择使用了。</p><h5 id="多文件上传-Multiple"><a href="#多文件上传-Multiple" class="headerlink" title="多文件上传 Multiple"></a>多文件上传 Multiple</h5><p>在Input标签上也出现了一个很实用的HTML5的新功能,那就是多文件上传,实现也非常简单,只要加上multiple的属性即可</p><pre><code><input type="file" name="files[]" multiple="multiple" /></code></pre><p>如此在上传的时候就可以选择多个文件,另外在后端接受数据时,每个属性都变成了一个数组,以PHP为例:</p><pre><code><?php header('Content-type: text/json'); print_r($_FILES["upload"]["name"]); $rtn = array( "code" => 0, "data" => '' ); if ($_FILES["upload"]["error"] > 0) { $rtn["code"] = -1; } else { $rtn["data"] = array( "name" => $_FILES["upload"]["name"], "type" => $_FILES["upload"]["type"], "size" => $_FILES["upload"]["size"], "path" => "" ); if (file_exists("img/".$rtn["data"]["name"])) { $rtn["code"] = 1; } else { move_uploaded_file($_FILES["upload"]["tmp_name"], "img/".$_FILES["upload"]["name"]); $rtn["code"] = 0; $rtn["data"]["path"] = "img/".$rtn["data"]["name"]; } } echo json_encode($rtn);?></code></pre><h3 id="参考文档:"><a href="#参考文档:" class="headerlink" title="参考文档:"></a>参考文档:</h3><ul><li><a href="http://www.w3.org/TR/FileAPI/">W3C FileReader Interface</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader?redirectlocale=en-US&redirectslug=DOM/FileReader">MDN FileReader</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/File">MDN DOM Files</a></li><li><a href="http://robertnyman.com/2013/02/11/using-formdata-to-send-forms-with-xhr-as-keyvalue-pairs/">Using FormData to send forms with xhr as key/value pairs</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">MDN FormData</a></li><li><a href="http://xhr.spec.whatwg.org/#interface-formdata">Whatwg Interface FormData</a></li><li><a href="http://www.w3.org/html/wg/drafts/html/master/forms.html#multipart-form-data">W3C Forms multiple</a></li><li><a href="http://www.sagarganatra.com/2011/04/file-upload-and-progress-events-with.html">File upload and Progress events with HTML5</a></li></ul>]]></content>
<summary type="html"><p>最近公司项目准备更换图片上传的插件,原来的是一个Flash控件,其实用起来还是不错的,还有进度条,浏览器支持情况也不错。不过因为某些页面的图片上传涉及到了跨域的问题,Flash似乎解决不了了,所以准备索性换成HTML5的,高端大气上档赤。然后这个HTML5上传图片功能自然落到了我的手上了。</p>
<p>一般来说图片上传无非就是文件操作的问题,本来这是服务器对文件流的一个操作问题,前端应该是管不上的,不过HTML5再次赋予了我们前端神圣而伟大的权利,有了HTML5部分后端失业了lol。</p>
<p>其实以前写过一个上传图片的插件,不过那时还活在IE6年代,只能用iframe搞定,虽然还挺好使的,不过在HTML5面前就是一个战斗力负5的渣渣,不仅需要后端返回各项数据,还必须把保存的临时图片地址再发给后端保存,实际请求是2次,而且还无法告诉用户上传进度与速度。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="html" scheme="https://blog.tychio.net/tags/html/"/>
<category term="html5" scheme="https://blog.tychio.net/tags/html5/"/>
<category term="file" scheme="https://blog.tychio.net/tags/file/"/>
<category term="upload" scheme="https://blog.tychio.net/tags/upload/"/>
</entry>
<entry>
<title>Javascript的Unit Test</title>
<link href="https://blog.tychio.net/2013/07/09/unit-test/"/>
<id>https://blog.tychio.net/2013/07/09/unit-test/</id>
<published>2013-07-09T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<h3 id="单元测试Unit-Test"><a href="#单元测试Unit-Test" class="headerlink" title="单元测试Unit Test"></a>单元测试Unit Test</h3><p>很早就知道单元测试这样一个概念,但直到几个月前,我真正开始接触和使用它。究竟什么是单元测试?我想也许很多使用了很久的人也不一定能描述的十分清楚,所以写了这篇文章来尝试描述它的特征和原则,以帮助更多人。</p><h5 id="什么是单元测试?"><a href="#什么是单元测试?" class="headerlink" title="什么是单元测试?"></a>什么是单元测试?</h5><p>先来看看单元测试的定义,在维基百科英文版中可以找到Kolawa Adam在 <em>Automated Defect Prevention: Best Practices in Software Management</em> 一书中对单元测试的定义:</p><blockquote><p>In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.</p></blockquote><p>重点在于最后,单元测试的目的显而易见,用来确定是否适合使用。而测试的方法则包括控制数据,使用和操作过程。那么以我的理解,每个单元测试就是一段用于测试一个模块或接口是否能达到预期结果的代码。开发人员需要使用代码来定义一个可用的衡量标准,并且可以快速检验。</p><p>很快我发现有一个误区,许多人认为单元测试必须是一个runner集中运行所有单元的测试,并一目了然。不,这仅仅是一种自动化单元测试的最佳实践,在一些小型项目中单元测试可能仅仅是一组去除其他特性的接口调用。甚至在一些图形处理或布局的项目中单元测试可以结合自身特性变的十分有趣,比如<a href="http://masonry.desandro.com/">Masonry</a>,一个网格布局库,在它的单元测试中不是一个红或绿的条目,而是一行一行的小格布局用以说明布局被完成的事实,这样比代码检查布局是否正确再以颜色显示结果来得更直观高效,也避免了测试程序本身的bug导致的失误。</p><p>打个比方,单元测试就像一把尺子,当测量的对象是一个曲面时,也许可以花费大力气去将它抽象成平面,但我更提倡量身定做一把弯曲的尺子去适应这个曲面。无论怎样,单元测试是为了生产代码而写,它应当足够的自由奔放,去适应各种各样的生产代码。</p><span id="more"></span><h5 id="为什么要单元测试?"><a href="#为什么要单元测试?" class="headerlink" title="为什么要单元测试?"></a>为什么要单元测试?</h5><p>也许定义中已经很清楚的指明了其意义,确认某段代码或模块或接口是否适合使用,但我想会有更多的人认为,直接在测试环境中使用软件可以更加确保软件是否可用。不,在实际使用过程中会伴随着一大批的附带操作大量增加测试时间,并且无法保证其测试覆盖率。所以我认为单元测试的目的并不仅仅是确认是否可用,而是更高效更稳定的确认其是否可用。</p><p>随着项目规模的增加,函数、方法、变量都在递增,尤其是进度的不足,来自产品经理的压力,还有QA所带来的各种Bug报告会让原本整洁的代码变得一片混乱。我甚至见过同一个接口以不同的名称出现在8个不同的控制器中。这时也许我们首先想到的是重构,可是等等,在重构结束时我们如何确定项目仅仅是被重构了,而不是被改写了?此时单元测试将是一根救命稻草,它是一个衡量标准,告诉开发人员这么做是否将改变结果。</p><p>不仅仅是这样。许多人认为单元测试,甚至整个测试都是在编码结束后的一道工序,而修复bug也不过是在做垃圾掩埋一类的工作。但测试应该伴随整个编码或软件周期进行,还有将在后面提到的TDD这样有趣的东西,单元测试将超前于编码。我的意思是,单元测试应该是一个框架、标准,经常被形容被脚手架,像建筑一样,脚手架的高度至少应该和大楼高度不相上下,甚至一开始就搭好脚手架。</p><h5 id="如何做单元测试?"><a href="#如何做单元测试?" class="headerlink" title="如何做单元测试?"></a>如何做单元测试?</h5><p>弄清了单元测试的目的和意义,但如何开始?很简单,首先它是一个检验,所以应该只有pass或fail两种情况。而检验的对象应该是某个接口或模块,所以应该调用它获得一个结果。检验这个结果就是单元测试的基本动作,就拿一个除法函数来做例子:</p><pre><code>function division (a, b) { return a / b;}var result = division(4, 2);if (result === 2) { alert('pass');} else { alert('fail');}</code></pre><p>显然,将会提示pass通过。但是问题来了,这个测试的用例太单一和普通了,如果使用0做除数呢?是NaN?还是Infinity?或者在实际使用时,产品需要一个0来代替这样一个不符合数学概念的结果去适应必须为数字类型的某种计算,于是division出现了一个bug。另外当覆盖率增加,也意味着用例的增加,我们需要把if条件语句提出来做成一个函数多次调用。还有alert方法,如果用例太多,我相信你会点确认点到手软,也许可以直接显示在页面上。</p><p>所以我添加了一个关于除数为0的用例,并重构了代码:</p><pre><code>function division (a, b) { if (b === 0) { return 0; } else { return a / b; }}function matcher (name, result, expect) { if (result === expect) { _print(name + '- pass'); } else { _print(name + '- fail'); } function _print (str) { var _bar = document.createElement('p'); _bar.innerText = str; document.body.appendChild(_bar); }}matcher('normal', division(4, 2), 2);matcher('zero', division(5, 0), 0);</code></pre><p>现在可以使用matcher方法添加许多测试用例,并且还能为该用例命名,在页面中直接显示每个用例是否通过。这样一个基本的单元测试就完成了,当然它的覆盖率还远远不够,这里仅作为一个例子。另外为了提高效率还应该使用颜色来标记是否通过,可以一目了然。</p><h5 id="测试驱动开发"><a href="#测试驱动开发" class="headerlink" title="测试驱动开发"></a>测试驱动开发</h5><p>TDD是Test Driven Development 的缩写,也就是测试驱动开发。</p><p>通常传统软件工程将测试描述为软件生命周期的一个环节,并且是在编码之后。但敏捷开发大师Kent Beck在2003年出版了 <em>Test Driven Development By Example</em> 一书,从而确立了测试驱动开发这个领域。</p><p>TDD需要遵循如下规则:</p><ul><li>写一个单元测试去描述程序的一个方面。</li><li>运行它应该会失败,因为程序还缺少这个特性。</li><li>为这个程序添加一些尽可能简单的代码保证测试通过。</li><li>重构这部分代码,直到代码没有重复、代码责任清晰并且结构简单。</li><li>持续重复这样做,积累代码。</li></ul><p>另外,衡量是否使用了TDD的一个重要标准是测试对代码的覆盖率,覆盖率在80%以下说明一个团队没有充分掌握TDD,当然高覆盖率也不能说一定使用了TDD,这仅仅是一个参考指标。</p><p>在我看来,TDD是一种开发技术,而非测试技术,所以它对于代码构建的意义远大于代码测试。也许最终的代码和先开发再测试写的测试代码基本一致,但它们仍然是有很大不同的。TDD具有很强的目的性,在直接结果的指导下开发生产代码,然后不断围绕这个目标去改进代码,其优势是高效和去冗余的。所以其特点应该是由需求得出测试,由测试代码得出生产代码。打个比方就像是自行车的两个轮子,虽然都是在向同一个方向转动,但是后轮是施力的,带动车子向前,而前轮是受力的,被向前的车子带动而转。</p><h5 id="行为驱动开发"><a href="#行为驱动开发" class="headerlink" title="行为驱动开发"></a>行为驱动开发</h5><p>所谓的BDD行为驱动开发,即Behaviour Driven Development,是一种新的敏捷开发方法。它更趋向于需求,需要共同利益者的参与,强调用户故事(User Story)和行为。2009年,在伦敦发表的“敏捷规格,BDD和极限测试交流”[3]中,<a href="http://dannorth.net/">Dan North</a>对BDD给出了如下定义:</p><blockquote><p>BDD是第二代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,可以具有带有良好定义的输出(即工作中交付的结果):已测试过的软件。</p></blockquote><p>另外最主观的区别就是用词,‘example’取代了‘test’,‘describe’取代了‘class’,‘behaviour’取代了‘method’等等。这正是其特征之一,自然语言的加入,使得非程序人员也能参与到测试用例的编写中来,也大大降低了客户、用户、项目管理者与开发者之间来回翻译的成本。</p><p>简单来说,我认为BDD更加注重业务需求而不是技术,虽然看起来BDD确实是比ATDD做的更好,但这是一种误导,这仅仅是就某种环境下而言的。而且以国内的现状来看TDD要比BDD更适合,因为它不需要所有人员的理解和加入。</p><h3 id="单元测试框架"><a href="#单元测试框架" class="headerlink" title="单元测试框架"></a>单元测试框架</h3><p>无论如何,单元测试永远是少不了的。其实在单元测试中测试代码和生产代码应该是等量的,正如Robert C. Martin在其 <em>Clean Code: A Handbook of Agile Software Craftsmanship</em> 一书中所写:</p><blockquote><p>测试必须随生产代码的演进而修改,测试越脏就越难修改</p></blockquote><p>于是新的测试很难被加入其中,测试代码的维护变得异常困难,最终在各种压力之中只有扔掉测试代码组。但是没有了测试代码,就失去了确保对代码的改动能如愿以偿的能力,各种问题随之而来。因此,单元测试也需要一种行之有效的实践来确保其质量和可维护性。</p><p>所以正如生产代码一样,测试代码也有框架,下面介绍几种主流的Javascript的单元测试框架。</p><h4 id="Jasmine"><a href="#Jasmine" class="headerlink" title="Jasmine"></a><a href="http://pivotal.github.io/jasmine/">Jasmine</a></h4><p>有一类框架叫做xUnit,来源于著名的JAVA测试框架JUnit,xUnit则代表了一种模式,并且使用这样的命名。在Javascript中也有这样的一个老牌框架JsUnit,他的作者是Edward Hieatt来自<a href="http://pivotallabs.com/">Pivotal Labs</a>,但在几年前JsUnit就已经停止维护了,他们带来了新的BDD框架Jasmine。</p><p>Jasmine不依赖于任何框架,所以适用于所有的Javascript代码。使用一个全局函数 <code>describe</code> 来描述每个测试,并且可以嵌套。describe函数有2个参数,一个是字符串用于描述,一个是函数用于测试。在该函数中可以使用全局函数 <code>it</code> 来定义Specs,也就是单元测试的主要内容, 使用 <code>expect</code> 函数来测试:</p><pre><code>describe('A suite', function () { it('is a spec', function () { var a = true; expect(a).toBe(true); });});</code></pre><p>另外如果想去掉某个describe,无须注释掉整段代码,只需要在describe前面加上x即可忽略该describe。</p><h5 id="Matcher"><a href="#Matcher" class="headerlink" title="Matcher"></a>Matcher</h5><p>toBe方法是一个基本的 <code>matcher</code> 用来定义判断规则,可以看得出来Jasmine的方法是非常语义化的,“expect ‘a’ to be true”,如果想判断否定条件,则只需要在toBe前调用 <code>not</code> 方法:</p><pre><code>expect(a).not().toBe(false);</code></pre><p>除了toBe这样基本的还有许多其他的<a href="https://github.com/pivotal/jasmine/wiki/Matchers">Matcher</a>,比如 <code>toEqual</code> 。很多初学Jasmine会弄不清和toBe的区别,一个简单的例子就能明白它们的区别:</p><pre><code>expect({}).not().toBe({});expect({}).toEqual({});</code></pre><p>一个新建的Object不是(not to be)另一个新建的Object,但是它们是相等(to equal)的。还有 <code>toMatch</code> 可以使用字符串或者正则表达式来验证,以及其他一些特殊验证,比如undefined或者boolean的判断, <code>toThrow</code> 可以检查函数所抛出的异常。另外Jasmine还支持自定义Matcher,以NaN的检查为例,像这样使用beforeEach方法在每个测试执行前添加一个matcher:</p><pre><code>beforeEach(function () { this.addMatchers({ toBeNaN: function (expected) { return isNaN(expected); } });});</code></pre><p>可以想到,其参数expected是传入的一个期望的字面量,而在expect方法中传入的参数,可以通过 <code>this.acturl</code> 获取,是否调用了 <code>not</code> 方法则可以通过 <code>this.isNot</code> 获取,这是一个boolean值。最后测试输出的失败信息应该使用 <code>this.message</code> 来定义,不过它是一个function,然后在其中返回一个信息。所以继续增进toBeNaN:</p><pre><code>beforeEach(function () { this.addMatchers({ toBeNaN: function (expected) { var actual = this.actual; var not = this.isNot ? ' not' : ''; this.message = function () { return 'Expected ' + actual + not + ' to be NaN ' + expected; }; return isNaN(expected); } });});</code></pre><p>这样一个完整的matcher就创建成了。</p><p>另外需要说明的是对应beforeEach是在每个spec之前执行, <code>afterEach</code> 方法则是在每个spec之后执行。这是一种AOP,即面向方面的编程(Aspect Oriented Programming)。比如有时候为了测试一个对象,可能需要多次创建和销毁它,所以为了避免冗余代码,使用它们是最佳选择。</p><p>还可以使用 <code>jasmine.any</code> 方法来代表一类数据传入matcher中,比如</p><pre><code>expect(123).toEqual(jasmine.any(Number));expect(function () {}).toEqual(jasmine.any(Function));</code></pre><h5 id="Spy方法"><a href="#Spy方法" class="headerlink" title="Spy方法"></a>Spy方法</h5><p>一个Spy能监测任何function的调用和获取其参数。这里有2个特殊的Matcher, <code>toHaveBeenCalled</code> 可以检查function是否被调用过,还有 <code>toHaveBeenCalledWith</code> 可以传入参数检查是否和这些参数一起被调用过,像这样使用 <code>spyOn</code> 来注册一个对象中的方法:</p><pre><code>var foo, a = null;beforeEach(function () { var foo = { set: function (str) { a = str; } } spyOn(foo, 'set'); foo.set(123);});it('tracks calls', function () { expect(foo.set).toHaveBeenCalled(); expect(foo.set).toHaveBeenCalled(123); expect(foo.set.calls[0].args[0]).toEqual(123); expect(foo.set.mostRecentCall.args[0]).toEqual(123); expect(a).toBeNull();});</code></pre><p>在测试时该function将带有一个被调用的数组 <code>calls</code> ,而 <code>args</code> 数组就是调用时传入的参数,另外特殊属性 <code>mostRencentCall</code> 则代表最后一次调用,和calls[calls.length]一致。需要特别注意的是,这些调用将不会对变量产生作用,所以 <code>a</code> 仍为null。</p><p>如果需要调用产生实际的作用,可以在spyOn方法后调用 <code>andCallThrough</code> 方法。还可以通过调用 <code>andReturn</code> 方法设定一个返回值给function。 <code>andCallFake</code> 则可以传入一个function作为参数去代替原本的function。</p><pre><code>spyOn(foo, 'set').andCallThrough();</code></pre><p>甚至在没有function的时候可以使用Jasmine的 <code>createSpy</code> 和 <code>createSpyObj</code> 创建一个spy:</p><pre><code>foo = jasmine.createSpy('foo');obj = jasmine.createSpyObj('obj', [set, do]);foo(123);obj.set(123);obj.do();</code></pre><p>其效果相当于spyOn使用在了已存在的function上。</p><h5 id="时间控制"><a href="#时间控制" class="headerlink" title="时间控制"></a>时间控制</h5><p>上面的方法都在程序顺序执行的前提下执行,但 <code>setTimeout</code> 以及 <code>setInterval</code> 两个方法会使代码分离在时间轴上。所以Jasmine提供了 <code>Clock</code> 方法来模拟时间,以获取setTimeout的不同状态。</p><pre><code>beforeEach(function () { jasmine.Clock.useMock();});it('set time', function () { var str = 0; setTimeout(function () { str++; }, 100); expect(str).toEqual(0); jasmine.Click.tick(101); expect(str).toEqual(1); jasmine.Click.tick(200); expect(str).toEqual(3);});</code></pre><p>使用Clock的方法 <code>useMock</code> 来开始时间控制,然后在it中使用 <code>tick</code> 方法来推进时间。</p><h5 id="异步"><a href="#异步" class="headerlink" title="异步"></a>异步</h5><p>Javascript最大的特色之一就是异步,之前介绍的方法如果存在异步调用,大部分测试时可能会不通过。因此,需要等异步回调之后再进行测试。</p><p>Jasmine提供了 <code>runs</code> 和 <code>waitsFor</code> 两个方法来完成这个异步的等待。需要将waitsFor方法夹在多个runs方法中,runs方法中的语句会按顺序直接执行,然后进入waitsFor方法,如果waitsFor返回false,则继续执行waitsFor,直到返回true才执行后面的runs方法。</p><pre><code>var cb = false;var ajax = { success: function () { cb = true; }};spyOn(ajax, 'success');it('async callback', function () { runs(function () { _toAjax(ajax); }); waitsFor(function () { return ajax.success.callCount > 0; }); runs(function () { expect(cb).toBeTruthy(); });});</code></pre><p>如此,只要在waitsFor中判断回调函数是否被调用了即可完成异步测试。上面代码中我使用一个方法名直接代替了ajax请求方法来缩减不必要的代码。在第一个runs方法中发出了一个ajax请求,然后在waitsFor中等待其被调用,当第二个runs执行时说明回调函数已经被调用了,进行测试。</p><h4 id="Qunit"><a href="#Qunit" class="headerlink" title="Qunit"></a><a href="http://qunitjs.com/">Qunit</a></h4><p>它是由jQuery团队开发的一款测试套件,最初依赖于jQuery库,在2009年时脱离jQuery的依赖,变成了一个真正的测试框架,适用于所有Javascript代码。</p><p>Qunit采用断言(Assert)来进行测试,相比于Jasmine的matcher更加多的类型,Qunit更集中在测试的度上。 <code>deepEqual</code> 用于比较一些纵向数据,比如Object或者Function等。而最常用的 <code>ok</code> 则直接判断是否为true。异步方面Qunit也很有趣,通过 <code>stop</code> 来停止测试等待异步返回,然后使用 <code>start</code> 继续测试,这要比Jasmine的过程化的等待更自由一些,不过有时也许会更难写一些。Qunit还拥有3组AOP的方法( <code>done</code> 和 ‘begin’ )来对应于整个测试,测试和模块。</p><p>对于Function的跟踪测试,Qunit似乎完全没有考虑。不过可以使用另外一个测试框架为Qunit带来的插件 <a href="http://sinonjs.org/qunit/">sinon-qunit</a>。这样就可以在test中使用 <code>spy</code> 方法了。</p><h4 id="Sinon"><a href="#Sinon" class="headerlink" title="Sinon"></a><a href="http://sinonjs.org/">Sinon</a></h4><p>Sinon并不是一个典型的单元测试框架,更像一个库,最主要的是对Function的测试,包括 <code>Spy</code> 和 <code>Stub</code> 两个部分,Spy用于侦测Function,而Stub更像是一个Spy的插件或者助手,在Function调用前后做一些特殊的处理,比如修改配置或者回调。它正好极大的弥补了Qunit的不足,所以通常会使用Qunit+Sinon来进行单元测试。</p><p>值得一提的是,Sinon的作者<a href="http://tddjs.com/">Christian Johansen</a>就是 <em>Test-Driven JavaScript Development</em> 一书的作者,这本书针对Javascript很详细的描述了单元测试的每个环节。</p><h4 id="Mocha"><a href="#Mocha" class="headerlink" title="Mocha"></a><a href="http://visionmedia.github.io/mocha">Mocha</a></h4><p>它的作者就是在Github上粉丝6K的超级Jser <a href="https://github.com/visionmedia">TJ Holowaychuk</a>,可以在他的页面上看到过去一年的提交量是5700多,拥有300多个项目,无论是谁都难以想象他是如何进行coding的。</p><p>理所当然的,Mocha充满了Geek感,不但可以在bash中进行测试,而且还拥有一整套命令对测试进行操作。甚至使用 <code>diff</code> 可以查看当前测试与上一次成功测试的代码不一致。</p><p>不仅仅是这样,Mocha非常得自由。Mocha将更多的方法集中在了describe和it中,比如异步的测试就非常棒,在it的回调函数中会获取一个参数 <code>done</code> ,类型是function,用于异步回调,当执行这个函数时就会继续测试。还可以使用 <code>only</code> 和 <code>skip</code> 去选择测试时需要的部分。Mocha的接口也一样自由,除了 <code>BDD</code> 风格和Jasmine类似的接口,还有 <code>TDD</code> 风格的 (suite test setup teardown suiteSetup suiteTeardown),还有AMD风格的 <code>exports</code>,Qunit风格等。同时测试报告也可以任意组织,无论是列表、进度条、还是飞机跑道这样奇特的样式都可以在bash中显示。</p><h3 id="前端测试工具"><a href="#前端测试工具" class="headerlink" title="前端测试工具"></a>前端测试工具</h3><h4 id="Client-Server-测试"><a href="#Client-Server-测试" class="headerlink" title="Client/Server 测试"></a>Client/Server 测试</h4><p>相比于服务端开发,前端开发在测试方面始终面临着一个严峻的问题,那就是浏览器兼容性。<a href="http://www.paulirish.com/">Paul Irish</a>曾发表文章<a href="http://www.paulirish.com/2011/browser-market-pollution-iex-is-the-new-ie6/">Browser Market Pollution: IE[x] Is the New IE6</a>阐述了一个奇怪的设想,未来你可能需要在76个浏览器上开发,因为每次IE的新版本都是一个特别的浏览器,而且还有它对之前所有版本的兼容模式也是一样。虽然没人认为微软会继续如此愚蠢,不过这也说明了一个问题,前端开发中浏览器兼容性是一个永远的问题,而且我认为即使解决了浏览器的兼容性问题,未来在移动开发方面,设备兼容性也是一个问题。</p><p>所以在自动化测试方面也是如此,即使所有的单元测试集中在了一个runner中,前端测试仍然要面对至少4个浏览器内核以及3个电脑操作系统加2个或更多移动操作系统,何况还有令移动开发人员头疼的Android的碎片化问题。不过可以安心的是,早已存在这样的工具可以捕获不同设备上的不同浏览器,并使之随时更新测试结果,甚至可以在一个终端上看到所有结果。</p><h5 id="工具介绍"><a href="#工具介绍" class="headerlink" title="工具介绍"></a>工具介绍</h5><p><a href="https://code.google.com/p/js-test-driver/">JSTD(Javascript Test Driver)</a>是一个最早的C/S测试工具,来自Google,基于JAVA编写,跨平台,使用命令行控制,还有很好的编辑器支持,最常用于eclipse。不过它无法显示测试对象的设备及浏览器版本,只有浏览器名是不够的。另外JSTD已经慢慢不再活跃,它的早正如它的老。</p><p>Google的新贵<a href="http://karma-runner.github.io/">Karma</a>出现了,它使用Nodejs构建,因此跨平台,还支持PhantomJS浏览器,还支持多种框架,包括以上介绍的Jasmine、Qunit和Mocha。一次可以在多个浏览器及设备中进行测试,并控制浏览器行为和测试报告。虽然它不支持Nodejs的测试,不过没什么影响,因为Nodejs并不依赖于浏览器。</p><p>还有<a href="http://swarm.jquery.org/">TestSwarm</a>,出自jQuery之父John Resig之手,看来jQuery的强大果然不是偶然的,在测试方面非常到位,各种工具齐全。它最特别的地方在于所有测试环境由服务器提供,包括各种版本的主流浏览器以及iOS5的iphone设备,不过目前加入已经受限。</p><p>最受瞩目的当属<a href="http://www.busterjs.org/">Buster</a>,其作者之一就是<a href="http://tddjs.com/">Christian Johansen</a>。和Karma很像,也使用Nodejs编写跨平台并且支持PhantomJS,一次测试所有客户端。更重要的是支持Nodejs的测试,同样支持各种主流测试框架。不过目前还在Beta测试中,很多bug而且不能很好的兼容Windows系统。它的目标还包括整合<a href="http://www.browserstack.com/">Browser Stack</a>。</p><h4 id="基于网页的测试"><a href="#基于网页的测试" class="headerlink" title="基于网页的测试"></a>基于网页的测试</h4><p>到目前为止我们的测试看起来十分的完美了,但是别忘了,在前端开发中存在交互问题,不能期待QA玩了命的点击某个按钮或者刷新一个页面并输入一句乱码之类的东西来测试代码。即使是开发者本身也会受不了,如果产品本身拥有一堆复杂的表单和逻辑的话。</p><p><a href="http://www.seleniumhq.org/">Selenium</a>是一个测试工具集,由Thoughtworks开发,分为两部分。Selenium IDE是一个Firefox浏览器的插件,可以录制用户行为,并快速测试。</p><p>而Selenium WebDriver是一个多语言的驱动浏览器的工具,支持Python、Java、Ruby、Perl、PHP或.Net。并且可以操作IE、Firefox、Safari和Chrome等主流浏览器。通过 <code>open</code> , <code>type</code> , <code>click</code> , <code>waitForxxx</code> 等指令来模拟用户行为,比如用Java测试:</p><pre><code>public void testNew() throws Exception { selenium.open("/"); selenium.type("q", "selenium rc"); selenium.click("btnG"); selenium.waitForPageToLoad("30000"); assertTrue(selenium.isTextPresent("Results * for selenium rc"));}</code></pre><p>首先跳转到跟目录,然后选择类型,点击按钮G,并等待页面载入30秒,然后使用断言测试。这样就完成了一次用户基本行为的模拟,不过复杂的模拟以及在一些非链接的操作还需要格外注意,比如Ajax请求或者Pjax的无刷新等等。</p><p>另外还有一款可以模拟用户行为的网页测试工具<a href="http://watir.com/">WATIR</a>,是Web Application Testing in Ruby的缩写,显然它只支持Ruby语言来操作浏览器模拟用户行为。官方声称它是一个简单而灵活的工具,无论怎样至少就官方网站的设计来看要比Selenium简约多了。同样支持模拟链接点击,按钮点击,还有表单的填写等行为。不过WATIR不支持Ajax的测试。和其他Ruby库一样需要gem来安装它:</p><pre><code>gem install watir --no-rdoc --no-ri</code></pre><p>然后使用它</p><pre><code>require 'rubygems'require 'watir'require 'watir-webdriver'browser = Watir::Browser.newbrowser.goto 'http://www.example.com/form'browser.test_field(:name => 'entry.0.single').set 'Watir'browser.radio(:value => 'Watir').setbrowser.radio(:value => 'Watir').clearbrowser.checkbox(:value => 'Ruby').setbrowser.checkbox(:value => 'Javascript').clearbrowser.button(:name => 'submit').click</code></pre><p>这样就使用watir完成了一次表单填写。</p><h4 id="持续集成测试"><a href="#持续集成测试" class="headerlink" title="持续集成测试"></a>持续集成测试</h4><p>持续集成就是通常所谓的CI(Continuous integration),持续不断的自动化测试新加入代码后的项目。它并不属于单元测试,而是另外的范畴,不过通过使用CI服务可以很容易的在Github上测试项目,而这也就是持续集成的意义。</p><p>下面以我的jQ小插件<a href="https://github.com/tychio/dialog#unit">Dialog</a>为例介绍一下Travis-CI的使用方法,注册<a href="https://travis-ci.org/">Travis</a>,然后链接自己的Github,选择要进行持续集成的项目。此时会显示build failing,那是因为还没有在项目中进行相关配置。</p><p>首先需要使用Grunt等工具配置好测试框架的自动化测试,细节可以参考我之前的文章<a href="http://www.tychio.net/tech/2013/09/25/improve-workflow.html">改进我的Workflow</a>。然后在 <code>package.json</code> 中添加一下代码来指定执行的脚本:</p><pre><code>"scripts": { "test": "grunt jasmine:test"}</code></pre><p>接着添加一个文件 <code>.travis.yml</code> 来配置travis:</p><pre><code>language: node_jsnode_js: - "0.8"before_script: - npm install -g grunt-cli</code></pre><p><code>language</code> 是集成测试所使用的语言,这里前端开发当然是使用Nodejs,在 <code>node_js</code> 中指定版本即可。当然Travis还支持其他多种语言,以及后端数据库等。</p><p><code>before_script</code> 则是在测试前执行的脚本程序,这里在全局安装Grunt-cli即可,因为默认的Travis会执行 <code>npm install</code> 将package.json中指定的Node包安装到项目。</p><p>最后在Github中还需要在项目的Setting中的Service Hooks中配置Travis,输入Token并保存。或者直接在Travis中点击该项目条目中的扳手图标进入Github,会自动配置好。</p><p>另外,如果在Github上为README文件添加一行</p><pre><code>[](https://travis-ci.org/tychio/dialog)</code></pre><p>就可以持续直观的显示其测试结果。</p><h3 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h3><ul><li><p><a href="http://esj.com/articles/2012/09/24/better-unit-testing.aspx">Better unit testing</a></p></li><li><p><a href="http://stackoverflow.com/questions/300855/javascript-unit-test-tools-for-tdd">Javascript Unit Test tools Q/A</a></p></li><li><p><a href="http://www.ibm.com/developerworks/web/library/wa-tools/">Tools to unit test your JavaScript</a></p></li><li><p><a href="http://guide.agilealliance.org/guide/tdd.html">Agile Aliiance Guide Test Driven Development</a></p></li><li><p><a href="http://guide.agilealliance.org/guide/bdd.html">Agile Aliiance Guide Behaviour Driven Development</a></p></li><li><p><a href="http://www.infoq.com/news/2011/02/BDD-ATDD">BDD: ATDD done well?</a></p></li><li><p><a href="http://en.wikipedia.org/wiki/Unit_test">Unit test wiki</a></p></li><li><p><a href="http://en.wikipedia.org/wiki/Behavior-driven_development">BDD Wiki</a></p></li><li><p><a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">Aspect-oriented Programming wiki</a></p></li><li><p><a href="https://www.guru99.com/selenium-tutorial.html">Selenium Tutorial Guru99</a></p></li></ul>]]></content>
<summary type="html"><h3 id="单元测试Unit-Test"><a href="#单元测试Unit-Test" class="headerlink" title="单元测试Unit Test"></a>单元测试Unit Test</h3><p>很早就知道单元测试这样一个概念,但直到几个月前,我真正开始接触和使用它。究竟什么是单元测试?我想也许很多使用了很久的人也不一定能描述的十分清楚,所以写了这篇文章来尝试描述它的特征和原则,以帮助更多人。</p>
<h5 id="什么是单元测试?"><a href="#什么是单元测试?" class="headerlink" title="什么是单元测试?"></a>什么是单元测试?</h5><p>先来看看单元测试的定义,在维基百科英文版中可以找到Kolawa Adam在 <em>Automated Defect Prevention: Best Practices in Software Management</em> 一书中对单元测试的定义:</p>
<blockquote>
<p>In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.</p>
</blockquote>
<p>重点在于最后,单元测试的目的显而易见,用来确定是否适合使用。而测试的方法则包括控制数据,使用和操作过程。那么以我的理解,每个单元测试就是一段用于测试一个模块或接口是否能达到预期结果的代码。开发人员需要使用代码来定义一个可用的衡量标准,并且可以快速检验。</p>
<p>很快我发现有一个误区,许多人认为单元测试必须是一个runner集中运行所有单元的测试,并一目了然。不,这仅仅是一种自动化单元测试的最佳实践,在一些小型项目中单元测试可能仅仅是一组去除其他特性的接口调用。甚至在一些图形处理或布局的项目中单元测试可以结合自身特性变的十分有趣,比如<a href="http://masonry.desandro.com/">Masonry</a>,一个网格布局库,在它的单元测试中不是一个红或绿的条目,而是一行一行的小格布局用以说明布局被完成的事实,这样比代码检查布局是否正确再以颜色显示结果来得更直观高效,也避免了测试程序本身的bug导致的失误。</p>
<p>打个比方,单元测试就像一把尺子,当测量的对象是一个曲面时,也许可以花费大力气去将它抽象成平面,但我更提倡量身定做一把弯曲的尺子去适应这个曲面。无论怎样,单元测试是为了生产代码而写,它应当足够的自由奔放,去适应各种各样的生产代码。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="test" scheme="https://blog.tychio.net/tags/test/"/>
<category term="unit" scheme="https://blog.tychio.net/tags/unit/"/>
<category term="jasmine" scheme="https://blog.tychio.net/tags/jasmine/"/>
<category term="CI" scheme="https://blog.tychio.net/tags/CI/"/>
<category term="karma" scheme="https://blog.tychio.net/tags/karma/"/>
<category term="单元测试" scheme="https://blog.tychio.net/tags/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/"/>
<category term="TDD" scheme="https://blog.tychio.net/tags/TDD/"/>
<category term="BDD" scheme="https://blog.tychio.net/tags/BDD/"/>
</entry>
<entry>
<title>键盘事件的KeyCode分析</title>
<link href="https://blog.tychio.net/2013/06/05/event-keycode/"/>
<id>https://blog.tychio.net/2013/06/05/event-keycode/</id>
<published>2013-06-05T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<h2 id="keyup和keydown事件以及keyCode和which属性"><a href="#keyup和keydown事件以及keyCode和which属性" class="headerlink" title="keyup和keydown事件以及keyCode和which属性"></a>keyup和keydown事件以及keyCode和which属性</h2><p>我想关于键盘事件最常见的实例就是回车提交表单了,恐怕每个前端都有一段烂熟于胸的代码用来实现这个功能。以前我也只是做了这样一个功能,也许它的代码是这样的:</p><pre><code>function enter (p_event) { var _keyCode = p_event.which ? p_event.which : p_event.keyCode; var _ENTER_CODE = 13; if (_ENTER_CODE === _keyCode) { //enter code... }}</code></pre><p>如果需要兼容ie8及以下,那还需要这几行代码:</p><pre><code>if (typeof p_event === 'undefined') { p_event = window.event;}</code></pre><p>当然,enter还需要绑定到一个键盘事件中,键盘事件有三种,我以前倾向于使用keyup [1],这样的体验比较好,符合人的习惯,在松开按键的时候生效。不过有时候keydown也是很棒的选择,比如说用户希望可以快速触发时,比如游戏中,我还记得WOW有一款插件是专门修改为按下触发技能的。</p><span id="more"></span><pre><code>document.getElementById('enter_input').onkeyup = enter;</code></pre><hr><p>也许enter回车这样没问题,但其他按键呢?于是我做了一组测试,发现了一些问题</p><p>在keyup和keydown事件中:</p><ul><li>IE只有keyCode,which为undefined。</li><li>Firefox的which有值,而keyCode为0,但F1-12键则恰好相反,which为0,keyCode有值。</li><li>Chrome和Opare中which和keyCode都有值。</li></ul><p>按键码的一些差异:</p><ul><li>在IE、Safari中和Chrome、Firefox、Opare中存在差异。</li><li>‘+ =’键是187 => 61。</li><li>‘; :’键是186 => 59。</li><li>‘- _’键是189 => 45, Opera很诡异的是109。</li><li>win键只有IE8和Safari是91,Opera是219,其他无法触发。</li><li>Meta [2] 键是93,但Opera和Chrome无法触发</li></ul><h2 id="keypress事件和charCode属性"><a href="#keypress事件和charCode属性" class="headerlink" title="keypress事件和charCode属性"></a>keypress事件和charCode属性</h2><p>这里其实有一个很重要但很多人都没搞清楚过的问题,那就是另外一个事件keypress,这个事情是怎么回事?曾经我只是单纯的认为它和keydown是一样的,因为它们在按下键盘后都会一直触发直到松开。而且w3school中文上的说法是</p><blockquote><p>onkeypress 事件会在键盘按键被按下并释放一个键时发生</p></blockquote><p>不过最近看到了一个属性charCode,让我初步认识到了区别所在,似乎w3school的说法不太对。起初我以为charCode和keyCode还有which是不同浏览器实现的不同名称,但似乎charCode是mozila弄出来的一个东西,那就应该和其他2个有什么区别,因此我决定好好实验一番,来分析一下这些事件与属性究竟是怎么回事。</p><p>首先在我认为最强大的Chrome中测试了一下,只有在keypress事件中,charCode才有值,而在keyup和keydown中都为0,另外我发现小键盘区域大部分按键是无法触发keypress的,还有win键、ctrl、alt、meta等都无法触发。</p><hr><p>经过上面的测试,我大概发现了keypress的意义,<strong>keypress只有按下可产出字符的按键时才会触发</strong>,也因此keypress才能使用charCode,charCode的意义也很明显了,是<strong>按键的字符的代码</strong>,而不是keyCode或者which按键代码的意思。</p><p>为了进一步检验我的理解,我试验了space、enter都有charCode值,而小键盘在点亮了Num Lock之后也有了charCode,并且按住shift或者切换Caps Lock后,charCode会发生变化也足以证明charCode是字符的unicode值,比如按下“A”时,会有小写和大写的65、97之分。</p><hr><p>另外关于浏览器的兼容性:</p><ul><li>IE8及以下和Opera12+是不支持charCode属性的。</li><li>而在Firefox中,keypress事件触发时keyCode是没有值的。</li></ul><p>可以用以下代码试验。</p><pre><code>document.body.onkeypress = function (p_event) { p_event = p_event ? p_event : event; alert('charCode is ' + p_event.charCode);}</code></pre><p>如果在测试的过程中回车之类的按键触发了某些浏览器行为,可以这样避免,在事件方法的最后加上这行代码:</p><pre><code>e.preventDefault ? e.preventDefault() : event.returnValue = false;</code></pre><hr><p>关于标准,在W3C标准中,其实无论是which、keyCode还是charCode都已经不推荐使用了,取而代之的是which和keyCode为key,charCode为char。不过遗憾的是目前所有浏览器都还没有实现key和char。</p><hr><blockquote><p>注1:不过IE6是不支持keyup的,只能用keydown。<br>注2:meta按键就是一般在win键旁边的一个鼠标+一个菜单样式的按键,按键一般是用来弹出鼠标右键菜单的。</p></blockquote><hr><p>参考文章:</p><ul><li><a href="http://www.javascripter.net/faq/keycodes.htm">Key codes of keydown and keyup events</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/Reference/Events/keypress">Mozilla - keypress</a></li><li><a href="http://www.w3.org/TR/DOM-Level-3-Events/#event-type-keypress">W3C Standard</a></li><li><a href="http://msdn.microsoft.com/en-us/library/ie/ff974890">MSDN - charCode property</a></li></ul>]]></content>
<summary type="html"><h2 id="keyup和keydown事件以及keyCode和which属性"><a href="#keyup和keydown事件以及keyCode和which属性" class="headerlink" title="keyup和keydown事件以及keyCode和which属性"></a>keyup和keydown事件以及keyCode和which属性</h2><p>我想关于键盘事件最常见的实例就是回车提交表单了,恐怕每个前端都有一段烂熟于胸的代码用来实现这个功能。以前我也只是做了这样一个功能,也许它的代码是这样的:</p>
<pre><code>function enter (p_event) &#123;
var _keyCode = p_event.which ? p_event.which : p_event.keyCode;
var _ENTER_CODE = 13;
if (_ENTER_CODE === _keyCode) &#123;
//enter code...
&#125;
&#125;
</code></pre>
<p>如果需要兼容ie8及以下,那还需要这几行代码:</p>
<pre><code>if (typeof p_event === &#39;undefined&#39;) &#123;
p_event = window.event;
&#125;
</code></pre>
<p>当然,enter还需要绑定到一个键盘事件中,键盘事件有三种,我以前倾向于使用keyup [1],这样的体验比较好,符合人的习惯,在松开按键的时候生效。不过有时候keydown也是很棒的选择,比如说用户希望可以快速触发时,比如游戏中,我还记得WOW有一款插件是专门修改为按下触发技能的。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="Javascript" scheme="https://blog.tychio.net/tags/Javascript/"/>
<category term="event" scheme="https://blog.tychio.net/tags/event/"/>
<category term="which" scheme="https://blog.tychio.net/tags/which/"/>
<category term="keycode" scheme="https://blog.tychio.net/tags/keycode/"/>
<category term="charcode" scheme="https://blog.tychio.net/tags/charcode/"/>
<category term="unicode" scheme="https://blog.tychio.net/tags/unicode/"/>
<category term="事件" scheme="https://blog.tychio.net/tags/%E4%BA%8B%E4%BB%B6/"/>
</entry>
<entry>
<title>推荐一些图片工具</title>
<link href="https://blog.tychio.net/2013/05/28/design-tools2/"/>
<id>https://blog.tychio.net/2013/05/28/design-tools2/</id>
<published>2013-05-28T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p>上周介绍了几种配色工具,这次要介绍的是一些图片和图标的生成工具。</p><p>就算你熟练掌握Photoshop之类的图片处理工具,要制作一个小小的图标还是挺费时费力的。可是网上下载的话,颜色尺寸又不一定能适合,所以制作图片的工具将是非常必要的。</p><h3 id="Online-Generator"><a href="#Online-Generator" class="headerlink" title="Online Generator"></a><a href="http://onlinegenerator.net/">Online Generator</a></h3><p><a href="http://preloaders.net/"><img src="http://onlinegenerator.net/images/preloaders.png"></a></p><p>Online Generator包括好几个很棒的工具,首先是Preloaders,它用于制作loading图片,它的图片种类很齐全,还有大量的3D图片,最重要的是它可以任意改变尺寸,当然也包括颜色,动画。动画可以选择方向频率等等,功能十分强大,基本上只要你能找到喜欢的图案,它就一定能制作出你想要的loading图片。</p><span id="more"></span><p><a href="http://iconizer.net/"><img src="http://onlinegenerator.net/images/iconizer.png"></a></p><p>Iconizer是一个icon搜索制作工具,只要输入关键字,比如home,就会出现海量的类似图标,然后选一个就可以继续定制颜色尺寸等相关信息,然后下载你要的图标或者先存起来,之后再打包下载。</p><p><a href="http://cssload.net/"><img src="http://onlinegenerator.net/images/cssload.png"></a></p><p>这样还要加载图片,是不是觉得有点烦,其实还有更简单的办法,只要你忽略ie8一下浏览器就行了,用CSS的animation实现loading动画。不会写?懒的写?好吧,那只有用CSSloader了,虽然样式有限,但是选择颜色尺寸后你就可以复制代码了。</p><h3 id="IconBench"><a href="#IconBench" class="headerlink" title="IconBench"></a><a href="http://iconbench.com/">IconBench</a></h3><p><img src="http://iconbench.com/Content/Images/iconbench.png"></p><p>上面提到的iconizer虽然有海量图标,但是每种图标风格迥异,不能成套怎么办,iconizer就可以解决这个问题。它的选择过程是反的,先选颜色尺寸等属性,再选样式,然后打包下载,还有css sprite图片和css代码。其属性也很丰富,包括内外阴影,渐变颜色,描边,未来还会添加特殊效果。而样式也是非常丰富,基本涵盖了所有用途,一共5套免费的,1套收费的,每套都有上百种样式。</p><h3 id="ProjectFondue"><a href="#ProjectFondue" class="headerlink" title="ProjectFondue"></a><a href="http://blog.projectfondue.com/">ProjectFondue</a></h3><p><a href="http://spritegen.website-performance.org/">CSS Sprite Generator</a></p><p>Iconbench很贴心的把图标制作成css精灵图片是不是很好?可是如果有自己的图片需要制作时,这个CSS Sprite Generator就帮得上忙了。最强大也最无聊的是这玩意支持十几种语言。将要制作CSS精灵的图片打包成zip,然后上传,最大1Mb。你还可以选择移除相同的图片,当然还是尺寸、比例、每个图片的尺寸颜色质量等等。另外css代码也有一些配置信息,比如前缀之类的。然后点生成就可以了,非常方便。</p><p><a href="http://favicon-generator.org/">Favicon Generator</a></p><p>这个工具的作用只有一个,每个网站都需要一个favorite.ico的文件,你不知道?就是打开网站后浏览器标签上面的小图标还有书签里的图标。因为必须是ico格式的,所以比较特殊,photoshop也歇菜了,用处虽然不大,但是非常必须。首先上传一个gif、png或者jpg格式的图片,然后最强大的地方是可以在线编辑,最后提交即可下载ico文件。</p><p>另外projectfondue还很奇葩的提供了一个unix权限计算工具,和另外2个工具好像没什么联系,大概他们的团队也只是顺便开发了这些工具(偷笑)。</p><h3 id="Base64-Image"><a href="#Base64-Image" class="headerlink" title="Base64-Image"></a><a href="http://www.base64-image.de/">Base64-Image</a></h3><p><img src="http://www.base64-image.de/img/layout/logo.png"></p><p>Base64是一种对称加密方式,就是说可以加也可以解,而标准浏览器已经实现了对base64图片的读取,也就是说一段字符可以代替一个图片了。这个工具自然就是把图片转换成base64码的,上传图片获取代码,不用说很简单。</p><h3 id="Fontello"><a href="#Fontello" class="headerlink" title="Fontello"></a><a href="http://fontello.com/">Fontello</a></h3><p>是不是觉得base64很高端了,别急,还有更geek的玩意。像github一样的icon,就是用字体做图标。这个fontello拥有海量的字体图标,选择一些图标,然后选择尺寸,最大只有30px。然后下载,这个压缩包里包含了各种格式的字体(woff、tff、svg、eot),还有css代码,包括兼容性问题,还有可能包括动画。甚至还有一个demo的html文件。</p><h3 id="Smush"><a href="#Smush" class="headerlink" title="Smush"></a><a href="http://www.smushit.com/">Smush</a></h3><p>这些工具应该已经足够用了,不过如果你还想精益求精的话,Smush可以帮你将图片文件优化,在保证质量的情况下降低文件大小。使用很简单,上传或者输入url,下载图片压缩包。它还会显示容量优化的比率。这是yahoo的一个工具。</p><h3 id="PhotoRaster"><a href="#PhotoRaster" class="headerlink" title="PhotoRaster"></a><a href="http://photoraster.com/">PhotoRaster</a></h3><p>这是一个在线的photoshop,如果以上这些工具都满足不了你,那这个工具一定能让你满意。不过说实在的,除了临时没有装photoshop需要用这个,一般谁会用呢,无论功能和性能,肯定都是不如photoshop的。</p>]]></content>
<summary type="html"><p>上周介绍了几种配色工具,这次要介绍的是一些图片和图标的生成工具。</p>
<p>就算你熟练掌握Photoshop之类的图片处理工具,要制作一个小小的图标还是挺费时费力的。可是网上下载的话,颜色尺寸又不一定能适合,所以制作图片的工具将是非常必要的。</p>
<h3 id="Online-Generator"><a href="#Online-Generator" class="headerlink" title="Online Generator"></a><a href="http://onlinegenerator.net/">Online Generator</a></h3><p><a href="http://preloaders.net/"><img src="http://onlinegenerator.net/images/preloaders.png"></a></p>
<p>Online Generator包括好几个很棒的工具,首先是Preloaders,它用于制作loading图片,它的图片种类很齐全,还有大量的3D图片,最重要的是它可以任意改变尺寸,当然也包括颜色,动画。动画可以选择方向频率等等,功能十分强大,基本上只要你能找到喜欢的图案,它就一定能制作出你想要的loading图片。</p></summary>
<category term="Tech" scheme="https://blog.tychio.net/categories/Tech/"/>
<category term="tool" scheme="https://blog.tychio.net/tags/tool/"/>
<category term="recommend" scheme="https://blog.tychio.net/tags/recommend/"/>
<category term="image" scheme="https://blog.tychio.net/tags/image/"/>
<category term="图片" scheme="https://blog.tychio.net/tags/%E5%9B%BE%E7%89%87/"/>
<category term="工具" scheme="https://blog.tychio.net/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>《编程珠玑》Programming Pearls</title>
<link href="https://blog.tychio.net/2013/05/25/Programming-Pearls/"/>
<id>https://blog.tychio.net/2013/05/25/Programming-Pearls/</id>
<published>2013-05-25T16:00:00.000Z</published>
<updated>2023-05-04T06:26:55.717Z</updated>
<content type="html"><![CDATA[<p><img src="/images/programmingpearls.jpg" alt="Programming Pearls">《编程珠玑》这本书读完感觉很诡异,在读的过程中感觉很有收获,但是却说不清从书中获得了什么,好像什么也没读懂。说实话,很难对这本书归类,有时它告诉你的是实际操作的性能问题,有时又在讲算法或者数据结构。不过确切的说,它告诉我们的是一种无招胜有招的境界。无论是实际操作、算法还是数据结构都是在为项目服务的,我们的目的只有一个,那就是完成项目。</p><p>一个项目与一个科研课题的区别就是它需要被实践,需要一种行之有效的解决方案。在一个系统被部署到实际环境中时,有时可能需要它无比精准,有时是快速运行,有时是超低成本,或者也有可能是兼而有之的权衡。此时工程师就需要调整一切可以调整的东西去满足这些需求,这些东西自然就是硬件环境、算法、数据结构了。作者Jon Bentley要告诉读者的就是这些调整所需要的手段。</p><span id="more"></span><p>第一部分讲的是基本操作,我的理解这部分讲的是“手段”,从算法、数据结构、程序设计到测试这样的细节,介绍了许多能影响实际部署结果的手法。而第二部分则是“效果”,是时间与空间的权衡,利用分析、估算确定性能,在算法设计和代码调优中节省成本开销。最后一部分则是介绍了一些实际的应用,通篇讲述的二分搜索,当然还有排序等,数据结构和基本类型也是一个重点。</p><p>其实这本书买了很久了,但是一直没怎么好好看过,主要是因为总觉得是在讲后端技术。但仔细阅读之后才感觉到了技术的相通,不在乎什么语言,什么时代。也许我一辈子也不会有机会去面对一个超大的系统部署的解决方面这样的问题,但只要还在写程序做项目,这些类似的问题就不会离去。</p><p>在前端开发中,IE就像一个性能奇差的老爷车般的大型机,要考虑它的性能与内存占用,有时需要函数节流,有时需要隐藏一些东西,还需要考虑图片的加载与缓存。而Webkit又几乎成为了移动互联网浏览器的代名词,需要考虑不同的交互,不同的操作习惯与视觉习惯体验,另外众多标准浏览器支持HTML5后,可以使用的手段越来越多。更重要的是,这些浏览器的差异,程序必须同时兼容,这对算法和数据结构的选择,时间与空间的权衡都是强大的考验。</p><p>比如说在以前的一个项目中,我面对的是4个没有规范的后端工程师提供的Ajax接口,然而时间不允许我逐一进行调用,如果是你会怎么做?很快我想到了一个解决方案,将接口名放入数组,然后通过数组下标将混乱的接口名变成了有序的数字。但是后来发现了一个Bug,这个问题有点严重了,由于在javascript中数组是引用类型,有点像指针的意思,所以当我把所谓的接口名传入Ajax封装方法中后,这个方法改变了引用的变量,然后这个接口名的数组的值也相应的改变了,很明显,结果是调用的接口失去了控制。随后我下意识的将这个数组每次获取的时候都复制了一遍,然后在IE6中,性能出现了问题,浏览器渲染就像结巴了一下,一跳一跳的,在chrome中测试发现效率比原来低了几倍。这时我无意识的使用了一个不错的手段,调整了数据类型和结构,采用字符串放置这些名称,然后使用下划线将不足的空位不足使所有接口名的长度一致,在使用的时候直接获取对应的区段,并利用正则去掉下划线,一切回复了正常。</p>]]></content>
<summary type="html"><p><img src="/images/programmingpearls.jpg" alt="Programming Pearls">&#12298;编程珠玑&#12299;这本书读完感觉很诡异,在读的过程中感觉很有收获,但是却说不清从书中获得了什么,好像什么也没读懂。说实话,很难对这本书归类,有时它告诉你的是实际操作的性能问题,有时又在讲算法或者数据结构。不过确切的说,它告诉我们的是一种无招胜有招的境界。无论是实际操作、算法还是数据结构都是在为项目服务的,我们的目的只有一个,那就是完成项目。</p>
<p>一个项目与一个科研课题的区别就是它需要被实践,需要一种行之有效的解决方案。在一个系统被部署到实际环境中时,有时可能需要它无比精准,有时是快速运行,有时是超低成本,或者也有可能是兼而有之的权衡。此时工程师就需要调整一切可以调整的东西去满足这些需求,这些东西自然就是硬件环境、算法、数据结构了。作者Jon Bentley要告诉读者的就是这些调整所需要的手段。</p></summary>
<category term="Read" scheme="https://blog.tychio.net/categories/Read/"/>
<category term="software" scheme="https://blog.tychio.net/tags/software/"/>
<category term="SE" scheme="https://blog.tychio.net/tags/SE/"/>
<category term="软件工程" scheme="https://blog.tychio.net/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
<category term="program" scheme="https://blog.tychio.net/tags/program/"/>
<category term="code" scheme="https://blog.tychio.net/tags/code/"/>
<category term="编程珠玑" scheme="https://blog.tychio.net/tags/%E7%BC%96%E7%A8%8B%E7%8F%A0%E7%8E%91/"/>
</entry>
</feed>