-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter3.html
1738 lines (1383 loc) · 140 KB
/
chapter3.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 lang="zh_CN">
<head>
<meta charset="utf-8" />
<title>第 3 章 基本静态的页面</title>
<meta name="author" content="Andor Chen" />
<link rel="stylesheet" href="assets/styles/style.css" />
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript" src="assets/js/global.js"></script>
</head>
<body>
<div class="wrapper">
<div class="header">
<h1 class="logo"><a class="ir" href="http://railstutorial-china.org/rails4">Ruby on Rails 教程</a></h1>
<p class="subtitle">Ruby on Rails Tutorial 原书第 2 版(涵盖 Rails 4)</p>
</div>
<div class="content">
<div class="item chapter">
<h1 id="chapter-3"><span>第 3 章</span> 基本静态的页面</h1>
<ol class="toc"> <li class="level-2">
<a href="#section-3-1">3.1 静态页面</a>
</li>
<li class="level-2">
<a href="#section-3-2">3.2 第一个测试</a>
</li>
<li class="level-3">
<a href="#section-3-2-1">3.2.1 测试驱动开发</a>
</li>
<li class="level-3">
<a href="#section-3-2-2">3.2.2 添加页面</a>
</li>
<li class="level-2">
<a href="#section-3-3">3.3 有点动态内容的页面</a>
</li>
<li class="level-3">
<a href="#section-3-3-1">3.3.1 测试标题的变化</a>
</li>
<li class="level-3">
<a href="#section-3-3-2">3.3.2 让标题测试通过</a>
</li>
<li class="level-3">
<a href="#section-3-3-3">3.3.3 嵌入式 Ruby</a>
</li>
<li class="level-3">
<a href="#section-3-3-4">3.3.4 使用布局文件来消除重复</a>
</li>
<li class="level-2">
<a href="#section-3-4">3.4 小结</a>
</li>
<li class="level-2">
<a href="#section-3-5">3.5 练习</a>
</li>
<li class="level-2">
<a href="#section-3-6">3.6 高级技术</a>
</li>
<li class="level-3">
<a href="#section-3-6-1">3.6.1 去掉 bundle exec</a>
</li>
<li class="level-3">
<a href="#section-3-6-2">3.6.2 使用 Guard 自动测试</a>
</li>
<li class="level-3">
<a href="#section-3-6-3">3.6.3 使用 Spork 加速测试</a>
</li>
<li class="level-3">
<a href="#section-3-6-4">3.6.4 在 Sublime Text 中进行测试</a>
</li>
</ol>
<div class="main">
<p>从本章开始我们要开发一个大型的示例程序,本书后续内容都会基于这个示例程序。最终完成的程序会包含用户、微博功能,以及完整的登录和用户身份验证系统,不过我们会从一个看似功能有限的话题出发——创建静态页面。这看似简单的一件事却是一个很好的锻炼,极具意义,对这个初建的程序而言也是个很好的开端。</p>
<p>虽然 Rails 是被设计用来开发基于数据库的动态网站的,不过它也能胜任使用纯 HTML 创建的静态页面。其实,使用 Rails 创建动态页面还有一点好处:我们可以方便的添加一小部分动态内容。这一章就会教你怎么做。在这个过程中我们还会一窥自动化测试(automated testing)的面目,自动化测试可以让我们确信自己编写的代码是正确的。而且,编写一个好的测试用例还可以让我们信心十足的重构(refactor)代码,修改实现过程但不影响最终效果。</p>
<p>本章有很多的代码,特别是在 <a href="chapter3.html#section-3-2">3.2 节</a>和<a href="chapter3.html#section-3-3">3.3 节</a>,如果你是 Ruby 初学者先不用担心没有理解这些代码。就像在 <a href="chapter1.html#section-1-1-1">1.1.1 节</a>中说过的,你可以直接复制粘贴测试代码,用来验证程序中代码的正确性而不用担心其工作原理。<a href="chapter4.html">第 4 章</a>会更详细的介绍 Ruby,你有的是机会来理解这些代码。还有 RSpec 测试,它在本书中会被反复使用,如果你现在有点卡住了,我建议你硬着头皮往下看,几章过后你就会惊奇地发现,原本看起来很费解的代码已经变得很容易理解了。(你还可以考虑学习 <a href="http://www.codeschool.com/courses/testing-with-rspec">Code School 的 RSpec 课程</a>,有位读者说这个课程解惑了很多 RSpec 疑问。)</p>
<p>类似第二章,在开始之前我们要先创建一个新的 Rails 项目,这里我们叫它 <code>sample_app</code>:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span><span class="nb">cd</span> ~/rails_projects
<span class="gp">$ </span>rails new sample_app --skip-test-unit
<span class="gp">$ </span><span class="nb">cd </span>sample_app
</pre></div>
</div>
<p>上面代码中传递给 <code>rails</code> 命令的 <code>--skip-test-unit</code> 选项的意思是让 Rails 不生成默认使用的 <code>Test::Unit</code> 测试框架对应的 <code>test</code> 文件夹。这样做并不是说我们不用写测试,而是从 <a href="chapter3.html#section-3-2">3.2 节</a>开始我们会使用另一个测试框架 RSpec 来写整个的测试用例。</p>
<p>类似 <a href="chapter2.html#section-2-1">2.1 节</a>,接下来我们要用文本编辑器打开并编辑 <code>Gemfile</code>,写入程序所需的 gem。这个示例程序会用到之前没用过的两个 gem:RSpec 所需的 gem 和针对 Rails 的 RSPec 库 gem。代码 3.1 所示的代码会包含这些 gem。(注意:如果此时你想安装这个示例程序用到的所有 gem,你应该使用代码 9.47 中的代码。)</p>
<div class="codeblock has-caption" id="codeblock-3-1"><p class="caption"><span>代码 3.1:</span>示例程序的 <code>Gemfile</code></p><div class="highlight type-ruby"><pre><span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
<span class="n">ruby</span> <span class="s1">'2.0.0'</span>
<span class="n">gem</span> <span class="s1">'rails'</span><span class="p">,</span> <span class="s1">'4.0.4'</span>
<span class="n">group</span> <span class="p">:</span><span class="n">development</span><span class="p">,</span> <span class="ss">:test</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'sqlite3'</span><span class="p">,</span> <span class="s1">'1.3.8'</span>
<span class="n">gem</span> <span class="s1">'rspec-rails'</span><span class="p">,</span> <span class="s1">'2.13.1'</span>
<span class="k">end</span>
<span class="n">group</span> <span class="p">:</span><span class="nb">test</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'selenium-webdriver'</span><span class="p">,</span> <span class="s1">'2.35.1'</span>
<span class="n">gem</span> <span class="s1">'capybara'</span><span class="p">,</span> <span class="s1">'2.1.0'</span>
<span class="k">end</span>
<span class="n">gem</span> <span class="s1">'sass-rails'</span><span class="p">,</span> <span class="s1">'4.0.1'</span>
<span class="n">gem</span> <span class="s1">'uglifier'</span><span class="p">,</span> <span class="s1">'2.1.1'</span>
<span class="n">gem</span> <span class="s1">'coffee-rails'</span><span class="p">,</span> <span class="s1">'4.0.1'</span>
<span class="n">gem</span> <span class="s1">'jquery-rails'</span><span class="p">,</span> <span class="s1">'2.2.1'</span>
<span class="n">gem</span> <span class="s1">'turbolinks'</span><span class="p">,</span> <span class="s1">'1.1.1'</span>
<span class="n">gem</span> <span class="s1">'jbuilder'</span><span class="p">,</span> <span class="s1">'1.0.2'</span>
<span class="n">group</span> <span class="p">:</span><span class="n">doc</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'sdoc'</span><span class="p">,</span> <span class="s1">'0.3.20'</span><span class="p">,</span> <span class="ss">require: </span><span class="kp">false</span>
<span class="k">end</span>
<span class="n">group</span> <span class="p">:</span><span class="n">production</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'pg'</span><span class="p">,</span> <span class="s1">'0.15.1'</span>
<span class="k">end</span>
</pre></div>
</div>
<p>上面的代码将 <code>rspec-rails</code> 放在了开发组中,这样我们就可以使用 RSpec 相关的生成器了,同样我们还把它放到了测试组中,这样才能在测试时使用。我们没必要单独的安装 RSpec,因为它是 rspec-rails 的依赖件(dependency),会被自动安装。我们还加入了 <a href="https://github.com/jnicklas/capybara">Capybara</a>,这个 gem 允许我们使用类似英语中的句法编写模拟与应用程序交互的代码,还有 Capybara 的一个依赖库 <a href="http://docs.seleniumhq.org/projects/webdriver/">Selenium</a>。<sup class="footnote" id="fnref-3-1"><a href="#fn-3-1" rel="footnote">1</a></sup> 和<a href="chapter2.html">第 2 章</a>一样,我们还要把 PostgreSQL 所需的 gem 加入生产组,这样才能部署到 Heroku:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">group</span> <span class="p">:</span><span class="n">production</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'pg'</span><span class="p">,</span> <span class="s1">'0.15.1'</span>
<span class="k">end</span>
</pre></div>
</div>
<p>Heroku 推荐在开发环境和生产环境使用相同的数据库,不过对我们的示例程序而言没什么影响,SQLite 比 PostgreSQL 更容易安装和配置。在你的电脑中安装和配置 PostgreSQL 会作为一个练习。(参见 <a href="chapter3.html#section-3-5">3.5 节</a>)</p>
<p>要安装和包含这些新加的 gem,请运行 <code>bundle update</code> 和 <code>bundle install</code>:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle install --without production
<span class="gp">$ </span>bundle update
<span class="gp">$ </span>bundle install
</pre></div>
</div>
<p>与 <a href="chapter1.html#section-1-4-1">1.4.1 节</a> 和第 2 章一样,我们使用 <code>-without production</code> 禁止安装生产环境所需的 gem。这个选项会被记住,所以后续调用 Bundler 就不用再指定这个选项,直接运行 <code>bundle install</code> 就可以自动不安装生产环境所需的 gem。<sup class="footnote" id="fnref-3-2"><a href="#fn-3-2" rel="footnote">2</a></sup></p>
<p>因为我们会公开分享这个示例程序的源码,所以一定要修改“安全权标”。Rails 使用安全权标来加密会话。我们要把硬编码的字符串改为动态生成的(如代码 3.2)。(代码 3.2 中的代码现在看来是很高级的,但这是一个很严重的安全隐患,所以我觉得有必要尽早加入这段代码。)请确保使用了代码 1.7 所示的 <code>.gitignore</code> 文件,不把 <code>.secret</code> 纳入仓库。</p>
<div class="codeblock has-caption" id="codeblock-3-2"><p class="caption"><span>代码 3.2:</span>动态生成安全权标</p><p class="file"><code>config/initializers/secret_token.rb</code></p><div class="highlight type-ruby"><pre><span class="c1"># Be sure to restart your server when you modify this file.</span>
<span class="c1"># Your secret key is used for verifying the integrity of signed cookies.</span>
<span class="c1"># If you change this key, all old signed cookies will become invalid!</span>
<span class="c1"># Make sure the secret is at least 30 characters and all random,</span>
<span class="c1"># no regular words or you'll be exposed to dictionary attacks.</span>
<span class="c1"># You can use `rake secret` to generate a secure secret key.</span>
<span class="c1"># Make sure your secret_key_base is kept private</span>
<span class="c1"># if you're sharing your code publicly.</span>
<span class="nb">require</span> <span class="s1">'securerandom'</span>
<span class="k">def</span> <span class="nf">secure_token</span>
<span class="n">token_file</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'.secret'</span><span class="p">)</span>
<span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">token_file</span><span class="p">)</span>
<span class="c1"># Use the existing token.</span>
<span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">token_file</span><span class="p">).</span><span class="nf">chomp</span>
<span class="k">else</span>
<span class="c1"># Generate a new token and store it in token_file.</span>
<span class="n">token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">token_file</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="n">token</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">SampleApp</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">secret_key_base</span> <span class="o">=</span> <span class="n">secure_token</span>
</pre></div>
</div>
<p>接着我们要设置一下让 Rails 使用 RSpec 而不用 <code>Test::Unit</code>。这个设置可以通过 <code>rails generate rspec:install</code> 命令实现:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate rspec:install
</pre></div>
</div>
<p>如果系统提示缺少 JavaScript 运行时,你可以访问 <a href="https://github.com/sstephenson/execjs">execjs 在 GitHub 的页面</a>查看可以使用的运行时。 我一般都建议安装 <a href="http://nodejs.org/">Node.js</a>。</p>
<p>然后剩下的就是初始化 Git 仓库了:<sup class="footnote" id="fnref-3-3"><a href="#fn-3-3" rel="footnote">3</a></sup></p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git init
<span class="gp">$ </span>git add .
<span class="gp">$ </span>git commit -m <span class="s2">"Initial commit"</span>
</pre></div>
</div>
<p>和第一个程序一样,我建议你更新一下 <code>README</code> 文件,更好的描述这个程序,还可以提供一些帮助信息,可参照代码 3.3。</p>
<div class="codeblock has-caption" id="codeblock-3-3"><p class="caption"><span>代码 3.3:</span>示例程序改善后的 <code>README</code> 文件</p><div class="highlight type-plaintext"><pre># Ruby on Rails Tutorial: sample application
This is the sample application for
the [*Ruby on Rails Tutorial*](http://railstutorial.org/)
by [Michael Hartl](http://michaelhartl.com/).
</pre></div>
</div>
<p>然后添加 <code>.md</code> 后缀将其更改为 Markdown 格式,再提交所做的修改:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git mv README.rdoc README.md
<span class="gp">$ </span>git commit -am <span class="s2">"Improve the README"</span>
</pre></div>
</div>
<p>你可能还记得,在 <a href="chapter1.html#section-1-3-5">1.3.5 节</a>中,我们使用了 <code>git commit -a -m "Message"</code> 命令,指定了“全部变化”的旗标 <code>-a</code> 和提交信息旗标 <code>-m</code>。如上面第二个命令所示,可以把这两个旗标和在一起,使用 <code>git commit -am "Message"</code> 命令。</p>
<p>这个程序在本书的后续章节会一直使用,所以建议你在 GitHub 新建一个仓库,然后将代码推送上去:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git remote add origin https://github.com/<username>/sample_app.git
<span class="gp">$ </span>git push -u origin master
</pre></div>
</div>
<p>我自己也做了这一步,你可以在 GitHub 上找到<a href="https://github.com/railstutorial/sample_app_rails_4">这个示例程序的代码</a>。(我用了一个稍微不同的名字)<sup class="footnote" id="fnref-3-4"><a href="#fn-3-4" rel="footnote">4</a></sup></p>
<p>当然我们也可以选择在这个早期阶段将程序部署到 Heroku:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>heroku create
<span class="gp">$ </span>rake assets:precompile
<span class="gp">$ </span>git add .
<span class="gp">$ </span>git commit -m <span class="s2">"Add precompiled assets for Heroku"</span>
<span class="gp">$ </span>git push heroku master
<span class="gp">$ </span>heroku run rake db:migrate
</pre></div>
</div>
<p>在阅读本书的过程中,我建议你经常地推送并部署这个程序:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git push
<span class="gp">$ </span>git push heroku
<span class="gp">$ </span>heroku run rake db:migrate
</pre></div>
</div>
<p>这样你可在远端做个备份,也可以尽早的获知生成环境中出现的错误。如果你在 Heroku 遇到了问题,可以看一下生产环境的日志文件尝试解决:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>heroku logs
</pre></div>
</div>
<p>确保你做了代码 3.3 所示的设置。</p>
<p>所有的准备工作都结束了,下面要开始开发这个示例程序了。</p>
<h2 id='section-3-1'><span>3.1</span> 静态页面</h2>
<p>本节,我们要向开发动态页面迈出第一步,创建一些 Rails 动作和视图,但只包含静态 HTML。[^1-5]Rails 动作放在控制器(MVC 中的 C,参见 <a href="chapter1.html#section-1-2-6">1.2.6 节</a>)中,控制器中包含的动作都是为了实现同一类功能的。<a href="chapter2.html">第 2 章</a> 已经简要介绍了控制器,当更完整的熟悉 <a href="http://en.wikipedia.org/wiki/Representational_State_Transfer">REST 架构</a>后(从<a href="chapter6.html">第 6 章</a>开始)会更深入的理解控制器,本质上,控制器就是一组网页(可能是动态的)的容器。</p>
<p>现在回想一下 <a href="chapter1.html#section-1-2-3">1.2.3 节</a> 中讲过的 Rails 目录结构(图 1.2)会对我们有点帮助。本节主要的工作都在 <code>app/controllers</code> 和 <code>app/views</code> 文件夹中。(<a href="chapter3.html#section-3-2">3.2 节</a>中我们还会新建一个文件夹。)我建议你现在在文本编辑器或 IDE 中打开示例程序(参见<a href="chapter3.html#aside-3-1">旁注 3.1</a>。)。</p>
<div class="aside box" id="aside-3-1">
<h4><span>旁注 3.1:</span>打开项目</h4>
<p>在文本编辑器或 IDE 中打开整个 Rails 目录会带来很多便利。不过怎么做却取决于你的系统,大多数情况下你可以在命令行中用你选择的浏览器命令打开当前应用程序所在的目录,在 Unix 中当前目录就是一个点号(<code>.</code>):</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span><span class="nb">cd</span> ~/rails_projects/sample_app
<span class="gp">$ </span><editor name> .
</pre></div>
</div>
<p>例如,用 Sublime Text 打开示例程序,你可以输入:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>subl .
</pre></div>
</div>
<p>对于 Vim 来说,输入</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>vim .
</pre></div>
</div>
<p>(你可以根据你使用的 Vim 变种,把 <code>vim</code> 替换成 <code>gvim</code> 或 <code>mvim</code>。)</p>
</div>
<p>如 <a href="chapter1.html#section-1-3-5">1.3.5 节</a> 所说,在开始之前,如果使用 Git,最好别在主分支上开发,要新建一个从分支。如果你正使用 Git 做版本控制,应该执行下面的命令:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git checkout -b static-pages
</pre></div>
</div>
<p>Rails 提供了一个脚本用来创建控制器,叫做 <code>generate</code>,只要提供控制器的名字就可以运行了。既然我们要创建的控制器是处理静态页面的,那就叫它 StaticPages 吧。我们计划要生成处理“首页”,“帮助”页面和“关于”页面的动作。<code>generate</code> 命令可以接受一个可选的参数,指定要生成的动作,现在我们只提供两个动作,如代码 3.4 所示。</p>
<div class="codeblock has-caption" id="codeblock-3-4"><p class="caption"><span>代码 3.4:</span>生成 StaticPages 控制器</p><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate controller StaticPages home <span class="nb">help</span> --no-test-framework
create app/controllers/static_pages_controller.rb
route get <span class="s2">"static_pages/help"</span>
route get <span class="s2">"static_pages/home"</span>
invoke erb
create app/views/static_pages
create app/views/static_pages/home.html.erb
create app/views/static_pages/help.html.erb
invoke helper
create app/helpers/static_pages_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/static_pages.js.coffee
invoke scss
create app/assets/stylesheets/static_pages.css.scss
</pre></div>
</div>
<p>我们使用了 <code>--no-test-framework</code> 选项禁止生成 RSpec 测试代码,因为我们不想自动生成,在 <a href="chapter3.html#section-3-2">3.2 节</a>会手动创建测试。同时我们还故意从命令行参数中省去了 <code>about</code> 动作,稍后我们会看到如何通过 TDD 添加它(<a href="chapter3.html#section-3-2">3.2 节</a>)。</p>
<p>注意,在代码 3.4 中,传入命令的控制器名使用的是<a href="https://en.wikipedia.org/wiki/CamelCase">驼峰式</a>命名法,生成的控制器文件名使用的是<a href="https://en.wikipedia.org/wiki/Snake_case">蛇底式</a>命名法,因此名为 StaticPages 的控制器对应的文件名为 <code>static_pages_controller.rb</code>。这只是一个约定,其实在命令行中使用蛇底式也可以,如下的命令</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate controller static_pages ...
</pre></div>
</div>
<p>同样会生成 <code>static_pages_controller.rb</code> 控制器文件。因为 Ruby 开发者使用驼峰式命名类名(参见 <a href="chapter4.html#section-4-4">4.4 节</a>),所以我喜欢用驼峰式引用控制器,当然这是个人喜好的问题。(因为 Ruby 文件习惯使用蛇底式名称,所以 Rails 生成器会调用 <a href="http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-underscore">underscore</a> 方法把驼峰式转换成蛇底式。)</p>
<p>顺便说一下,如果在生成代码时出现了错误,知道如何撤销操作就很有用了。<a href="chapter3.html#aside-3-2">旁注 3.2</a> 中介绍了一些如何在 Rails 中撤销操作的方法。</p>
<div class="aside box" id="aside-3-2">
<h4><span>旁注 3.2:</span>撤销操作</h4>
<p>即使再小心,在开发 Rails 应用程序过程中仍然可能犯错。幸运的是,Rails 提供了一些工具能够帮助你进行复原。</p>
<p>举例来说,一个常见的情况是,你想更改控制器的名字,这时你就要撤销生成的代码。生成控制器时,除了控制器文件本身之外,Rails 还会生成很多其他的文件(参见代码 3.4)。撤销生成的文件不仅仅要删除主要的文件,还要删除一些辅助的文件。(事实上,我们还要撤销对 <code>routes.rb</code> 文件自动做的一些改动。)在 Rails 中,我们可以通过 <code>rails destroy</code> 命令完成这些操作。一般来说,下面的两个命令是相互抵消的:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate controller FooBars baz quux
<span class="gp">$ </span>rails destroy controller FooBars baz quux
</pre></div>
</div>
<p>同样的,在<a href="chapter6.html">第 6 章</a>中会使用下面的命令生成模型:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate model Foo bar:string baz:integer
</pre></div>
</div>
<p>生成的模型可通过下面的命令撤销:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails destroy model Foo
</pre></div>
</div>
<p>(对模型来说我们可以省略命令行中其余的参数。当阅读到<a href="chapter6.html">第六章</a>时,看看你能否发现为什么可以这么做。)</p>
<p>对模型来说涉及到的另一个技术是撤销迁移。<a href="chapter2.html">第 2 章</a>已经简要的介绍了迁移,<a href="chapter6.html">第 6 章</a>开始会更深入的介绍。迁移通过下面的命令改变数据库的状态:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rake db:migrate
</pre></div>
</div>
<p>我们可以使用下面的命令撤销一个迁移操作:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rake db:rollback
</pre></div>
</div>
<p>如果要回到最开始的状态,可以使用:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rake db:migrate <span class="nv">VERSION</span><span class="o">=</span>0
</pre></div>
</div>
<p>你可能已经猜到了,将数字 0 换成其他的数字就会回到相应的版本状态,这些版本数字是按照迁移顺序排序的。</p>
<p>拥有这些技术,我们就可以得心的应对开发过程中遇到的各种<a href="http://en.wikipedia.org/wiki/SNAFU">混乱(snafu)</a>了。</p>
</div>
<p>代码 3.4 中生成 StaticPages 控制器的命令会自动更新路由文件(route),叫做 <code>config/routes.rb</code>,Rails 会通过这个文件寻找 URL 和网页之间的对应关系。这是我们第一次讲到 <code>config</code> 目录,所以让我们看一下该目录的结构吧(如图 3.1)。<code>config</code> 目录如其名字所示,是存储 Rails 应用程序中的设置文件的。</p>
<div class="figure" id="figure-3-1">
<img src="figures/config_directory_rails_4.png" alt="config directory rails 4" />
<p class="caption"><span>图 3.1:</span>示例程序的 <code>config</code> 文件夹</p>
</div>
<p>因为我们生成了 <code>home</code> 和 <code>help</code> 动作,路由文件中已经为它们生成了配置,如代码 3.5。</p>
<div class="codeblock has-caption" id="codeblock-3-5"><p class="caption"><span>代码 3.5:</span>StaticPages 控制器中 <code>home</code> 和 <code>help</code> 动作的路由配置</p><p class="file"><code>config/routes.rb</code></p><div class="highlight type-ruby"><pre><span class="no">SampleApp</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">get</span> <span class="s2">"static_pages/home"</span>
<span class="n">get</span> <span class="s2">"static_pages/help"</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>如下的规则</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">get</span> <span class="s2">"static_pages/home"</span>
</pre></div>
</div>
<p>将来自 /static_pages/home 的请求映射到 StaticPages 控制器的 <code>home</code> 动作上。另外,当使用 <code>get</code> 时会将其对应到 GET 请求方法上,GET 是 HTTP(超文本传输协议,Hypertext Transfer Protocol)支持的基本方法之一(参见<a href="chapter3.html#aside-3-3">旁注 3.3</a>)。在我们这个例子中,当我们在 StaticPages 控制器中生成 <code>home</code> 动作时,就自动的在 /static_pages/home 地址上获得了一个页面。访问 <a href="http://localhost:3000/static_pages/home">/static_pages/home</a> 可以查看这个页面(如图 3.2)。</p>
<div class="aside box" id="aside-3-3">
<h4><span>旁注 3.3:</span>GET 等</h4>
<p>超文本传输协议(HTTP)定义了几个基本操作,GET、POST、PATCH 和 DELETE。这四个动词表现了客户端电脑(通常会运行一个浏览器,例如 Firefox 或 Safari)和服务器(通常会运行一个 Web 服务器,例如 Apache 或 Nginx)之间的操作。(有一点很重要需要你知道,当在本地电脑上开发 Rails 应用程序时,客户端和服务器是在同一个物理设备上的,但是二者是不同的概念。)受 REST 架构影响的 Web 框架(包括 Rails)都很重视对 HTTP 动词的实现,我们在<a href="chapter2.html">第 2 章</a>已经简要介绍了 REST,从<a href="chapter7.html">第 7 章</a>开始会做更详细的介绍。</p>
<p>GET 是最常用的 HTTP 操作,用来从网络上读取数据,它的意思是“读取一个网页”,当你访问 google.com 或 wikipedia.org 时,你的浏览器发出的就是 GET 请求。POST 是第二种最常用的操作,当你提交表单时浏览器发送的就是 POST 请求。在 Rails 应用程序中,POST 请求一般被用来创建某个东西(不过 HTTP 也允许 POST 进行更新操作)。例如,你提交注册表单时发送的 POST 请求就会在网站中创建一个新用户。剩下的两个动词,PATCH 和 DELETE 分别用来更新和销毁服务器上的某个东西。这两个操作比 GET 和 POST 少用一些,因为浏览器没有内建对这两种请求的支持,不过有些 Web 框架(包括 Rails)通过一些聪明的处理方式,看起来就像是浏览器发出的一样。</p>
<p>顺便说一下,在 Rails 之前的版本中,没有使用 PATCH,用的是 PUT,Rails 4 仍然支持 PUT,但 PATCH 能<a href="http://weblog.rubyonrails.org/2012/2/25/edge-rails-patch-is-the-new-primary-http-method-for-updates/">更好的表明 HTTP 请求的意图</a>,所以最好在新版中使用 PATCH。</p>
</div>
<div class="figure" id="figure-3-2">
<img src="figures/raw_home_view_31.png" alt="raw home view 31" />
<p class="caption"><span>图 3.2:</span>简陋的“首页”视图(<a href="http://localhost:3000/static_pages/home">/static_pages/home</a>)</p>
</div>
<p>要想弄明白这个页面是怎么来的,让我们在浏览器中看一下 StaticPages 控制器文件吧,你应该会看到类似代码 3.6 的内容。你可能已经注意到了,不像第 2 章中的 Users 和 Microposts 控制器,StaticPages 控制器没有使用标准的 REST 动作。这对静态页面来说是很常见的,REST 架构并不能解决所有的问题。</p>
<div class="codeblock has-caption" id="codeblock-3-6"><p class="caption"><span>代码 3.6:</span>代码 3.4 生成的 StaticPages 控制器</p><p class="file"><code>app/controllers/static_pages_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">StaticPagesController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">home</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">help</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>从上面代码中的 <code>class</code> 可以看到 <code>static_pages_controller.rb</code> 文件定义了一个类(class),叫做 <code>StaticPagesController</code>。类是一种组织函数(也叫方法)的有效方式,例如 <code>home</code> 和 <code>help</code> 动作就是方法,使用 <code>def</code> 关键字定义。尖括号 <code><</code> 说明 <code>StaticPagesController</code> 是继承自 Rails 的 <code>ApplicationController</code> 类,这就意味着我们定义的页面拥有了 Rails 提供的大量功能。(我们会在 <a href="chapter4.html#section-4-4">4.4 节</a>中更详细的介绍类和继承。)</p>
<p>在本例中,StaticPages 控制器的两个方法默认都是空的:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">def</span> <span class="nf">home</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">help</span>
<span class="k">end</span>
</pre></div>
</div>
<p>如果是普通的 Ruby 代码,这两个方法什么也做不了。不过在 Rails 中就不一样了,<code>StaticPagesController</code> 是一个 Ruby 类,因为它继承自 <code>ApplicationController</code>,它的方法对 Rails 来说就有特殊的意义了:访问 /static_pages/home 时,Rails 在 StaticPages 控制器中寻找 <code>home</code> 动作,然后执行该动作,再渲染相应的视图(<a href="chapter1.html#section-1-2-6">1.2.6 节</a>中介绍的 MVC 中的 V)。在本例中,<code>home</code> 动作是空的,所以访问 /static_pages/home 后只会渲染视图。那么,视图是什么样子,怎么才能找到它呢?</p>
<p>如果你再看一下代码 3.4 的输出,或许你能猜到动作和视图之间的对应关系:<code>home</code> 动作对应的视图叫做 <code>home.html.erb</code>。<a href="chapter3.html#section-3-3">3.3 节</a>将告诉你 <code>.erb</code> 是什么意思。看到 <code>.html</code> 你或许就不会奇怪了,它基本上就是 HTML(代码 3.7)。</p>
<div class="codeblock has-caption" id="codeblock-3-7"><p class="caption"><span>代码 3.7:</span>为“首页”生成的视图</p><p class="file"><code>app/views/static_pages/home.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><h1></span>StaticPages#home<span class="nt"></h1></span>
<span class="nt"><p></span>Find me in app/views/static_pages/home.html.erb<span class="nt"></p></span>
</pre></div>
</div>
<p><code>help</code> 动作的视图代码类似(参见代码 3.8)。</p>
<div class="codeblock has-caption" id="codeblock-3-8"><p class="caption"><span>代码 3.8:</span>为“帮助”页面生成的视图</p><p class="file"><code>app/views/static_pages/help.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><h1></span>StaticPages#help<span class="nt"></h1></span>
<span class="nt"><p></span>Find me in app/views/static_pages/help.html.erb<span class="nt"></p></span>
</pre></div>
</div>
<p>这两个视图只是占位用的,它们的内容都包含了一个一级标题(<code>h1</code> 标签)和一个显示视图文件完整的相对路径的段落(<code>p</code> 标签)。我们会在 <a href="chapter3.html#section-3-3">3.3 节</a>中添加一些简单的动态内容。这些静态内容的存在是为了强调一个很重要的事情:Rails 的视图可以只包含静态的 HTML。</p>
<p>在本章剩下的内容中,我们会为“首页”和“帮助”页面添加一些内容,然后补上 <a href="chapter3.html#section-3-1">3.1 节</a>中丢下的“关于”页面。然后会添加少量的动态内容,在每个页面显示不同的标题。</p>
<p>在继续下面的内容之前,如果你使用 Git 的话最好将 StaticPages 控制器相关的文件加入仓库:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git add .
<span class="gp">$ </span>git commit -m <span class="s2">"Add a StaticPages controller"</span>
</pre></div>
</div>
<h2 id='section-3-2'><span>3.2</span> 第一个测试</h2>
<p>本书采用了一种直观的测试应用程序表现的方法,而不关注具体的实现过程,这是 TDD 的一个变种,叫做 BDD(行为驱动开发,Behavior-driven Development)。我们使用的主要工具是集成测试(integration test)和单元测试(unit test)。集成测试在 RSpec 中叫做 request spec,它允许我们模拟用户在浏览器中和应用程序进行交互的操作。和 Capybara 提供的自然语言句法(natural-language syntax)一起使用,集成测试提供了一种强大的方法来测试应用程序的功能,而不用在浏览器中手动检查每个页面。(BDD 另外一个受欢迎的选择是 Cucumber,在 <a href="chapter8.html#section-8-3">8.3 节</a>中会介绍。)</p>
<p>TDD 的好处在于测试优先,比编写应用程序的代码还早。刚接触的话要花一段时间才能适应这种方式,不过好处很明显。我们先写一个失败测试(failing test),然后编写代码使这个测试通过,这样我们就会相信测试真的是针对我们设想的功能。这种“失败-实现-通过”的开发循环包含了一个<a href="http://en.wikipedia.org/wiki/Flow_\(psychology\)">心流</a>,可以提高编程的乐趣并提高效率。测试还扮演着应用程序代码客户的角色,会提高软件设计的优雅性。</p>
<p>关于 TDD 有一点很重要需要你知道,它不是万用良药,没必要固执的认为总是要先写测试、测试要囊括程序所有的功能、所有情况都要写测试。例如,当你不确定如何处理某些编程问题时,通常推荐你跳过测试先编写代码看一下解决方法能否解决问题。(在<a href="http://en.wikipedia.org/wiki/Extreme_Programming">极限编程</a>中,这个过程叫做“探针实验(spike)”。)一旦看到了解决问题的曙光,你就可以使用 TDD 实现一个更完美的版本。</p>
<p>本节我们会使用 RSpec 提供的 <code>rspec</code> 命令运行测试。初看起来这样做是理所当然的,不过却不完美,如果你是个高级用户我建议你按照 <a href="chapter3.html#section-3-6">3.6 节</a>的内容设置一下你的系统。</p>
<h3 id='section-3-2-1'><span>3.2.1</span> 测试驱动开发</h3>
<p>在测试驱动开发中,我们先写一个会失败的测试,在很多测试工具中会将其显示为红色。然后编写代码让测试通过,显示为绿色。最后,如果需要的话,我们还会重构代码,改变实现的方式(例如消除代码重复)但不改变功能。这样的开发过程叫做“遇红,变绿,重构(Red, Green, Refactor)”。</p>
<p>我们先来使用 TDD 为“首页”增加一些内容,一个内容为 <code>Sample App</code> 的顶级标题(<code><h1></code>)。第一步要做的是为这些静态页面生成集成测试(request spec):</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate integration_test static_pages
invoke rspec
create spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>上面的代码会在 <code>spec/requests</code> 文件夹中生成 <code>static_pages_spec.rb</code> 文件。自动生成的代码不能满足我们的需求,用文本编辑器打开 <code>static_pages_spec.rb</code>,将其内容替换成代码 3.9 所示的代码。</p>
<div class="codeblock has-caption" id="codeblock-3-9"><p class="caption"><span>代码 3.9:</span>测试“首页”内容的代码</p><p class="file"><code>spec/requests/static_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Static pages"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"Home page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Sample App'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sample App'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>代码 3.9 是纯粹的 Ruby,不过即使你以前学习过 Ruby 也看不太懂,这是因为 RSpec 利用了 Ruby 语言的延展性定义了一套“领域专属语言”(Domain-specific Language, DSL)用来写测试代码。重要的是,如果你想使用 RSpec 不是一定要知道 RSpec 的句法。初看起来是有些神奇,RSpec 和 Capybara 就是这样设计的,读起来很像英语,如果你多看本书中的示例,很快你就会熟练了。</p>
<p>代码 3.9 包含了一个 <code>describe</code> 块以及其中的一个测试用例(sample),以 <code>it "..." do</code> 开头的代码块就是一个用例:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">describe</span> <span class="s2">"Home page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Sample App'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sample App'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>第一行代码指明我们描绘的是“首页”,内容就是一个字符串,如果需要你可以使用任何的字符串,RSpec 不做强制要求,不过你以及其他的人类读者或许会关心你用的字符串。然后测试说,如果你访问地址为 <code>/static_pages/home</code> 的“首页”时,其内容应该包含“Sample App”这两个词。和第一行一样,这个双引号中的内容 RSpec 没做要求,只要能为人类读者提供足够的信息就行了。下面这一行:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
</pre></div>
</div>
<p>使用了 Capybara 中的 <code>visit</code> 函数来模拟在浏览器中访问 <code>/static_pages/home</code> 的操作。下面这一行:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sample App'</span><span class="p">)</span>
</pre></div>
</div>
<p>使用了 <code>page</code> 变量(同样由 Capybara 提供)来测试页面中是否包含了正确的内容。</p>
<div class="figure" id="figure-3-3">
<img src="figures/red_failing_spec_4_0.png" alt="red failing spec 40" />
<p class="caption"><span>图 3.3:</span>红色(失败)的测试</p>
</div>
<p>若要测试正确运行,我们要在 <code>spec_helper.rb</code> 中加入一行代码,如代码 3.10 所示。(在本书后续的草稿中,我计划不再使用这行代码,启用<a href="https://www.relishapp.com/rspec/rspec-rails/docs/feature-specs/feature-spec">功能测试</a>中的新技术。)</p>
<div class="codeblock has-caption" id="codeblock-3-10"><p class="caption"><span>代码 3.10:</span>把 Capybara DSL 加入 RSpec 帮助文件</p><p class="file"><code>spec/spec_helper.rb</code></p><div class="highlight type-ruby"><pre><span class="c1"># This file is copied to spec/ when you run 'rails generate rspec:install'</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="o">.</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">config</span><span class="p">.</span><span class="nf">include</span> <span class="no">Capybara</span><span class="o">::</span><span class="no">DSL</span>
<span class="k">end</span>
</pre></div>
</div>
<p>我们有很多种方式来运行测试代码,<a href="chapter3.html#section-3-6">3.6 节</a>中还提供了一些便利且高级的方法。现在,我们在命令行中执行 <code>rspec</code> 命令(前面会加上 <code>bundle exec</code> 来保证 RSpec 运行在 <code>Gemfile</code> 指定的环境中):<sup class="footnote" id="fnref-3-6"><a href="#fn-3-6" rel="footnote">5</a></sup></p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>上述命令会输出一个失败测试。失败测试的具体样子取决于你的系统,在我的系统中它是红色的,如图 3.3。<sup class="footnote" id="fnref-3-7"><a href="#fn-3-7" rel="footnote">6</a></sup></p>
<p>要想让测试通过,我们要用代码 3.11 中的 HTML 替换掉默认的“首页”内容。</p>
<div class="codeblock has-caption" id="codeblock-3-11"><p class="caption"><span>代码 3.11:</span>让“首页”测试通过的代码</p><p class="file"><code>app/views/static_pages/home.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><h1></span>Sample App<span class="nt"></h1></span>
<span class="nt"><p></span>
This is the home page for the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/"</span><span class="nt">></span>Ruby on Rails Tutorial<span class="nt"></a></span>
sample application.
<span class="nt"></p></span>
</pre></div>
</div>
<p>这段代码中一级标题(<code><h1></code>)的内容是 <code>Sample App</code> 了,会让测试通过。我们还加了一个锚记标签 <code><a></code>,链接到一个给定的地址(在锚记标签中地址由“href”(hypertext reference)指定):</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/"</span><span class="nt">></span>Ruby on Rails Tutorial<span class="nt"></a></span>
</pre></div>
</div>
<p>现在再运行测试看一下结果:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>在我的系统中,通过的测试显示如图 3.4 所示。</p>
<p>基于上面针对“首页”的例子,或许你已经猜到了“帮助”页面类似的测试和程序代码。我们先来测试一下相应的内容,现在字符串变成“<code>Help</code>”了(参见代码 3.12)。</p>
<div class="codeblock has-caption" id="codeblock-3-12"><p class="caption"><span>代码 3.12:</span>添加测试“帮助”页面内容的代码</p><p class="file"><code>spec/requests/static_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Static pages"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"Home page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Sample App'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sample App'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"Help page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Help'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/help'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Help'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="figure" id="figure-3-4">
<img src="figures/green_passing_spec_4_0.png" alt="green passing spec 40" />
<p class="caption"><span>图 3.4:</span>绿色(通过)的测试</p>
</div>
<p>然后运行测试:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>有一个测试会失败。(因为系统的不同,而且统计每个阶段的测试数量很难,从现在开始我就不会再截图 RSpec 的输出结果了。)</p>
<p>程序所需的代码和代码 3.11 类似,如代码 3.13 所示。</p>
<div class="codeblock has-caption" id="codeblock-3-13"><p class="caption"><span>代码 3.13:</span>让“帮助”页面的测试通过的代码</p><p class="file"><code>app/views/static_pages/help.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><h1></span>Help<span class="nt"></h1></span>
<span class="nt"><p></span>
Get help on the Ruby on Rails Tutorial at the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/help"</span><span class="nt">></span>Rails Tutorial help page<span class="nt"></a></span>.
To get help on this sample app, see the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/book"</span><span class="nt">></span>Rails Tutorial book<span class="nt"></a></span>.
<span class="nt"></p></span>
</pre></div>
</div>
<p>现在测试应该可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<h3 id='section-3-2-2'><span>3.2.2</span> 添加页面</h3>
<p>看过了上面简单的 TDD 开发过程,下面我们要用这个技术完成一个稍微复杂一些的任务,添加一个新页面,就是 <a href="chapter3.html#section-3-1">3.1 节</a>中没有生成的“关于”页面。通过每一步中编写测试和运行 RSpec 的过程,我们会看到 TDD 是如何引导我们进行应用程序开发的。</p>
<h4 id='section-3-2-2-1'><span></span>遇红</h4>
<p>先来到“遇红-变绿”过程中的“遇红”部分,为“关于”页面写一个失败测试。参照代码 3.12 的代码,或许你已经知道如何写这个测试了(参见代码 3.14)。</p>
<div class="codeblock has-caption" id="codeblock-3-14"><p class="caption"><span>代码 3.14:</span>添加测试“关于”页面内容的代码</p><p class="file"><code>spec/requests/static_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Static pages"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"Home page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Sample App'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sample App'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"Help page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Help'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/help'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Help'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"About page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'About Us'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/about'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'About Us'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<h4 id='section-3-2-2-2'><span></span>变绿</h4>
<p>回顾一下 <a href="chapter3.html#section-3-1">3.1 节</a>的内容,在 Rails 中我们可以通过创建一个动作并添加相应的视图文件来生成静态页面。所以首先我们要在 StaticPages 控制器中添加一个 <code>about</code> 动作。我们已经写过失败测试了,现在已经确信,如果能通过,就创建了一个可以运行的“关于”页面。</p>
<p>如果你运行 RSpec 测试:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>输出的结果会提示下面的错误:</p>
<div class="codeblock"><div class="highlight type-shell"><pre>No route matches <span class="o">[</span>GET] <span class="s2">"/static_pages/about"</span>
</pre></div>
</div>
<p>这提醒我们要在路由文件中添加 <code>static_pages/about</code>,我们可以按照代码 3.5 所示的格式添加,结果如代码 3.15 所示。</p>
<div class="codeblock has-caption" id="codeblock-3-15"><p class="caption"><span>代码 3.15:</span>添加“关于”页面的路由</p><p class="file"><code>config/routes.rb</code></p><div class="highlight type-ruby"><pre><span class="no">SampleApp</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">get</span> <span class="s2">"static_pages/home"</span>
<span class="n">get</span> <span class="s2">"static_pages/help"</span>
<span class="n">get</span> <span class="s2">"static_pages/about"</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>现在运行测试</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>将提示如下错误</p>
<div class="codeblock"><div class="highlight type-shell"><pre>The action <span class="s1">'about'</span> could not be found <span class="k">for </span>StaticPagesController
</pre></div>
</div>
<p>为了解决这个问题,我们按照代码 3.6 中 <code>home</code> 和 <code>help</code> 的格式在 StaticPages 控制器中添加 <code>about</code> 动作的代码(如代码 3.16 所示)。</p>
<div class="codeblock has-caption" id="codeblock-3-16"><p class="caption"><span>代码 3.16:</span>添加了 <code>about</code> 动作的 StaticPages 控制器</p><p class="file"><code>app/controllers/static_pages_controller.rb</code></p><div class="highlight type-shell"><pre>class StaticPagesController < ApplicationController
def home
end
def <span class="nb">help
</span>end
def about
end
end
</pre></div>
</div>
<p>再运行测试</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>会提示缺少模板(template,例如一个视图):</p>
<div class="codeblock"><div class="highlight type-shell"><pre>Missing template static_pages/about
</pre></div>
</div>
<p>要解决这个问题,我们要添加 <code>about</code> 动作对应的视图。我们需要在 <code>app/views/static_pages</code> 目录下创建一个名为 <code>about.html.erb</code> 的新文件,写入代码 3.17 所示的内容。</p>
<div class="codeblock has-caption" id="codeblock-3-17"><p class="caption"><span>代码 3.17:</span>“关于”页面视图代码</p><p class="file"><code>app/views/static_pages/about.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><h1></span>About Us<span class="nt"></h1></span>
<span class="nt"><p></span>
The <span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/"</span><span class="nt">></span>Ruby on Rails Tutorial<span class="nt"></a></span>
is a project to make a book and screencasts to teach web development
with <span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://rubyonrails.org/"</span><span class="nt">></span>Ruby on Rails<span class="nt"></a></span>. This
is the sample application for the tutorial.
<span class="nt"></p></span>
</pre></div>
</div>
<p>再运行 RSpec 就应该“变绿”了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>当然,在浏览器中查看一下这个页面来确保测试没有失效也是个不错的主意。(如图 3.5)</p>
<h4 id='section-3-2-2-3'><span></span>重构</h4>
<p>现在测试已经变绿了,我们可以很自信的尽情重构了。我们的代码经常会“变味”(意思是代码会变得丑陋、啰嗦、大量的重复),电脑不会在意,但是人类会,所以经常重构让代码变得简洁是很重要的。这时候一个好的测试就显出其价值了,因为它可以降低重构过程中引入 bug 的风险。</p>
<p>我们的示例程序现在还很小没什么可重构的,不过代码无时无刻不在变味,所以我们的重构也不会等很久:在 <a href="chapter3.html#section-3-3-4">3.3.4 节</a>中就要忙于重构了。</p>
<h2 id='section-3-3'><span>3.3</span> 有点动态内容的页面</h2>
<p>到目前为止,我们已经为一些静态页面创建了动作和视图,我们还改变了每一个页面显示的内容(标题)让它看起来是动态的。改变标题到底算不算真正动态还有争议,不过前面的内容却可以为<a href="chapter7.html">第 7 章</a>介绍的真正动态打下基础。</p>
<p>如果你跳过了 <a href="chapter3.html#section-3-2">3.2 节</a>中的 TDD 部分,在继续阅读之前请先按照代码 3.15、代码 3.16 和代码 3.17 创建“关于”页面。</p>
<div class="figure" id="figure-3-5">
<img src="figures/about_us_2nd_edition.png" alt="about us 2nd edition" />
<p class="caption"><span>图 3.5:</span>新添加的“关于”页面(<a href="http://localhost:3000/static_pages/about">/static_pages/about</a>)</p>
</div>
<h3 id='section-3-3-1'><span>3.3.1</span> 测试标题的变化</h3>
<p>我们计划修改“首页”、“帮助”页面和“关于”页面的标题,在每一页都有所变化。这个过程将使用视图中的 <code><title></code> 标签。大多数浏览器会在浏览器窗口的顶部显示标题的内容(Google Chrome 是个特例),标题对搜索引擎优化也是很重要的。我们会先写测试标题的代码,然后添加标题,最后使用布局(layout)文件进行重构,削除重复。</p>
<p>你可能已经注意到了,<code>rails new</code> 命令已经创建了布局文件。稍后我们会介绍这个文件的作用,现在在继续之前先将其重命名:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>mv app/views/layouts/application.html.erb foobar <span class="c"># 临时修改</span>
</pre></div>
</div>
<p>(<code>mv</code> 是 Unix 命令,在 Windows 中你可以在文件浏览器中重命名或者使用 <code>rename</code> 命令。)在真正的应用程序中你不需要这么做,不过没有了这个文件之后你就能更容易理解它的作用。</p>
<p>本节结束后,三个静态页面的标题都会是“Ruby on Rails Tutorial Sample App | Home”这种形式,标题的后面一部分会根据所在的页面而有所不同(参见<a href="chapter3.html#table-3-1">表格 3.1</a>)。我们接着代码 3.14 中的测试,添加代码 3.18 所示测试标题的代码。</p>
<div class="codeblock has-caption" id="codeblock-3-18"><p class="caption"><span>代码 3.18:</span>标题测试</p><div class="highlight type-ruby"><pre><span class="n">it</span> <span class="s2">"should have the right title"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Ruby on Rails Tutorial Sample App | Home"</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
</div>
<p><code>have_title</code> 方法会测试一个 HTML 页面标题中是否含有指定的内容。换句话说,下面的代码:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Ruby on Rails Tutorial Sample App | Home"</span><span class="p">)</span>
</pre></div>
</div>
<p>会检查 <code>title</code> 标签的内容是否为</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="s2">"Ruby on Rails Tutorial Sample App | Home"</span>
</pre></div>
</div>
<p>要注意一下,检查的内容不一定要完全匹配,任何的子字符串都可以,所以</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Home"</span><span class="p">)</span>
</pre></div>
</div>
<p>也会匹配完整形式的标题。</p>
<div class="table has-caption" id="table-3-1"><p class="caption"><span>表格 3.1:</span>示例程序中基本上是静态内容的页面</p><table>
<thead>
<tr>
<th>页面</th>
<th>URL</th>
<th>基本标题</th>
<th>变动部分</th>
</tr>
</thead>
<tbody>
<tr>
<td>首页</td>
<td>/static_pages/home</td>
<td><code>"Ruby on Rails Tutorial Sample App"</code></td>
<td><code>"Home"</code></td>
</tr>
<tr>
<td>帮助</td>
<td>/static_pages/help</td>
<td><code>"Ruby on Rails Tutorial Sample App"</code></td>
<td><code>"Help"</code></td>
</tr>
<tr>
<td>关于</td>
<td>/static_pages/about</td>
<td><code>"Ruby on Rails Tutorial Sample App"</code></td>
<td><code>"About"</code></td>
</tr>
</tbody>
</table>
</div>
<p>我们按照代码 3.18 的格式为三个静态页面都加上测试代码,结果参照代码 3.19。注意,测试中有很多重复,我们会在 <a href="chapter3.html#section-3-4">5.3.4 节</a>重构。</p>
<div class="codeblock has-caption" id="codeblock-3-19"><p class="caption"><span>代码 3.19:</span>StaticPages 控制器的测试文件,包含标题测试</p><p class="file"><code>spec/requests/static_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Static pages"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"Home page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Sample App'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sample App'</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"should have the title 'Home'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/home'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Ruby on Rails Tutorial Sample App | Home"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"Help page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'Help'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/help'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Help'</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"should have the title 'Help'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/help'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Ruby on Rails Tutorial Sample App | Help"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"About page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should have the content 'About Us'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/about'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'About Us'</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"should have the title 'About Us'"</span> <span class="k">do</span>
<span class="n">visit</span> <span class="s1">'/static_pages/about'</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Ruby on Rails Tutorial Sample App | About Us"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>现在已经有了如代码 3.19 所示的测试,你应该运行</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>确保得到的结果是红色的(失败的测试)。</p>
<h3 id='section-3-3-2'><span>3.3.2</span> 让标题测试通过</h3>
<p>现在我们要让标题测试通过,同时我们还要完善 HTML,让它通过验证。符合最新标准的页面结构是下面这种形式的:</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Greeting<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><p></span>Hello, world!<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<p>这种结构的最顶部包含一个文档类型声明(document type declaration,简称 doctype),告知浏览器所用的 HTML 版本(本例使用 <a href="http://en.wikipedia.org/wiki/HTML5">HTML5</a>)<sup class="footnote" id="fnref-3-8"><a href="#fn-3-8" rel="footnote">7</a></sup>。随后是 <code>head</code> 部分,包含一个 <code>title</code> 标签,其内容为“Greeting”。然后是 <code>body</code> 部分,包含一个 <code>p</code> 标签(表示段落),其内容为“Hello, world!”。(文件内容的缩进是可选的,HTML 不会特别对待空白,Tab 和空格都会被忽略,但缩进可以让文档更易阅读。)</p>
<p>在“首页”中使用这种基本结构后得到的文档如代码 3.20 所示。</p>
<div class="codeblock has-caption" id="codeblock-3-20"><p class="caption"><span>代码 3.20:</span>“首页”的完整 HTML</p><p class="file"><code>app/views/static_pages/home.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Ruby on Rails Tutorial Sample App | Home<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Sample App<span class="nt"></h1></span>
<span class="nt"><p></span>
This is the home page for the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/"</span><span class="nt">></span>Ruby on Rails Tutorial<span class="nt"></a></span>
sample application.
<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<p>代码 3.20 使用了代码 3.19 中测试用到的标题:</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><title></span>Ruby on Rails Tutorial Sample App | Home<span class="nt"></title></span>
</pre></div>
</div>
<p>所以,“首页”的测试现在应该可以通过了。你还会看到红色的错误提示是因为“帮助”页面和“关于”页面的测试还是失败的,我们使用代码 3.21 和代码 3.22 中的代码让它们也通过测试。</p>
<div class="codeblock has-caption" id="codeblock-3-21"><p class="caption"><span>代码 3.21:</span>“帮助”页面的完整 HTML</p><p class="file"><code>app/views/static_pages/help.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Ruby on Rails Tutorial Sample App | Help<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Help<span class="nt"></h1></span>
<span class="nt"><p></span>
Get help on the Ruby on Rails Tutorial at the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/help"</span><span class="nt">></span>Rails Tutorial help page<span class="nt"></a></span>.
To get help on this sample app, see the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/book"</span><span class="nt">></span>Rails Tutorial book<span class="nt"></a></span>.
<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-3-22"><p class="caption"><span>代码 3.22:</span>“关于”页面的完整 HTML</p><p class="file"><code>app/views/static_pages/about.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Ruby on Rails Tutorial Sample App | About Us<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>About Us<span class="nt"></h1></span>
<span class="nt"><p></span>
The <span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/"</span><span class="nt">></span>Ruby on Rails Tutorial<span class="nt"></a></span>
is a project to make a book and screencasts to teach web development
with <span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://rubyonrails.org/"</span><span class="nt">></span>Ruby on Rails<span class="nt"></a></span>. This
is the sample application for the tutorial.
<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<h3 id='section-3-3-3'><span>3.3.3</span> 嵌入式 Ruby</h3>
<p>本节到目前为止已经做了很多事情,我们通过 Rails 控制器和动作生成了三个可以通过句法验证的页面,不过这些页面都是纯静态的 HTML,没有体现出 Rails 的强大所在。而且,它们的代码充斥着重复:</p>
<ul>
<li>页面的标签几乎(但不完全)是一模一样的</li>
<li>每个标题中都有“Ruby on Rails Tutorial Sample App”</li>
<li>HTML 结构在每个页面都重复的出现了</li>
</ul>
<p>代码重复的问题违反了很重要的“不要自我重复”(Don’t Repeat Yourself, DRY)原则,本小节和下一小节将按照 DRY 原则去掉重复的代码。</p>
<p>不过我们去除重复的第一步却是要增加一些代码让页面的标题看起来是一样的。这样我们就可以更容易的去掉重复的代码了。</p>
<p>这个过程会在视图中使用嵌入式 Ruby(Embedded Ruby)。既然“首页”、“帮助”页面和“关于”页面的标题有一个变动的部分,那我们就利用一个 Rails 中特别的函数 <code>provide</code> 在每个页面设定不同的标题。通过将视图 <code>home.html.erb</code> 标题中的“Home”换成如代码 3.23 所示的代码,我们可以看一下实现的过程。</p>
<div class="codeblock has-caption" id="codeblock-3-23"><p class="caption"><span>代码 3.23:</span>标题中使用了嵌入式 Ruby 代码的“首页”视图</p><p class="file"><code>app/views/static_pages/home.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'Home'</span><span class="p">)</span> <span class="cp">%></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Ruby on Rails Tutorial Sample App | <span class="cp"><%=</span> <span class="k">yield</span><span class="p">(</span><span class="ss">:title</span><span class="p">)</span> <span class="cp">%></span><span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Sample App<span class="nt"></h1></span>
<span class="nt"><p></span>
This is the home page for the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/"</span><span class="nt">></span>Ruby on Rails Tutorial<span class="nt"></a></span>
sample application.
<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<p>代码 3.23 中我们第一次使用了嵌入式 Ruby,简称 ERb。(现在你应该知道为什么 HTML 视图文件的扩展名是 <code>.html.erb</code> 了。)ERb 是为网页添加动态内容使用的主要模板系统。<sup class="footnote" id="fnref-3-9"><a href="#fn-3-9" rel="footnote">8</a></sup>下面的代码</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'Home'</span><span class="p">)</span> <span class="cp">%></span>
</pre></div>
</div>
<p>通过 <code><% ... %></code> 调用 Rails 中的 <code>provide</code> 函数,然后将字符串 <code>'Home'</code> 赋给 <code>:title</code>。<sup class="footnote" id="fnref-3-10"><a href="#fn-3-10" rel="footnote">9</a></sup>然后,在标题中,我们使用类似的符号 <code><%= ... %></code> 通过 Ruby 的 <code>yield</code> 函数将标题插入模板中:<sup class="footnote" id="fnref-3-11"><a href="#fn-3-11" rel="footnote">10</a></sup></p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="nt"><title></span>Ruby on Rails Tutorial Sample App | <span class="cp"><%=</span> <span class="k">yield</span><span class="p">(</span><span class="ss">:title</span><span class="p">)</span> <span class="cp">%></span><span class="nt"></title></span>
</pre></div>
</div>
<p>(这两种嵌入 Ruby 代码的方式区别在于,<code><% ... %></code> <strong>执行</strong>其中的代码,<code><%= ... %></code> 也会执行其中的代码,而且会把执行的结果<strong>插入</strong>模板中。)最终得到的结果和以前是一样的,只不过标题中变动的部分现在是通过 ERb 动态生成的。</p>
<p>我们可以运行 <a href="chapter3.html#section-3-3-1">3.3.1 节</a>中的测试来证实一下,测试还是会通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/static_pages_spec.rb
</pre></div>
</div>
<p>然后我们要对“帮助”页面和“关于”页面做相应的修改了。(参见代码 3.24 和代码 3.25。)</p>
<div class="codeblock has-caption" id="codeblock-3-24"><p class="caption"><span>代码 3.24:</span>标题中使用了嵌入式 Ruby 代码的“帮助”页面视图</p><p class="file"><code>app/views/static_pages/help.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'Help'</span><span class="p">)</span> <span class="cp">%></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Ruby on Rails Tutorial Sample App | <span class="cp"><%=</span> <span class="k">yield</span><span class="p">(</span><span class="ss">:title</span><span class="p">)</span> <span class="cp">%></span><span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Help<span class="nt"></h1></span>
<span class="nt"><p></span>
Get help on the Ruby on Rails Tutorial at the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/help"</span><span class="nt">></span>Rails Tutorial help page<span class="nt"></a></span>.
To get help on this sample app, see the
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://railstutorial.org/book"</span><span class="nt">></span>Rails Tutorial book<span class="nt"></a></span>.
<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-3-25"><p class="caption"><span>代码 3.25:</span>标题中使用了嵌入式 Ruby 代码的“关于”页面视图</p><p class="file"><code>app/views/static_pages/about.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'About Us'</span><span class="p">)</span> <span class="cp">%></span>
<span class="cp"><!DOCTYPE html></span>