forked from nqdeng/7-days-nodejs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
2090 lines (1908 loc) · 120 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype HTML>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<title>七天学会NodeJS</title>
<style>
body {
color: #333;
font-family: sans-serif;
font-size: 12pt;
line-height: 170%;
padding: 0 30px 0 270px;
}
header .banner {
margin: 0 0 1em 0;
}
header .banner, nav .banner {
color: #777;
font-size: 10pt;
font-weight: bold;
}
header h1 {
background: #0c3;
border-radius: 4px;
color: #fff;
font-size: 24pt;
margin: 0;
padding: 1.2em 0;
text-align: center;
}
nav {
font-size: 10pt;
overflow-x: hidden;
overflow-y: auto;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 240px;
}
nav .menubar {
border-bottom: solid 1px #ccc;
display: none;
height: 48px;
line-height: 48px;
padding: 0 10px;
}
nav .button {
background: #777;
border: 1px solid #333;
color: #fff;
font-size: 10pt;
font-weight: bold;
padding: 8px;
border-radius: 4px;
}
nav ul {
padding: 0 0 0 10px;
}
nav ul a {
color: #333;
text-decoration: none;
}
nav ul a:hover {
text-decoration: underline;
}
nav li {
line-height: 180%;
list-style: none;
margin: 0;
padding: 0;
}
nav .level2 {
font-size: 11pt;
font-weight: bold;
}
nav .level3 {
padding-left: 1em;
}
nav .level3:before {
content: "» ";
}
nav .level4 {
padding-left: 2em;
}
nav .level4:before {
content: "› ";
}
article h2 {
border-bottom: dotted 1px #777;
font-size: 12pt;
line-height: 100%;
margin: 4em 0 1em 0;
padding: 0 0 0.3em 0;
}
article h3 {
font-size: 12pt;
line-height: 100%;
margin: 2em 0 1em 0;
padding: 0;
}
article h4 {
font-size: 12pt;
font-style:italic;
font-weight: normal;
line-height: 100%;
margin: 1.2em 0 1em 0;
padding: 0;
}
article p {
margin: 1em 0;
}
article p code {
background: #eee;
border: 1px solid #ccc;
}
article p strong {
color: #f00;
}
article pre {
background: #eee;
border-left: solid 2px #3c0;
font-size: 10pt;
margin: 1em 0;
padding: 0 0 0 1em;
overflow-x: auto;
overflow-y: padding;
}
article blockquote {
background: #fff;
border: dashed 1px #777;
border-left: solid 2px #777;
color: #000;
margin: 0;
padding: 0 0 0 1em;
}
article ul, article ol {
padding-left: 2em;
}
footer {
border-top: 1px solid #ccc;
font-size: 10pt;
margin-top: 4em;
}
@media (max-width: 768px) {
body {
padding: 0 10px 0 230px;
}
nav {
width: 230px;
}
}
@media (max-width: 480px) {
body {
padding: 64px 10px 0 10px;
}
header .banner {
display: none;
}
nav {
position: absolute;
width: 100%;
}
nav .menubar {
display: block;
}
nav .banner {
float: right;
}
nav ul {
background: #fff;
display: none;
font-size: 14pt;
margin: 0;
padding: 0 0 0 8px;
}
nav .level2 {
font-size: 16pt;
font-weight: bold;
}
nav li {
line-height: 240%;
}
.index nav ul {
display: block;
}
.index article {
display: none;
}
}
</style>
<script>
(function (tags) {
var i = 0, len = tags.length;
for (; i < len; ++i) {
document.createElement(tags[i]);
}
}([ 'header', 'nav', 'article', 'footer' ]));
</script>
</head>
<body>
<header>
<h1 id="-nodejs">七天学会NodeJS</h1>
</header>
<nav>
<div class="menubar">
<a class="button">☰ 索引</a>
</div>
<ul>
<li class="level2"><a href="#1">NodeJS基础</a></li><li class="level3"><a href="#1.1">什么是NodeJS</a></li><li class="level3"><a href="#1.2">有啥用处</a></li><li class="level3"><a href="#1.3">如何安装</a></li><li class="level4"><a href="#1.3.1">安装程序</a></li><li class="level4"><a href="#1.3.2">编译安装</a></li><li class="level3"><a href="#1.4">如何运行</a></li><li class="level4"><a href="#1.4.1">权限问题</a></li><li class="level3"><a href="#1.5">模块</a></li><li class="level4"><a href="#1.5.1">require</a></li><li class="level4"><a href="#1.5.2">exports</a></li><li class="level4"><a href="#1.5.3">module</a></li><li class="level4"><a href="#1.5.4">模块初始化</a></li><li class="level4"><a href="#1.5.5">主模块</a></li><li class="level4"><a href="#1.5.6">完整示例</a></li><li class="level3"><a href="#1.6">二进制模块</a></li><li class="level3"><a href="#1.7">小结</a></li><li class="level2"><a href="#2">代码的组织和部署</a></li><li class="level3"><a href="#2.1">模块路径解析规则</a></li><li class="level3"><a href="#2.2">包(package)</a></li><li class="level4"><a href="#2.2.1">index.js</a></li><li class="level4"><a href="#2.2.2">package.json</a></li><li class="level3"><a href="#2.3">命令行程序</a></li><li class="level4"><a href="#2.3.1">Linux</a></li><li class="level4"><a href="#2.3.2">Windows</a></li><li class="level3"><a href="#2.4">工程目录</a></li><li class="level3"><a href="#2.5">NPM</a></li><li class="level4"><a href="#2.5.1">下载三方包</a></li><li class="level4"><a href="#2.5.2">安装命令行程序</a></li><li class="level4"><a href="#2.5.3">发布代码</a></li><li class="level4"><a href="#2.5.4">版本号</a></li><li class="level4"><a href="#2.5.5">灵机一点</a></li><li class="level3"><a href="#2.6">小结</a></li><li class="level2"><a href="#3">文件操作</a></li><li class="level3"><a href="#3.1">开门红</a></li><li class="level4"><a href="#3.1.1">小文件拷贝</a></li><li class="level4"><a href="#3.1.2">大文件拷贝</a></li><li class="level3"><a href="#3.2">API走马观花</a></li><li class="level4"><a href="#3.2.1">Buffer(数据块)</a></li><li class="level4"><a href="#3.2.2">Stream(数据流)</a></li><li class="level4"><a href="#3.2.3">File System(文件系统)</a></li><li class="level4"><a href="#3.2.4">Path(路径)</a></li><li class="level3"><a href="#3.3">遍历目录</a></li><li class="level4"><a href="#3.3.1">递归算法</a></li><li class="level4"><a href="#3.3.2">遍历算法</a></li><li class="level4"><a href="#3.3.3">同步遍历</a></li><li class="level4"><a href="#3.3.4">异步遍历</a></li><li class="level3"><a href="#3.4">文本编码</a></li><li class="level4"><a href="#3.4.1">BOM的移除</a></li><li class="level4"><a href="#3.4.2">GBK转UTF8</a></li><li class="level4"><a href="#3.4.3">单字节编码</a></li><li class="level3"><a href="#3.5">小结</a></li><li class="level2"><a href="#4">网络操作</a></li><li class="level3"><a href="#4.1">开门红</a></li><li class="level3"><a href="#4.2">API走马观花</a></li><li class="level4"><a href="#4.2.1">HTTP</a></li><li class="level4"><a href="#4.2.2">HTTPS</a></li><li class="level4"><a href="#4.2.3">URL</a></li><li class="level4"><a href="#4.2.4">Query String</a></li><li class="level4"><a href="#4.2.5">Zlib</a></li><li class="level4"><a href="#4.2.6">Net</a></li><li class="level3"><a href="#4.3">灵机一点</a></li><li class="level3"><a href="#4.4">小结</a></li><li class="level2"><a href="#5">进程管理</a></li><li class="level3"><a href="#5.1">开门红</a></li><li class="level3"><a href="#5.2">API走马观花</a></li><li class="level4"><a href="#5.2.1">Process</a></li><li class="level4"><a href="#5.2.2">Child Process</a></li><li class="level4"><a href="#5.2.3">Cluster</a></li><li class="level3"><a href="#5.3">应用场景</a></li><li class="level4"><a href="#5.3.1">如何获取命令行参数</a></li><li class="level4"><a href="#5.3.2">如何退出程序</a></li><li class="level4"><a href="#5.3.3">如何控制输入输出</a></li><li class="level4"><a href="#5.3.4">如何降权</a></li><li class="level4"><a href="#5.3.5">如何创建子进程</a></li><li class="level4"><a href="#5.3.6">进程间如何通讯</a></li><li class="level4"><a href="#5.3.7">如何守护子进程</a></li><li class="level3"><a href="#5.4">小结</a></li><li class="level2"><a href="#6">异步编程</a></li><li class="level3"><a href="#6.1">回调</a></li><li class="level3"><a href="#6.2">代码设计模式</a></li><li class="level4"><a href="#6.2.1">函数返回值</a></li><li class="level4"><a href="#6.2.2">遍历数组</a></li><li class="level4"><a href="#6.2.3">异常处理</a></li><li class="level3"><a href="#6.3">域(Domain)</a></li><li class="level4"><a href="#6.3.1">陷阱</a></li><li class="level3"><a href="#6.4">小结</a></li><li class="level2"><a href="#7">大示例</a></li><li class="level3"><a href="#7.1">需求</a></li><li class="level3"><a href="#7.2">第一次迭代</a></li><li class="level4"><a href="#7.2.1">设计</a></li><li class="level4"><a href="#7.2.2">实现</a></li><li class="level3"><a href="#7.3">第二次迭代</a></li><li class="level4"><a href="#7.3.1">设计</a></li><li class="level4"><a href="#7.3.2">实现</a></li><li class="level3"><a href="#7.4">第三次迭代</a></li><li class="level4"><a href="#7.4.1">设计</a></li><li class="level4"><a href="#7.4.2">实现</a></li><li class="level3"><a href="#7.5">第四次迭代</a></li><li class="level4"><a href="#7.5.1">设计</a></li><li class="level4"><a href="#7.5.2">实现</a></li><li class="level3"><a href="#7.6">后续迭代</a></li><li class="level3"><a href="#7.7">小结</a></li></ul>
</nav>
<article>
<h2 id="1">NodeJS基础</h2>
<h3 id="1.1">什么是NodeJS</h3>
<p>JS是脚本语言,脚本语言都需要一个解析器才能运行。对于写在HTML页面里的JS,浏览器充当了解析器的角色。而对于需要独立运行的JS,NodeJS就是一个解析器。</p>
<p>每一种解析器都是一个运行环境,不但允许JS定义各种数据结构,进行各种计算,还允许JS使用运行环境提供的内置对象和方法做一些事情。例如运行在浏览器中的JS的用途是操作DOM,浏览器就提供了<code>document</code>之类的内置对象。而运行在NodeJS中的JS的用途是操作磁盘文件或搭建HTTP服务器,NodeJS就相应提供了<code>fs</code>、<code>http</code>等内置对象。</p>
<h3 id="1.2">有啥用处</h3>
<p>尽管存在一听说可以直接运行JS文件就觉得很酷的同学,但大多数同学在接触新东西时首先关心的是有啥用处,以及能带来啥价值。</p>
<p>NodeJS的作者说,他创造NodeJS的目的是为了实现高性能Web服务器,他首先看重的是事件机制和异步IO模型的优越性,而不是JS。但是他需要选择一种编程语言实现他的想法,这种编程语言不能自带IO功能,并且需要能良好支持事件机制。JS没有自带IO功能,天生就用于处理浏览器中的DOM事件,并且拥有一大群程序员,因此就成为了天然的选择。</p>
<p>如他所愿,NodeJS在服务端活跃起来,出现了大批基于NodeJS的Web服务。而另一方面,NodeJS让前端众如获神器,终于可以让自己的能力覆盖范围跳出浏览器窗口,更大批的前端工具如雨后春笋。</p>
<p>因此,对于前端而言,虽然不是人人都要拿NodeJS写一个服务器程序,但简单可至使用命令交互模式调试JS代码片段,复杂可至编写工具提升工作效率。</p>
<p>NodeJS生态圈正欣欣向荣。</p>
<h3 id="1.3">如何安装</h3>
<h4 id="1.3.1">安装程序</h4>
<p>NodeJS提供了一些安装程序,都可以在<a href="http://nodejs.org/download/">nodejs.org</a>这里下载并安装。</p>
<p>Windows系统下,选择和系统版本匹配的<code>.msi</code>后缀的安装文件。Mac OS X系统下,选择<code>.pkg</code>后缀的安装文件。</p>
<h4 id="1.3.2">编译安装</h4>
<p>Linux系统下没有现成的安装程序可用,虽然一些发行版可以使用<code>apt-get</code>之类的方式安装,但不一定能安装到最新版。因此Linux系统下一般使用以下方式编译方式安装NodeJS。</p>
<ol>
<li><p>确保系统下g++版本在4.6以上,python版本在2.6以上。</p>
</li>
<li><p>从<a href="http://nodejs.org/download/">nodejs.org</a>下载<code>tar.gz</code>后缀的NodeJS最新版源代码包并解压到某个位置。</p>
</li>
<li><p>进入解压到的目录,使用以下命令编译和安装。</p>
<pre><code> $ ./configure
$ make
$ sudo make install</code></pre>
</li>
</ol>
<h3 id="1.4">如何运行</h3>
<p>打开终端,键入<code>node</code>进入命令交互模式,可以输入一条代码语句后立即执行并显示结果,例如:</p>
<pre><code>$ node
> console.log('Hello World!');
Hello World!</code></pre>
<p>如果要运行一大段代码的话,可以先写一个JS文件再运行。例如有以下<code>hello.js</code>。</p>
<pre><code>function hello() {
console.log('Hello World!');
}
hello();</code></pre>
<p>写好后在终端下键入<code>node hello.js</code>运行,结果如下:</p>
<pre><code>$ node hello.js
Hello World!</code></pre>
<h4 id="1.4.1">权限问题</h4>
<p>在Linux系统下,使用NodeJS监听80或443端口提供HTTP(S)服务时需要root权限,有两种方式可以做到。</p>
<p>一种方式是使用<code>sudo</code>命令运行NodeJS。例如通过以下命令运行的<code>server.js</code>中有权限使用80和443端口。一般推荐这种方式,可以保证仅为有需要的JS脚本提供root权限。</p>
<pre><code>$ sudo node server.js</code></pre>
<p>另一种方式是使用<code>chmod +s</code>命令让NodeJS总是以root权限运行,具体做法如下。因为这种方式让任何JS脚本都有了root权限,不太安全,因此在需要很考虑安全的系统下不推荐使用。</p>
<pre><code>$ sudo chown root /usr/local/bin/node
$ sudo chmod +s /usr/local/bin/node</code></pre>
<h3 id="1.5">模块</h3>
<p>编写稍大一点的程序时一般都会将代码模块化。在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文件就是一个模块,而文件路径就是模块名。</p>
<p>在编写每个模块时,都有<code>require</code>、<code>exports</code>、<code>module</code>三个预先定义好的变量可供使用。</p>
<h4 id="1.5.1">require</h4>
<p><code>require</code>函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象。模块名可使用相对路径(以<code>./</code>开头),或者是绝对路径(以<code>/</code>或<code>C:</code>之类的盘符开头)。另外,模块名中的<code>.js</code>扩展名可以省略。以下是一个例子。</p>
<pre><code>var foo1 = require('./foo');
var foo2 = require('./foo.js');
var foo3 = require('/home/user/foo');
var foo4 = require('/home/user/foo.js');
// foo1至foo4中保存的是同一个模块的导出对象。</code></pre>
<p>另外,可以使用以下方式加载和使用一个JSON文件。</p>
<pre><code>var data = require('./data.json');</code></pre>
<h4 id="1.5.2">exports</h4>
<p><code>exports</code>对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过<code>require</code>函数使用当前模块时得到的就是当前模块的<code>exports</code>对象。以下例子中导出了一个公有方法。</p>
<pre><code>exports.hello = function () {
console.log('Hello World!');
};</code></pre>
<h4 id="1.5.3">module</h4>
<p>通过<code>module</code>对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式。</p>
<pre><code>module.exports = function () {
console.log('Hello World!');
};</code></pre>
<p>以上代码中,模块默认导出对象被替换为一个函数。</p>
<h4 id="1.5.4">模块初始化</h4>
<p>一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。</p>
<h4 id="1.5.5">主模块</h4>
<p>通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序的其它模块完成工作。例如通过以下命令启动程序时,<code>main.js</code>就是主模块。</p>
<pre><code>$ node main.js</code></pre>
<h4 id="1.5.6">完整示例</h4>
<p>例如有以下目录。</p>
<pre><code>- /home/user/hello/
- util/
counter.js
main.js</code></pre>
<p>其中<code>counter.js</code>内容如下:</p>
<pre><code>var i = 0;
function count() {
return ++i;
}
exports.count = count;</code></pre>
<p>该模块内部定义了一个私有变量<code>i</code>,并在<code>exports</code>对象导出了一个公有方法<code>count</code>。</p>
<p>主模块<code>main.js</code>内容如下:</p>
<pre><code>var counter1 = require('./util/counter');
var counter2 = require('./util/counter');
console.log(counter1.count());
console.log(counter2.count());
console.log(counter2.count());</code></pre>
<p>运行该程序的结果如下:</p>
<pre><code>$ node main.js
1
2
3</code></pre>
<p>可以看到,<code>counter.js</code>并没有因为被require了两次而初始化两次。</p>
<h3 id="1.6">二进制模块</h3>
<p>虽然一般我们使用JS编写模块,但NodeJS也支持使用C/C++编写二进制模块。编译好的二进制模块除了文件扩展名是<code>.node</code>外,和JS模块的使用方式相同。虽然二进制模块能使用操作系统提供的所有功能,拥有无限的潜能,但对于前端同学而言编写过于困难,并且难以跨平台使用,因此不在本教程的覆盖范围内。</p>
<h3 id="1.7">小结</h3>
<p>本章介绍了有关NodeJS的基本概念和使用方法,总结起来有以下知识点:</p>
<ul>
<li><p>NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用<code>node</code>命令。</p>
</li>
<li><p>终端下直接输入<code>node</code>命令可进入命令交互模式,很适合用来测试一些JS代码片段,比如正则表达式。</p>
</li>
<li><p>NodeJS使用<a href="http://wiki.commonjs.org/">CMD</a>模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次。</p>
</li>
<li><p>除非JS模块不能满足需求,否则不要轻易使用二进制模块,否则你的用户会叫苦连天。</p>
</li>
</ul>
<h2 id="2">代码的组织和部署</h2>
<p>有经验的C程序员在编写一个新程序时首先从make文件写起。同样的,使用NodeJS编写程序前,为了有个良好的开端,首先需要准备好代码的目录结构和部署方式,就如同修房子要先搭脚手架。本章将介绍与之相关的各种知识。</p>
<h3 id="2.1">模块路径解析规则</h3>
<p>我们已经知道,<code>require</code>函数支持斜杠(<code>/</code>)或盘符(<code>C:</code>)开头的绝对路径,也支持<code>./</code>开头的相对路径。但这两种路径在模块之间建立了强耦合关系,一旦某个模块文件的存放位置需要变更,使用该模块的其它模块的代码也需要跟着调整,变得牵一发动全身。因此,<code>require</code>函数支持第三种形式的路径,写法类似于<code>foo/bar</code>,并依次按照以下规则解析路径,直到找到模块位置。</p>
<ol>
<li><p>内置模块</p>
<p> 如果传递给<code>require</code>函数的是NodeJS内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如<code>require('fs')</code>。</p>
</li>
<li><p>node_modules目录</p>
<p> NodeJS定义了一个特殊的<code>node_modules</code>目录用于存放模块。例如某个模块的绝对路径是<code>/home/user/hello.js</code>,在该模块中使用<code>require('foo/bar')</code>方式加载模块时,则NodeJS依次尝试使用以下路径。</p>
<pre><code> /home/user/node_modules/foo/bar
/home/node_modules/foo/bar
/node_modules/foo/bar</code></pre>
</li>
<li><p>NODE_PATH环境变量</p>
<p> 与PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来指定额外的模块搜索路径。NODE_PATH环境变量中包含一到多个目录路径,路径之间在Linux下使用<code>:</code>分隔,在Windows下使用<code>;</code>分隔。例如定义了以下NODE_PATH环境变量:</p>
<pre><code> NODE_PATH=/home/user/lib:/home/lib</code></pre>
<p> 当使用<code>require('foo/bar')</code>的方式加载模块时,则NodeJS依次尝试以下路径。</p>
<pre><code> /home/user/lib/foo/bar
/home/lib/foo/bar</code></pre>
</li>
</ol>
<h3 id="2.2">包(package)</h3>
<p>我们已经知道了JS模块的基本单位是单个JS文件,但复杂些的模块往往由多个子模块组成。为了便于管理和使用,我们可以把由多个子模块组成的大模块称做<code>包</code>,并把所有子模块放在同一个目录里。</p>
<p>在组成一个包的所有子模块中,需要有一个入口模块,入口模块的导出对象被作为包的导出对象。例如有以下目录结构。</p>
<pre><code>- /home/user/lib/
- cat/
head.js
body.js
main.js</code></pre>
<p>其中<code>cat</code>目录定义了一个包,其中包含了3个子模块。<code>main.js</code>作为入口模块,其内容如下:</p>
<pre><code>var head = require('./head');
var body = require('./body');
exports.create = function (name) {
return {
name: name,
head: head.create(),
body: body.create()
};
};</code></pre>
<p>在其它模块里使用包的时候,需要加载包的入口模块。接着上例,使用<code>require('/home/user/lib/cat/main')</code>能达到目的,但是入口模块名称出现在路径里看上去不是个好主意。因此我们需要做点额外的工作,让包使用起来更像是单个模块。</p>
<h4 id="2.2.1">index.js</h4>
<p>当模块的文件名是<code>index.js</code>,加载模块时可以使用模块所在目录的路径代替模块文件路径,因此接着上例,以下两条语句等价。</p>
<pre><code>var cat = require('/home/user/lib/cat');
var cat = require('/home/user/lib/cat/index');</code></pre>
<p>这样处理后,就只需要把包目录路径传递给<code>require</code>函数,感觉上整个目录被当作单个模块使用,更有整体感。</p>
<h4 id="2.2.2">package.json</h4>
<p>如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个<code>package.json</code>文件,并在其中指定入口模块的路径。上例中的<code>cat</code>模块可以重构如下。</p>
<pre><code>- /home/user/lib/
- cat/
+ doc/
- lib/
head.js
body.js
main.js
+ tests/
package.json</code></pre>
<p>其中<code>package.json</code>内容如下。</p>
<pre><code>{
"name": "cat",
"main": "./lib/main.js"
}</code></pre>
<p>如此一来,就同样可以使用<code>require('/home/user/lib/cat')</code>的方式加载模块。NodeJS会根据包目录下的<code>package.json</code>找到入口模块所在位置。</p>
<h3 id="2.3">命令行程序</h3>
<p>使用NodeJS编写的东西,要么是一个包,要么是一个命令行程序,而前者最终也会用于开发后者。因此我们在部署代码时需要一些技巧,让用户觉得自己是在使用一个命令行程序。</p>
<p>例如我们用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内实现了所有功能。并且写好后,我们把该程序部署在<code>/home/user/bin/node-echo.js</code>这个位置。为了在任何目录下都能运行该程序,我们需要使用以下终端命令。</p>
<pre><code>$ node /home/user/bin/node-echo.js Hello World
Hello World</code></pre>
<p>这种使用方式看起来不怎么像是一个命令行程序,下边的才是我们期望的方式。</p>
<pre><code>$ node-echo Hello World</code></pre>
<h4 id="2.3.1">Linux</h4>
<p>在Linux系统下,我们可以把JS文件当作shell脚本来运行,从而达到上述目的,具体步骤如下:</p>
<ol>
<li><p>在shell脚本中,可以通过<code>#!</code>注释来指定当前脚本使用的解析器。所以我们首先在<code>node-echo.js</code>文件顶部增加以下一行注释,表明当前脚本使用NodeJS解析。</p>
<pre><code> #! /usr/bin/env node</code></pre>
<p> NodeJS会忽略掉位于JS模块首行的<code>#!</code>注释,不必担心这行注释是非法语句。</p>
</li>
<li><p>然后,我们使用以下命令赋予<code>node-echo.js</code>文件执行权限。</p>
<pre><code> $ chmod +x /home/user/bin/node-echo.js</code></pre>
</li>
<li><p>最后,我们在PATH环境变量中指定的某个目录下,例如在<code>/usr/local/bin</code>下边创建一个软链文件,文件名与我们希望使用的终端命令同名,命令如下:</p>
<pre><code> $ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo</code></pre>
</li>
</ol>
<p>这样处理后,我们就可以在任何目录下使用<code>node-echo</code>命令了。</p>
<h4 id="2.3.2">Windows</h4>
<p>在Windows系统下的做法完全不同,我们得靠<code>.cmd</code>文件来解决问题。假设<code>node-echo.js</code>存放在<code>C:\Users\user\bin</code>目录,并且该目录已经添加到PATH环境变量里了。接下来需要在该目录下新建一个名为<code>node-echo.cmd</code>的文件,文件内容如下:</p>
<pre><code>@node "C:\User\user\bin\node-echo.js" %*</code></pre>
<p>这样处理后,我们就可以在任何目录下使用<code>node-echo</code>命令了。</p>
<h3 id="2.4">工程目录</h3>
<p>了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例,一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看起来像下边这样。</p>
<pre><code>- /home/user/workspace/node-echo/ # 工程目录
- bin/ # 存放命令行相关代码
node-echo
+ doc/ # 存放文档
- lib/ # 存放API相关代码
echo.js
- node_modules/ # 存放三方包
+ argv/
+ tests/ # 存放测试用例
package.json # 元数据文件
README.md # 说明文件</code></pre>
<p>其中部分文件内容如下:</p>
<pre><code>/* bin/node-echo */
var argv = require('argv'),
echo = require('../lib/echo');
console.log(echo(argv.join(' ')));
/* lib/echo.js */
module.exports = function (message) {
return message;
};
/* package.json */
{
"name": "node-echo",
"main": "./lib/echo.js"
}</code></pre>
<p>以上例子中分类存放了不同类型的文件,并通过<code>node_moudles</code>目录直接使用三方包名加载模块。此外,定义了<code>package.json</code>之后,<code>node-echo</code>目录也可被当作一个包来使用。</p>
<h3 id="2.5">NPM</h3>
<p>NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种:</p>
<ul>
<li><p>允许用户从NPM服务器下载别人编写的三方包到本地使用。</p>
</li>
<li><p>允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。</p>
</li>
<li><p>允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。</p>
</li>
</ul>
<p>可以看到,NPM建立了一个NodeJS生态圈,NodeJS开发者和用户可以在里边互通有无。以下分别介绍这三种场景下怎样使用NPM。</p>
<h4 id="2.5.1">下载三方包</h4>
<p>需要使用三方包时,首先得知道有哪些包可用。虽然<a href="https://npmjs.org/">npmjs.org</a>提供了个搜索框可以根据包名来搜索,但如果连想使用的三方包的名字都不确定的话,就请百度一下吧。知道了包名后,比如上边例子中的<code>argv</code>,就可以在工程目录下打开终端,使用以下命令来下载三方包。</p>
<pre><code>$ npm install argv
...
argv@0.0.2 node_modules\argv</code></pre>
<p>下载好之后,<code>argv</code>包就放在了工程目录下的<code>node_modules</code>目录中,因此在代码中只需要通过<code>require('argv')</code>的方式就好,无需指定三方包路径。</p>
<p>以上命令默认下载最新版三方包,如果想要下载指定版本的话,可以在包名后边加上<code>@<version></code>,例如通过以下命令可下载0.0.1版的<code>argv</code>。</p>
<pre><code>$ npm install argv@0.0.1
...
argv@0.0.1 node_modules\argv</code></pre>
<p>如果使用到的三方包比较多,在终端下一个包一条命令地安装未免太人肉了。因此NPM对<code>package.json</code>的字段做了扩展,允许在其中申明三方包依赖。因此,上边例子中的<code>package.json</code>可以改写如下:</p>
<pre><code>{
"name": "node-echo",
"main": "./lib/echo.js",
"dependencies": {
"argv": "0.0.2"
}
}</code></pre>
<p>这样处理后,在工程目录下就可以使用<code>npm install</code>命令批量安装三方包了。更重要的是,当以后<code>node-echo</code>也上传到了NPM服务器,别人下载这个包时,NPM会根据包中申明的三方包依赖自动下载进一步依赖的三方包。例如,使用<code>npm install node-echo</code>命令时,NPM会自动创建以下目录结构。</p>
<pre><code>- project/
- node_modules/
- node-echo/
- node_modules/
+ argv/
...
...</code></pre>
<p>如此一来,用户只需关心自己直接使用的三方包,不需要自己去解决所有包的依赖关系。</p>
<h4 id="2.5.2">安装命令行程序</h4>
<p>从NPM服务上下载安装一个命令行程序的方法与三方包类似。例如上例中的<code>node-echo</code>提供了命令行使用方式,只要<code>node-echo</code>自己配置好了相关的<code>package.json</code>字段,对于用户而言,只需要使用以下命令安装程序。</p>
<pre><code>$ npm install node-echo -g</code></pre>
<p>参数中的<code>-g</code>表示全局安装,因此<code>node-echo</code>会默认安装到以下位置,并且NPM会自动创建好Linux系统下需要的软链文件或Windows系统下需要的<code>.cmd</code>文件。</p>
<pre><code>- /usr/local/ # Linux系统下
- lib/node_modules/
+ node-echo/
...
- bin/
node-echo
...
...
- %APPDATA%\npm\ # Windows系统下
- node_modules\
+ node-echo\
...
node-echo.cmd
...</code></pre>
<h4 id="2.5.3">发布代码</h4>
<p>第一次使用NPM发布代码前需要注册一个账号。终端下运行<code>npm adduser</code>,之后按照提示做即可。账号搞定后,接着我们需要编辑<code>package.json</code>文件,加入NPM必需的字段。接着上边<code>node-echo</code>的例子,<code>package.json</code>里必要的字段如下。</p>
<pre><code>{
"name": "node-echo", # 包名,在NPM服务器上须要保持唯一
"version": "1.0.0", # 当前版本号
"dependencies": { # 三方包依赖,需要指定包名和版本号
"argv": "0.0.2"
},
"main": "./lib/echo.js", # 入口模块位置
"bin" : {
"node-echo": "./bin/node-echo" # 命令行程序名和主模块位置
}
}</code></pre>
<p>之后,我们就可以在<code>package.json</code>所在目录下运行<code>npm publish</code>发布代码了。</p>
<h4 id="2.5.4">版本号</h4>
<p>使用NPM下载和发布代码时都会接触到版本号。NPM使用语义版本号来管理代码,这里简单介绍一下。</p>
<p>语义版本号分为<code>X.Y.Z</code>三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。</p>
<pre><code>+ 如果只是修复bug,需要更新Z位。
+ 如果是新增了功能,但是向下兼容,需要更新Y位。
+ 如果有大变动,向下不兼容,需要更新X位。</code></pre>
<p>版本号有了这个保证后,在申明三方包依赖时,除了可依赖于一个固定版本号外,还可依赖于某个范围的版本号。例如<code>"argv": "0.0.x"</code>表示依赖于<code>0.0.x</code>系列的最新版<code>argv</code>。NPM支持的所有版本号范围指定方式可以查看<a href="https://npmjs.org/doc/files/package.json.html#dependencies">官方文档</a>。</p>
<h4 id="2.5.5">灵机一点</h4>
<p>除了本章介绍的部分外,NPM还提供了很多功能,<code>package.json</code>里也有很多其它有用的字段。除了可以在<a href="https://npmjs.org/doc/">npmjs.org/doc/</a>查看官方文档外,这里再介绍一些NPM常用命令。</p>
<ul>
<li><p>NPM提供了很多命令,例如<code>install</code>和<code>publish</code>,使用<code>npm help</code>可查看所有命令。</p>
</li>
<li><p>使用<code>npm help <command></code>可查看某条命令的详细帮助,例如<code>npm help install</code>。</p>
</li>
<li><p>在<code>package.json</code>所在目录下使用<code>npm install . -g</code>可先在本地安装当前命令行程序,可用于发布前的本地测试。</p>
</li>
<li><p>使用<code>npm update <package></code>可以把当前目录下<code>node_modules</code>子目录里边的对应模块更新至最新版本。</p>
</li>
<li><p>使用<code>npm update <package> -g</code>可以把全局安装的对应命令行程序更新至最新版。</p>
</li>
<li><p>使用<code>npm cache clear</code>可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。</p>
</li>
<li><p>使用<code>npm unpublish <package>@<version></code>可以撤销发布自己发布过的某个版本代码。</p>
</li>
</ul>
<h3 id="2.6">小结</h3>
<p>本章介绍了使用NodeJS编写代码前需要做的准备工作,总结起来有以下几点:</p>
<ul>
<li><p>编写代码前先规划好目录结构,才能做到有条不紊。</p>
</li>
<li><p>稍大些的程序可以将代码拆分为多个模块管理,更大些的程序可以使用包来组织模块。</p>
</li>
<li><p>合理使用<code>node_modules</code>和<code>NODE_PATH</code>来解耦包的使用方式和物理路径。</p>
</li>
<li><p>使用NPM加入NodeJS生态圈互通有无。</p>
</li>
<li><p>想到了心仪的包名时请提前在NPM上抢注。</p>
</li>
</ul>
<h2 id="3">文件操作</h2>
<p>让前端觉得如获神器的不是NodeJS能做网络编程,而是NodeJS能够操作文件。小至文件查找,大至代码编译,几乎没有一个前端工具不操作文件。换个角度讲,几乎也只需要一些数据处理逻辑,再加上一些文件操作,就能够编写出大多数前端工具。本章将介绍与之相关的NodeJS内置模块。</p>
<h3 id="3.1">开门红</h3>
<p>NodeJS提供了基本的文件操作API,但是像文件拷贝这种高级功能就没有提供,因此我们先拿文件拷贝程序练手。与<code>copy</code>命令类似,我们的程序需要能接受源文件路径与目标文件路径两个参数。</p>
<h4 id="3.1.1">小文件拷贝</h4>
<p>我们使用NodeJS内置的<code>fs</code>模块简单实现这个程序如下。</p>
<pre><code>var fs = require('fs');
function copy(src, dst) {
fs.writeFileSync(dst, fs.readFileSync(src));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));</code></pre>
<p>以上程序使用<code>fs.readFileSync</code>从源路径读取文件内容,并使用<code>fs.writeFileSync</code>将文件内容写入目标路径。</p>
<blockquote>
<p> <strong>豆知识:</strong> <code>process</code>是一个全局变量,可通过<code>process.argv</code>获得命令行参数。由于<code>argv[0]</code>固定等于NodeJS执行程序的绝对路径,<code>argv[1]</code>固定等于主模块的绝对路径,因此第一个命令行参数从<code>argv[2]</code>这个位置开始。</p>
</blockquote>
<h4 id="3.1.2">大文件拷贝</h4>
<p>上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完成拷贝。因此上边的程序需要改造如下。</p>
<pre><code>var fs = require('fs');
function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));</code></pre>
<p>以上程序使用<code>fs.createReadStream</code>创建了一个源文件的只读数据流,并使用<code>fs.createWriteStream</code>创建了一个目标文件的只写数据流,并且用<code>pipe</code>方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。</p>
<h3 id="3.2">API走马观花</h3>
<p>我们先大致看看NodeJS提供了哪些和文件操作有关的API。这里并不逐一介绍每个API的使用方法,官方文档已经做得很好了。</p>
<h4 id="3.2.1">Buffer(数据块)</h4>
<blockquote>
<p> <strong>官方文档: </strong> <a href="http://nodejs.org/api/buffer.html"><a href="http://nodejs.org/api/buffer.html">http://nodejs.org/api/buffer.html</a></a></p>
</blockquote>
<p>JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与<code>String</code>对等的全局构造函数<code>Buffer</code>来提供对二进制数据的操作。除了可以读取文件得到<code>Buffer</code>的实例外,还能够直接构造,例如:</p>
<pre><code>var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);</code></pre>
<p><code>Buffer</code>与字符串类似,除了可以用<code>.length</code>属性得到字节长度外,还可以用<code>[index]</code>方式读取指定位置的字节,例如:</p>
<pre><code>bin[0]; // => 0x68;</code></pre>
<p><code>Buffer</code>与字符串能够互相转化,例如可以使用指定编码将二进制数据转化为字符串:</p>
<pre><code>var str = bin.toString('utf-8'); // => "hello"</code></pre>
<p>或者反过来,将字符串转换为指定编码下的二进制数据:</p>
<pre><code>var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f></code></pre>
<p><code>Buffer</code>与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。至于<code>Buffer</code>,更像是可以做指针操作的C语言数组。例如,可以用<code>[index]</code>方式直接修改某个位置的字节。</p>
<pre><code>bin[0] = 0x48;</code></pre>
<p>而<code>.slice</code>方法也不是返回一个新的<code>Buffer</code>,而更像是返回了指向原<code>Buffer</code>中间的某个位置的指针,如下所示。</p>
<pre><code>[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]
^ ^
| |
bin bin.slice(2)</code></pre>
<p>因此对<code>.slice</code>方法返回的<code>Buffer</code>的修改会作用于原<code>Buffer</code>,例如:</p>
<pre><code>var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var sub = bin.slice(2);
sub[0] = 0x65;
console.log(bin); // => <Buffer 68 65 65 6c 6f></code></pre>
<p>也因此,如果想要拷贝一份<code>Buffer</code>,得首先创建一个新的<code>Buffer</code>,并通过<code>.copy</code>方法把原<code>Buffer</code>中的数据复制过去。这个类似于申请一块新的内存,并把已有内存中的数据复制过去。以下是一个例子。</p>
<pre><code>var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var dup = new Buffer(bin.length);
bin.copy(dup);
dup[0] = 0x48;
console.log(bin); // => <Buffer 68 65 6c 6c 6f>
console.log(dup); // => <Buffer 48 65 65 6c 6f></code></pre>
<p>总之,<code>Buffer</code>将JS的数据处理能力从字符串扩展到了任意二进制数据。</p>
<h4 id="3.2.2">Stream(数据流)</h4>
<blockquote>
<p> <strong>官方文档: </strong> <a href="http://nodejs.org/api/stream.html"><a href="http://nodejs.org/api/stream.html">http://nodejs.org/api/stream.html</a></a></p>
</blockquote>
<p>当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种<code>Stream</code>来提供对数据流的操作。</p>
<p>以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下:</p>
<pre><code>var rs = fs.createReadStream(pathname);
rs.on('data', function (chunk) {
doSomething(chunk);
});
rs.on('end', function () {
cleanUp();
});</code></pre>
<blockquote>
<p> <strong>豆知识:</strong> <code>Stream</code>基于事件机制工作,所有<code>Stream</code>的实例都继承于NodeJS提供的<a href="http://nodejs.org/api/events.html">EventEmitter</a>。</p>
</blockquote>
<p>上边的代码中<code>data</code>事件会源源不断地被触发,不管<code>doSomething</code>函数是否处理得过来。代码可以继续做如下改造,以解决这个问题。</p>
<pre><code>var rs = fs.createReadStream(src);
rs.on('data', function (chunk) {
rs.pause();
doSomething(chunk, function () {
rs.resume();
});
});
rs.on('end', function () {
cleanUp();
});</code></pre>
<p>以上代码给<code>doSomething</code>函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。</p>
<p>此外,我们也可以为数据目标创建一个只写数据流,示例如下:</p>
<pre><code>var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);
rs.on('data', function (chunk) {
ws.write(chunk);
});
rs.on('end', function () {
ws.end();
});</code></pre>
<p>我们把<code>doSomething</code>换成了往只写数据流里写入数据后,以上代码看起来就像是一个文件拷贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流内部的缓存会爆仓。我们可以根据<code>.write</code>方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据<code>drain</code>事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。因此代码可以改造如下:</p>
<pre><code>var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);
rs.on('data', function (chunk) {
if (ws.write(chunk) === false) {
rs.pause();
}
});
rs.on('end', function () {
ws.end();
});
ws.on('drain', function () {
rs.resume();
});</code></pre>
<p>以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场景很多,例如上边的大文件拷贝程序,NodeJS直接提供了<code>.pipe</code>方法来做这件事情,其内部实现方式与上边的代码类似。</p>
<h4 id="3.2.3">File System(文件系统)</h4>
<blockquote>
<p> <strong>官方文档: </strong> <a href="http://nodejs.org/api/fs.html"><a href="http://nodejs.org/api/fs.html">http://nodejs.org/api/fs.html</a></a></p>
</blockquote>
<p>NodeJS通过<code>fs</code>内置模块提供对文件的操作。<code>fs</code>模块提供的API基本上可以分为以下三类:</p>
<ul>
<li><p>文件属性读写。</p>
<p> 其中常用的有<code>fs.stat</code>、<code>fs.chmod</code>、<code>fs.chown</code>等等。</p>
</li>
<li><p>文件内容读写。</p>
<p> 其中常用的有<code>fs.readFile</code>、<code>fs.readdir</code>、<code>fs.writeFile</code>、<code>fs.mkdir</code>等等。</p>
</li>
<li><p>底层文件操作。</p>
<p> 其中常用的有<code>fs.open</code>、<code>fs.read</code>、<code>fs.write</code>、<code>fs.close</code>等等。</p>
</li>
</ul>
<p>NodeJS最精华的异步IO模型在<code>fs</code>模块里有着充分的体现,例如上边提到的这些API都通过回调函数传递结果。以<code>fs.readFile</code>为例:</p>
<pre><code>fs.readFile(pathname, function (err, data) {
if (err) {
// Deal with error.
} else {
// Deal with data.
}
});</code></pre>
<p>如上边代码所示,基本上所有<code>fs</code>模块API的回调参数都有两个。第一个参数在有错误发生时等于异常对象,第二个参数始终用于返回API方法执行结果。</p>
<p>此外,<code>fs</code>模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作更方便时的情况。同步API除了方法名的末尾多了一个<code>Sync</code>之外,异常对象与执行结果的传递方式也有相应变化。同样以<code>fs.readFileSync</code>为例:</p>
<pre><code>try {
var data = fs.readFileSync(pathname);
// Deal with data.
} catch (err) {
// Deal with error.
}</code></pre>
<p><code>fs</code>模块提供的API很多,这里不一一介绍,需要时请自行查阅官方文档。</p>
<h4 id="3.2.4">Path(路径)</h4>
<blockquote>
<p> <strong>官方文档: </strong> <a href="http://nodejs.org/api/path.html"><a href="http://nodejs.org/api/path.html">http://nodejs.org/api/path.html</a></a></p>
</blockquote>
<p>操作文件时难免不与文件路径打交道。NodeJS提供了<code>path</code>内置模块来简化路径相关操作,并提升代码可读性。以下分别介绍几个常用的API。</p>
<ul>
<li><p>path.normalize</p>
<p> 将传入的路径转换为标准路径,具体讲的话,除了解析路径中的<code>.</code>与<code>..</code>外,还能去掉多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。以下是一个例子:</p>
<pre><code> var cache = {};
function store(key, value) {
cache[path.normalize(key)] = value;
}
store('foo/bar', 1);
store('foo//baz//../bar', 2);
console.log(cache); // => { "foo/bar": 2 }</code></pre>
<blockquote>
<p> <strong>坑出没注意: </strong> 标准化之后的路径里的斜杠在Windows系统下是<code>\</code>,而在Linux系统下是<code>/</code>。如果想保证任何系统下都使用<code>/</code>作为路径分隔符的话,需要用<code>.replace(/\\/g, '/')</code>再替换一下标准路径。</p>
</blockquote>
</li>
<li><p>path.join</p>
<p> 将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。以下是一个例子:</p>
<pre><code> path.join('foo/', 'baz/', '../bar'); // => "foo/bar"</code></pre>
</li>
<li><p>path.extname</p>
<p> 当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。以下是一个例子:</p>
<pre><code> path.extname('foo/bar.js'); // => ".js"</code></pre>
</li>
</ul>
<p><code>path</code>模块提供的其余方法也不多,稍微看一下官方文档就能全部掌握。</p>
<h3 id="3.3">遍历目录</h3>
<p>遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS文件时,就需要遍历整个目录。</p>
<h4 id="3.3.1">递归算法</h4>
<p>遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通过不断缩小问题的规模来解决问题。以下示例说明了这种方法。</p>
<pre><code>function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}</code></pre>
<p>上边的函数用于计算N的阶乘(N!)。可以看到,当N大于1时,问题简化为计算N乘以N-1的阶乘。当N等于1时,问题达到最小规模,不需要再简化,因此直接返回1。</p>
<blockquote>
<p> <strong>陷阱:</strong> 使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。</p>
</blockquote>
<h4 id="3.3.2">遍历算法</h4>
<p>目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是<code>A > B > D > E > C > F</code>。</p>
<pre><code> A
/ \
B C
/ \ \
D E F</code></pre>
<h4 id="3.3.3">同步遍历</h4>
<p>了解了必要的算法后,我们可以简单地实现以下目录遍历函数。</p>
<pre><code>function travel(dir, callback) {
fs.readdirSync(dir).forEach(function (file) {
var pathname = path.join(dir, file);
if (fs.statSync(pathname).isDirectory()) {
travel(pathname, callback);
} else {
callback(pathname);
}
});
}</code></pre>
<p>可以看到,该函数以某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断和处理。因此假设有以下目录:</p>
<pre><code>- /home/user/
- foo/
x.js
- bar/
y.js
z.css</code></pre>
<p>使用以下代码遍历该目录时,得到的输入如下。</p>
<pre><code>travel('/home/user', function (pathname) {
console.log(pathname);
});
------------------------
/home/user/foo/x.js
/home/user/bar/y.js
/home/user/z.css</code></pre>
<h4 id="3.3.4">异步遍历</h4>
<p>如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理完全相同。<code>travel</code>函数的异步版本如下。</p>
<pre><code>function travel(dir, callback, finish) {
fs.readdir(dir, function (err, files) {
(function next(i) {
if (i < files.length) {
var pathname = path.join(dir, files[i]);
fs.stat(pathname, function (err, stats) {
if (stats.isDirectory()) {
travel(pathname, callback, function () {
next(i + 1);
});
} else {
callback(pathname, function () {
next(i + 1);
});
}
});
} else {
finish && finish();
}
}(0));
});
}</code></pre>
<p>这里不详细介绍异步遍历函数的编写技巧,在后续章节中会详细介绍这个。总之我们可以看到异步编程还是蛮复杂的。</p>
<h3 id="3.4">文本编码</h3>
<p>使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。我们常用的文本编码有<code>UTF8</code>和<code>GBK</code>两种,并且<code>UTF8</code>文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的<code>UTF8</code>编码字符串后才能正常处理。</p>
<h4 id="3.4.1">BOM的移除</h4>
<p>BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符("\uFEFF"),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:</p>
<pre><code> Bytes Encoding
----------------------------
FE FF UTF16BE
FF FE UTF16LE
EF BB BF UTF8</code></pre>
<p>因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误。因此,使用NodeJS读取文本文件时,一般需要去掉BOM。例如,以下代码实现了识别和去除UTF8 BOM的功能。</p>
<pre><code>function readText(pathname) {
var bin = fs.readFileSync(pathname);
if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
bin = bin.slice(3);
}
return bin.toString('utf-8');
}</code></pre>
<h4 id="3.4.2">GBK转UTF8</h4>
<p>NodeJS支持在读取文本文件时,或者在<code>Buffer</code>转换为字符串时指定文本编码,但遗憾的是,GBK编码不在NodeJS自身支持范围内。因此,一般我们借助<code>iconv-lite</code>这个三方包来转换编码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。</p>
<pre><code>var iconv = require('iconv-lite');
function readGBKText(pathname) {
var bin = fs.readFileSync(pathname);
return iconv.decode(bin, 'gbk');
}</code></pre>
<h4 id="3.4.3">单字节编码</h4>
<p>有时候,我们无法预知需要读取的文件采用哪种编码,因此也就无法指定正确的编码。比如我们要处理的某些CSS文件中,有的用GBK编码,有的用UTF8编码。虽然可以一定程度可以根据文件的字节内容猜测出文本编码,但这里要介绍的是有些局限,但是要简单得多的一种技术。</p>
<p>首先我们知道,如果一个文本文件只包含英文字符,比如<code>Hello World</code>,那无论用GBK编码或是UTF8编码读取这个文件都是没问题的。这是因为在这些编码下,ASCII0~128范围内字符都使用相同的单字节编码。</p>
<p>反过来讲,即使一个文本文件中有中文等字符,如果我们需要处理的字符仅在ASCII0~128范围内,比如除了注释和字符串以外的JS代码,我们就可以统一使用单字节编码来读取文件,不用关心文件的实际编码是GBK还是UTF8。以下示例说明了这种方法。</p>
<pre><code>1. GBK编码源文件内容:
var foo = '中文';
2. 对应字节:
76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B
3. 使用单字节编码读取后得到的内容:
var foo = '{乱码}{乱码}{乱码}{乱码}';
4. 替换内容:
var bar = '{乱码}{乱码}{乱码}{乱码}';
5. 使用单字节编码保存后对应字节:
76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B
6. 使用GBK编码读取后得到内容:
var bar = '中文';</code></pre>
<p>这里的诀窍在于,不管大于0xEF的单个字节在单字节编码下被解析成什么乱码字符,使用同样的单字节编码保存这些乱码字符时,背后对应的字节保持不变。</p>
<p>NodeJS中自带了一种<code>binary</code>编码可以用来实现这个方法,因此在下例中,我们使用这种编码来演示上例对应的代码该怎么写。</p>
<pre><code>function replace(pathname) {
var str = fs.readFileSync(pathname, 'binary');
str = str.replace('foo', 'bar');
fs.writeFileSync(pathname, str, 'binary');
}</code></pre>
<h3 id="3.5">小结</h3>
<p>本章介绍了使用NodeJS操作文件时需要的API以及一些技巧,总结起来有以下几点:</p>
<ul>
<li><p>学好文件操作,编写各种程序都不怕。</p>
</li>
<li><p>如果不是很在意性能,<code>fs</code>模块的同步API能让生活更加美好。</p>
</li>
<li><p>需要对文件读写做到字节级别的精细控制时,请使用<code>fs</code>模块的文件底层操作API。</p>
</li>
<li><p>不要使用拼接字符串的方式来处理路径,使用<code>path</code>模块。</p>
</li>
<li><p>掌握好目录遍历和文件编码处理技巧,很实用。</p>
</li>
</ul>
<h2 id="4">网络操作</h2>
<p>不了解网络编程的程序员不是好前端,而NodeJS恰好提供了一扇了解网络编程的窗口。通过NodeJS,除了可以编写一些服务端程序来协助前端开发和测试外,还能够学习一些HTTP协议与Socket协议的相关知识,这些知识在优化前端性能和排查前端故障时说不定能派上用场。本章将介绍与之相关的NodeJS内置模块。</p>
<h3 id="4.1">开门红</h3>
<p>NodeJS本来的用途是编写高性能Web服务器。我们首先在这里重复一下官方文档里的例子,使用NodeJS内置的<code>http</code>模块简单实现一个HTTP服务器。</p>
<pre><code>var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('Hello World\n');
}).listen(8124);</code></pre>
<p>以上程序创建了一个HTTP服务器并监听<code>8124</code>端口,打开浏览器访问该端口<code>http://127.0.0.1:8124/</code>就能够看到效果。</p>
<blockquote>
<p> <strong>豆知识:</strong> 在Linux系统下,监听1024以下端口需要root权限。因此,如果想监听80或443端口的话,需要使用<code>sudo</code>命令启动程序。</p>
</blockquote>
<h3 id="4.2">API走马观花</h3>
<p>我们先大致看看NodeJS提供了哪些和网络操作有关的API。这里并不逐一介绍每个API的使用方法,官方文档已经做得很好了。</p>
<h4 id="4.2.1">HTTP</h4>
<blockquote>
<p> <strong>官方文档: </strong> <a href="http://nodejs.org/api/http.html"><a href="http://nodejs.org/api/http.html">http://nodejs.org/api/http.html</a></a></p>
</blockquote>
<p>'http'模块提供两种使用方式:</p>
<ul>
<li><p>作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。</p>
</li>
<li><p>作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。</p>
</li>
</ul>
<p>首先我们来看看服务端模式下如何工作。如开门红中的例子所示,首先需要使用<code>.createServer</code>方法创建一个服务器,然后调用<code>.listen</code>方法监听端口。之后,每当来了一个客户端请求,创建服务器时传入的回调函数就被调用一次。可以看出,这是一种事件机制。</p>
<p>HTTP请求本质上是一个数据流,由请求头(headers)和请求体(body)组成。例如以下是一个完整的HTTP请求数据内容。</p>
<pre><code>POST / HTTP/1.1
User-Agent: curl/7.26.0
Host: localhost
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
Hello World</code></pre>
<p>可以看到,空行之上是请求头,之下是请求体。HTTP请求在发送给服务器时,可以认为是按照从头到尾的顺序一个字节一个字节地以数据流方式发送的。而<code>http</code>模块创建的HTTP服务器在接收到完整的请求头后,就会调用回调函数。在回调函数中,除了可以使用<code>request</code>对象访问请求头数据外,还能把<code>request</code>对象当作一个只读数据流来访问请求体数据。以下是一个例子。</p>
<pre><code>http.createServer(function (request, response) {
var body = [];
console.log(request.method);
console.log(request.headers);
request.on('data', function (chunk) {
body.push(chunk);
});
request.on('end', function () {
body = Buffer.concat(body);
console.log(body.toString());
});