-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathchapter7.html
1750 lines (1497 loc) · 162 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>Ruby on Rails 教程 - 第 7 章 注册</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="最好的 Ruby on Rails 入门教程"/>
<meta name="keywords" content="ruby, rails, tutorial"/>
<meta name="author" content="Michael Hartl"/>
<meta name="translator" content="安道"/>
<meta name="generator" content="persie 0.0.1.beta.3"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="assets/style.css"/>
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/js/collapse.min.js"></script>
<script type="text/javascript" src="assets/global.js"></script>
</head>
<body>
<header class="navbar navbar-default navbar-fixed-top navbar-book">
<div class="container">
<div class="navbar-header">
<a href="http://railstutorial-china.org" class="navbar-brand">Ruby on Rails 教程</a>
<button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target=".book-navbar-collapse">
<span class="sr-only">导航</span>
<i class="fa fa-bars"></i>
</button>
<a href="http://railstutorial-china.org/#purchase" id="navbar-purchase-xs" class="btn btn-warning navbar-btn visible-xs collapsed-purchase-btn">购买</a>
</div>
<nav class="collapse navbar-collapse book-navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="http://railstutorial-china.org" title="首页">首页</a></li>
<li class="active"><a href="http://railstutorial-china.org/read/" title="在线阅读">阅读</a></li>
<li><a href="http://railstutorial-china.org/blog/" title="最新消息">博客</a></li>
<li><a href="https://selfstore.io/products/189/topics" title="论坛">论坛</a></li>
<li class="hidden-xs"><div><a href="http://railstutorial-china.org/#purchase" id="navbar-purchase" class="btn btn-warning navbar-btn" title="购买电子书">购买</a></div></li>
</ul>
</nav>
</div>
</header>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<div class="alert alert-warning">
<p>在线版的内容可能落后于电子书,如果想及时获得更新,请<a href="http://railstutorial-china.org/#purchase" title="购买电子书">购买电子书</a>。</p>
</div>
<article class="article">
<section data-type="chapter" id="sign-up">
<h1><span class="title-label">第 7 章</span> 注册</h1>
<p>用户模型可以使用了,接下来要实现大多数网站都离不开的功能:注册。在 <a href="#signup-form">7.2 节</a>我们会创建一个表单,提交用户注册时填写的信息,然后在 <a href="#successful-signups">7.4 节</a>使用提交的数据创建新用户,把属性值存入数据库。注册功能实现后,还要创建一个用户资料页面,显示用户的个人信息——这是实现 REST 架构(<a href="chapter2.html#mvc-in-action">2.2.2 节</a>)用户资源的第一步。在实现这些功能的过程中,我们会在 <a href="chapter5.html#layout-link-tests">5.3.4 节</a>的基础上编写简练生动的集成测试。</p>
<p>本章要依赖<a href="chapter6.html#modeling-users">第 6 章</a>编写的用户模型验证,尽量保证新用户的电子邮件地址有效,<a href="chapter10.html#account-activation-and-password-reset">第 10 章</a>会在用户注册过程中添加账户激活功能,确保电子邮件地址确实可用。</p>
<section data-type="sect1" id="showing-users">
<h1><span class="title-label">7.1</span> 显示用户的信息</h1>
<div id="fig-profile-mockup-profile-name" class="figure"><img src="images/chapter7/profile_mockup_profile_name_bootstrap.png" alt="profile mockup profile name bootstrap" /><div class="figcaption"><span class="title-label">图 7.1</span>:本节实现的用户资料页面构思图</div></div>
<p>本节要实现的用户资料页面是完整页面的一小部分,只显示用户的名字和头像,构思图如<a href="#fig-profile-mockup-profile-name">图 7.1</a> 所示。<sup>[<a id="fn-ref-1" href="#fn-1">1</a>]</sup> 最终完成的用户资料页面会显示用户的头像、基本信息和微博列表,构思图如<a href="#fig-profile-mockup">图 7.2</a> 所示。<sup>[<a id="fn-ref-2" href="#fn-2">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="chapter12.html#following-users">第 12 章</a>完成。</p>
<p>如果你一直坚持使用版本控制,现在像之前一样,新建一个主题分支:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>git checkout master
<span class="nv">$ </span>git checkout -b sign-up
</pre></div>
</div>
<div id="fig-profile-mockup" class="figure"><img src="images/chapter7/profile_mockup_bootstrap.png" alt="profile mockup bootstrap" /><div class="figcaption"><span class="title-label">图 7.2</span>:最终实现的用户资料页面构思图</div></div>
<section data-type="sect2" id="debug-and-rails-environments">
<h2><span class="title-label">7.1.1</span> 调试信息和 Rails 环境</h2>
<p>本节要实现的用户资料页面是第一个真正意义上的动态页面。虽然视图的代码不会动态改变,不过每个用户资料页面显示的内容却是从数据库中读取的。添加动态页面之前,最好做些准备工作,现在我们能做的就是在网站布局中加入一些调试信息,如<a href="#listing-rails-debug">代码清单 7.1</a> 所示。这段代码使用 Rails 内置的 <code>debug</code> 方法和 <code>params</code> 变量(<a href="#a-users-resource">7.1.2 节</a>会详细介绍),在各个页面显示一些对开发有帮助的信息。</p>
<div id="listing-rails-debug" data-type="listing">
<h5><span class="title-label">代码清单 7.1</span>:在网站布局中添加一些调试信息</h5>
<div class="source-file">app/views/layouts/application.html.erb</div>
<div class="highlight language-erb"><pre><span class="x"><!DOCTYPE html></span>
<span class="x"><html></span>
<span class="x"> .</span>
<span class="x"> .</span>
<span class="x"> .</span>
<span class="x"> <body></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'layouts/header'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <div class="container"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="k">yield</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'layouts/footer'</span> <span class="cp">%></span><span class="x"></span>
<span class="hll"><span class="x"> </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="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span> <span class="cp">%></span><span class="x"></span>
</span><span class="x"> </div></span>
<span class="x"> </body></span>
<span class="x"></html></span>
</pre></div>
</div>
<p>因为我们不想在线上网站中向用户显示调试信息,所以上述代码使用 <code>if Rails.env.development?</code> 限制只在开发环境中显示调试信息。开发环境是 Rails 默认支持的三个环境之一(<a href="#aside-rails-environments">旁注 7.1</a>)。<sup>[<a id="fn-ref-3" href="#fn-3">3</a>]</sup><code>Rails.env.development?</code> 的返回值只在开发环境中才是 <code>true</code>,所以下面这行嵌入式 Ruby 代码</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><%<span class="o">=</span> debug<span class="o">(</span>params<span class="o">)</span> <span class="k">if</span> Rails.env.development? %>
</pre></div>
</div>
<p>不会在生产环境和测试环境中执行。(在测试环境中显示调试信息虽然没有坏处,但也没什么好处,所以最好只在开发环境中显示。)</p>
<div data-type="sidebar" id="aside-rails-environments" class="sidebar">
<h5><span class="title-label">旁注 7.1</span>:Rails 环境</h5>
<p>Rails 定义了三个环境,分别是测试环境、开发环境和生产环境。Rails 控制台默认使用的是开发环境:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console</span>
<span class="go">Loading development environment</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span>
<span class="go">=> "development"</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>
<span class="go">=> true</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">test?</span>
<span class="go">=> false</span>
</pre></div>
</div>
<p>如前所示,<code>Rails</code> 对象有一个 <code>env</code> 属性,属性上还可以调用各环境对应的布尔值方法,例如,<code>Rails.env.test?</code>,在测试环境中的返回值是 <code>true</code>,在其他两个环境中的返回值则是 <code>false</code>。</p>
<p>如果需要在其他环境中使用控制台(例如,在测试环境中调试),只需把环境名传给 <code>console</code> 命令即可:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console test</span>
<span class="go">Loading test environment</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span>
<span class="go">=> "test"</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">test?</span>
<span class="go">=> true</span>
</pre></div>
</div>
<p>Rails 本地服务器和控制台一样,默认使用开发环境,不过也可以在其他环境中运行:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails server --environment production
</pre></div>
</div>
<p>如果要在生产环境中运行应用,先要有一个生产数据库。在生产环境中执行 <code>rake db:migrate</code> 命令可以生成这个数据库:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </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 后,可以使用 <code>heroku run console</code> 命令进入控制台查看使用的环境:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ heroku run console</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span>
<span class="go">=> "production"</span>
<span class="gp">>> </span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">production?</span>
<span class="go">=> true</span>
</pre></div>
</div>
<p>Heroku 是用来部署网站的平台,自然会在生产环境中运行应用。</p>
</div>
<p>为了让调试信息看起来漂亮一些,我们在<a href="chapter5.html#filling-in-the-layout">第 5 章</a>创建的自定义样式表文件中加入一些样式规则,如<a href="#listing-mixin-and-debug">代码清单 7.2</a> 所示。</p>
<div id="listing-mixin-and-debug" data-type="listing">
<h5><span class="title-label">代码清单 7.2</span>:添加美化调试信息的样式,使用了一个 Sass 混入</h5>
<div class="source-file">app/assets/stylesheets/custom.css.scss</div>
<div class="highlight language-scss"><pre><span class="k">@import</span> <span class="s2">"bootstrap-sprockets"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"bootstrap"</span><span class="p">;</span>
<span class="cm">/* mixins, variables, etc. */</span>
<span class="nv">$gray-medium-light</span><span class="o">:</span> <span class="mh">#eaeaea</span><span class="p">;</span>
<span class="hll"><span class="k">@mixin</span><span class="nf"> box_sizing</span> <span class="p">{</span>
</span><span class="hll"> <span class="na">-moz-box-sizing</span><span class="o">:</span> <span class="no">border</span><span class="o">-</span><span class="n">box</span><span class="p">;</span>
</span><span class="hll"> <span class="na">-webkit-box-sizing</span><span class="o">:</span> <span class="no">border</span><span class="o">-</span><span class="n">box</span><span class="p">;</span>
</span><span class="hll"> <span class="na">box-sizing</span><span class="o">:</span> <span class="no">border</span><span class="o">-</span><span class="n">box</span><span class="p">;</span>
</span><span class="hll"><span class="p">}</span>
</span><span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="hll"><span class="o">/*</span> <span class="nt">miscellaneous</span> <span class="o">*/</span>
</span>
<span class="hll"><span class="nc">.debug_dump</span> <span class="p">{</span>
</span><span class="hll"> <span class="na">clear</span><span class="o">:</span> <span class="no">both</span><span class="p">;</span>
</span><span class="hll"> <span class="na">float</span><span class="o">:</span> <span class="no">left</span><span class="p">;</span>
</span><span class="hll"> <span class="na">width</span><span class="o">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
</span><span class="hll"> <span class="na">margin-top</span><span class="o">:</span> <span class="mi">45</span><span class="kt">px</span><span class="p">;</span>
</span><span class="hll"> <span class="k">@include</span><span class="nd"> box_sizing</span><span class="p">;</span>
</span><span class="hll"><span class="p">}</span>
</span></pre></div>
</div>
<p>这段代码用到了 Sass 的“混入”(mixin)功能,创建的这个混入名为 <code>box-sizing</code>。混入用来打包一系列样式规则,可多次使用。预处理器会把</p>
<div data-type="listing">
<div class="highlight language-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="o">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>转换成</p>
<div data-type="listing">
<div class="highlight language-css"><pre><span class="nc">.debug_dump</span> <span class="p">{</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="n">box</span><span class="o">-</span><span class="n">sizing</span><span class="o">:</span> <span class="k">border</span><span class="o">-</span><span class="n">box</span><span class="p">;</span>
<span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="n">box</span><span class="o">-</span><span class="n">sizing</span><span class="o">:</span> <span class="k">border</span><span class="o">-</span><span class="n">box</span><span class="p">;</span>
<span class="n">box</span><span class="o">-</span><span class="n">sizing</span><span class="o">:</span> <span class="k">border</span><span class="o">-</span><span class="n">box</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p><a href="#using-form-for">7.2.1 节</a>会再次用到这个混入。美化后的调试信息如<a href="#fig-home-page-with-debug">图 7.3</a> 所示。</p>
<p><a href="#fig-home-page-with-debug">图 7.3</a> 中的调试信息显示了当前页面的一些有用信息:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><span class="nn">---</span>
<span class="l-Scalar-Plain">controller</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">static_pages</span>
<span class="l-Scalar-Plain">action</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">home</span>
</pre></div>
</div>
<p>这是 <code>params</code> 变量的 YAML <sup>[<a id="fn-ref-4" href="#fn-4">4</a>]</sup>形式,和哈希类似,显示当前页面的控制器名和动作名。<a href="#a-users-resource">7.1.2 节</a>会介绍其他调试信息的意思。</p>
<div id="fig-home-page-with-debug" class="figure"><img src="images/chapter7/home_page_with_debug_3rd_edition.png" alt="home page with debug 3rd edition" /><div class="figcaption"><span class="title-label">图 7.3</span>:显示有调试信息的演示应用首页</div></div>
</section>
<section data-type="sect2" id="a-users-resource">
<h2><span class="title-label">7.1.2</span> 用户资源</h2>
<p>为了实现用户资料页面,数据库中要有用户记录,这引出了“先有鸡还是先有蛋”的问题:网站还没有注册页面,怎么可能有用户呢?其实这个问题在 <a href="chapter6.html#creating-and-authenticating-a-user">6.3.4 节</a>已经解决了,那时我们自己动手在 Rails 控制台中创建了一个用户,所以数据库中应该有一个用户记录:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console</span>
<span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">count</span>
<span class="go">=> 1</span>
<span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">first</span>
<span class="go">=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",</span>
<span class="go">created_at: "2014-08-29 02:58:28", updated_at: "2014-08-29 02:58:28",</span>
<span class="go">password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW..."></span>
</pre></div>
</div>
<p>(如果你的数据库中现在没有用户记录,回到 <a href="chapter6.html#creating-and-authenticating-a-user">6.3.4 节</a>,在继续阅读之前完成那里的操作。)从控制台的输出可以看出,这个用户的 ID 是 <code>1</code>,我们现在的目标就是创建一个页面,显示这个用户的信息。我们会遵从 Rails 使用的 REST 架构(<a href="chapter2.html#aside-rest">旁注 2.2</a>),把数据视为资源,可以创建、显示、更新和删除。这四个操作分别对应 <a href="http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol">HTTP 标准</a>中的 <code>POST</code>、<code>GET</code>、<code>PATCH</code> 和 <code>DELETE</code> 请求方法(<a href="chapter3.html#aside-get-etc">旁注 3.2</a>)。</p>
<p>按照 REST 架构的规则,资源一般由资源名加唯一标识符表示。我们把用户看做一个资源,若要查看 ID 为 1 的用户,就要向 /users/1 发送 <code>GET</code> 请求。这里没必要指明用哪个动作,Rails 的 REST 功能解析时,会自动把这个 <code>GET</code> 请求交给 <code>show</code> 动作处理。</p>
<p><a href="chapter2.html#a-user-tour">2.2.1 节</a>介绍过,ID 为 1 的用户对应的 URL 是 /users/1,不过现在访问这个 URL 的话,会显示错误信息,如<a href="#fig-profile-routing-error">图 7.4</a> 中的服务器日志所示。</p>
<div id="fig-profile-routing-error" class="figure"><img src="images/chapter7/profile_routing_error_3rd_edition.png" alt="profile routing error 3rd edition" /><div class="figcaption"><span class="title-label">图 7.4</span>:访问 /users/1 时服务器日志中显示的错误</div></div>
<p>我们只需在路由文件 <code>config/routes.rb</code> 中添加如下的一行代码就可以正常访问 /users/1 了:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">resources</span> <span class="ss">:users</span>
</pre></div>
</div>
<p>修改后的路由文件如<a href="#listing-users-resource">代码清单 7.3</a> 所示。</p>
<div id="listing-users-resource" data-type="listing">
<h5><span class="title-label">代码清单 7.3</span>:在路由文件中添加用户资源的规则</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">routes</span><span class="o">.</span><span class="n">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'help'</span> <span class="o">=></span> <span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'about'</span> <span class="o">=></span> <span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'contact'</span> <span class="o">=></span> <span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'signup'</span> <span class="o">=></span> <span class="s1">'users#new'</span>
<span class="hll"> <span class="n">resources</span> <span class="ss">:users</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>我们的目的只是为了显示用户资料页面,可是 <code>resources :users</code> 不仅让 /users/1 可以访问,而且还为演示应用中的用户资源提供了符合 REST 架构的所有动作,<sup>[<a id="fn-ref-5" href="#fn-5">5</a>]</sup>以及用来获取相应 URL 的具名路由(<a href="chapter5.html#using-named-routes">5.3.3 节</a>)。最终得到的 URL、动作和具名路由的对应关系如<a href="#table-restful-users">表 7.1</a> 所示(和<a href="chapter2.html#table-demo-restful-users">表 2.2</a> 对比一下)。接下来的三章会介绍<a href="#table-restful-users">表 7.1</a> 中的所有动作,并不断完善,把用户打造成完全符合 REST 架构的资源。</p>
<table id="table-restful-users" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 7.1</span>:<a href="#listing-users-resource">代码清单 7.3</a> 中添加的用户资源规则实现的 REST 架构路由</caption>
<colgroup>
<col style="width: 15%;" />
<col style="width: 15%;" />
<col style="width: 15%;" />
<col style="width: 20%;" />
<col style="width: 35%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">HTTP 请求</th>
<th class="tableblock halign-left valign-top">URL</th>
<th class="tableblock halign-left valign-top">动作</th>
<th class="tableblock halign-left valign-top">具名路由</th>
<th class="tableblock halign-left valign-top">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>GET</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>index</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>users_path</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">显示所有用户的页面</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>GET</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>show</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user_path(user)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">显示单个用户的页面</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>GET</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/new</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>new</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>new_user_path</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建(注册)新用户的页面</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>POST</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>create</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>users_path</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建新用户</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>GET</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1/edit</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>edit</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>edit_user_path(user)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">编辑 ID 为 1 的用户页面</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>PATCH</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>update</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user_path(user)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">更新用户信息</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>DELETE</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>destroy</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user_path(user)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">删除用户</p></td>
</tr>
</tbody>
</table>
<p>添加<a href="#listing-users-resource">代码清单 7.3</a> 中的代码之后,路由就生效了,但是页面还不存在(<a href="#fig-user-show-unknown-action">图 7.5</a>)。下面我们在页面中添加一些简单的内容,<a href="#a-gravatar-image-and-a-sidebar">7.1.4 节</a>还会添加更多内容。</p>
<div id="fig-user-show-unknown-action" class="figure"><img src="images/chapter7/user_show_unknown_action_3rd_edition.png" alt="user show unknown action 3rd edition" /><div class="figcaption"><span class="title-label">图 7.5</span>:/users/1 的路由生效了,但页面不存在</div></div>
<p>用户资料页面的视图保存在标准的位置,即 <code>app/views/users/show.html.erb</code>。这个视图和自动生成的 <code>new.html.erb</code>(<a href="chapter5.html#listing-generate-users-controller">代码清单 5.28</a>)不同,现在不存在,要手动创建,然后写入<a href="#listing-stub-user-view">代码清单 7.4</a> 中的代码。</p>
<div id="listing-stub-user-view" data-type="listing">
<h5><span class="title-label">代码清单 7.4</span>:用户资料页面的临时视图</h5>
<div class="source-file">app/views/users/show.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="cp">%></span><span class="x">, </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">email</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>在这段代码中,我们假设存在一个 <code>@user</code> 变量,使用 ERb 代码显示这个用户的名字和电子邮件地址。这和最终实现的视图有点不一样,届时不会公开显示用户的电子邮件地址。</p>
<p>我们要在用户控制器的 <code>show</code> 动作中定义 <code>@user</code> 变量,用户资料页面才能正常渲染。你可能猜到了,我们要在用户模型上调用 <code>find</code> 方法(<a href="chapter6.html#finding-user-objects">6.1.4 节</a>),从数据库中取出用户记录,如<a href="#listing-user-show-action">代码清单 7.5</a> 所示。</p>
<div id="listing-user-show-action" data-type="listing">
<h5><span class="title-label">代码清单 7.5</span>:含有 <code>show</code> 动作的用户控制器</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-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="hll"> <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
</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。当我们向用户控制器发送请求时,<code>params[:id]</code> 会返回用户的 ID,即 1,所以这就和 <a href="chapter6.html#finding-user-objects">6.1.4 节</a>中直接调用 <code>User.find(1)</code> 的效果一样。(严格来说,<code>params[:id]</code> 返回的是字符串 <code>"1"</code>,<code>find</code> 方法会自动将其转换成整数。)</p>
<p>定义视图和动作之后,/users/1 就可以正常访问了,如<a href="#fig-user-show-rails">图 7.6</a> 所示。(如果添加 bcrypt 之后没重启过 Rails 服务器,现在或许要重启。)留意一下调试信息,证实了 <code>params[:id]</code> 的值和前面分析的一样:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><span class="nn">---</span>
<span class="l-Scalar-Plain">action</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">show</span>
<span class="l-Scalar-Plain">controller</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">users</span>
<span class="l-Scalar-Plain">id</span><span class="p-Indicator">:</span> <span class="s">'1'</span>
</pre></div>
</div>
<p>所以,<a href="#listing-user-show-action">代码清单 7.5</a> 中的 <code>User.find(params[:id])</code> 才会取回 ID 为 1 的用户。</p>
<div id="fig-user-show-rails" class="figure"><img src="images/chapter7/user_show_3rd_edition.png" alt="user show 3rd edition" /><div class="figcaption"><span class="title-label">图 7.6</span>:添加 <code>show</code> 动作后的用户资料页面</div></div>
</section>
<section data-type="sect2" id="debugger">
<h2><span class="title-label">7.1.3</span> 调试器</h2>
<p>在 <a href="#a-users-resource">7.1.2 节</a>看到,调试信息能帮助我们理解应用的运作方式。从 Rails 4.2 开始,可以使用 <code>byebug</code> gem(<a href="chapter3.html#listing-gemfile-sample-app">代码清单 3.2</a>)更直接地获取调试信息。我们把 <code>debugger</code> 加到应用中,看一下这个 gem 的作用,如<a href="#listing-debugger">代码清单 7.6</a> 所示。</p>
<div id="listing-debugger" data-type="listing">
<h5><span class="title-label">代码清单 7.6</span>:在用户控制器中使用调试器</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-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="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="hll"> <span class="n">debugger</span>
</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>现在访问 /users/1 时,会在 Rails 服务器的输出中显示 <code>byebug</code> 提示符:</p>
<div data-type="listing">
<pre class="highlight language-sh"><code><span class="o">(</span>byebug<span class="o">)</span></code></pre>
</div>
<p>我们可以把它当成 Rails 控制台,在其中执行代码,看一下应用的状态:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="o">(</span>byebug<span class="o">)</span> @user.name
<span class="s2">"Example User"</span>
<span class="o">(</span>byebug<span class="o">)</span> @user.email
<span class="s2">"[email protected]"</span>
<span class="o">(</span>byebug<span class="o">)</span> params<span class="o">[</span>:id<span class="o">]</span>
<span class="s2">"1"</span>
</pre></div>
</div>
<p>若想退出 <code>byebug</code>,继续执行应用,可以按 Ctrl-D 键。然后把 <code>show</code> 动作中的 <code>debugger</code> 删除,如<a href="#listing-debugger-removed">代码清单 7.7</a> 所示。</p>
<div id="listing-debugger-removed" data-type="listing">
<h5><span class="title-label">代码清单 7.7</span>:删除调试器后的用户控制器</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-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="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</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>只要你觉得 Rails 应用中哪部分有问题,就可以在可能导致问题的代码附近加上 <code>debugger</code>。<code>byebug</code> 很强大,可以查看系统的状态,查找应用错误,以及交互式调试应用。</p>
</section>
<section data-type="sect2" id="a-gravatar-image-and-a-sidebar">
<h2><span class="title-label">7.1.4</span> Gravatar 头像和侧边栏</h2>
<p>前面创建了一个略显简陋的用户资料页面,这一节要再添加一些内容:用户头像和侧边栏。首先,我们要在用户资料页面中添加一个“全球通用识别”的头像,或者叫 <a href="http://gravatar.com/">Gravatar</a>。<sup>[<a id="fn-ref-6" href="#fn-6">6</a>]</sup>这是一个免费服务,让用户上传图片,将其关联到自己的电子邮件地址上。使用 Gravatar 可以简化在网站中添加用户头像的过程,开发者不必分心去处理图片上传、剪裁和存储,只要使用用户的电子邮件地址构成头像的 URL 地址,用户的头像就会显示出来。(<a href="chapter11.html#micropost-images">11.4 节</a>会介绍如何处理图片上传。)</p>
<p>我们的计划是,定义一个名为 <code>gravatar_for</code> 的辅助方法,返回指定用户的 Gravatar 头像,如<a href="#listing-user-show-view-with-gravatar">代码清单 7.8</a> 所示。</p>
<div id="listing-user-show-view-with-gravatar" data-type="listing">
<h5><span class="title-label">代码清单 7.8</span>:显示用户名字和 Gravatar 头像的用户资料页面视图</h5>
<div class="source-file">app/views/users/show.html.erb</div>
<div class="highlight language-ruby"><pre><span class="o"><</span><span class="sx">% provide(:title, </span><span class="vi">@user</span><span class="o">.</span><span class="n">name</span><span class="p">)</span> <span class="sx">%></span>
<span class="sx"><h1></span>
<span class="o"><</span><span class="sx">%= gravatar_for @user %></span>
<span class="sx"> <%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="sx">%></span>
<span class="sx"></h1></span>
</pre></div>
</div>
<p>默认情况下,所有辅助方法文件中定义的方法都自动在任意视图中可用,不过为了便于管理,我们会把 <code>gravatar_for</code> 方法放在用户控制器对应的辅助方法文件中。根据 <a href="http://en.gravatar.com/site/implement/hash/">Gravatar 的文档</a>,头像的 URL 地址中要使用用户电子邮件地址的 <a href="http://en.wikipedia.org/wiki/MD5">MD5 哈希值</a>。在 Ruby 中,MD5 哈希算法由 <code>Digest</code> 库中的 <code>hexdigest</code> 方法实现:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">email</span> <span class="o">=</span> <span class="s2">"[email protected]"</span><span class="o">.</span>
<span class="hll"><span class="gp">>> </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">email</span><span class="o">.</span><span class="n">downcase</span><span class="p">)</span>
</span><span class="go">=> "1fda4469bcbec3badf5418269ffc5968"</span>
</pre></div>
</div>
<p>电子邮件地址不区分大小写,但是 MD5 哈希算法区分,所以我们要先调用 <code>downcase</code> 方法把电子邮件地址转换成小写形式,然后再传给 <code>hexdigest</code> 方法。(在<a href="chapter6.html#listing-email-downcase">代码清单 6.31</a> 中的回调里我们已经把电子邮件地址转换成小写形式了,但这里最好也转换,以防电子邮件地址来自其他地方。)我们定义的 <code>gravatar_for</code> 辅助方法如<a href="#listing-gravatar-for-helper">代码清单 7.9</a> 所示。</p>
<div id="listing-gravatar-for-helper" data-type="listing">
<h5><span class="title-label">代码清单 7.9</span>:定义 <code>gravatar_for</code> 辅助方法</h5>
<div class="source-file">app/helpers/users_helper.rb</div>
<div class="highlight language-ruby"><pre><span class="k">module</span> <span class="nn">UsersHelper</span>
<span class="c1"># 返回指定用户的 Gravatar</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="o">.</span><span class="n">email</span><span class="o">.</span><span class="n">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="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</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> 方法的返回值是一个 <code>img</code> 元素,用于显示 Gravatar 头像。<code>img</code> 标签的 CSS 类为 <code>gravatar</code>,<code>alt</code> 属性的值是用户的名字(对视觉障碍人士使用的屏幕阅读器特别有用)。</p>
<p>用户资料页面如<a href="#fig-profile-with-gravatar">图 7.7</a> 所示,页面中显示的头像是 Gravatar 的默认图片,因为 <code>[email protected]</code> 不是真的电子邮件地址(example.com 这个域名是专门用来举例的)。</p>
<div id="fig-profile-with-gravatar" class="figure"><img src="images/chapter7/profile_with_gravatar_3rd_edition.png" alt="profile with gravatar 3rd edition" /><div class="figcaption"><span class="title-label">图 7.7</span>:显示 Gravatar 默认头像的用户资料页面</div></div>
<div id="fig-profile-custom-gravatar" class="figure"><img src="images/chapter7/profile_custom_gravatar_3rd_edition.png" alt="profile custom gravatar 3rd edition" /><div class="figcaption"><span class="title-label">图 7.8</span>:显示真实头像的用户资料页面</div></div>
<p>我们调用 <code>update_attributes</code> 方法(<a href="chapter6.html#updating-user-objects">6.1.5 节</a>)更新一下数据库中的用户记录,然后就可以显示用户真正的头像了:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console</span>
<span class="gp">>> </span><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">first</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">update_attributes</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span>
<span class="gp">?> </span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="gp">?> </span> <span class="ss">password</span><span class="p">:</span> <span class="s2">"foobar"</span><span class="p">,</span>
<span class="gp">?> </span> <span class="ss">password_confirmation</span><span class="p">:</span> <span class="s2">"foobar"</span><span class="p">)</span>
<span class="go">=> true</span>
</pre></div>
</div>
<p>我们把用户的电子邮件地址改成 <code>[email protected]</code>。我已经把这个地址的头像设为了本书网站的 LOGO,修改后的结果如<a href="#fig-profile-custom-gravatar">图 7.8</a> 所示。</p>
<p>我们还要添加一个侧边栏,才能完成<a href="#fig-profile-mockup-profile-name">图 7.1</a> 中的构思图。我们要使用 <code>aside</code> 标签定义侧边栏。<code>aside</code> 中的内容一般是对主体内容的补充(例如侧边栏),不过也可以自成一体。我们要把 <code>aside</code> 标签的类设为 <code>row col-md-4</code>,这两个类都是 Bootstrap 提供的。在用户资料页面中添加侧边栏所需的代码如<a href="#listing-user-show-with-sidebar">代码清单 7.10</a> 所示。</p>
<div id="listing-user-show-with-sidebar" data-type="listing">
<h5><span class="title-label">代码清单 7.10</span>:在 <code>show</code> 动作的视图中添加侧边栏</h5>
<div class="source-file">app/views/users/show.html.erb</div>
<div class="highlight language-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="o">.</span><span class="n">name</span><span class="p">)</span> <span class="cp">%></span><span class="x"></span>
<span class="x"><div class="row"></span>
<span class="x"> <aside class="col-md-4"></span>
<span class="x"> <section class="user_info"></span>
<span class="x"> <h1></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </h1></span>
<span class="x"> </section></span>
<span class="x"> </aside></span>
<span class="x"></div></span>
</pre></div>
</div>
<p>添加 HTML 结构和 CSS 类之后,我们再用 SCSS 为资料页面定义一些样式,如<a href="#listing-sidebar-css">代码清单 7.11</a> 所示。<sup>[<a id="fn-ref-7" href="#fn-7">7</a>]</sup>(注意:因为 Asset Pipeline 使用 Sass 预处理器,所以样式中才可以使用嵌套。)实现的效果如<a href="#fig-user-show-sidebar-css">图 7.9</a> 所示。</p>
<div id="listing-sidebar-css" data-type="listing">
<h5><span class="title-label">代码清单 7.11</span>:用户资料页面的样式,包括侧边栏的样式</h5>
<div class="source-file">app/assets/stylesheets/custom.css.scss</div>
<div class="highlight language-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="nc">.user_info</span> <span class="p">{</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">section</span> <span class="p">{</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span> <span class="mi">0</span><span class="p">;</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
<span class="k">&</span><span class="nd">:first-child</span> <span class="p">{</span>
<span class="na">border</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="na">padding-top</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">span</span> <span class="p">{</span>
<span class="na">display</span><span class="o">:</span> <span class="no">block</span><span class="p">;</span>
<span class="na">margin-bottom</span><span class="o">:</span> <span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
<span class="na">line-height</span><span class="o">:</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h1</span> <span class="p">{</span>
<span class="na">font-size</span><span class="o">:</span> <span class="mi">1</span><span class="mf">.4</span><span class="kt">em</span><span class="p">;</span>
<span class="na">text-align</span><span class="o">:</span> <span class="no">left</span><span class="p">;</span>
<span class="na">letter-spacing</span><span class="o">:</span> <span class="mi">-1</span><span class="kt">px</span><span class="p">;</span>
<span class="na">margin-bottom</span><span class="o">:</span> <span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">0</span><span class="kt">px</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="na">float</span><span class="o">:</span> <span class="no">left</span><span class="p">;</span>
<span class="na">margin-right</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.gravatar_edit</span> <span class="p">{</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">15</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
</section>
</section>
<section data-type="sect1" id="signup-form">
<h1><span class="title-label">7.2</span> 注册表单</h1>
<p>用户资料页面已经可以访问了,但内容还不完整。下面我们要为网站创建一个注册表单。如<a href="chapter5.html#fig-new-signup-page">图 5.9</a> 和<a href="#fig-blank-signup-page-recap">图 7.10</a> 所示,“注册”页面还没有什么内容,无法注册新用户。本节会实现如<a href="#fig-signup-mockup">图 7.11</a> 所示的注册表单,添加注册功能。</p>
<div id="fig-user-show-sidebar-css" class="figure"><img src="images/chapter7/user_show_sidebar_css_3rd_edition.png" alt="user show sidebar css 3rd edition" /><div class="figcaption"><span class="title-label">图 7.9</span>:添加侧边栏和 CSS 后的用户资料页面</div></div>
<div id="fig-blank-signup-page-recap" class="figure"><img src="images/chapter7/new_signup_page_3rd_edition.png" alt="new signup page 3rd edition" /><div class="figcaption"><span class="title-label">图 7.10</span>:注册页面现在的样子</div></div>
<p>因为我们要实现通过网页创建用户的功能,那么就把 <a href="chapter6.html#creating-and-authenticating-a-user">6.3.4 节</a>在控制台中创建的用户删除吧。最简单的方法是使用 <code>db:migrate:reset</code> 命令:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate:reset
</pre></div>
</div>
<p>在某些系统中可能还要重启 Web 服务器才能生效。</p>
<div id="fig-signup-mockup" class="figure"><img src="images/chapter7/signup_mockup_bootstrap.png" alt="signup mockup bootstrap" /><div class="figcaption"><span class="title-label">图 7.11</span>:用户注册页面的构思图</div></div>
<section data-type="sect2" id="using-form-for">
<h2><span class="title-label">7.2.1</span> 使用 <code>form_for</code></h2>
<p>注册页面的核心是一个表单,用于提交注册相关的信息(名字,电子邮件地址,密码和密码确认)。在 Rails 中,创建表单可以使用 <code>form_for</code> 辅助方法,传入 Active Record 对象后,使用该对象的属性构建一个表单。</p>
<p>注册页面的地址是 /signup,由用户控制器的 <code>new</code> 动作处理(<a href="chapter5.html#listing-signup-route">代码清单 5.33</a>)。首先,我们要创建传给 <code>form_for</code> 的用户对象,然后赋值给 <code>@user</code> 变量,如<a href="#listing-new-action-with-user">代码清单 7.12</a> 所示。</p>
<div id="listing-new-action-with-user" data-type="listing">
<h5><span class="title-label">代码清单 7.12</span>:在 <code>new</code> 动作中添加 <code>@user</code> 变量</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-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="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">new</span>
<span class="hll"> <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span>
</span> <span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>表单的代码参见<a href="#listing-signup-form">代码清单 7.13</a>。<a href="#signup-form-html">7.2.2 节</a>会详细分析这个表单,现在我们先添加一些 SCSS,如<a href="#listing-form-css">代码清单 7.14</a> 所示。(注意,这里重用了<a href="#listing-mixin-and-debug">代码清单 7.2</a> 中的混入。)添加样式后的注册页面如<a href="#fig-signup-form">图 7.12</a> 所示。</p>
<div id="listing-signup-form" data-type="listing">
<h5><span class="title-label">代码清单 7.13</span>:用户注册表单</h5>
<div class="source-file">app/views/users/new.html.erb</div>
<div class="highlight language-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="x"></span>
<span class="x"><h1>Sign up</h1></span>
<span class="x"><div class="row"></span>
<span class="x"> <div class="col-md-6 col-md-offset-3"></span>
<span class="x"> </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="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">text_field</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:email</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">email_field</span> <span class="ss">:email</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:password</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">password_field</span> <span class="ss">:password</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="s2">"Confirmation"</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">password_field</span> <span class="ss">:password_confirmation</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">submit</span> <span class="s2">"Create my account"</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s2">"btn btn-primary"</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="x"></div></span>
</pre></div>
</div>
<div id="listing-form-css" data-type="listing">
<h5><span class="title-label">代码清单 7.14</span>:注册表单的样式</h5>
<div class="source-file">app/assets/stylesheets/custom.css.scss</div>
<div class="highlight language-ruby"><pre><span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="sr">/* forms */</span>
<span class="n">input</span><span class="p">,</span> <span class="n">textarea</span><span class="p">,</span> <span class="nb">select</span><span class="p">,</span> <span class="o">.</span><span class="n">uneditable</span><span class="o">-</span><span class="n">input</span> <span class="p">{</span>
<span class="ss">border</span><span class="p">:</span> <span class="mi">1</span><span class="n">px</span> <span class="n">solid</span> <span class="c1">#bbb;</span>
<span class="ss">width</span><span class="p">:</span> <span class="mi">100</span><span class="sx">%;</span>
<span class="sx"> margin-bottom: 15px;</span>
<span class="vi">@include</span> <span class="n">box_sizing</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">input</span> <span class="p">{</span>
<span class="ss">height</span><span class="p">:</span> <span class="n">auto</span> <span class="o">!</span><span class="n">important</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<div id="fig-signup-form" class="figure"><img src="images/chapter7/signup_form_3rd_edition.png" alt="signup form 3rd edition" /><div class="figcaption"><span class="title-label">图 7.12</span>:用户注册页面</div></div>
</section>
<section data-type="sect2" id="signup-form-html">
<h2><span class="title-label">7.2.2</span> 注册表单的 HTML</h2>
<p>为了能更好地理解<a href="#listing-signup-form">代码清单 7.13</a> 中定义的表单,可以分成几段来看。我们先看外层结构——开头在 ERb 中调用 <code>form_for</code> 方法,结尾是 <code>end</code>:</p>
<div data-type="listing">
<div class="highlight language-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="x"></span>
<span class="x"> .</span>
<span class="x"> .</span>
<span class="x"> .</span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>这段代码中有关键字 <code>do</code>,说明 <code>form_for</code> 方法可以接受一个块,而且有一个块变量 <code>f</code>(代表表单)。</p>
<p>我们一般无需了解 Rails 辅助方法的内部实现,但是对于 <code>form_for</code> 来说,我们要知道 <code>f</code> 对象的作用是什么:在这个对象上调用<a href="http://www.w3schools.com/html/html_forms.asp">表单字段</a>(例如,文本字段、单选按钮和密码字段)对应的方法,生成的字段元素可以用来设定 <code>@user</code> 对象的属性。也就是说:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">text_field</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>生成的 HTML 是一个有标注(label)的文本字段,用来设定用户模型的 <code>name</code> 属性。</p>
<p>在浏览器中按右键,然后选择“审查元素”,会看到页面的源码,如<a href="#listing-signup-form-html">代码清单 7.15</a> 所示。下面花点儿时间介绍一下表单的结构。</p>
<div id="listing-signup-form-html" data-type="listing">
<h5><span class="title-label">代码清单 7.15</span>:<a href="#fig-signup-form">图 7.12</a> 中表单的源码</h5>
<div class="highlight language-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"><input</span> <span class="na">name=</span><span class="s">"utf8"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"&#x2713;"</span> <span class="nt">/></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"authenticity_token"</span> <span class="na">type=</span><span class="s">"hidden"</span>
<span class="na">value=</span><span class="s">"NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo="</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">"email"</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-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>
<p>先看表单里的结构。比较一下<a href="#listing-signup-form">代码清单 7.13</a> 和<a href="#listing-signup-form-html">代码清单 7.15</a>,我们发现,下面的 ERb 代码</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">text_field</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>生成的 HTML 是</p>
<div data-type="listing">
<div class="highlight language-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>
<p>下面的 ERb 代码</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:password</span> <span class="cp">%></span><span class="x"></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">password_field</span> <span class="ss">:password</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>生成的 HTML 是</p>
<div data-type="listing">
<div class="highlight language-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></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>如<a href="#fig-filled-in-form">图 7.13</a> 所示,文本字段(<code>type="text"</code>)会直接显示填写的内容,而密码字段(<code>type="password"</code>)基于安全考虑会遮盖输入的内容。</p>
<div id="fig-filled-in-form" class="figure"><img src="images/chapter7/filled_in_form_bootstrap_3rd_edition.png" alt="filled in form bootstrap 3rd edition" /><div class="figcaption"><span class="title-label">图 7.13</span>:在表单的文本字段和密码字段中填写内容</div></div>
<p><a href="#successful-signups">7.4 节</a>会介绍,之所以能创建用户,全靠 <code>input</code> 元素的 <code>name</code> 属性:</p>
<div data-type="listing">
<div class="highlight language-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>
<p><a href="#unsuccessful-signups">7.3 节</a>会介绍,Rails 会以这些 <code>name</code> 属性的值为键,用户输入的内容为值,构成一个名为 <code>params</code> 的哈希,用来创建用户。</p>
<p>另外一个重要的标签是 <code>form</code>。Rails 使用 <code>@user</code> 对象创建这个 <code>form</code> 元素,因为每个 Ruby 对象都知道它所属的类(<a href="chapter4.html#constructors">4.4.1 节</a>),所以 Rails 知道 <code>@user</code> 所属的类是 <code>User</code>,而且,<code>@user</code> 是一个新用户,Rails 知道要使用 <code>post</code> 方法——这正是创建新对象所需的 HTTP 请求(参见<a href="chapter3.html#aside-get-etc">旁注 3.2</a>):</p>
<div data-type="listing">
<div class="highlight language-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>
<p>你可能还会注意到,<code>form</code> 标签中有下面这段代码:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><span class="nt"><div</span> <span class="na">style=</span><span class="s">"display:none"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"utf8"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"&#x2713;"</span> <span class="nt">/></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"authenticity_token"</span> <span class="na">type=</span><span class="s">"hidden"</span>
<span class="na">value=</span><span class="s">"NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo="</span> <span class="nt">/></span>
<span class="nt"></div></span>
</pre></div>
</div>
<p>这段代码不会在浏览器中显示,只在 Rails 内部有用,所以你并不需要知道它的作用。简单来说,这段代码首先使用 Unicode 字符 <code>&#x2713;</code>(对号 ✓)强制浏览器使用正确的字符编码提交数据,然后是一个“真伪令牌”(authenticity token),Rails 用它抵御“跨站请求伪造”(Cross-Site Request Forgery,简称 CSRF)攻击。<sup>[<a id="fn-ref-8" href="#fn-8">8</a>]</sup></p>
</section>
</section>
<section data-type="sect1" id="unsuccessful-signups">
<h1><span class="title-label">7.3</span> 注册失败</h1>
<p>虽然上一节大概介绍了<a href="#fig-signup-form">图 7.12</a> 中表单的 HTML 结构(参见<a href="#listing-signup-form-html">代码清单 7.15</a>),但并没涉及什么细节,其实注册失败时才能更好地理解这个表单的作用。本节,我们会在注册表单中填写一些无效的数据,提交表单后,页面不会转向其他页面,而是返回“注册”页面,显示一些错误消息,如<a href="#fig-signup-failure-mockup">图 7.14</a> 中的构思图所示。</p>
<div id="fig-signup-failure-mockup" class="figure"><img src="images/chapter7/signup_failure_mockup_bootstrap.png" alt="signup failure mockup bootstrap" /><div class="figcaption"><span class="title-label">图 7.14</span>:注册失败时显示的页面构思图</div></div>
<section data-type="sect2" id="a-working-form">
<h2><span class="title-label">7.3.1</span> 可正常使用的表单</h2>
<p>回顾一下 <a href="#a-users-resource">7.1.2 节</a>的内容,在 <code>routes.rb</code> 文件中设置 <code>resources :users</code> 之后(<a href="#listing-users-resource">代码清单 7.3</a>),Rails 应用就可以响应<a href="#table-restful-users">表 7.1</a>中符合 REST 架构的 URL 了。其中,发送到 /users 地址上的 <code>POST</code> 请求由 <code>create</code> 动作处理。在 <code>create</code> 动作中,我们可以调用 <code>User.new</code> 方法,使用提交的数据创建一个新用户对象,尝试存入数据库,失败后再重新渲染“注册”页面,让访客重新填写注册信息。我们先来看一下生成的 <code>form</code> 元素:</p>
<div data-type="listing">
<div class="highlight language-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><a href="#signup-form-html">7.2.2 节</a>说过,这个表单会向 /users 地址发送 <code>POST</code> 请求。</p>
<p>为了让这个表单可用,首先我们要添加<a href="#listing-first-create-action">代码清单 7.16</a> 中的代码。这段代码再次用到了 <code>render</code> 方法,上一次是在局部视图中(<a href="chapter5.html#partials">5.1.3 节</a>),不过如你所见,在控制器的动作中也可以使用 <code>render</code> 方法。同时,我们在这段代码中介绍了 <code>if-else</code> 分支结构的用法:根据 <code>@user.save</code> 的返回值,分别处理用户存储成功和失败两种情况(<a href="chapter6.html#creating-user-objects">6.1.3 节</a>介绍过,存储成功时返回值为 <code>true</code>,失败时返回值为 <code>false</code>)。</p>
<div id="listing-first-create-action" data-type="listing">
<h5><span class="title-label">代码清单 7.16</span>:能处理注册失败的 <code>create</code> 动作</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-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="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">new</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="hll"> <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:user</span><span class="o">]</span><span class="p">)</span> <span class="c1"># 不是最终的实现方式</span>
</span> <span class="k">if</span> <span class="vi">@user</span><span class="o">.</span><span class="n">save</span>
<span class="c1"># 处理注册成功的情况</span>
<span class="k">else</span>
<span class="n">render</span> <span class="s1">'new'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>留意上述代码中的注释——这不是最终的实现方式,但现在完全够用。最终版会在 <a href="#strong-parameters">7.3.2 节</a>实现。</p>
<p>我们要实际操作一下,提交一些无效的注册数据,这样才能更好地理解<a href="#listing-first-create-action">代码清单 7.16</a> 中代码的作用,结果如<a href="#fig-signup-failure">图 7.15</a> 所示,底部完整的调试信息如<a href="#fig-signup-failure-rails-debug">图 7.16</a> 所示。(<a href="#fig-signup-failure">图 7.15</a> 中还显示了 Web 控制台,这是个 Rails 控制台,只不过显示在浏览器中,用来协助调试。我们可以在其中查看用户模型,不过这里我们想审查 <code>params</code>,可是在 Web 控制台中无法获取。)</p>
<div id="fig-signup-failure" class="figure"><img src="images/chapter7/signup_failure_3rd_edition.png" alt="signup failure 3rd edition" /><div class="figcaption"><span class="title-label">图 7.15</span>:注册失败</div></div>
<div id="fig-signup-failure-rails-debug" class="figure"><img src="images/chapter7/signup_failure_debug_3rd_edition.png" alt="signup failure debug 3rd edition" /><div class="figcaption"><span class="title-label">图 7.16</span>:注册失败时显示的调试信息</div></div>
<p>下面我们来分析一下调试信息中请求参数哈希的 <code>user</code> 部分(<a href="#fig-signup-failure-rails-debug">图 7.16</a>),以便深入理解 Rails 处理表单的过程:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="s2">"user"</span> <span class="o">=></span> <span class="p">{</span> <span class="s2">"name"</span> <span class="o">=></span> <span class="s2">"Foo Bar"</span><span class="p">,</span>
<span class="s2">"email"</span> <span class="o">=></span> <span class="s2">"foo@invalid"</span><span class="p">,</span>
<span class="s2">"password"</span> <span class="o">=></span> <span class="s2">"[FILTERED]"</span><span class="p">,</span>
<span class="s2">"password_confirmation"</span> <span class="o">=></span> <span class="s2">"[FILTERED]"</span>
<span class="p">}</span>
</pre></div>
</div>
<p>这个哈希是 <code>params</code> 的一部分,会传给用户控制器。<a href="#a-users-resource">7.1.2 节</a>说过,<code>params</code> 哈希中包含每次请求的信息,例如向 /users/1 发送请求时,<code>params[:id]</code> 的值是用户的 ID,即 1。提交表单发送 <code>POST</code> 请求时,<code>params</code> 是一个嵌套哈希。嵌套哈希在 <a href="chapter4.html#hashes-and-symbols">4.3.3 节</a>中使用控制台介绍 <code>params</code> 时用过。上面的调试信息说明,提交表单后,Rails 会构建一个名为 <code>user</code> 的哈希,哈希中的键是 <code>input</code> 标签的 <code>name</code> 属性值(<a href="#listing-signup-form">代码清单 7.13</a>),键对应的值是用户在字段中填写的内容。例如:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><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">"email"</span> <span class="nt">/></span>
</pre></div>
</div>
<p><code>name</code> 属性的值是 <code>user[email]</code>,表示 <code>user</code> 哈希中的 <code>email</code> 元素。</p>
<p>虽然调试信息中的键是字符串形式,不过却以符号形式传给用户控制器。<code>params[:user]</code> 这个嵌套哈希实际上就是 <code>User.new</code> 方法创建用户所需的参数。我们在 <a href="chapter4.html#a-user-class">4.4.5 节</a>介绍过 <code>User.new</code> 的用法,<a href="#listing-first-create-action">代码清单 7.16</a> 也用到了。也就是说,如下代码:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:user</span><span class="o">]</span><span class="p">)</span>
</pre></div>
</div>
<p>基本上等同于</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Foo Bar"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"foo@invalid"</span><span class="p">,</span>
<span class="ss">password</span><span class="p">:</span> <span class="s2">"foo"</span><span class="p">,</span> <span class="ss">password_confirmation</span><span class="p">:</span> <span class="s2">"bar"</span><span class="p">)</span>
</pre></div>
</div>
<p>在旧版 Rails 中,使用</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:user</span><span class="o">]</span><span class="p">)</span>
</pre></div>
</div>
<p>就行了,但默认情况下这种用法并不安全,需要谨慎处理,避免恶意用户篡改应用的数据库。在 Rails 4.0 之后的版本中,这行代码会抛出异常(如<a href="#fig-signup-failure">图 7.15</a> 和<a href="#fig-signup-failure-rails-debug">图 7.16</a> 所示),增强了安全。</p>
</section>
<section data-type="sect2" id="strong-parameters">
<h2><span class="title-label">7.3.2</span> 健壮参数</h2>
<p>我们在 <a href="chapter4.html#a-user-class">4.4.5 节</a>提到过“批量赋值”——使用一个哈希初始化 Ruby 变量,如下所示:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:user</span><span class="o">]</span><span class="p">)</span> <span class="c1"># 不是最终的实现方法</span>
</pre></div>
</div>
<p>上述代码中的注释<a href="#listing-first-create-action">代码清单 7.16</a> 中也有,说明这不是最终的实现方式。因为初始化整个 <code>params</code> 哈希十分危险,会把用户提交的所有数据传给 <code>User.new</code> 方法。假设除了前述的属性,用户模型中还有一个 <code>admin</code> 属性,用来标识网站的管理员。(我们会在 <a href="chapter9.html#administrative-users">9.4.1 节</a>加入这个属性。)如果想把这个属性设为 <code>true</code>,要在 <code>params[:user]</code> 中包含 <code>admin='1'</code>。这个操作可以使用 <code>curl</code> 等命令行 HTTP 客户端轻易实现。如果把整个 <code>params</code> 哈希传给 <code>User.new</code>,那么网站中的任何用户都可以在请求中包含 <code>admin='1'</code> 来获取管理员权限。</p>
<p>旧版 Rails 使用模型中的 <code>attr_accessible</code> 方法解决这个问题,在一些早期的 Rails 应用中可能还会看到这种用法。但是,从 Rails 4.0 起,推荐在控制器层使用一种叫做“健壮参数”(strong parameter)的技术。这个技术可以指定需要哪些请求参数,以及允许传入哪些请求参数。而且,如果按照上面的方式传入整个 <code>params</code> 哈希,应用会抛出异常。所以,现在默认情况下,Rails 应用已经堵住了批量赋值漏洞。</p>
<p>本例,我们需要 <code>params</code> 哈希包含 <code>:user</code> 元素,而且只允许传入 <code>name</code>、<code>email</code>、<code>password</code> 和 <code>password_confirmation</code> 属性。我们可以使用下面的代码实现:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">params</span><span class="o">.</span><span class="n">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span><span class="o">.</span><span class="n">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">:password_confirmation</span><span class="p">)</span>
</pre></div>
</div>
<p>这行代码会返回一个 <code>params</code> 哈希,只包含允许使用的属性。而且,如果没有指定 <code>:user</code> 元素还会抛出异常。</p>
<p>为了使用方便,可以定义一个名为 <code>user_params</code> 的方法,换掉 <code>params[:user]</code>,返回初始化所需的哈希:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
</pre></div>
</div>
<p><code>user_params</code> 方法只会在用户控制器内部使用,不需要开放给外部用户,所以我们可以使用 Ruby 中的 <code>private</code> 关键字<sup>[<a id="fn-ref-9" href="#fn-9">9</a>]</sup>把这个方法的作用域设为“私有”,如<a href="#listing-create-action-strong-parameters">代码清单 7.17</a> 所示。(我们会在 <a href="chapter8.html#remember-me">8.4 节</a>详细介绍 <code>private</code>。)</p>
<div id="listing-create-action-strong-parameters" data-type="listing">
<h5><span class="title-label">代码清单 7.17</span>:在 <code>create</code> 动作中使用健壮参数</h5>
<div class="source-file">app/controller/users_controller.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="hll"> <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
</span> <span class="k">if</span> <span class="vi">@user</span><span class="o">.</span><span class="n">save</span>
<span class="c1"># 处理注册成功的情况</span>
<span class="k">else</span>
<span class="n">render</span> <span class="s1">'new'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">user_params</span>
<span class="hll"> <span class="n">params</span><span class="o">.</span><span class="n">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span><span class="o">.</span><span class="n">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span>
</span><span class="hll"> <span class="ss">:password_confirmation</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>顺便说一下,<code>private</code> 后面的 <code>user_params</code> 方法多了一层缩进,目的是为了从视觉上容易辨认哪些是私有方法。(经验证明,这么做很明智。如果一个类中有很多方法,容易不小心把方法定义为“私有”,在相应的对象上无法调用时会觉得非常奇怪。)</p>
<p>现在,注册表单可以使用了,至少提交后不会显示错误了。但是,如<a href="#fig-invalid-submission-no-feedback">图 7.17</a>,提交无效数据后,(除了只在开发环境中显示的调试信息之外)表单没有显示任何反馈信息,容易让人误解。而且也没真正创建一个新用户。第一个问题在 <a href="#signup-error-messages">7.3.3 节</a>解决,第二个问题在 <a href="#successful-signups">7.4 节</a>解决。</p>
<div id="fig-invalid-submission-no-feedback" class="figure"><img src="images/chapter7/invalid_submission_no_feedback.png" alt="invalid submission no feedback" /><div class="figcaption"><span class="title-label">图 7.17</span>:提交无效信息后显示的注册表单</div></div>
</section>
<section data-type="sect2" id="signup-error-messages">
<h2><span class="title-label">7.3.3</span> 注册失败错误消息</h2>
<p>处理注册失败的最后一步,要加入有用的错误消息,说明注册失败的原因。默认情况下,Rails 基于用户模型的验证,提供了这种消息。假设我们使用无效的电子邮件地址和长度较短的密码创建用户:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console</span>
<span class="gp">>> </span><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Foo Bar"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"foo@invalid"</span><span class="p">,</span>
<span class="gp">?> </span> <span class="ss">password</span><span class="p">:</span> <span class="s2">"dude"</span><span class="p">,</span> <span class="ss">password_confirmation</span><span class="p">:</span> <span class="s2">"dude"</span><span class="p">)</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">save</span>
<span class="go">=> false</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">full_messages</span>
<span class="go">=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]</span>
</pre></div>
</div>
<p>如上所示,<code>errors.full_message</code> 对象是一个由错误消息组成的数组(<a href="chapter6.html#validating-presence">6.2.2 节</a>简介过)。</p>
<p>和上面的控制台会话类似,在<a href="#listing-first-create-action">代码清单 7.16</a> 中,保存失败时也会生成一组和 <code>@user</code> 对象相关的错误消息。如果想在浏览器中显示这些错误消息,我们要在 <code>new</code> 视图中渲染一个错误消息局部视图,并把表单中每个输入框的 CSS 类设为 <code>form-control</code>(在 Bootstrap 中有特殊意义),如<a href="#listing-f-error-messages">代码清单 7.18</a> 所示。注意,这个错误消息局部视图只是临时的,最终版会在 <a href="chapter11.html#creating-microposts">11.3.2 节</a>实现。</p>
<div id="listing-f-error-messages" data-type="listing">
<h5><span class="title-label">代码清单 7.18</span>:在注册表单中显示错误消息</h5>
<div class="source-file">app/views/users/new.html.erb</div>
<div class="highlight language-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="x"></span>
<span class="x"><h1>Sign up</h1></span>
<span class="x"><div class="row"></span>
<span class="x"> <div class="col-md-6 col-md-offset-3"></span>
<span class="x"> </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="x"></span>
<span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/error_messages'</span> <span class="cp">%></span><span class="x"></span>
</span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:name</span> <span class="cp">%></span><span class="x"></span>
<span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">text_field</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s1">'form-control'</span> <span class="cp">%></span><span class="x"></span>
</span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:email</span> <span class="cp">%></span><span class="x"></span>
<span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">email_field</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s1">'form-control'</span> <span class="cp">%></span><span class="x"></span>
</span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:password</span> <span class="cp">%></span><span class="x"></span>
<span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">password_field</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s1">'form-control'</span> <span class="cp">%></span><span class="x"></span>
</span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">label</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="s2">"Confirmation"</span> <span class="cp">%></span><span class="x"></span>
<span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">password_field</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s1">'form-control'</span> <span class="cp">%></span><span class="x"></span>
</span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">submit</span> <span class="s2">"Create my account"</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s2">"btn btn-primary"</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="x"></div></span>
</pre></div>
</div>
<p>注意,在上面的代码中,渲染的局部视图名为 <code>shared/error_messages</code>,这里用到了 Rails 的一个约定:如果局部视图要在多个控制器中使用(<a href="chapter9.html#edit-form">9.1.1 节</a>),则把它存放在专门的 <code>shared/</code> 文件夹中。所以我们要使用 <code>mkdir</code>(<a href="chapter1.html#table-unix-commands">表 1.1</a>)新建 <code>app/views/shared</code> 文件夹:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>mkdir app/views/shared
</pre></div>
</div>
<p>然后像之前一样,在文本编辑器中新建局部视图 <code>_error_messages.html.erb</code> 文件。这个局部视图的内容如<a href="#listing-errors-partial">代码清单 7.19</a> 所示。</p>
<div id="listing-errors-partial" data-type="listing">
<h5><span class="title-label">代码清单 7.19</span>:显示表单错误消息的局部视图</h5>
<div class="source-file">app/views/shared/_error_messages.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%</span> <span class="k">if</span> <span class="vi">@user</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">any?</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <div id="error_explanation"></span>
<span class="x"> <div class="alert alert-danger"></span>
<span class="x"> The form contains </span><span class="cp"><%=</span> <span class="n">pluralize</span><span class="p">(</span><span class="vi">@user</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="s2">"error"</span><span class="p">)</span> <span class="cp">%></span><span class="x">.</span>
<span class="x"> </div></span>
<span class="x"> <ul></span>
<span class="x"> </span><span class="cp"><%</span> <span class="vi">@user</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">full_messages</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">msg</span><span class="o">|</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <li></span><span class="cp"><%=</span> <span class="n">msg</span> <span class="cp">%></span><span class="x"></li></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </ul></span>
<span class="x"> </div></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>这个局部视图的代码使用了几个之前没用过的 Rails/Ruby 结构,还有 Rails 错误对象上的两个新方法。第一个新方法是 <code>count</code>,它的返回值是错误的数量:</p>