forked from manuelkiessling/nodebeginner.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index-zh-tw.html
2707 lines (2321 loc) · 219 KB
/
index-zh-tw.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 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Node入門 » 一本全面的Node.js教學課程</title>
<meta name="description" content="一本適合Node.js初學者的全面教學課程:教你如何使用伺服器端JavaScript來建構一個完整的web應用" />
<link rel="icon" href="favicon.png" type="image/png" />
<link rel="stylesheet" type="text/css" href="olddefault.css" />
<link rel="canonical" href="http://www.nodebeginner.org/index-zh-tw.html" />
<style>
#book p {
text-align: left;
}
</style>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-2127388-6', 'auto');
ga('send', 'pageview');
</script>
<script type="text/javascript">
// Disqus
var disqus_shortname = 'nodebeginner';
var disqus_identifier = 'nodebeginner-book-chinese';
var disqus_url = 'http://www.nodebeginner.org/index-zh-cn.html';
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
</head>
<body>
<img style="display: none;" src="the_node_beginner_book_cover_medium.png" height="256" width="171" />
<div id="forkmeongithub">
<a href="https://github.com/ManuelKiessling/NodeBeginnerBook"><img src="fork_me_on_github.png" width="149" height="149" alt="Fork me on GitHub" /></a>
</div>
<div id="translations">
<table>
<tr>
<td>
<a href="index-jp.html">
<div class="flag"><img src="jp-flag.png" width="24" height="24" alt="japanese flag" /></div>
<div class="text">日本語で読む</div>
</a>
</td>
<td>
<a href="index-es.html">
<div class="flag"><img src="es-flag.png" width="24" height="24" alt="spanish flag" /></div>
<div class="text">Lee este tutorial en Español</div>
</a>
</td>
<td>
<a href="index-kr.html">
<div class="flag"><img src="kr-flag.png" width="24" height="24" alt="korean flag" /></div>
<div class="text">이 튜토리얼을 한글로 보세요</div>
</a>
</td>
</tr>
<tr>
<td>
<a href="index-zh-cn.html">
<div class="flag"><img src="cn-flag.png" width="24" height="24" alt="chinese flag" /></div>
<div class="text">阅读本书中文版</div>
</a>
</td>
<td>
<a href="./">
<div class="flag"><img src="us-flag.png" width="24" height="24" alt="usa flag" /></div>
<div class="text">Read this tutorial in english</div>
</a>
</td>
<td>
<a href="http://www.nodebeginner.ru">
<div class="flag"><img src="ru-flag.png" width="24" height="24" alt="russian flag" /></div>
<div class="text">Читать этот учебник на русском</div>
</a>
</td>
</tr>
<tr>
<td>
<a href="index-vi.html">
<div class="flag"><img src="vi-flag.png" width="24" height="24" alt="vietnamese flag" /></div>
<div class="text">Đọc bằng tiếng Việt</div>
</a>
</td>
<td>
</td>
<td>
</td>
</tr>
</table>
</div>
<div class="buy-the-bundle cn">
<div class="cover">
<a href="/buy-chinese/"><img src="the_node_beginner_book_cover_medium_chinese.png" height="120" width="80" /></a>
</div>
<div class="description">
<p>
購買 "Node入門" 中文版電子書
</p>
<p>
<strong class="price dollarsign">$</strong><strong class="price">0.99</strong>
</p>
<p>
<a class="buttonlink" href="/buy-chinese/">
<div class="button">立即購買</div>
</a>
</p>
</div>
<div class="buy">
<p>
本書共42頁
<br />
支援PDF格式,Kindle以及ePub格式
<br />
直接下載,免費更新
</p>
</div>
</div>
<div id="book">
<h1>Node入門</h1>
<div id="author">作者: <a href="http://twitter.com/manuelkiessling">Manuel Kiessling</a><br />
翻譯: <a href="http://weibo.com/goddyzhao">goddyzhao</a> &
<a href="http://www.otakustay.com">GrayZhang</a> &
<a href="http://weibo.com/cmonday">MondayChen</a> &
<a href="http://blog.miniasp.com/">Will 保哥</a>
</div>
<a name="about"></a>
<h2>關於</h2>
<p>
本書致力於教會你如何用Node.js來開發應用,過程中會傳授你所有所需的 "進階" JavaScript知識。本書絕不是一本 "Hello World" 的教學課程。
</p>
<a name="status"></a>
<h3>狀態</h3>
<p>
你正在閱讀的已經是本書的最終版。因此,只有當進行錯誤更正以及針對新版本Node.js的改動進行對應的修正時,才會進行更新。
</p>
<p>
本書中的程式碼範例都在Node.js 0.6.11版本中測試過,可以正確工作。
</p>
<a name="intended-audience"></a>
<h3>讀者對象</h3>
<p>
本書最適合與我有相似技術背景的讀者: 至少對一門諸如Ruby、Python、PHP或者Java這樣物件導向程式語言有一定的經驗;對JavaScript處於初學階段,並且完全是一個Node.js的新手。
</p>
<p>
這裡指的適合對其他程式語言有一定經驗的開發者,意思是說,本書不會對諸如資料類型、變數、控制結構等等之類非常基礎的概念作介紹。要讀懂本書,這些基礎的概念我都預設你已經會了。
</p>
<p>
然而,本書還是會對JavaScript中的函數和物件作詳細介紹,因為它們與其他同類程式語言中的函數和物件有很大的不同。
</p>
<a name="structure"></a>
<h3>本書結構</h3>
<p>
讀完本書之後,你將完成一個完整的web應用,該應用允許用戶瀏覽頁面以及上傳檔案。
</p>
<p>
當然了,應用本身並沒有什麼了不起的,相比為了實現該功能書寫的程式碼本身,我們更關注的是如何建立一個框架來對我們應用的不同模組進行乾淨地剝離。 是不是很玄乎?稍後你就明白了。
</p>
<p>
本書先從介紹在Node.js環境中進行JavaScript開發和在瀏覽器環境中進行JavaScript開發的差異開始。
</p>
<p>
緊接著,會帶領大家完成一個最傳統的 "Hello World" 應用,這也是最基礎的Node.js應用。
</p>
<p>
最後,會和大家討論如何設計一個 "真正" 完整的應用,剖析要完成該應用需要實現的不同模組,並一步一步介紹如何來實現這些模組。
</p>
<p>
可以確保的是,在這過程中,大家會學到JavaScript中一些進階的概念、如何使用它們以及為什麼使用這些概念就可以實現而其他程式語言中同類的概念就無法實現。
</p>
<p>
該應用程式所有的原始碼都可以透過
<a href="https://github.com/ManuelKiessling/NodeBeginnerBook/tree/master/code/application">本書Github repository</a>下載.
</p>
<div id="table-of-contents-headline">目錄</div>
<div id="table-of-contents">
<ul>
<li><a href="#about">關於</a>
<ul>
<li><a href="#status">狀態</a></li>
<li><a href="#intended-audience">讀者對象</a></li>
<li><a href="#structure">本書結構</a></li>
</ul>
</li>
<li><a href="#javascript-and-nodejs">JavaScript與Node.js</a>
<ul>
<li><a href="#javascript-and-you">JavaScript與你</a></li>
<li><a href="#a-word-of-warning">簡短申明</a></li>
<li><a href="#server-side-javascript">伺服器端JavaScript</a></li>
<li><a href="#hello-world"> "Hello World" </a></li>
</ul>
</li>
<li><a href="#a-full-blown-web-application-with-nodejs">一個完整的基於Node.js的web應用</a>
<ul>
<li><a href="#the-use-cases">使用案例</a></li>
<li><a href="#the-application-stack">應用不同模組分析</a></li>
</ul>
</li>
<li><a href="#building-the-application-stack">建構應用的模組</a>
<ul>
<li><a href="#a-basic-http-server">一個基礎的HTTP伺服器</a></li>
<li><a href="#analyzing-our-http-server">分析HTTP伺服器</a></li>
<li><a href="#passing-functions-around">進行函數傳遞</a></li>
<li><a href="#how-function-passing-makes-our-http-server-work">函數傳遞是如何讓HTTP伺服器工作的</a></li>
<li><a href="#event-driven-callbacks">基於事件驅動的回呼(callback)</a></li>
<li><a href="#how-our-server-handles-requests">伺服器是如何處理請求的</a></li>
<li><a href="#finding-a-place-for-our-server-module">伺服器端的模組放在哪裡</a>
</li>
<li><a href="#whats-needed-to-route-requests">如何來進行請求的 "路由" </a></li>
<li><a href="#execution-in-the-kongdom-of-verbs">行為驅動執行</a></li>
<li><a href="#routing-to-real-request-handlers">路由給真正的請求處理程序</a></li>
<li><a href="#making-the-request-handlers-respond">讓請求處理程序作出回應</a>
<ul>
<li><a href="#how-to-not-do-it">不好的實現方式</a></li>
<li><a href="#blocking-and-non-blocking">Blocking與Non-Blocking</a></li>
<li><a href="#responding-request-handlers-with-non-blocking-operations">以Non-Blocking操作進行請求回應</a>
</li>
</ul>
</li>
<li><a href="#serving-something-useful">更有用的場景</a>
<ul>
<li><a href="#handling-post-requests">處理POST請求</a></li>
<li><a href="#handling-file-uploads">處理檔案上傳</a></li>
</ul>
</li>
<li><a href="#conclusion-and-outlook">總結與展望</a></li>
</ul>
</li>
</ul>
</div>
<a name="javascript-and-nodejs"></a>
<h2>JavaScript與Node.js</h2>
<a name="javascript-and-you"></a>
<h3>JavaScript與你</h3>
<p>
拋開技術,我們先來聊聊你以及你和JavaScript的關系。本章的主要目的是想讓你看看,對你而言是否有必要繼續閱讀後續章節的內容。
</p>
<p>
如果你和我一樣,那麼你很早就開始利用HTML進行 "開發" ,正因如此,你接觸到了這個叫JavaScript有趣的東西,而對於JavaScript,你只會基本的操作——為web頁面增加互動。
</p>
<p>
而你真正想要的是 "實用的東西" ,你想要知道如何建構複雜的Web站台 —— 於是,你學習了一種諸如PHP、Ruby、Java這樣的程式語言,並開始書寫 "後端" 程式碼。
</p>
<p>
與此同時,你還始終關注著JavaScript,隨著透過一些對jQuery,Prototype之類技術的介紹,你慢慢了解到了很多JavaScript中的進階技能,同時也感受到了JavaScript絕非僅僅是<em>window.open() </em>那麼簡單。 .
</p>
<p>
不過,這些畢竟都是前端技術,盡管當想要增強頁面的時候,使用jQuery總讓你覺得很爽,但到最後,你頂多是個JavaScript<em>用戶</em>,而非JavaScript<em>開發者</em>。
</p>
<p>
然後,出現了Node.js,伺服器端的JavaScript,這有多酷啊?
</p>
<p>
於是,你覺得是時候該重新拾起既熟悉又陌生的JavaScript了。但是別急,寫Node.js應用是一件事情;理解為什麼它們要以它們書寫的這種方式來書寫則意味著——你要懂JavaScript。這次是玩真的了。
</p>
<p>
問題來了: 由於JavaScript真正意義上以兩種,甚至可以說是三種形態存在(從中世紀90年代的作為對DHTML進行增強的小玩具,到像jQuery那樣嚴格意義上的前端技術,一直到現在的伺服器端技術),因此,很難找到一個 "正確" 的方式來學習JavaScript,使得讓你書寫Node.js應用的時候感覺自己是在真正開發它而不僅僅是使用它。
</p>
<p>
因為這就是關鍵: 你本身已經是個有經驗的開發者,你不想透過到處尋找各種解決方案(其中可能還有不正確的)來學習新的技術,你要確保自己是透過正確的方式來學習這項技術。
</p>
<p>
當然了,外面不乏很優秀的學習JavaScript的文章。但是,有的時候光靠那些文章是遠遠不夠的。你需要的是指導。
</p>
<p>
本書的目標就是給你提供指導。
</p>
<a name="a-word-of-warning"></a>
<h3>簡短申明</h3>
<p>
業界有非常優秀的JavaScript程式設計師。而我並非其中一員。
</p>
<p>
我就是上一節中描述的那個我。我熟悉如何開發後端web應用,但是對 "真正" 的JavaScript以及Node.js,我都只是新手。我也只是最近學習了一些JavaScript的進階概念,並沒有實踐經驗。
</p>
<p>
因此,本書並不是一本 "從入門到精通" 的書,更像是一本 "從初級入門到進階入門" 的書。
</p>
<p>
如果成功的話,那麼本書就是我當初開始學習Node.js最希望擁有的教學課程。
</p>
<a name="server-side-javascript"></a>
<h3>伺服器端JavaScript</h3>
<p>
JavaScript最早是運行在瀏覽器中,然而瀏覽器只是提供了一個上下文,它定義了使用JavaScript可以做什麼,但並沒有 "說" 太多關於JavaScript語言本身可以做什麼。事實上,JavaScript是一門 "完整" 的語言: 它可以使用在不同的上下文中,其能力與其他同類語言相比有過之而無不及。
</p>
<p>
Node.js事實上就是另外一種上下文,它允許在後端(脫離瀏覽器環境)運行JavaScript程式碼。
</p>
<p>
要實現在後台運行JavaScript程式碼,程式碼需要先被解釋然後正確的執行。Node.js的原理正是如此,它使用了Google的V8虛擬機(Google的Chrome瀏覽器使用的JavaScript執行環境),來解釋和執行JavaScript程式碼。
</p>
<p>
除此之外,伴隨著Node.js的還有許多有用的模組,它們可以簡化很多重復的勞作,比如向終端輸出字串。
</p>
<p>
因此,Node.js事實上既是一個運行時環境,同時又是一個函式庫。
</p>
<p>
要使用Node.js,首先需要進行安裝。關於如何安裝Node.js,這裡就不贅述了,可以直接參考<a href="https://github.com/joyent/node/wiki/Installation" title="Building and Installing Node.js">官方的安裝指南</a>。安裝完成後,繼續回來閱讀本書下面的內容。
</p>
<a name="hello-world"></a>
<h3> "Hello World" </h3>
<p>
好了, "廢話" 不多說了,馬上開始我們第一個Node.js應用: "Hello World" 。
</p>
<p>
打開你最喜歡的編輯器,建立一個<em>helloworld.js</em>檔案。我們要做就是向STDOUT輸出 "Hello World" ,如下是實現該功能的程式碼:
</p>
<pre class="prettyprint lang-js"><span class="pln">console</span><span class="pun">.</span><span
class="pln">log</span><span class="pun">(</span><span class="str">"Hello World"</span><span class="pun">);</span></pre>
<p>
儲存該檔案,並透過Node.js來執行:
</p>
<pre>node helloworld.js</pre>
<p>
正常的話,就會在終端輸出<em>Hello World</em> 。
</p>
<p>
好吧,我承認這個應用是有點無趣,那麼下面我們就來點 "實用的東西" 。
</p>
<a name="a-full-blown-web-application-with-nodejs"></a>
<h2>一個完整的基於Node.js的web應用</h2>
<a name="the-use-cases"></a>
<h3>使用案例</h3>
<p>我們來把目標設定得簡單點,不過也要夠實際才行:</p>
<ul>
<li>用戶可以透過瀏覽器使用我們的應用。</li>
<li>當用戶請求<em>http://domain/start</em>時,可以看到一個歡迎頁面,頁面上有一個檔案上傳的表單。</li>
<li>用戶可以選擇一個圖片並送出表單,隨後檔案將被上傳到<em>http://domain/upload</em>,該頁面完成上傳後會把圖片顯示在頁面上。</li>
</ul>
<p>差不多了,你現在也可以去Google一下,找點東西亂搞一下來完成功能。但是我們現在先不做這個。</p>
<p>更進一步地說,在完成這一目標的過程中,我們不僅僅需要基礎的程式碼而不管程式碼是否優雅。我們還要對此進行抽象,來尋找一種適合建構更為複雜的Node.js應用的方式。</p>
<h3>應用不同模組分析</h3>
<p>我們來分解一下這個應用,為了實現上文的使用案例,我們需要實現哪些部分呢?</p>
<ul>
<li>我們需要提供Web頁面,因此需要一個<em>HTTP伺服器</em></li>
<li>對於不同的請求,根據請求的URL,我們的伺服器需要給予不同的回應,因此我們需要一個<em>路由</em>,用於把請求對應到請求處理程序(request handler)</li>
<li>當請求被伺服器接收並透過路由傳遞之後,需要可以對其進行處理,因此我們需要最終的<em>請求處理程序</em></li>
<li>路由還應該能處理POST資料,並且把資料封裝成更友好的格式傳遞給請求處理入程序,因此需要<em>請求資料處理功能</em></li>
<li>我們不僅僅要處理URL對應的請求,還要把內容顯示出來,這意味著我們需要一些<em>視圖邏輯</em>供請求處理程序使用,以便將內容發送給用戶的瀏覽器</li>
<li>最後,用戶需要上傳圖片,所以我們需要<em>上傳處理功能</em>來處理這方面的細節</li>
</ul>
<p>我們先來想想,使用PHP的話我們會怎麼建構這個結構。一般來說我們會用一個Apache HTTP伺服器並配上mod_php5模組。<br />從這個角度看,整個 "接收HTTP請求並提供Web頁面" 的需求根本不需要PHP來處理。</p>
<p>不過對Node.js來說,概念完全不一樣了。使用Node.js時,我們不僅僅在實現一個應用,同時還實現了整個HTTP伺服器。事實上,我們的Web應用以及對應的Web伺服器基本上是一樣的。</p>
<p>聽起來好像有一大堆活要做,但隨後我們會逐漸意識到,對Node.js來說這並不是什麼麻煩的事。</p>
<p>現在我們就來開始實現之路,先從第一個部分--HTTP伺服器著手。</p>
<a name="building-the-application-stack"></a>
<h2>建構應用的模組</h2>
<a name="a-basic-http-server"></a>
<h3>一個基礎的HTTP伺服器</h3>
<p>
當我準備開始寫我的第一個 "真正的" Node.js應用的時候,我不但不知道怎麼寫Node.js程式碼,也不知道怎麼組織這些程式碼。
<br>
我應該把所有東西都放進一個檔案裡嗎?網上有很多教學課程都會教你把所有的邏輯都放進一個用Node.js寫的基礎HTTP伺服器裡。但是如果我想加入更多的內容,同時還想保持程式碼的可讀性呢?
</p>
<p>
實際上,只要把不同功能的程式碼放入不同的模組中,保持程式碼分離還是相當簡單的。
</p>
<p>
這種方法允許你擁有一個乾淨的主檔案(main file),你可以用Node.js執行它;同時你可以擁有乾淨的模組,它們可以被主檔案和其他的模組執行。
</p>
<p>
那麼,現在我們來建立一個用於啟動我們的應用的主檔案,和一個儲存著我們的HTTP伺服器程式碼的模組。
</p>
<p>
在我的印象裡,把主檔案叫做<em>index.js</em>或多或少是個標準格式。把伺服器模組放進叫<em>server.js</em>的檔案裡則很好理解。
</p>
<p>
讓我們先從伺服器模組開始。在你的項目的根目錄下建立一個叫<em>server.js</em>的檔案,並寫入以下程式碼:
</p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br>http</span><span
class="pun">.</span><span class="pln">createServer</span><span class="pun">(</span><span class="kwd">function</span><span
class="pun">(</span><span class="pln">request</span><span class="pun">,</span><span class="pln"> response</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> response</span><span
class="pun">.</span><span class="pln">writeHead</span><span class="pun">(</span><span
class="lit">200</span><span class="pun">,</span><span class="pln"> </span><span
class="pun">{</span><span class="str">"Content-Type"</span><span class="pun">:</span><span
class="pln"> </span><span class="str">"text/plain"</span><span class="pun">});</span><span
class="pln"><br> response</span><span class="pun">.</span><span class="pln">write</span><span
class="pun">(</span><span class="str">"Hello World"</span><span class="pun">);</span><span
class="pln"><br> response</span><span class="pun">.</span><span class="pln">end</span><span
class="pun">();</span><span class="pln"><br></span><span class="pun">}).</span><span
class="pln">listen</span><span class="pun">(</span><span class="lit">8888</span><span
class="pun">);</span></pre>
<p>
搞定!你剛剛完成了一個可以工作的HTTP伺服器。為了證明這一點,我們來運行並且測試這段程式碼。首先,用Node.js執行你的腳本:
</p>
<pre>node server.js</pre>
<p>
接下來,打開瀏覽器存取<a href="http://localhost:8888/" rel="nofollow">http://localhost:8888/</a>,你會看到一個寫著 "Hello World" 的網頁。
</p>
<p>
這很有趣,不是嗎?讓我們先來談談HTTP伺服器的問題,把如何組織項目的事情先放一邊吧,你覺得如何?我保證之後我們會解決那個問題的。
</p>
<a name="analyzing-our-http-server"></a>
<h3>分析HTTP伺服器</h3>
<p>
那麼接下來,讓我們分析一下這個HTTP伺服器的構成。
</p>
<p>
第一行<em>請求(require)</em>Node.js自帶的 <em>http</em> 模組,並且把它賦值給 <em>http</em> 變數。
</p>
<p>
接下來我們執行http模組提供的函數: <em>createServer</em> 。這個函數會回傳一個物件,這個物件有一個叫做 <em>listen</em> 的方法,這個方法有一個數值參數,指定這個HTTP伺服器監聽的埠號號。
</p>
<p>
咱們暫時先不管 <em>http.createServer</em> 的括號裡的那個函數定義。
</p>
<p>
我們本來可以用這樣的程式碼來啟動伺服器並監聽8888埠號:
</p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br></span><span class="kwd">var</span><span
class="pln"> server </span><span class="pun">=</span><span class="pln"> http</span><span
class="pun">.</span><span class="pln">createServer</span><span class="pun">();</span><span
class="pln"><br>server</span><span class="pun">.</span><span class="pln">listen</span><span class="pun">(</span><span
class="lit">8888</span><span class="pun">);</span></pre>
<p>
這段程式碼只會啟動一個監聽8888埠號的伺服器,它不做任何別的事情,甚至連請求都不會應答。
</p>
<p>
最有趣(而且,如果你之前習慣使用一個更加保守的語言,比如PHP,它還很奇怪)的部分是 <em>createSever()</em> 的第一個參數,一個函數定義。
</p>
<p>
實際上,這個函數定義是 <em>createServer()</em> 的第一個也是唯一一個參數。因為在JavaScript中,函數和其他變數一樣都是可以被傳遞的。
</p>
<a name="passing-functions-around"></a>
<h3>進行函數傳遞</h3>
<p>
舉例來說,你可以這樣做:
</p>
<pre class="prettyprint lang-js"><span class="kwd">function</span><span class="pln"> say</span><span
class="pun">(</span><span class="pln">word</span><span class="pun">)</span><span
class="pln"> </span><span class="pun">{</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span
class="pln">word</span><span class="pun">);</span><span class="pln"><br></span><span
class="pun">}</span><span class="pln"><br><br></span><span class="kwd">function</span><span class="pln"> execute</span><span
class="pun">(</span><span class="pln">someFunction</span><span class="pun">,</span><span class="pln"> value</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> someFunction</span><span
class="pun">(</span><span class="pln">value</span><span class="pun">);</span><span
class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>execute</span><span
class="pun">(</span><span class="pln">say</span><span class="pun">,</span><span
class="pln"> </span><span class="str">"Hello"</span><span class="pun">);</span></pre>
<p>
請仔細閱讀這段程式碼!在這裡,我們把 <em>say</em> 函數作為<em>execute</em>函數的第一個變數進行了傳遞。這裡回傳的不是 <em>say</em> 的回傳值,而是 <em>say</em> 本身!
</p>
<p>
這樣一來, <em>say</em> 就變成了<em>execute</em> 中的區域變數 <em>someFunction</em> ,execute可以透過執行 <em>someFunction()</em> (帶括號的形式)來使用 <em>say</em> 函數。
</p>
<p>
當然,因為 <em>say</em> 有一個變數, <em>execute</em> 在執行 <em>someFunction</em> 時可以傳遞這樣一個變數。
</p>
<p>
我們可以,就像剛才那樣,用它的名字把一個函數作為變數傳遞。但是我們不一定要繞這個 "先定義,再傳遞" 的圈子,我們可以直接在另一個函數的括號中定義和傳遞這個函數:
</p>
<pre class="prettyprint lang-js"><span class="kwd">function</span><span class="pln"> execute</span><span
class="pun">(</span><span class="pln">someFunction</span><span class="pun">,</span><span class="pln"> value</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> someFunction</span><span
class="pun">(</span><span class="pln">value</span><span class="pun">);</span><span
class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>execute</span><span
class="pun">(</span><span class="kwd">function</span><span class="pun">(</span><span
class="pln">word</span><span class="pun">){</span><span class="pln"> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span
class="pln">word</span><span class="pun">)</span><span class="pln"> </span><span
class="pun">},</span><span class="pln"> </span><span class="str">"Hello"</span><span
class="pun">);</span></pre>
<p>
我們在 <em>execute</em> 接受第一個參數的地方直接定義了我們準備傳遞給 <em>execute</em> 的函數。
</p>
<p>
用這種方式,我們甚至不用給這個函數起名字,這也是為什麼它被叫做 <em>匿名函數</em> 。
</p>
<p>
這是我們和我所認為的 "進階" JavaScript的第一次親密接觸,不過我們還是得循序漸進。現在,我們先接受這一點:在JavaScript中,一個函數可以作為另一個函數接收一個參數。我們可以先定義一個函數,然後傳遞,也可以在傳遞參數的地方直接定義函數。
</p>
<a name="how-function-passing-makes-our-http-server-work"></a>
<h3>函數傳遞是如何讓HTTP伺服器工作的</h3>
<p>帶著這些知識,我們再來看看我們簡約而不簡單的HTTP伺服器:</p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br>http</span><span
class="pun">.</span><span class="pln">createServer</span><span class="pun">(</span><span class="kwd">function</span><span
class="pun">(</span><span class="pln">request</span><span class="pun">,</span><span class="pln"> response</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> response</span><span
class="pun">.</span><span class="pln">writeHead</span><span class="pun">(</span><span
class="lit">200</span><span class="pun">,</span><span class="pln"> </span><span
class="pun">{</span><span class="str">"Content-Type"</span><span class="pun">:</span><span
class="pln"> </span><span class="str">"text/plain"</span><span class="pun">});</span><span
class="pln"><br> response</span><span class="pun">.</span><span class="pln">write</span><span
class="pun">(</span><span class="str">"Hello World"</span><span class="pun">);</span><span
class="pln"><br> response</span><span class="pun">.</span><span class="pln">end</span><span
class="pun">();</span><span class="pln"><br></span><span class="pun">}).</span><span
class="pln">listen</span><span class="pun">(</span><span class="lit">8888</span><span
class="pun">);</span></pre>
<p>現在它看上去應該清晰了很多:我們向 <em>createServer</em> 函數傳遞了一個匿名函數。 </p>
<p>用這樣的程式碼也可以達到同樣的目的: </p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br></span><span class="kwd">function</span><span
class="pln"> onRequest</span><span class="pun">(</span><span class="pln">request</span><span
class="pun">,</span><span class="pln"> response</span><span class="pun">)</span><span
class="pln"> </span><span class="pun">{</span><span class="pln"><br> response</span><span
class="pun">.</span><span class="pln">writeHead</span><span class="pun">(</span><span
class="lit">200</span><span class="pun">,</span><span class="pln"> </span><span
class="pun">{</span><span class="str">"Content-Type"</span><span class="pun">:</span><span
class="pln"> </span><span class="str">"text/plain"</span><span class="pun">});</span><span
class="pln"><br> response</span><span class="pun">.</span><span class="pln">write</span><span
class="pun">(</span><span class="str">"Hello World"</span><span class="pun">);</span><span
class="pln"><br> response</span><span class="pun">.</span><span class="pln">end</span><span
class="pun">();</span><span class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>http</span><span
class="pun">.</span><span class="pln">createServer</span><span class="pun">(</span><span class="pln">onRequest</span><span
class="pun">).</span><span class="pln">listen</span><span class="pun">(</span><span
class="lit">8888</span><span class="pun">);</span></pre>
<p>也許現在我們該問這個問題了:我們為什麼要用這種方式呢? </p>
<a name="event-driven-callbacks"></a>
<h3>基於事件驅動的回呼(callback)</h3>
<p>這個問題可不好回答(至少對我來說),不過這是Node.js原生的工作方式。它是事件驅動的,這也是它為什麼這麼快的原因。 </p>
<p>你也許會想花點時間讀一下Felix Geisendörfer的大作<a href="http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb">Understanding node.js</a>,它介紹了一些背景知識。 </p>
<p>這一切都歸結於 "Node.js是事件驅動的" 這一事實。好吧,其實我也不是特別確切的了解這句話的意思。不過我會試著解釋,為什麼它對我們用Node.js寫網絡應用(Web based application)是有意義的。 </p>
<p>當我們使用 <em>http.createServer</em> 方法的時候,我們當然不只是想要一個監聽某個埠號的伺服器,我們還想要它在伺服器收到一個HTTP請求的時候做點什麼。 </p>
<p>問題是,這是非同步的:請求任何時候都可能到達,但是我們的伺服器卻跑在一個單程序中。 </p>
<p>寫PHP應用的時候,我們一點也不為此擔心:任何時候當有請求進入的時候,網頁伺服器(通常是Apache)就為這一請求新建一個程序,並且開始從頭到尾執行相應的PHP腳本。 </p>
<p>那麼在我們的Node.js程序中,當一個新的請求到達8888埠號的時候,我們怎麼控制流程呢? </p>
<p>嗯,這就是Node.js/JavaScript的事件驅動設計能夠真正幫上忙的地方了——雖然我們還得學一些新概念才能掌握它。讓我們來看看這些概念是怎麼應用在我們的伺服器程式碼裡的。 </p>
<p>我們建立了伺服器,並且向建立它的方法傳遞了一個函數。無論何時我們的伺服器收到一個請求,這個函數就會被執行。 </p>
<p>我們不知道這件事情什麼時候會發生,但是我們現在有了一個處理請求的地方:它就是我們傳遞過去的那個函數。至於它是被預先定義的函數還是匿名函數,就無關緊要了。 </p>
<p>這個就是傳說中的 <em>回呼(callback)</em> 。我們給某個方法傳遞了一個函數,這個方法在有相應事件發生時執行這個函數來進行 <em>回呼(callback)</em> 。 </p>
<p>至少對我來說,需要一些功夫才能弄懂它。你如果還是不太確定的話就再去讀讀Felix的部落格文章。 </p>
<p>讓我們再來琢磨琢磨這個新概念。我們怎麼證明,在建立完伺服器之後,即使沒有HTTP請求進來、我們的回呼(callback)函數也沒有被執行的情況下,我們的程式碼還繼續有效呢?我們試試這個: </p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br></span><span class="kwd">function</span><span
class="pln"> onRequest</span><span class="pun">(</span><span class="pln">request</span><span
class="pun">,</span><span class="pln"> response</span><span class="pun">)</span><span
class="pln"> </span><span class="pun">{</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Request received."</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">writeHead</span><span class="pun">(</span><span class="lit">200</span><span
class="pun">,</span><span class="pln"> </span><span class="pun">{</span><span
class="str">"Content-Type"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"text/plain"</span><span
class="pun">});</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">write</span><span class="pun">(</span><span class="str">"Hello World"</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">end</span><span class="pun">();</span><span class="pln"><br></span><span
class="pun">}</span><span class="pln"><br><br>http</span><span class="pun">.</span><span class="pln">createServer</span><span
class="pun">(</span><span class="pln">onRequest</span><span class="pun">).</span><span class="pln">listen</span><span
class="pun">(</span><span class="lit">8888</span><span class="pun">);</span><span class="pln"><br><br>console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Server has started."</span><span
class="pun">);</span></pre>
<p>注意:在 <em>onRequest</em> (我們的回呼(callback)函數)觸發的地方,我用 <em>console.log</em> 輸出了一段文字。在HTTP伺服器開始工作<em>之後</em>,也輸出一段文字。 </p>
<p>
當我們與往常一樣,運行它<em>node server.js</em>時,它會馬上在命令行上輸出 "Server has started." 。當我們向伺服器發出請求(在瀏覽器存取<a href="http://localhost:8888/" rel="nofollow">http://localhost:8888/</a> ), "Request received." 這條消息就會在命令行中出現。
</p>
<p>這就是事件驅動的非同步伺服器端JavaScript和它的回呼(callback)啦!</p>
<p>(請注意,當我們在伺服器存取網頁時,我們的伺服器可能會輸出兩次 "Request received." 。那是因為大部分伺服器都會在你存取 http://localhost:8888 /時嘗試讀取 http://localhost:8888/favicon.ico )</p>
<a name="how-our-server-handles-requests"></a>
<h3>伺服器是如何處理請求的</h3>
<p>好的,接下來我們簡單分析一下我們伺服器程式碼中剩下的部分,也就是我們的回呼(callback)函數 <em>onRequest()</em> 的主體部分。 </p>
<p>當回呼(callback)啟動,我們的 <em>onRequest()</em> 函數被觸發的時候,有兩個參數被傳入: <em>request</em> 和 <em>response</em> 。 </p>
<p>它們是物件,你可以使用它們的方法來處理HTTP請求的細節,並且回應請求(比如向發出請求的瀏覽器發回一些東西)。 </p>
<p>所以我們的程式碼就是:當收到請求時,使用 <em>response.writeHead()</em> 函數發送一個HTTP狀態200和HTTP頭的內容類型(content-type),使用 <em>response.write()</em> 函數在HTTP相應主體中發送文字 "Hello World"。 </p>
<p>最後,我們執行 <em>response.end()</em> 完成回應。 </p>
<p>目前來說,我們對請求的細節並不在意,所以我們沒有使用 <em>request</em> 物件。 </p>
<a name="finding-a-place-for-our-server-module"></a>
<h3>伺服器端的模組放在哪裡</h3>
<p>OK,就像我保證過的那樣,我們現在可以回到我們如何組織應用這個問題上了。我們現在在 <em>server.js</em> 檔案中有一個非常基礎的HTTP伺服器程式碼,而且我提到通常我們會有一個叫 <em>index.js</em> 的檔案去執行應用的其他模組(比如 <em>server.js</em> 中的HTTP伺服器模組)來引導和啟動應用。 </p>
<p>我們現在就來談談怎麼把server.js變成一個真正的Node.js模組,使它可以被我們(還沒動工)的 <em>index.js</em> 主檔案使用。</p>
<p>也許你已經注意到,我們已經在程式碼中使用了模組了。像這樣: </p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br></span><span class="pun">...</span><span
class="pln"><br><br>http</span><span class="pun">.</span><span class="pln">createServer</span><span
class="pun">(...);</span></pre>
<p>Node.js中自帶了一個叫做 "http" 的模組,我們在我們的程式碼中請求它並把回傳值賦給一個區域變數。 </p>
<p>這把我們的區域變數變成了一個擁有所有 <em>http</em> 模組所提供的公共方法的物件。</p>
<p>給這種區域變數起一個和模組名稱一樣的名字是一種慣例,但是你也可以按照自己的喜好來: </p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> foo </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br></span><span class="pun">...</span><span
class="pln"><br><br>foo</span><span class="pun">.</span><span class="pln">createServer</span><span
class="pun">(...);</span></pre>
<p>很好,怎麼使用Node.js內部模組已經很清楚了。我們怎麼建立自己的模組,又怎麼使用它呢? </p>
<p>等我們把 <em>server.js</em> 變成一個真正的模組,你就能搞明白了。 </p>
<p>事實上,我們不用做太多的修改。把某段程式碼變成模組意味著我們需要把我們希望提供其功能的部分 <em>匯出</em> 到請求這個模組的腳本。 </p>
<p>目前,我們的HTTP伺服器需要匯出的功能非常簡單,因為請求伺服器模組的腳本僅僅是需要啟動伺服器而已。 </p>
<p>我們把我們的伺服器腳本放到一個叫做 <em>start</em> 的函數裡,然後我們會匯出這個函數。 </p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br><br></span><span class="kwd">function</span><span
class="pln"> start</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span
class="pln"><br> </span><span class="kwd">function</span><span class="pln"> onRequest</span><span
class="pun">(</span><span class="pln">request</span><span class="pun">,</span><span class="pln"> response</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Request received."</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">writeHead</span><span class="pun">(</span><span class="lit">200</span><span
class="pun">,</span><span class="pln"> </span><span class="pun">{</span><span
class="str">"Content-Type"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"text/plain"</span><span
class="pun">});</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">write</span><span class="pun">(</span><span class="str">"Hello World"</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">end</span><span class="pun">();</span><span class="pln"><br> </span><span class="pun">}</span><span
class="pln"><br><br> http</span><span class="pun">.</span><span
class="pln">createServer</span><span class="pun">(</span><span class="pln">onRequest</span><span
class="pun">).</span><span class="pln">listen</span><span class="pun">(</span><span
class="lit">8888</span><span class="pun">);</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Server has started."</span><span
class="pun">);</span><span class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>exports</span><span
class="pun">.</span><span class="pln">start </span><span class="pun">=</span><span
class="pln"> start</span><span class="pun">;</span></pre>
<p>這樣,我們現在就可以建立我們的主檔案 <em>index.js</em> 並在其中啟動我們的HTTP了,雖然伺服器的程式碼還在 <em>server.js</em> 中。 </p>
<p>建立 <em>index.js</em> 檔案並寫入以下內容: </p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> server </span><span class="pun">=</span><span
class="pln"> require</span><span class="pun">(</span><span class="str">"./server"</span><span
class="pun">);</span><span class="pln"><br><br>server</span><span class="pun">.</span><span class="pln">start</span><span
class="pun">();</span></pre>
<p>正如你所看到的,我們可以像使用任何其他的內置模組一樣使用server模組:請求這個檔案並把它指向一個變數,其中已匯出的函數就可以被我們使用了。</p>
<p>好了。我們現在就可以從我們的主要腳本啟動我們的的應用了,而它還是老樣子:</p>
<pre>node index.js</pre>
<p>非常好,我們現在可以把我們的應用的不同部分放入不同的檔案裡,並且透過建置模組的方式把它們連接到一起了。 </p>
<p>我們仍然只擁有整個應用的最初部分:我們可以接收HTTP請求。但是我們得做點什麼——對於不同的URL請求,伺服器應該有不同的反應。 </p>
<p>對於一個非常簡單的應用來說,你可以直接在回呼(callback)函數 <em>onRequest()</em> 中做這件事情。不過就像我說過的,我們應該加入一些抽象的元素,讓我們的例子變得更有趣一點兒。 </p>
<p>處理不同的HTTP請求在我們的程式碼中是一個不同的部分,叫做 "路由選擇" ——那麼,我們接下來就創造一個叫做 <em>路由</em> 的模組吧。 </p>
<a name="whats-needed-to-route-requests"></a>
<h3>如何來進行請求的 "路由" </h3>
<p>我們要為路由提供請求的URL和其他需要的GET及POST參數,隨後路由需要根據這些資料來執行相應的程式碼(這裡 "程式碼" 對應整個應用的第三部分:一系列在接收到請求時真正工作的處理程序)。</p>
<p>因此,我們需要查看HTTP請求,從中提取出請求的URL以及GET/POST參數。這一功能應當屬於路由還是伺服器(甚至作為一個模組自身的功能)確實值得探討,但這裡暫定其為我們的HTTP伺服器的功能。</p>
<p>我們需要的所有資料都會包含在request物件中,該物件作為<em>onRequest()</em>回呼(callback)函數的第一個參數傳遞。但是為了解析這些資料,我們需要額外的Node.JS模組,它們分別是<em>url</em>和<em>querystring</em>模組。</p>
<pre> url.parse(string).query
|
url.parse(string).pathname |
| |
| |
------ -------------------
http://localhost:8888/start?foo=bar&hello=world
--- -----
| |
| |
querystring(string)["foo"] |
|
querystring(string)["hello"]
</pre>
<p>當然我們也可以用<em>querystring</em>模組來解析POST請求體中的參數,稍後會有示範。</p>
<p>現在我們來給<em>onRequest()</em>函數加上一些邏輯,用來找出瀏覽器請求的URL路徑:</p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br></span><span
class="kwd">var</span><span class="pln"> url </span><span class="pun">=</span><span
class="pln"> require</span><span class="pun">(</span><span class="str">"url"</span><span
class="pun">);</span><span class="pln"><br><br></span><span class="kwd">function</span><span
class="pln"> start</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span
class="pln"><br> </span><span class="kwd">function</span><span class="pln"> onRequest</span><span
class="pun">(</span><span class="pln">request</span><span class="pun">,</span><span class="pln"> response</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> </span><span
class="kwd">var</span><span class="pln"> pathname </span><span class="pun">=</span><span class="pln"> url</span><span
class="pun">.</span><span class="pln">parse</span><span class="pun">(</span><span
class="pln">request</span><span class="pun">.</span><span class="pln">url</span><span
class="pun">).</span><span class="pln">pathname</span><span class="pun">;</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Request for "</span><span
class="pln"> </span><span class="pun">+</span><span class="pln"> pathname </span><span
class="pun">+</span><span class="pln"> </span><span class="str">" received."</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">writeHead</span><span class="pun">(</span><span class="lit">200</span><span
class="pun">,</span><span class="pln"> </span><span class="pun">{</span><span
class="str">"Content-Type"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"text/plain"</span><span
class="pun">});</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">write</span><span class="pun">(</span><span class="str">"Hello World"</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">end</span><span class="pun">();</span><span class="pln"><br> </span><span class="pun">}</span><span
class="pln"><br><br> http</span><span class="pun">.</span><span
class="pln">createServer</span><span class="pun">(</span><span class="pln">onRequest</span><span
class="pun">).</span><span class="pln">listen</span><span class="pun">(</span><span
class="lit">8888</span><span class="pun">);</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Server has started."</span><span
class="pun">);</span><span class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>exports</span><span
class="pun">.</span><span class="pln">start </span><span class="pun">=</span><span
class="pln"> start</span><span class="pun">;</span></pre>
<p>好了,我們的應用現在可以透過請求的URL路徑來區別不同請求了--這使我們得以使用路由(還未完成)來將請求以URL路徑為基準映射到處理程序上。</p>
<p>在我們所要建構的應用中,這意味著來自<em>/start</em>和<em>/upload</em>的請求可以使用不同的程式碼來處理。稍後我們將看到這些內容是如何整合到一起的。</p>
<p>現在我們可以來編寫路由了,建立一個名為<em>router.js</em>的檔案,增加以下內容:</p>
<pre class="prettyprint lang-js"><span class="kwd">function</span><span class="pln"> route</span><span
class="pun">(</span><span class="pln">pathname</span><span class="pun">)</span><span
class="pln"> </span><span class="pun">{</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"About to route a request for "</span><span
class="pln"> </span><span class="pun">+</span><span class="pln"> pathname</span><span
class="pun">);</span><span class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>exports</span><span
class="pun">.</span><span class="pln">route </span><span class="pun">=</span><span
class="pln"> route</span><span class="pun">;</span></pre>
<p>如你所見,這段程式碼什麼也沒幹,不過對於現在來說這是應該的。在增加更多的邏輯以前,我們先來看看如何把路由和伺服器整合起來。</p>
<p>我們的伺服器應當知道路由的存在並加以有效利用。我們當然可以透過硬編碼的方式將這一依賴項綁定到伺服器上,但是其它語言的編程經驗告訴我們這會是一件非常痛苦的事,因此我們將使用依賴注入的方式較松散地增加路由模組(你可以讀讀<a href="http://martinfowler.com/articles/injection.html">Martin Fowlers關於依賴注入的大作</a>來作為背景知識)。</p>
<p>首先,我們來擴充一下伺服器的<em>start()</em>函數,以便將路由函數作為參數傳遞過去:</p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> http </span><span
class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"http"</span><span class="pun">);</span><span class="pln"><br></span><span
class="kwd">var</span><span class="pln"> url </span><span class="pun">=</span><span
class="pln"> require</span><span class="pun">(</span><span class="str">"url"</span><span
class="pun">);</span><span class="pln"><br><br></span><span class="kwd">function</span><span
class="pln"> start</span><span class="pun">(</span><span class="pln">route</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span
class="pln"><br> </span><span class="kwd">function</span><span class="pln"> onRequest</span><span
class="pun">(</span><span class="pln">request</span><span class="pun">,</span><span class="pln"> response</span><span
class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> </span><span
class="kwd">var</span><span class="pln"> pathname </span><span class="pun">=</span><span class="pln"> url</span><span
class="pun">.</span><span class="pln">parse</span><span class="pun">(</span><span
class="pln">request</span><span class="pun">.</span><span class="pln">url</span><span
class="pun">).</span><span class="pln">pathname</span><span class="pun">;</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Request for "</span><span
class="pln"> </span><span class="pun">+</span><span class="pln"> pathname </span><span
class="pun">+</span><span class="pln"> </span><span class="str">" received."</span><span
class="pun">);</span><span class="pln"><br><br> route</span><span class="pun">(</span><span
class="pln">pathname</span><span class="pun">);</span><span
class="pln"><br><br> response</span><span class="pun">.</span><span
class="pln">writeHead</span><span class="pun">(</span><span class="lit">200</span><span
class="pun">,</span><span class="pln"> </span><span class="pun">{</span><span
class="str">"Content-Type"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"text/plain"</span><span
class="pun">});</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">write</span><span class="pun">(</span><span class="str">"Hello World"</span><span
class="pun">);</span><span class="pln"><br> response</span><span class="pun">.</span><span
class="pln">end</span><span class="pun">();</span><span class="pln"><br> </span><span class="pun">}</span><span
class="pln"><br><br> http</span><span class="pun">.</span><span
class="pln">createServer</span><span class="pun">(</span><span class="pln">onRequest</span><span
class="pun">).</span><span class="pln">listen</span><span class="pun">(</span><span
class="lit">8888</span><span class="pun">);</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Server has started."</span><span
class="pun">);</span><span class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>exports</span><span
class="pun">.</span><span class="pln">start </span><span class="pun">=</span><span
class="pln"> start</span><span class="pun">;</span></pre>
<p>同時,我們會相應擴充<em>index.js</em>,使得路由函數可以被注入到伺服器中:</p>
<pre class="prettyprint lang-js"><span class="kwd">var</span><span class="pln"> server </span><span class="pun">=</span><span
class="pln"> require</span><span class="pun">(</span><span class="str">"./server"</span><span
class="pun">);</span><span class="pln"><br></span><span class="kwd">var</span><span
class="pln"> router </span><span class="pun">=</span><span class="pln"> require</span><span class="pun">(</span><span
class="str">"./router"</span><span class="pun">);</span><span class="pln"><br><br>server</span><span
class="pun">.</span><span class="pln">start</span><span class="pun">(</span><span
class="pln">router</span><span class="pun">.</span><span class="pln">route</span><span
class="pun">);</span><span class="pln"><br></span></pre>
<p>在這裡,我們傳遞的函數依舊什麼也沒做。</p>
<p>如果現在啟動應用(<em>node index.js,始終記得這個命令行</em>),隨後請求一個URL,你將會看到應用輸出相應的訊息,這表明我們的HTTP伺服器已經在使用路由模組了,並會將請求的路徑傳遞給路由:</p>
<pre>bash$ node index.js
Request for /foo received.
About to route a request for /foo</pre>
<p>(以上輸出已經去掉了比較煩人的/favicon.ico請求相關的部分)。</p>
<a name="execution-in-the-kongdom-of-verbs"></a>
<h3>行為驅動執行</h3>
<p>請允許我再次脫離主題,在這裡談一談函數編程。</p>
<p>將函數作為參數傳遞並不僅僅出於技術上的考量。對軟體設計來說,這其實是個哲學問題。想想這樣的場景:在index檔案中,我們可以將<em>router</em>物件傳遞進去,伺服器隨後可以執行這個物件的<em>route</em>函數。</p>
<p>就像這樣,我們傳遞一個東西,然後伺服器利用這個東西來完成一些事。嗨~那個叫路由的東西,能幫我把這個路由一下嗎?</p>
<p>但是伺服器其實不需要這樣的東西。它只需要把事情做完就行,其實為了把事情做完,你根本不需要東西,你需要的是動作。也就是說,你不需要<em>名詞</em>,你需要<em>動詞</em>。</p>
<p>理解了這個概念裡最核心、最基本的思想轉換後,我自然而然地理解了函數編程。</p>
<p>我是在讀了Steve Yegge的大作<a href="http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html">名詞王國中的死刑</a>之後理解函數編程。你也去讀一讀這本書吧,真的。這是曾給予我閱讀的快樂的關於軟體的書籍之一。</p>
<a name="routing-to-real-request-handlers"></a>
<h3>路由給真正的請求處理程序</h3>
<p>回到正題,現在我們的HTTP伺服器和請求路由模組已經如我們的期望,可以相互交流了,就像一對親密無間的兄弟。</p>
<p>當然這還遠遠不夠,路由,顧名思義,是指我們要針對不同的URL有不同的處理方式。例如處理<em>/start</em>的 "業務邏輯" 就應該和處理<em>/upload</em>的不同。</p>
<p>在現在的實現下,路由過程會在路由模組中 "結束" ,並且路由模組並不是真正針對請求 "採取行動" 的模組,否則當我們的應用程式變得更為複雜時,將無法很好地擴充。</p>
<p>我們暫時把作為路由目標的函數稱為請求處理程序。現在我們不要急著來開發路由模組,因為如果請求處理程序沒有就緒的話,再怎麼完善路由模組也沒有多大意義。</p>
<p>應用程式需要新的部件,因此加入新的模組 -- 已經無需為此感到新奇了。我們來建立一個叫做requestHandlers的模組,並對於每一個請求處理程序,增加一個占位用函數,隨後將這些函數作為模組的方法匯出:</p>
<pre class="prettyprint lang-js"><span class="kwd">function</span><span class="pln"> start</span><span
class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Request handler 'start' was called."</span><span
class="pun">);</span><span class="pln"><br></span><span class="pun">}</span><span
class="pln"><br><br></span><span class="kwd">function</span><span class="pln"> upload</span><span
class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br> console</span><span
class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">"Request handler 'upload' was called."</span><span
class="pun">);</span><span class="pln"><br></span><span class="pun">}</span><span class="pln"><br><br>exports</span><span
class="pun">.</span><span class="pln">start </span><span class="pun">=</span><span
class="pln"> start</span><span class="pun">;</span><span class="pln"><br>exports</span><span
class="pun">.</span><span class="pln">upload </span><span class="pun">=</span><span
class="pln"> upload</span><span class="pun">;</span></pre>
<p>這樣我們就可以把請求處理程序和路由模組連接起來,讓路由 "有路可尋"。</p>