-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter7.html
1602 lines (1332 loc) · 141 KB
/
chapter7.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>第 7 章 用户注册</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-7"><span>第 7 章</span> 用户注册</h1>
<ol class="toc"> <li class="level-2">
<a href="#section-7-1">7.1 显示用户信息</a>
</li>
<li class="level-3">
<a href="#section-7-1-1">7.1.1 调试信息和 Rails 环境</a>
</li>
<li class="level-3">
<a href="#section-7-1-2">7.1.2 Users 资源</a>
</li>
<li class="level-3">
<a href="#section-7-1-3">7.1.3 使用预构件测试用户资料页面</a>
</li>
<li class="level-3">
<a href="#section-7-1-4">7.1.4 添加 Gravatar 头像和侧边栏</a>
</li>
<li class="level-2">
<a href="#section-7-2">7.2 注册表单</a>
</li>
<li class="level-3">
<a href="#section-7-2-1">7.2.1 测试用户注册功能</a>
</li>
<li class="level-3">
<a href="#section-7-2-2">7.2.2 使用 form_for</a>
</li>
<li class="level-3">
<a href="#section-7-2-3">7.2.3 表单的 HTML</a>
</li>
<li class="level-2">
<a href="#section-7-3">7.3 注册失败</a>
</li>
<li class="level-3">
<a href="#section-7-3-1">7.3.1 可正常使用的表单</a>
</li>
<li class="level-3">
<a href="#section-7-3-2">7.3.2 健壮参数</a>
</li>
<li class="level-3">
<a href="#section-7-3-3">7.3.3 注册时的错误提示信息</a>
</li>
<li class="level-2">
<a href="#section-7-4">7.4 注册成功</a>
</li>
<li class="level-3">
<a href="#section-7-4-1">7.4.1 完整的注册表单</a>
</li>
<li class="level-3">
<a href="#section-7-4-2">7.4.2 Flash 消息</a>
</li>
<li class="level-3">
<a href="#section-7-4-3">7.4.3 首次注册</a>
</li>
<li class="level-3">
<a href="#section-7-4-4">7.4.4 部署到生产环境,并开启 SSL</a>
</li>
<li class="level-2">
<a href="#section-7-5">7.5 小结</a>
</li>
<li class="level-2">
<a href="#section-7-6">7.6 练习</a>
</li>
</ol>
<div class="main">
<p>User 模型可以正常使用了,接下来要实现的功能大多数网站都离不开:用户注册。在 <a href="chapter7.html#section-7-2">7.2 节</a>我们会创建一个表单,提交用户注册时填写的信息,然后在 <a href="chapter7.html#section-7-4">7.4 节</a>中使用提交的数据创建新用户,把相应的属性值存入数据库。注册功能实现后,还要创建一个用户资料页面,显示用户的个人信息,这是实现 Users 资源 REST 架构(参见 <a href="chapter2.html#section-2-2-2">2.2.2 节</a>)的第一步。和之前一样,开发的过程中要编写测试,结合 RSpec 和 Capybara 写出简洁有效的集成测试。</p>
<p>创建资料页面之前,数据库中先要有用户记录。这有点类似“先有鸡还是先有蛋”的问题:网站还没实现注册功能,数据库中怎么会有用户记录呢?其实这个问题在 <a href="chapter6.html#section-6-3-5">6.3.5 节</a>中已经解决了,我们在控制台中向数据库中存储了一个用户记录。如果你跳过了那一节,现在赶快往回翻,完成相应的操作。</p>
<p>如果你一直坚持使用版本控制系统,现在要新建一个从分支了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git checkout master
<span class="gp">$ </span>git checkout -b sign-up
</pre></div>
</div>
<h2 id='section-7-1'><span>7.1</span> 显示用户信息</h2>
<p>本节要实现的用户资料页面是完整页面的一小部分,只显示用户的名字和头像,构思图如图 7.1 所示。<sup class="footnote" id="fnref-7-1"><a href="#fn-7-1" rel="footnote">1</a></sup> 最终完成的用户资料页面会显示用户的头像、基本信息和一些微博,构思图如图 7.2 所示。<sup class="footnote" id="fnref-7-2"><a href="#fn-7-2" rel="footnote">2</a></sup>(在图 7.2 中,我们第一次用到了“lorem ipsum”占位文字,<a href="http://www.straightdope.com/columns/read/2290/what-does-the-filler-text-lorem-ipsum-mean">这些文字背后的故事</a>很有意思,有空的话你可以了解一下。)整个资料页面会和整个示例程序一起在 <a href="chapter11.html">第 11 章</a>完成。</p>
<h3 id='section-7-1-1'><span>7.1.1</span> 调试信息和 Rails 环境</h3>
<p>本节要实现的用户资料页面是第一个真正意义上的动态页面。虽然视图的代码不会动态改变,不过每个用户资料页面显示的内容却是动态的从数据库中读取的。添加动态页面之前,最好做些准备工作,现在我们能做的就是在网站布局中加入一些调试信息(参见代码 7.1)。代码 7.1 使用 Rails 内置的 <code>debug</code> 方法和 <code>params</code> 变量(<a href="chapter7.html#section-7-1-2">7.1.2 节</a>会详细介绍),在每一页中都显示一些对开发有所帮助的信息。</p>
<div class="codeblock has-caption" id="codeblock-7-1"><p class="caption"><span>代码 7.1:</span>把调试信息加入网站的布局中</p><p class="file"><code>app/views/layouts/application.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
.
.
.
<span class="nt"><body></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'layouts/header'</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="k">yield</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'layouts/footer'</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">debug</span><span class="p">(</span><span class="n">params</span><span class="p">)</span> <span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</pre></div>
</div>
<div class="figure" id="figure-7-1">
<img src="figures/profile_mockup_profile_name_bootstrap.png" alt="profile mockup profile name bootstrap" />
<p class="caption"><span>图 7.1:</span>本节要实现的用户资料页面构思图</p>
</div>
<p>我们要在<a href="chapter5.html">第 5 章</a>中创建的自定义样式表文件中加入一些样式规则(参见代码 7.2),美化一下这些调试信息。</p>
<div class="codeblock has-caption" id="codeblock-7-2"><p class="caption"><span>代码 7.2:</span>添加美化调试信息的样式,使用了一个 Sass mixin</p><p class="file"><code>app/assets/stylesheets/custom.css.scss</code></p><div class="highlight type-scss"><pre><span class="k">@import</span> <span class="s2">"bootstrap"</span><span class="p">;</span>
<span class="cm">/* mixins, variables, etc. */</span>
<span class="nv">$grayMediumLight</span><span class="p">:</span> <span class="mh">#eaeaea</span><span class="p">;</span>
<span class="k">@mixin</span> <span class="nf">box_sizing</span> <span class="p">{</span>
<span class="na">-moz-box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="na">-webkit-box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">/*</span> <span class="nt">miscellaneous</span> <span class="o">*/</span>
<span class="nc">.debug_dump</span> <span class="p">{</span>
<span class="nl">clear</span><span class="p">:</span> <span class="nb">both</span><span class="p">;</span>
<span class="nl">float</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">45px</span><span class="p">;</span>
<span class="k">@include</span> <span class="nd">box_sizing</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>上面的代码用到了 Sass 的 mixin 功能,创建的这个 mixin 名为 <code>box-sizing</code>。mixin 可以打包一系列的样式规则,供多次使用。预处理器处理时,会把</p>
<div class="codeblock"><div class="highlight type-scss"><pre><span class="nc">.debug_dump</span> <span class="p">{</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">@</span><span class="nt">include</span> <span class="nt">box_sizing</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>转换成</p>
<div class="codeblock"><div class="highlight type-scss"><pre><span class="nc">.debug_dump</span> <span class="p">{</span>
<span class="err">.
.
.
</span><span class="na">-moz-box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="na">-webkit-box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>在 <a href="chapter7.html#section-7-2-2">7.2.2 节</a>中还会再次用到这个 mixin。美化后的调试信息如图 7.3 所示。</p>
<div class="figure" id="figure-7-2">
<img src="figures/profile_mockup_bootstrap.png" alt="profile mockup bootstrap" />
<p class="caption"><span>图 7.2:</span>最终实现的用户资料页面构思图</p>
</div>
<p>图 7.3 中显示的调试信息给出了当前页面的一些信息:</p>
<div class="codeblock"><div class="highlight type-yaml"><pre><span class="nn">---</span>
<span class="s">controller</span><span class="pi">:</span> <span class="s">static_pages</span>
<span class="s">action</span><span class="pi">:</span> <span class="s">home</span>
</pre></div>
</div>
<p>这是 <code>params</code> 变量的 YAML<sup class="footnote" id="fnref-7-3"><a href="#fn-7-3" rel="footnote">3</a></sup>形式,和 Hash 类似,显示了当前页面的控制器名和动作名。在 <a href="chapter7.html#section-7-1-2">7.1.2 节</a>中会介绍其他调试信息的意思。</p>
<p>我们不想让部署后的示例程序显示这个调试信息,所以代码 7.1 中用如下的代码做了限制,只在“开发环境”中显示:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span>
</pre></div>
</div>
<div class="figure" id="figure-7-3">
<img src="figures/home_page_with_debug_4_0.png" alt="home page with debug 40" />
<p class="caption"><span>图 7.3:</span>显示有调试信息的示例程序首页(<a href="http://localhost:3000/">/</a>)</p>
</div>
<p>“开发环境”是 Rails 定义的三个环境之一(详细介绍参见<a href="chapter7.html#aside-7-1">旁注 7.1</a>)<sup class="footnote" id="fnref-7-4"><a href="#fn-7-4" rel="footnote">4</a></sup>,只有在“开发环境”中 <code>Rails.env.development?</code> 才会返回 <code>true</code>,所以下面的 ERb 代码</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">debug</span><span class="p">(</span><span class="n">params</span><span class="p">)</span> <span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span> <span class="cp">%></span>
</pre></div>
</div>
<p>不会在“生产环境”和“测试环境”中执行。(在“测试环境”中显示调试信息虽然没有坏处,但也没什么好处,所以最好只在“开发环境”中显示。)</p>
<div class="aside box" id="aside-7-1">
<h4><span>旁注 7.1:</span>Rails 的三个环境</h4>
<p>Rails 定义了三个环境,分别是“生产环境”、“开发环境”和“测试环境”。Rails 控制台默认使用的是“开发环境”:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails console
Loading development environment
<span class="gp">>> </span>Rails.env
<span class="gp">=> </span><span class="s2">"development"</span>
<span class="gp">>> </span>Rails.env.development?
<span class="gp">=> </span><span class="nb">true</span>
<span class="gp">>> </span>Rails.env.test?
<span class="gp">=> </span><span class="nb">false</span>
</pre></div>
</div>
<p>如前所示,Rails 对象有一个 <code>env</code> 属性,属性上还可以调用各环境对应的布尔值方法,例如,<code>Rails.env.test?</code>,在“测试环境”中的返回值是 <code>true</code>,而在其他两个环境中的返回值则是 <code>false</code>。</p>
<p>如果需要在其他环境中使用控制台(例如,在“测试环境”中进行调试),只需把环境名称传递给 <code>console</code> 命令即可:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails console <span class="nb">test
</span>Loading <span class="nb">test </span>environment
<span class="gp">>> </span>Rails.env
<span class="gp">=> </span><span class="s2">"test"</span>
<span class="gp">>> </span>Rails.env.test?
<span class="gp">=> </span><span class="nb">true</span>
</pre></div>
</div>
<p>Rails 本地服务器和控制台一样,默认使用“开发环境”,不过也可以在其他环境中运行:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails server --environment production
</pre></div>
</div>
<p>如果要在“生产环境”中运行应用程序,先要提供生产环境数据库。在“生产环境”中执行 <code>rake db:migrate</code> 命令可以生成“生产环境”所需的数据库:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rake db:migrate <span class="nv">RAILS_ENV</span><span class="o">=</span>production
</pre></div>
</div>
<p>(我发现在控制台、服务器和迁移命令中指定其他环境的方法不一样,这可能会产生混淆,所以我特意演示了三个命令的用法。)</p>
<p>顺便说一下,把应用程序部署到 Heroku 后,可以使用如下的命令进入远端的控制台:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>heroku run console
Ruby console <span class="k">for </span>yourapp.herokuapp.com
<span class="gp">>> </span>Rails.env
<span class="gp">=> </span><span class="s2">"production"</span>
<span class="gp">>> </span>Rails.env.production?
<span class="gp">=> </span><span class="nb">true</span>
</pre></div>
</div>
<p>Heroku 是用来部署网站的平台,自然会在“生产环境”中运行应用程序。</p>
</div>
<h3 id='section-7-1-2'><span>7.1.2</span> Users 资源</h3>
<p>在<a href="chapter6.html">第 6 章</a>末尾,我们在数据库中存储了一个用户记录,在 <a href="chapter6.html#section-6-3-5">6.3.5 节</a>查看过,用户的 id 是 1,现在我们就来创建一个页面,显示这个用户的信息。我们会遵从 Rails 使用的 REST 架构,把数据视为资源(resource),可以创建、显示、更新和删除,这四个操作分别对应了 HTTP 标准中的 <code>POST</code>、<code>GET</code>、<code>PATCH</code> 和 <code>DELETE</code> 请求方法(参见<a href="chapter3.html#aside-3-3">旁注 3.3</a>)。</p>
<p>按照 REST 约定,资源一般是由资源名加唯一标识符表示的。对 User 而言,我们把它看做一个资源,若要查看 id 为 1 的用户,就要向 /users/1 地址发送一个 <code>GET</code> 请求。REST 架构解析时,会自动把这个 <code>GET</code> 请求分发到 <code>show</code> 动作上,因此这里没必要指明用哪个动作。</p>
<p>在 <a href="chapter2.html#section-2-2-1">2.2.1 节</a>中曾经见过,id 为 1 的用户对应的地址是 /users/1,现在访问这个地址的话会显示错误提示信息(如图 7.4)。</p>
<div class="figure" id="figure-7-4">
<img src="figures/profile_routing_error.png" alt="profile routing error" />
<p class="caption"><span>图 7.4:</span>/users/1 地址显示的错误</p>
</div>
<p>我们只需在路由文件 <code>config/routes.rb</code> 中添加如下的一行代码就可以正常访问 REST 架构对应的 URL 地址了:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">resources</span> <span class="p">:</span><span class="n">users</span>
</pre></div>
</div>
<p>修改后的路由文件如代码 7.3 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-3"><p class="caption"><span>代码 7.3:</span>在路由文件中添加 Users 资源设置</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">resources</span> <span class="p">:</span><span class="n">users</span>
<span class="n">root</span> <span class="ss">to: </span><span class="s1">'static_pages#home'</span>
<span class="n">match</span> <span class="s1">'/signup'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'users#new'</span><span class="p">,</span> <span class="ss">via: </span><span class="s1">'get'</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>你可能发现了,我们把下面这行在代码 5.33 中出现的代码删掉了:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">get</span> <span class="s2">"users/new"</span>
</pre></div>
</div>
<p>这是因为 <code>resources :users</code> 不仅使 /users/1 地址可以访问了,而且还为示例程序的 Users 资源提供了符合 REST 架构的一系列动作<sup class="footnote" id="fnref-7-5"><a href="#fn-7-5" rel="footnote">5</a></sup>,以及用来获取相应 URL 地址的具名路由(named route,参见 <a href="chapter5.html#section-5-3-3">5.3.3 节</a>)。最终得到的 URI、动作和具名路由的对应关系如<a href="chapter7.html#table-7-1">表格 7.1</a> 所示(可以和<a href="chapter2.html#table-2-2">表格 2.2</a> 对比一下)。接下来的三章会介绍 <code>show</code> 之外的所有动作,并不断完善,把 Users 打造成完全符合 REST 架构的资源。</p>
<div class="table has-caption" id="table-7-1"><p class="caption"><span>表格 7.1:</span>Users 资源对应的路由</p><table>
<thead>
<tr>
<th>HTTP 请求</th>
<th>URL</th>
<th>动作</th>
<th>具名路由</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET</code></td>
<td>/users</td>
<td><code>index</code></td>
<td><code>users_path</code></td>
<td>显示所有用户的页面</td>
</tr>
<tr>
<td><code>GET</code></td>
<td>/users/1</td>
<td><code>show</code></td>
<td><code>user_path(user)</code></td>
<td>显示某个用户的页面</td>
</tr>
<tr>
<td><code>GET</code></td>
<td>/users/new</td>
<td><code>new</code></td>
<td><code>new_user_path</code></td>
<td>创建(注册)新用户的页面</td>
</tr>
<tr>
<td><code>POST</code></td>
<td>/users</td>
<td><code>create</code></td>
<td><code>users_path</code></td>
<td>创建新用户</td>
</tr>
<tr>
<td><code>GET</code></td>
<td>/users/1/edit</td>
<td><code>edit</code></td>
<td><code>edit_user_path(user)</code></td>
<td>编辑 id 为 1 的用户页面</td>
</tr>
<tr>
<td><code>PATCH</code></td>
<td>/users/1</td>
<td><code>update</code></td>
<td><code>user_path(user)</code></td>
<td>更新用户信息</td>
</tr>
<tr>
<td><code>DELETE</code></td>
<td>/users/1</td>
<td><code>destroy</code></td>
<td><code>user_path(user)</code></td>
<td>删除用户</td>
</tr>
</tbody>
</table>
</div>
<p>添加代码 7.3 之后,路由就生效了,但是页面还不存在(如图 7.5)。下面我们就来为页面添加一些内容,<a href="chapter7.html#section-7-1-4">7.1.4 节</a>还会添加更多的内容。</p>
<p>用户资料页面的视图存放在应用程序特定的目录中,即 <code>app/views/users/show.html.erb</code>。这个视图和自动生成的 <code>new.html.erb</code>(参见代码 5.29)不同,现在不存在,要手动创建。新建 <code>show</code> 视图后请写入代码 7.4 中的代码。</p>
<div class="figure" id="figure-7-5">
<img src="figures/user_show_unknown_action_31.png" alt="user show unknown action 31" />
<p class="caption"><span>图 7.5:</span>/users/1 地址生效了,但是页面不存在</p>
</div>
<div class="codeblock has-caption" id="codeblock-7-4"><p class="caption"><span>代码 7.4:</span>用户资料页面的临时视图</p><p class="file"><code>app/views/users/show.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span>, <span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">email</span> <span class="cp">%></span>
</pre></div>
</div>
<p>在上面的代码中,我们假设 <code>@user</code> 变量是存在的,使用 ERb 代码显示用户的名字和 Email 地址。这和最终实现的视图有点不一样,在最终的视图中不会公开显示用户的 Email 地址。</p>
<p>我们要在 Users 控制器的 <code>show</code> 动作中定义 <code>@user</code> 变量,用户资料页面才能正常渲染。你可能猜到了,我们要在 User 模型上调用 <code>find</code> 方法,从数据库中取出用户记录,如代码 7.5 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-5"><p class="caption"><span>代码 7.5:</span>含有 <code>show</code> 动作的 Users 控制器</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">new</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>在上面的代码中,我们使用 <code>params</code> 获取用户的 id。当我们向 Users 控制器发送请求时,<code>params[:id]</code> 会返回用户的 id,即 1,所以这就和 <a href="chapter6.html#section-6-1-4">6.1.4 节</a>中直接调用 <code>User.find(1)</code> 的效果一样。(严格来说,<code>params[:id]</code> 返回的是字符串 <code>"1"</code>,<code>find</code> 方法会自动将其转换成整数形式。)</p>
<p>定义了视图和动作之后,/users/1 地址就可以正常显示了(如图 7.6)。留意一下调试信息,其内容证实了 <code>params[:id]</code> 的值和前面分析的一样:</p>
<div class="codeblock"><div class="highlight type-yaml"><pre><span class="nn">---</span>
<span class="s">action</span><span class="pi">:</span> <span class="s">show</span>
<span class="s">controller</span><span class="pi">:</span> <span class="s">users</span>
<span class="s">id</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1'</span>
</pre></div>
</div>
<p>所以,代码 7.5 中的 <code>User.find(params[:id])</code> 才会取回 id 为 1 的用户记录。</p>
<div class="figure" id="figure-7-6">
<img src="figures/user_show_bootstrap.png" alt="user show bootstrap" />
<p class="caption"><span>图 7.6:</span>设置 Users 资源后的用户资料页面 <a href="http://localhost:3000/users/1">/users/1</a></p>
</div>
<h3 id='section-7-1-3'><span>7.1.3</span> 使用预构件测试用户资料页面</h3>
<p>至此,用户资料页面已经可以正常访问了,接下来我们要实现图 7.1 所示的构思了。与创建静态页面和 User 模型一样,开发的过程会践行 TDD 思想。</p>
<p>在 <a href="chapter5.html#section-5-4-2">5.4.2 节</a>中,我们使用集成测试对 Users 资源相关的页面进行了测试,以“注册”页面为例,测试先访问 <code>signup_path</code>,然后检测页面中 <code>h1</code> 和 <code>title</code> 标签的内容是否正确。代码 7.6 是对代码 5.32 的补充。(注意,我们删除了 <a href="chapter5.html#section-5-3-4">5.3.4 节</a>用到的 <code>full_title</code> 帮助方法,因为标题已经测试过了。)</p>
<div class="codeblock has-caption" id="codeblock-7-6"><p class="caption"><span>代码 7.6:</span>补充用户相关页面的测试</p><p class="file"><code>spec/requests/user_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">"User pages"</span> <span class="k">do</span>
<span class="n">subject</span> <span class="p">{</span> <span class="n">page</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"signup page"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">signup_path</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sign up'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">full_title</span><span class="p">(</span><span class="s1">'Sign up'</span><span class="p">))</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>为了测试用户资料页面,先要有一个 User 模型对象,代码 7.5 中的 <code>show</code> 动作才能执行查询操作:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">describe</span> <span class="s2">"profile page"</span> <span class="k">do</span>
<span class="c1"># Replace with code to make a user variable</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p>我们要把上面代码中的注释换成相关的代码才行。在注释后面,调用 <code>user_path</code> 具名路由(参见<a href="chapter7.html#table-7-1">表格 7.1</a>)访问用户资料页面的地址,然后检测页面内容和标题是否都包含用户的名字。</p>
<p>一般情况下,创建 User 模型需要调用 Active Record 提供的 <code>User.create</code> 方法,不过经验告诉我们,使用预构件(factory)创建用户对象更方便,存入数据库也更容易。</p>
<p>我们要使用 <a href="http://github.com/thoughtbot/factory_girl">Factory Girl</a> 来生成预构件,这个 gem 是由 thoughtbot 公司的达人开发的。和 RSpec 类似,Factory Girl 也定义了一套领域专属语言(Domain-specific Language, DSL),用来生成 Active Record 对象。Factory Girl 的句法很简单,使用块和方法定义对象的属性值。本章还没有显出预构件的优势,不过后续的内容会多次使用预构件的高级更能,到时你就可以看到它的强大之处了。例如,在 <a href="chapter9.html#section-9-3-3">9.3.3 节</a>中,需要生成一批 Email 地址各不相同的用户对象,用预构件就可以很轻松的完成这种操作。</p>
<p>和其他的 gem 一样,我们要在 Bundler 的 <code>Gemfile</code> 中加入如代码 7.7 所示的代码来安装 Factory Girl。(因为只有测试时才会用到 Factory Girl,所以把它归入测试组中。)</p>
<div class="codeblock has-caption" id="codeblock-7-7"><p class="caption"><span>代码 7.7:</span>把 Factory Girl 加入 <code>Gemfile</code></p><div class="highlight type-ruby"><pre><span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">group</span> <span class="ss">:test</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">gem</span> <span class="s1">'factory_girl_rails'</span><span class="p">,</span> <span class="s1">'4.2.1'</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="o">.</span>
</pre></div>
</div>
<p>然后和往常一样,运行以下命令安装:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle install
</pre></div>
</div>
<p>Factory Girl 生成的预构件都保存在 <code>spec/factories.rb</code> 中,RSpec 会自动加载这个文件。用户相关的预构件如代码 7.8 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-8"><p class="caption"><span>代码 7.8:</span>模拟 User 模型对象的预构件</p><p class="file"><code>spec/factories.rb</code></p><div class="highlight type-ruby"><pre><span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
<span class="n">factory</span> <span class="p">:</span><span class="n">user</span> <span class="k">do</span>
<span class="nb">name</span> <span class="s2">"Michael Hartl"</span>
<span class="n">email</span> <span class="s2">"[email protected]"</span>
<span class="n">password</span> <span class="s2">"foobar"</span>
<span class="n">password_confirmation</span> <span class="s2">"foobar"</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p><code>factory</code> 方法的 <code>:user</code> 参数说明,块中的代码定义了一个 User 模型对象。</p>
<p>加入代码 7.8 之后,就可以在测试中使用 <code>let</code> 方法(参见<a href="chapter6.html#aside-6-3">旁注 6.3</a>)和 Factory Girl 提供的 <code>FactoryGirl</code> 方法来生成 User 对象:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
</pre></div>
</div>
<p>修改后的测试如代码 7.9 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-9"><p class="caption"><span>代码 7.9:</span>用户资料页面的测试</p><p class="file"><code>spec/requests/user_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">"User pages"</span> <span class="k">do</span>
<span class="n">subject</span> <span class="p">{</span> <span class="n">page</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"profile page"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"signup page"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">signup_path</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'Sign up'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">full_title</span><span class="p">(</span><span class="s1">'Sign up'</span><span class="p">))</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">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/
</pre></div>
</div>
<p>加入代码 7.10 之后,测试就可以通过了(绿色)。</p>
<div class="codeblock has-caption" id="codeblock-7-10"><p class="caption"><span>代码 7.10:</span>在用户资料页面视图中加入标题和标头</p><p class="file"><code>app/views/users/show.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="vi">@user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><h1></span><span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span><span class="nt"></h1></span>
</pre></div>
</div>
<p>再次运行 RSpec,确认代码 7.9 中的测试是否可以通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<p>使用 Factory Girl 后,明显可以察觉测试变慢了,这不是 Factory Girl 导致的,而是有意为之,并不是 bug。变慢的原因在于 <a href="chapter6.html#section-6-3-1">6.3.1 节</a>中用来加密密码的 BCrypt,其加密算法设计如此,因为慢速加密的密码很难破解。慢速加密的过程会延长测试的运行时间,不过我们可以做个简单的设置改变这种情况。<code>bcrypt-ruby</code> 使用耗时因子(cost factor)设定加密过程的耗时,耗时因子的默认值倾向于安全性而不是速度,在生产环境这种设置很好,但测试时的关注点却有所不同:测试追求的是速度,而不用在意测试数据库中用户的密码强度。我们可以在“测试环境”配置文件 <code>config/environments/test.rb</code> 中加入一行代码来解决速度慢的问题:把耗时因子的默认值修改为最小值,提升加密的速度,如代码 7.11 所示。即使测试量很少,修改设置之后速度的提升也是很明显的,所以我建议每个读者都在 <code>test.rb</code> 文件中加入代码 7.11 的内容。</p>
<div class="codeblock has-caption" id="codeblock-7-11"><p class="caption"><span>代码 7.11:</span>为测试环境重新设置 BCrypt 耗时因子</p><p class="file"><code>config/environments/test.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">configure</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="o">.</span>
<span class="c1"># Speed up tests by lowering bcrypt's cost function.</span>
<span class="no">ActiveModel</span><span class="o">::</span><span class="no">SecurePassword</span><span class="p">.</span><span class="nf">min_cost</span> <span class="o">=</span> <span class="kp">true</span>
<span class="k">end</span>
</pre></div>
</div>
<h3 id='section-7-1-4'><span>7.1.4</span> 添加 Gravatar 头像和侧边栏</h3>
<p>上一小节创建了一个略显简陋的用户资料页面,这一小节要再添加一些内容:用户头像和侧边栏。构建视图时,我们关注的是显示的内容,而不是页面底层的结构,所以我们暂时不测试视图,等遇到容易出错的页面结构时,例如 <a href="chapter9.html#section-9-3-3">9.3.3 节</a>中的分页导航,再使用 TDD 理念。</p>
<p>首先,我们要在用户资料页面中添加一个“全球通用识别”的头像,这个头像也称作 Gravatar<sup class="footnote" id="fnref-7-6"><a href="#fn-7-6" rel="footnote">6</a></sup>,由 Tom Preston-Werner(GitHub 的联合创始人)开发,后被 Automattic(开发 WordPress 的公司)收购。Gravatar 是一个免费服务,用户只需上传图片并将其关联到 Email 地址上即可。使用 Gravatar 可以简单的在网站中加入用户头像,开发者不必再分心去处理图片上传、剪裁和存储,只要使用用户的 Email 地址构成头像的 URL 地址,关联的头像就可以显示出来了。<sup class="footnote" id="fnref-7-7"><a href="#fn-7-7" rel="footnote">7</a></sup></p>
<p>我们计划定义一个名为 <code>gravatar_for</code> 的方法,返回指定用户的 Gravatar 头像,如代码 7.12 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-12"><p class="caption"><span>代码 7.12:</span>显示用户名字和 Gravatar 头像的用户资料页面视图</p><p class="file"><code>app/views/users/show.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="vi">@user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><h1></span>
<span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span>
<span class="nt"></h1></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/
</pre></div>
</div>
<p>因为还没定义 <code>gravatar_for</code> 方法,所以用户资料页面会显示错误提示。(测试视图最大的作用大概就是可以捕获这种错误,所以一定要掌握视图测试的量。)</p>
<p>默认情况下,所有帮助方法文件中定义的方法都可以直接用在任意的视图中,不过为了便于管理,我们会把 <code>gravatar_for</code> 放在 Users 控制器对应的帮助文件中。Gravatar 的首页中有介绍说,头像的 URL 地址要使用 MD5 加密的 Email 地址。在 Ruby 中,MD5 加密算法由 <code>Digest</code> 库的 <code>hexdigest</code> 方法实现:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">>> </span>email <span class="o">=</span> <span class="s2">"[email protected]"</span>
<span class="gp">>> </span>Digest::MD5::hexdigest<span class="o">(</span>email.downcase<span class="o">)</span>
<span class="gp">=> </span><span class="s2">"1fda4469bcbec3badf5418269ffc5968"</span>
</pre></div>
</div>
<p>Email 地址不区分大小写,而 MD5 加密算法却区分,所以,我们要先调用 <code>downcase</code> 方法把 Email 地址转换成小写形式,然后再传递给 <code>hexdigest</code> 方法。我们定义的 <code>gravatar_for</code> 方法如代码 7.13 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-13"><p class="caption"><span>代码 7.13:</span>定义 <code>gravatar_for</code> 帮助方法</p><p class="file"><code>app/helpers/users_helper.rb</code></p><div class="highlight type-ruby"><pre><span class="k">module</span> <span class="nn">UsersHelper</span>
<span class="c1"># Returns the Gravatar (http://gravatar.com/) for the given user.</span>
<span class="k">def</span> <span class="nf">gravatar_for</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">gravatar_id</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">MD5</span><span class="o">::</span><span class="n">hexdigest</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">email</span><span class="p">.</span><span class="nf">downcase</span><span class="p">)</span>
<span class="n">gravatar_url</span> <span class="o">=</span> <span class="s2">"https://secure.gravatar.com/avatar/</span><span class="si">#{</span><span class="n">gravatar_id</span><span class="si">}</span><span class="s2">"</span>
<span class="n">image_tag</span><span class="p">(</span><span class="n">gravatar_url</span><span class="p">,</span> <span class="ss">alt: </span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"gravatar"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p><code>gravatar_for</code> 方法的返回值是 Gravatar 头像的 <code>img</code> 元素,<code>img</code> 标签的 class 设为 <code>gravatar</code>,<code>alt</code> 属性值是用户的名字(对视觉障碍人士使用的屏幕阅读器很友好)。现在你可以验证一下测试是否可以通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<p>用户资料页面的效果如图 7.7 所示,页面中显示的头像是 Gravatar 的默认图片,因为 <code>[email protected]</code> 不是真的 Email 地址(example.com 这个域名是专门用来举例的)。</p>
<p>我们调用 <code>update_attributes</code> 方法(参见 <a href="chapter6.html#section-6-1-5">6.1.5 节</a>)更新一下数据库中的用户记录,然后就可以显示用户真正的头像了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails console
<span class="gp">>> </span>user <span class="o">=</span> User.first
<span class="gp">>> </span>user.update_attributes<span class="o">(</span>name: <span class="s2">"Example User"</span>,
<span class="gp">?> </span>email: <span class="s2">"[email protected]"</span>,
<span class="gp">?> </span>password: <span class="s2">"foobar"</span>,
<span class="gp">?> </span>password_confirmation: <span class="s2">"foobar"</span><span class="o">)</span>
<span class="gp">=> </span><span class="nb">true</span>
</pre></div>
</div>
<p>上面的代码,把用户的 Email 地址设为 <code>[email protected]</code>,我已经把这个 Email 地址的头像设为了本书网站的 LOGO。修改后的结果如图 7.8 所示。</p>
<p>我们还要添加一个侧边栏,才能完整的实现图 7.1 中的构思。我们要使用 <code>aside</code> 标签定义侧边栏,<code>aside</code> 中的内容一般是对主体内容的补充,不过也可以自成一体。我们要把 <code>aside</code> 标签的 class 设为 <code>row span4</code>,这也是 Bootstrap 会用到的。在用户资料页面中添加侧边栏用到的代码如代码 7.14 所示。</p>
<div class="figure" id="figure-7-7">
<img src="figures/profile_with_gravatar_bootstrap_4_0.png" alt="profile with gravatar bootstrap 40" />
<p class="caption"><span>图 7.7:</span>显示默认 Gravatar 头像的用户资料页面 <a href="http://localhost:3000/users/1">/users/1</a></p>
</div>
<div class="codeblock has-caption" id="codeblock-7-14"><p class="caption"><span>代码 7.14:</span>为用户资料页面添加侧边栏</p><p class="file"><code>app/views/users/show.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="vi">@user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><aside</span> <span class="na">class=</span><span class="s">"span4"</span><span class="nt">></span>
<span class="nt"><section></span>
<span class="nt"><h1></span>
<span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span>
<span class="nt"></h1></span>
<span class="nt"></section></span>
<span class="nt"></aside></span>
<span class="nt"></div></span>
</pre></div>
</div>
<p>添加了 HTML 结构和 CSS class 后,我们再用 SCSS 为资料页面定义样式,如代码 7.15 所示。(注意:因为 asset pipeline 使用了 Sass 预处理器,所以样式中才可以使用嵌套。)最终的效果如图 7.9 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-15"><p class="caption"><span>代码 7.15:</span>用户资料页面的样式,包括侧边栏的样式</p><p class="file"><code>app/assets/stylesheets/custom.css.scss</code></p><div class="highlight type-scss"><pre><span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">/*</span> <span class="nt">sidebar</span> <span class="o">*/</span>
<span class="nt">aside</span> <span class="p">{</span>
<span class="nt">section</span> <span class="p">{</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">border-top</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nv">$grayLighter</span><span class="p">;</span>
<span class="k">&</span><span class="nd">:first-child</span> <span class="p">{</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">padding-top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">span</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">3px</span><span class="p">;</span>
<span class="nl">line-height</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h1</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">1</span><span class="mi">.4em</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="nl">letter-spacing</span><span class="p">:</span> <span class="m">-1px</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">3px</span><span class="p">;</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">0px</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nc">.gravatar</span> <span class="p">{</span>
<span class="nl">float</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<h2 id='section-7-2'><span>7.2</span> 注册表单</h2>
<p>用户资料页面已经可以访问了,但内容还不完整。下面我们要为网站创建一个注册表单。如图 5.9 和图 7.10 所示,“注册”页面还没有什么内容,无法注册新用户。本节会实现如图 7.11 所示的注册表单,添加注册功能。</p>
<p>因为我们要实现通过网页创建用户的功能,现在就把 <a href="chapter6.html#section-6-3-5">6.3.5 节</a>在控制台中创建的用户删除吧。最简单的方法是使用 <code>db:reset</code> 命令:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rake db:reset
</pre></div>
</div>
<p>还原数据库后,在有些系统中还要重新准备测试数据库:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:prepare
</pre></div>
</div>
<div class="figure" id="figure-7-8">
<img src="figures/profile_custom_gravatar_bootstrap_4_0.png" alt="profile custom gravatar bootstrap 40" />
<p class="caption"><span>图 7.8:</span>显示用户真实头像的用户资料页面 <a href="http://localhost:3000/users/1">/users/1</a></p>
</div>
<p>在某些系统中还要重启 Web 服务器,还原数据库的操作才能生效。<sup class="footnote" id="fnref-7-8"><a href="#fn-7-8" rel="footnote">8</a></sup></p>
<h3 id='section-7-2-1'><span>7.2.1</span> 测试用户注册功能</h3>
<p>在 Web 框架没有完全支持测试之前,测试是件很痛苦的事,也很容易出错。例如,手动测试“注册”页面时,我们要在浏览器中访问这个页面,然后分别提交不合法的和合法的数据,检查在这两种情况下应用程序的表现是否正常。而且,每次修改程序后,都要重复上述的操作。使用 RSpec 和 Capybara 之后,以前需要手动进行的测试,现在可以编写测试用例自动执行了。</p>
<p>前面的章节已经介绍过 Capybara 访问网页时使用的很直观的句法,其中用的最多的就是访问某个页面的 <code>visit</code> 方法。Capybara 的功能可不仅限于此,它还可以填写如图 7.11 所示的表单字段,然后点击提交按钮,句法如下:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">visit</span> <span class="n">signup_path</span>
<span class="n">fill_in</span> <span class="s2">"Name"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"Example User"</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">click_button</span> <span class="s2">"Create my account"</span>
</pre></div>
</div>
<p>现在我们要分别提交不合法的和合法的注册数据,验证注册功能是否可以正常使用。我们要用到的测试相对高级一些,所以会慢慢分析。如果你想查看最终的测试代码(以及测试文件的位置),可以直接跳到代码 7.16。先来测试没有正确填写信息的注册表单,我们访问“注册”页面,什么也不填,直接点击注册按钮(调用 <code>click_button</code> 方法),这个操作模拟的就是提交不合法数据的情况:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">visit</span> <span class="n">signup_path</span>
<span class="n">click_button</span> <span class="s2">"Create my account"</span>
</pre></div>
</div>
<div class="figure" id="figure-7-9">
<img src="figures/user_show_sidebar_css_bootstrap.png" alt="user show sidebar css bootstrap" />
<p class="caption"><span>图 7.9:</span>添加侧边栏并定义了样式之后的用户资料页面 <a href="http://localhost:3000/users/1">/users/1</a></p>
</div>
<p>上面的代码,等同于手动访问注册页面,然后提交空白的不合法注册信息。相对的,我们要调用 <code>fill_in</code> 方法填写合法信息,以此来模拟提交合法数据的情况:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">visit</span> <span class="n">signup_path</span>
<span class="n">fill_in</span> <span class="s2">"Name"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"Example User"</span>
<span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"[email protected]"</span>
<span class="n">fill_in</span> <span class="s2">"Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"foobar"</span>
<span class="n">fill_in</span> <span class="s2">"Confirmation"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"foobar"</span>
<span class="n">click_button</span> <span class="s2">"Create my account"</span>
</pre></div>
</div>
<p>我们测试的最终目的,是要检测点击“Create my account”按钮之后,程序的表现是否正常,即当提交合法的数据时,创建新用户;当提交不合法的数据时,不创建新用户。检测是否创建了新用户,我们要看用户的数量是否发生了变化,在测试中,我们使用每个 Active Record 对象都可以响应的 <code>count</code> 方法来获取对象的数量,以用户为例,即:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails console
<span class="gp">>> </span>User.count
<span class="gp">=> </span>0
</pre></div>
</div>
<div class="figure" id="figure-7-10">
<img src="figures/new_signup_page_bootstrap.png" alt="new signup page bootstrap" />
<p class="caption"><span>图 7.10:</span>“注册”页面现在的样子 <a href="http://localhost:3000/signup">/signup</a></p>
</div>
<p>现在 <code>User.count</code> 的返回值是 0,因为本节开头我们还原了数据库。提交不合法数据时,我们希望用户的数量是不变的;提交合法数据时,我们希望用户的数量增加 1 个。在 RSpec 中,上面的设想要结合 <code>expect</code> 和 <code>to</code>,或者和 <code>not_to</code> 方法来表述。我们先从不合法的数据开始,因为这种情况比较简单。我们先访问“注册”页面,然后点击提交按钮,希望用户的数量不变:</p>
<div class="codeblock"><div class="highlight type-shell"><pre>visit signup_path
expect <span class="o">{</span> click_button <span class="s2">"Create my account"</span> <span class="o">}</span>.not_to change<span class="o">(</span>User, :count<span class="o">)</span>
</pre></div>
</div>
<p>注意,通过花括号我们可以看出,<code>expect</code> 把 <code>click_button</code> 包含在一个块中(参见 <a href="chapter4.html#section-4-3-2">4.3.2 节</a>),这是为 <code>change</code> 方法做的特殊处理。<code>change</code> 方法可接受两个参数,第一个参数是对象名,第二个是 Symbol。<code>change</code> 方法会在 <code>expect</code> 块中的代码执行前后,分别计算在第一个参数上调用第二参数代表的方法返回的结果。也就是说,如下的代码</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span> <span class="p">{</span> <span class="n">click_button</span> <span class="s2">"Create my account"</span> <span class="p">}.</span><span class="nf">not_to</span> <span class="n">change</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">:count</span><span class="p">)</span>
</pre></div>
</div>
<p>会在执行</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">click_button</span> <span class="s2">"Create my account"</span>
</pre></div>
</div>
<p>前后,两次计算</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="no">User</span><span class="p">.</span><span class="nf">count</span>
</pre></div>
</div>
<p>的结果。</p>
<div class="figure" id="figure-7-11">
<img src="figures/signup_mockup_bootstrap.png" alt="signup mockup bootstrap" />
<p class="caption"><span>图 7.11:</span>“注册”页面的构思图</p>
</div>
<p>本例,我们用 <code>not_to</code> 方法表示不愿看到用户数量发生变化。把点击按钮的代码放入块中,相当于把</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">initial</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">count</span>
<span class="n">click_button</span> <span class="s2">"Create my account"</span>
<span class="n">final</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">count</span>
<span class="n">expect</span><span class="p">(</span><span class="n">initial</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="n">final</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">click_button</span> <span class="s2">"Create my account"</span> <span class="p">}.</span><span class="nf">not_to</span> <span class="n">change</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">:count</span><span class="p">)</span>
</pre></div>
</div>
<p>这样读起来更顺口,代码也更简洁。(注意,<code>eq</code> 是 RSpec 提供的方法,用来测试是否相等。)</p>
<p>提交合法数据的情况和上述不合法数据的情况类似,不过用户数量不是不变,而是增加了 1 个:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">visit_signup</span> <span class="n">path</span>
<span class="n">fill_in</span> <span class="s2">"Name"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"Example User"</span>
<span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"[email protected]"</span>
<span class="n">fill_in</span> <span class="s2">"Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"foobar"</span>
<span class="n">fill_in</span> <span class="s2">"Confirmation"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"foobar"</span>
<span class="n">expect</span> <span class="k">do</span>
<span class="n">click_button</span> <span class="s2">"Create my account"</span>
<span class="k">end</span><span class="p">.</span><span class="nf">to</span> <span class="n">change</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">:count</span><span class="p">).</span><span class="nf">by</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</pre></div>
</div>
<p>这里使用了 <code>to</code> 方法,我们希望点击提交按钮后,这些合法的数据可以用来创建一个新用户。我们把上面两种情况放入一个 <code>describe</code> 块中,再把共用的代码放入 <code>before</code> 块中,最终得到的注册功能测试代码如代码 7.16 所示。我们还做了一项重构,用 <code>let</code> 方法定义了 <code>submit</code> 变量,表示注册按钮的文本。</p>
<div class="codeblock has-caption" id="codeblock-7-16"><p class="caption"><span>代码 7.16:</span>测试用户注册功能的代码</p><p class="file"><code>spec/requests/user_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">"User pages"</span> <span class="k">do</span>
<span class="n">subject</span> <span class="p">{</span> <span class="n">page</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"signup"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">signup_path</span> <span class="p">}</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:submit</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"Create my account"</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"with invalid information"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should not create a user"</span> <span class="k">do</span>
<span class="n">expect</span> <span class="p">{</span> <span class="n">click_button</span> <span class="n">submit</span> <span class="p">}.</span><span class="nf">not_to</span> <span class="n">change</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">:count</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"with valid information"</span> <span class="k">do</span>
<span class="n">before</span> <span class="k">do</span>
<span class="n">fill_in</span> <span class="s2">"Name"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"Example User"</span>
<span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"[email protected]"</span>
<span class="n">fill_in</span> <span class="s2">"Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"foobar"</span>
<span class="n">fill_in</span> <span class="s2">"Confirmation"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"foobar"</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"should create a user"</span> <span class="k">do</span>
<span class="n">expect</span> <span class="p">{</span> <span class="n">click_button</span> <span class="n">submit</span> <span class="p">}.</span><span class="nf">to</span> <span class="n">change</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">:count</span><span class="p">).</span><span class="nf">by</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>后续几节还会添加更多的测试,不过现在这个测试已经可以检测相当多的功能表现是否正常了。若要使这个测试通过,先得创建包含正确元素的注册页面,提交注册信息后页面要转向正确的地址,而且如果数据是合法的,还要创建一个新用户,并存入数据库中。</p>
<p>当然了,现在测试还是失败的:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h3 id='section-7-2-2'><span>7.2.2</span> 使用 form_for</h3>
<p>我们已经为用户注册功能编写了适当的测试代码,接下来要创建用户注册表单了。在 Rails 中,创建表单可以使用 <code>form_for</code> 帮助方法,指定其参数为 Active Record 对象,然后使用对象的属性构建表单的字段。注册表单的视图如代码 7.17 所示。(熟悉 Rails 2.x 的读者要注意一下,这里 <code>form_for</code> 使用的是 <code><%=...%></code> 形式,而 Rails 2.x 使用的是 <code><%...%></code> 形式。)</p>
<div class="codeblock has-caption" id="codeblock-7-17"><p class="caption"><span>代码 7.17:</span>用户注册表单</p><p class="file"><code>app/views/users/new.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">'Sign up'</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><h1></span>Sign up<span class="nt"></h1></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"span6 offset3"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:email</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:email</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:password</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="s2">"Confirmation"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password_confirmation</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Create my account"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn btn-large btn-primary"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</pre></div>
</div>
<p>我们来分析一下这些代码。在上面的代码中,我们使用了关键词 <code>do</code>,说明 <code>form_for</code> 后面可以跟着块,而且可以传入一个块参数 <code>f</code>,代表这个表单:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%></span>
.
.
.
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</pre></div>
</div>
<p>我们一般无需了解 Rails 帮助方法的内部实现,但是对于 <code>form_for</code> 来说,我们要知道 <code>f</code> 对象的作用是什么:调用表单字段(例如,文本字段、单选按钮、密码字段)对应的方法时,生成的表单字段元素可以用来设定 <code>@user</code> 对象的属性。也就是说:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:name</span> <span class="cp">%></span>
</pre></div>
</div>
<p>生成的 HTML 是一个有标号(label)的文本字段,可以用来设定 User 模型的 <code>name</code> 属性。(<a href="chapter7.html#section-7-2-3">7.2.3 节</a>会看到生成的 HTML)看过生成的 HTML 才能理解为什么字段可以设定属性。在此之前,还有个问题要解决,因为没有定义 <code>@user</code> 变量,页面无法显示。和其他未定义的实例变量一样,<code>@user</code> 的值现在是 <code>nil</code>。所以如果运行测试的话,会看到代码 7.6 中针对注册页面内容的测试是失败的:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/user_pages_spec.rb -e <span class="s2">"signup page"</span>
</pre></div>
</div>
<p>(上述命令中的 <code>-e</code> 参数指定只运行描述文本包含“signup page”字符串的测试用例。如果改成“signup”,则会运行代码 7.16 中的所有测试。)</p>
<p>要使这个测试通过,同时也让页面可以正常显示,我们要在 <code>new.html.erb</code> 视图对应的 <code>new</code> 动作中定义 <code>@user</code> 变量。<code>form_for</code> 方法的参数需要一个 User 对象,而且我们要创建新用户,所以我们可以使用 <code>User.new</code> 方法,如代码 7.18 所示。</p>
<div class="codeblock has-caption" id="codeblock-7-18"><p class="caption"><span>代码 7.18:</span>在 <code>new</code> 动作中定义 <code>@user</code> 变量</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="kp">new</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>定义 <code>@user</code> 变量后,注册页面的测试就可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/user_pages_spec.rb -e <span class="s2">"signup page"</span>
</pre></div>
</div>
<p>再添加代码 7.19 中的样式,表单的效果如图 7.12 所示。注意,我们再次用到了代码 7.2 中的 <code>box-sizing</code> 这个 mixin。</p>
<div class="codeblock has-caption" id="codeblock-7-19"><p class="caption"><span>代码 7.19:</span>注册表单的样式</p><p class="file"><code>app/assets/stylesheets/custom.css.scss</code></p><div class="highlight type-scss"><pre><span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">/*</span> <span class="nt">forms</span> <span class="o">*/</span>
<span class="nt">input</span><span class="o">,</span> <span class="nt">textarea</span><span class="o">,</span> <span class="nt">select</span><span class="o">,</span> <span class="nc">.uneditable-input</span> <span class="p">{</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="mh">#bbb</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="k">@include</span> <span class="nd">box_sizing</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span> <span class="p">{</span>
<span class="nl">height</span><span class="p">:</span> <span class="nb">auto</span> <span class="o">!</span><span class="n">important</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<h3 id='section-7-2-3'><span>7.2.3</span> 表单的 HTML</h3>
<p>如图 7.12 所示,“注册”页面现在可以正常显示了,说明代码 7.17 中的 <code>form_for</code> 方法生成了合法的 HTML。生成的表单 HTML(可以使用 Firebug 或浏览器的“查看源文件”菜单查看)如代码7.20 所示。某些细节现在无需关心,我们只解说最重要的结构。</p>
<div class="codeblock has-caption" id="codeblock-7-20"><p class="caption"><span>代码 7.20:</span>图 7.12 中表单的 HTML</p><div class="highlight type-html"><pre><span class="nt"><form</span> <span class="na">accept-charset=</span><span class="s">"UTF-8"</span> <span class="na">action=</span><span class="s">"/users"</span> <span class="na">class=</span><span class="s">"new_user"</span>
<span class="na">id=</span><span class="s">"new_user"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"user_name"</span><span class="nt">></span>Name<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_name"</span> <span class="na">name=</span><span class="s">"user[name]"</span> <span class="na">type=</span><span class="s">"text"</span> <span class="nt">/></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"user_email"</span><span class="nt">></span>Email<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_email"</span> <span class="na">name=</span><span class="s">"user[email]"</span> <span class="na">type=</span><span class="s">"text"</span> <span class="nt">/></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"user_password"</span><span class="nt">></span>Password<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_password"</span> <span class="na">name=</span><span class="s">"user[password]"</span>
<span class="na">type=</span><span class="s">"password"</span> <span class="nt">/></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"user_password_confirmation"</span><span class="nt">></span>Confirmation<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_password_confirmation"</span>
<span class="na">name=</span><span class="s">"user[password_confirmation]"</span> <span class="na">type=</span><span class="s">"password"</span> <span class="nt">/></span>
<span class="nt"><input</span> <span class="na">class=</span><span class="s">"btn btn-large btn-primary"</span> <span class="na">name=</span><span class="s">"commit"</span> <span class="na">type=</span><span class="s">"submit"</span>
<span class="na">value=</span><span class="s">"Create my account"</span> <span class="nt">/></span>
<span class="nt"></form></span>
</pre></div>
</div>
<div class="figure" id="figure-7-12">
<img src="figures/signup_form_bootstrap.png" alt="signup form bootstrap" />
<p class="caption"><span>图 7.12:</span>注册用户的表单 <a href="http://localhost:3000/signup">/signup</a></p>
</div>
<p>(上面的代码省略了“鉴别权标(authenticity token)”相关的 HTML。Rails 使用鉴别权标来防止跨站请求伪造(cross-site request forgery, CSRF)攻击。如果你对鉴别权标感兴趣,可以阅读一下 Stack Overflow 网站中的《<a href="http://stackoverflow.com/questions/941594/understand-rails-authenticity-token">Understand Rails Authenticity Token!</a>》一文,这篇文章介绍了鉴别权标的工作原理及重要意义。)</p>
<p>下面看一下表单的字段。比较代码 7.17 和代码 7.20 之后,我们可以看到,如下的 ERb 代码</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:name</span> <span class="cp">%></span>
</pre></div>
</div>
<p>生成的 HTML 是</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><label</span> <span class="na">for=</span><span class="s">"user_name"</span><span class="nt">></span>Name<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_name"</span> <span class="na">name=</span><span class="s">"user[name]"</span> <span class="na">type=</span><span class="s">"text"</span> <span class="nt">/></span>
</pre></div>
</div>
<div class="figure" id="figure-7-13">
<img src="figures/filled_in_form_bootstrap.png" alt="filled in form bootstrap" />
<p class="caption"><span>图 7.13:</span>填写了文本字段和密码字段的表单</p>
</div>
<p>下面的 ERb 代码</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:password</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password</span> <span class="cp">%></span>
</pre></div>
</div>
<p>生成的 HTML 是</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><label</span> <span class="na">for=</span><span class="s">"user_password"</span><span class="nt">></span>Password<span class="nt"></label><br</span> <span class="nt">/></span>
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_password"</span> <span class="na">name=</span><span class="s">"user[password]"</span> <span class="na">type=</span><span class="s">"password"</span> <span class="nt">/></span>
</pre></div>
</div>
<p>如图 7.13 所示,文本字段(<code>type="text"</code>)会直接显示填写的内容,而密码字段(<code>type="password"</code>)基于安全考虑会遮盖输入的内容。</p>
<p>在 <a href="chapter7.html#section-7-4">7.4 节</a>中我们会介绍,之所以可以创建用户,全赖于 <code>input</code> 元素的 <code>name</code> 属性:</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_name"</span> <span class="na">name=</span><span class="s">"user[name]"</span> <span class="na">-</span> <span class="na">-</span> <span class="na">-</span> <span class="nt">/></span>
.
.
.
<span class="nt"><input</span> <span class="na">id=</span><span class="s">"user_password"</span> <span class="na">name=</span><span class="s">"user[password]"</span> <span class="na">-</span> <span class="na">-</span> <span class="na">-</span> <span class="nt">/></span>
</pre></div>
</div>
<div class="figure" id="figure-7-14">
<img src="figures/signup_failure_mockup_bootstrap.png" alt="signup failure mockup bootstrap" />
<p class="caption"><span>图 7.14:</span>注册失败后的页面构思图</p>
</div>
<p>Rails 会以 <code>name</code> 属性的值为键,用户输入的内容为值,构成一个名为 <code>params</code> 的 Hash,用来创建用户。另外一个重要的标签是 <code>form</code>。我们使用 <code>@user</code> 对象来创建 <code>form</code>元素,因为每个 Ruby 对象都知道它所属的类(参见 <a href="chapter4.html#section-4-4-1">4.4.1 节</a>),所以 Rails 知道 <code>@user</code> 所属的类是 <code>User</code>;而且,<code>@user</code> 代表的是新创建的用户,Rails 知道要使用 <code>POST</code> 请求方法,这正是创建新对象所需的 HTTP 请求(参见<a href="chapter3.html#aside-3-3">旁注 3.3</a>):</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><form</span> <span class="na">action=</span><span class="s">"/users"</span> <span class="na">class=</span><span class="s">"new_user"</span> <span class="na">id=</span><span class="s">"new_user"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
</pre></div>
</div>
<p>先不看 <code>class</code> 和 <code>id</code> 属性,我们现在关注的是 <code>action="/users"</code> 和 <code>method="post"</code>。设定这两个属性后,Rails 就会向 /users 地址发送一个 <code>POST</code> 请求。接下来的两节会介绍这个请求产生的效果。</p>
<h2 id='section-7-3'><span>7.3</span> 注册失败</h2>
<p>虽然上一节大概的介绍了图 7.12 中表单的 HTML 结构(参见代码 7.20),不过注册失败时才能更好的理解这个表单的作用。本节,我们会在注册表单中填写一些不合法的数据,提交表单后,页面不会转向其他页面,而是返回“注册”页面,显示一些错误提示信息,页面的构思图如图7.14 所示。</p>
<h3 id='section-7-3-1'><span>7.3.1</span> 可正常使用的表单</h3>
<p>回顾一下 <a href="chapter7.html#section-7-1-2">7.1.2 节</a>中的内容,在 <code>routes.rb</code> 中设置 <code>resources :users</code> 之后(参见代码 7.3),Rails 应用程序就可以响应<a href="chapter7.html#table-7-1">表格 7.1</a>中符合 REST 架构的 URI 地址了。一般来说,发送到 /users 地址的 <code>POST</code> 请求是由 <code>create</code> 动作处理的。在 <code>create</code> 动作中,我们可以调用 <code>User.new</code> 方法,使用提交的数据创建一个新用户对象,尝试存入数据库,失败后再重新渲染“注册”页面,允许访客重新填写注册信息。我们先来看一下生成的 <code>form</code> 元素:</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><form</span> <span class="na">action=</span><span class="s">"/users"</span> <span class="na">class=</span><span class="s">"new_user"</span> <span class="na">id=</span><span class="s">"new_user"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
</pre></div>