-
Notifications
You must be signed in to change notification settings - Fork 3
/
chapter7.html
1940 lines (1660 loc) · 180 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.5.1"/>
<link rel="stylesheet" type="text/css" href="//railstutorial-china.org/assets/css/main.css"/>
<link rel="stylesheet" type="text/css" href="book.css"/>
<script type="text/javascript" src="//railstutorial-china.org/assets/js/global.js"></script>
</head>
<body class="book-page">
<nav class="navbar">
<div class="container">
<div class="clearfix">
<a class="navbar-brand hidden-sm-up" href="//railstutorial-china.org/" title="Ruby on Rails 教程">Ruby on Rails 教程</a>
<button class="navbar-toggler hidden-sm-up pull-xs-right" type="button" data-toggle="collapse" data-target="#main-nav">☰</button>
</div>
<a class="navbar-brand hidden-xs-down" href="//railstutorial-china.org/" title="Ruby on Rails 教程">Ruby on Rails 教程</a>
<div class="collapse navbar-toggleable-xs pull-sm-right" id="main-nav">
<ul class="nav navbar-nav">
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/" title="首页">首页</a></li>
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/blog/" title="博客">博客</a></li>
<li class="nav-item active"><a class="nav-link" href="//railstutorial-china.org/book/" title="阅读">阅读</a></li>
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/#ebook" title="电子书">电子书</a></li>
</ul>
</div>
</div>
</nav>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<div class="book-versions">
选择版本:
<a class="btn btn-primary" href="//railstutorial-china.org/book/" title="Ruby on Rails 教程(原书第 4 版,针对 Rails 5)">Rails 5</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails42/" title="Ruby on Rails 教程(原书第 3 版,针对 Rails 4.2)">Rails 4.2</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails4/" title="Ruby on Rails 教程(原书第 3 版,针对 Rails 4.0)">Rails 4.0</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails3/" title="Ruby on Rails 教程(原书第 2 版,针对 Rails 3.2)">Rails 3.2</a>
</div>
<div class="alert alert-warning">
<p>在线版的内容可能落后于电子书,如果想及时获得更新,请<a href="//railstutorial-china.org/#ebook" title="购买电子书">购买电子书</a>。</p>
</div>
<article class="article">
<section data-type="chapter" id="sign-up">
<h1><span class="title-label">第 7 章</span> 注册</h1>
<p><code>User</code> 模型可以使用了,接下来要实现大多数网站都离不开的功能——注册。在 <a class="xref-link" href="#signup-form">7.2 节</a>我们将创建一个 HTML 表单,提交用户注册时填写的信息,然后在 <a class="xref-link" href="#successful-signups">7.4 节</a>使用提交的数据创建新用户,把属性的值存入数据库。注册功能实现后,还将创建一个用户资料页面,显示用户的个人信息——这是实现 REST 架构(<a class="xref-link" href="chapter2.html#mvc-in-action">2.2.2 节</a>)用户资源的第一步。在实现这些功能的过程中,我们会在 <a class="xref-link" href="chapter5.html#layout-link-tests">5.3.4 节</a>的基础上编写简练生动的集成测试。</p>
<p>本章要依赖<a class="xref-link" href="chapter6.html#modeling-users">第 6 章</a>为 <code>User</code> 模型编写的验证,尽量保证新用户的电子邮件地址有效。<a class="xref-link" href="chapter11.html#account-activation">第 11 章</a>会在用户注册过程中添加账户激活功能,确保电子邮件地址确实可用。</p>
<p>本书内容力求通俗易懂,但是要兼顾专业性,毕竟 Web 开发是个复杂的话题。本章难度明显增加了,建议你多花点时间,认真理解。(有些读者反馈说,读两遍能加深理解。)</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 class="pagebreak-before">本节要实现的用户资料页面是完整页面的一小部分,只显示用户的名字和头像,构思图如<a class="xref-link" href="#fig-profile-mockup-profile-name">图 7.1</a> 所示。<sup>[<a id="fn-ref-1" href="#fn-1">1</a>]</sup>最终完成的用户资料页面会显示用户的头像、基本信息和微博列表,构思图如<a class="xref-link" href="#fig-profile-mockup">图 7.2</a> 所示。<sup>[<a id="fn-ref-2" href="#fn-2">2</a>]</sup>(在<a class="xref-link" href="#fig-profile-mockup">图 7.2</a> 中,我们第一次用到了“lorem ipsum”占位文字,<a href="http://www.straightdope.com/columns/read/2290/what-does-the-filler-text-lorem-ipsum-mean" class="external-link">这些文字背后的故事</a>很有意思,有空的话你可以了解一下。)这个资料页面将和整个演示应用一起在<a class="xref-link" href="chapter14.html#following-users">第 14 章</a>完成。</p>
<p>如果你一直坚持使用版本控制,现在像之前一样,新建一个主题分支:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>git checkout <span class="nt">-b</span> sign-up
</code></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="rails-environments">
<h2><span class="title-label">7.1.1</span> 调试信息和 Rails 环境</h2>
<p>本节要实现的用户资料页面是我们这个应用中第一个真正意义上的动态页面。虽然视图的代码不会动态改变,不过每个用户资料页面显示的内容却是从数据库中读取的。添加动态页面之前,最好做些准备工作,现在我们能做的就是在网站布局中加入一些调试信息,如<a class="xref-link" href="#listing-rails-debug">代码清单 7.1</a> 所示。这段代码使用 Rails 内置的 <code>debug</code> 方法和 <code>params</code> 变量(<a class="xref-link" 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><code><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="hll"> <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>
<span class="nt"></div></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div>
</div>
<p>因为我们不想在线上网站中向用户显示调试信息,所以上述代码使用 <code>if Rails.env.development?</code> 限制只在开发环境中显示调试信息。开发环境是 Rails 默认支持的三个环境之一(<a class="xref-link" 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><code><%<span class="o">=</span> debug<span class="o">(</span>params<span class="o">)</span> <span class="k">if </span>Rails.env.development? %>
</code></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><code><span class="go">$ rails console</span>
<span class="go">Loading development environment</span>
<span class="go">>> Rails.env</span>
<span class="p">=></span> <span class="s2">"development"</span>
<span class="o">>></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="o">=></span> <span class="kp">true</span>
<span class="o">>></span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">test?</span>
<span class="o">=></span> <span class="kp">false</span>
</code></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><code><span class="go">$ rails console test</span>
<span class="go">Loading test environment</span>
<span class="go">>> Rails.env</span>
<span class="p">=></span> <span class="s2">"test"</span>
<span class="o">>></span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">test?</span>
<span class="o">=></span> <span class="kp">true</span>
</code></pre></div>
</div>
<p>Rails 本地服务器和控制台一样,默认使用开发环境,不过也可以在其他环境中运行:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails server <span class="nt">--environment</span> production
</code></pre></div>
</div>
<p>如果想在生产环境中运行应用,要先有一个生产数据库。在生产环境中执行 <code>rails db:migrate</code> 命令可以生成这个数据库:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails db:migrate <span class="nv">RAILS_ENV</span><span class="o">=</span>production
</code></pre></div>
</div>
<p>(我发现在控制台、服务器和迁移命令中指定环境的习惯方法不一样,你可能会混淆,所以特意演示了这三个命令的用法。不过注意,在这几个命令中都可以使用 <code>RAILS_ENV=<env></code>,例如 <code>RAILS_ENV=production rails server</code>。)</p>
<p>顺便说一下,把应用部署到 Heroku 后,可以使用 <code>heroku run rails console</code> 命令进入控制台查看所用的环境:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">$ heroku run rails console</span>
<span class="go">>> Rails.env</span>
<span class="p">=></span> <span class="s2">"production"</span>
<span class="o">>></span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">production?</span>
<span class="o">=></span> <span class="kp">true</span>
</code></pre></div>
</div>
<p>Heroku 是用来部署网站的平台,自然会在生产环境中运行应用。</p>
</div>
<p>为了让调试信息看起来漂亮一些,我们在<a class="xref-link" href="chapter5.html#filling-in-the-layout">第 5 章</a>创建的自定义样式表中加入一些样式规则,如<a class="xref-link" 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.scss</div>
<div class="highlight language-scss"><pre><code><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="p">:</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="p">:</span> <span class="n">border-box</span><span class="p">;</span></span>
<span class="hll"> <span class="na">-webkit-box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span></span>
<span class="hll"> <span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-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="nl">clear</span><span class="p">:</span> <span class="nb">both</span><span class="p">;</span></span>
<span class="hll"> <span class="nl">float</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span></span>
<span class="hll"> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span></span>
<span class="hll"> <span class="nl">margin-top</span><span class="p">:</span> <span class="m">45px</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>
</code></pre></div>
</div>
<p>这段代码用到了 Sass 的混入(mixin)功能,创建的这个混入名为 <code>box_sizing</code>。混入的作用是打包一系列样式规则,供多次使用。预处理器会把</p>
<div data-type="listing">
<div class="highlight language-scss"><pre><code><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>
</code></pre></div>
</div>
<p>转换成</p>
<div data-type="listing">
<div class="highlight language-css"><pre><code><span class="nc">.debug_dump</span> <span class="p">{</span>
<span class="err">.</span>
<span class="err">.</span>
<span class="err">.</span>
<span class="nl">-moz-box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="nl">-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>
</code></pre></div>
</div>
<p><a class="xref-link" href="#using-form-for">7.2.1 节</a>会再次用到这个混入。美化后的调试信息如<a class="xref-link" href="#fig-home-page-with-debug">图 7.3</a> 所示。<sup>[<a id="fn-ref-4" href="#fn-4">4</a>]</sup></p>
<p><a class="xref-link" href="#fig-home-page-with-debug">图 7.3</a> 中的调试信息显示了当前页面的一些有用信息:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><code><span class="nn">---</span>
<span class="na">controller</span><span class="pi">:</span> <span class="s">static_pages</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">home</span>
</code></pre></div>
</div>
<p>这是 <code>params</code> 变量的 YAML <sup>[<a id="fn-ref-5" href="#fn-5">5</a>]</sup>形式(与散列类似),显示当前页面的控制器名和动作名。<a class="xref-link" href="#a-users-resource">7.1.2 节</a>会介绍其他调试信息的意思。</p>
<h5 id="exercises-rails-environments" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>在浏览器中访问 /about,通过调试信息确定 <code>params</code> 散列中的控制器和动作。</p>
</li>
<li>
<p>在 Rails 控制台中获取数据库中的第一个用户,把它赋值给 <code>user</code> 变量。<code>puts user.attributes.to_yaml</code> 的输出是什么?<code>y user.attributes</code> 的输出呢?</p>
</li>
</ol>
<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> <code>Users</code> 资源</h2>
<p>为了实现用户资料页面,数据库中要有用户记录,这引出了“先有鸡还是先有蛋”的问题:网站还没有注册页面,怎么可能有用户呢?其实这个问题在 <a class="xref-link" href="chapter6.html#creating-and-authenticating-a-user">6.3.4 节</a>已经解决了,那时我们自己动手在 Rails 控制台中创建了一个用户,所以数据库中应该有一条用户记录:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">$ rails console</span>
<span class="go">>> User.count</span>
<span class="p">=></span> <span class="mi">1</span>
<span class="o">>></span> <span class="no">User</span><span class="p">.</span><span class="nf">first</span>
<span class="o">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="s2">"2016-05-23 20:36:46"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 20:36:46"</span><span class="p">,</span>
<span class="ss">password_digest: </span><span class="s2">"$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3..."</span><span class="kt">></span>
</code></pre></div>
</div>
<p>(如果你的数据库中现在没有用户记录,回到 <a class="xref-link" href="chapter6.html#creating-and-authenticating-a-user">6.3.4 节</a>,在继续阅读之前先完成那里的操作。)从控制台的输出可以看出,这个用户的 ID 是 <code>1</code>,我们现在的目标就是创建一个页面,显示这个用户的信息。我们会遵从 Rails 使用的 REST 架构(<a class="xref-link" href="chapter2.html#aside-rest">旁注 2.2</a>),把数据视为资源,可以创建、显示、更新和删除。这四个操作分别对应 <a href="http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol" class="external-link">HTTP 标准</a>中的 <code>POST</code>、<code>GET</code>、<code>PATCH</code> 和 <code>DELETE</code> 请求方法(<a class="xref-link" 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 class="xref-link" href="chapter2.html#a-user-tour">2.2.1 节</a>介绍过,ID 为 1 的用户对应的 URL 是 /users/1,不过现在访问这个 URL 的话,会显示错误信息,如<a class="xref-link" href="#fig-profile-routing-error">图 7.4</a> 中的服务器日志所示。</p>
<div id="fig-profile-routing-error" class="figure"><img src="images/chapter7/profile_routing_error_4th_edition.png" alt="profile routing error 4th 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><code><span class="n">resources</span> <span class="ss">:users</span>
</code></pre></div>
</div>
<p>修改后的路由文件如<a class="xref-link" href="#listing-users-resource">代码清单 7.3</a> 所示。</p>
<div id="listing-users-resource" data-type="listing">
<h5><span class="title-label">代码清单 7.3</span>:在路由文件中添加 <code>Users</code> 资源的规则</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><code><span class="no">Rails</span><span class="p">.</span><span class="nf">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">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'/help'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'/about'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'/contact'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'/signup'</span><span class="p">,</span> <span class="ss">to: </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>
</code></pre></div>
</div>
<p>我们的目的只是为了显示用户资料页面,可是 <code>resources :users</code> 不仅让 /users/1 可以访问,而且还为演示应用中的 <code>Users</code> 资源提供了符合 REST 架构的所有动作,<sup>[<a id="fn-ref-6" href="#fn-6">6</a>]</sup>以及用来获取相应 URL 的具名路由(<a class="xref-link" href="chapter5.html#named-routes">5.3.3 节</a>)。最终得到的 URL、动作和具名路由的对应关系如<a class="xref-link" href="#table-restful-users">表 7.1</a> 所示(与<a class="xref-link" href="chapter2.html#table-demo-restful-users">表 2.2</a> 对比一下)。接下来的三章会介绍<a class="xref-link" 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 class="xref-link" href="#listing-users-resource">代码清单 7.3</a> 中添加的 <code>Users</code> 资源规则实现的 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 class="xref-link" href="#listing-users-resource">代码清单 7.3</a> 中的代码之后,路由就生效了,但是页面还不存在(<a class="xref-link" href="#fig-user-show-unknown-action">图 7.5</a>)。下面我们在页面中添加一些简单的内容,<a class="xref-link" 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 class="xref-link" href="chapter5.html#listing-generate-users-controller">代码清单 5.38</a>)不同,现在不存在,要手动创建,<sup>[<a id="fn-ref-7" href="#fn-7">7</a>]</sup>然后写入<a class="xref-link" 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><code><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>
</code></pre></div>
</div>
<p>在这段代码中,我们假设存在一个 <code>@user</code> 变量,使用 ERb 代码显示这个用户的名字和电子邮件地址。这和最终实现的视图有点不一样,届时不会公开显示用户的电子邮件地址。</p>
<p>我们要在 <code>Users</code> 控制器的 <code>show</code> 动作中定义 <code>@user</code> 变量,这样用户资料页面才能正常渲染。你可能猜到了,我们要在 <code>User</code> 模型上调用 <code>find</code> 方法(<a class="xref-link" href="chapter6.html#finding-user-objects">6.1.4 节</a>),从数据库中检索用户,如<a class="xref-link" 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> 动作的 <code>Users</code> 控制器</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><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="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>
<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>
</code></pre></div>
</div>
<p>在这段代码中,我们使用 <code>params</code> 获取用户的 ID。当我们向 <code>Users</code> 控制器发送请求时,<code>params[:id]</code> 会返回用户的 ID,即 1,所以这就和 <a class="xref-link" 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 class="xref-link" href="#fig-user-show-rails">图 7.6</a> 所示。(如果添加 bcrypt 之后没重启过 Rails 服务器,现在或许要重启。这也体现了“<a class="xref-link" href="chapter1.html#aside-technical-sophistication">技术是复杂的</a>”。)留意一下调试信息,它证实了 <code>params[:id]</code> 的值与前面分析的一样:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><code><span class="nn">---</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">show</span>
<span class="na">controller</span><span class="pi">:</span> <span class="s">users</span>
<span class="na">id</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1'</span>
</code></pre></div>
</div>
<p>所以,<a class="xref-link" 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>
<h5 id="exercises-a-users-resource" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>使用嵌入式 Ruby,在<a class="xref-link" href="#listing-stub-user-view">代码清单 7.4</a> 中添加 <code>created_at</code> 和 <code>updated_at</code> 两个属性。</p>
</li>
<li>
<p>使用嵌入式 Ruby,在用户资料页面添加 <code>Time.now</code>。刷新浏览器后会发生什么?</p>
</li>
</ol>
</section>
<section data-type="sect2" id="debugger">
<h2><span class="title-label">7.1.3</span> 调试器</h2>
<p>在 <a class="xref-link" href="#a-users-resource">7.1.2 节</a>中我们看到,调试信息能帮助我们理解应用的运作方式。不过,使用 <code>byebug</code> gem(<a class="xref-link" href="chapter3.html#listing-gemfile-sample-app">代码清单 3.2</a>)可以更直接地获取调试信息。我们把 <code>debugger</code> 方法加到应用中,看一下这个 gem 的作用,如<a class="xref-link" href="#listing-debugger">代码清单 7.6</a> 所示。</p>
<div id="listing-debugger" data-type="listing">
<h5><span class="title-label">代码清单 7.6</span>:在 <code>Users</code> 控制器中添加 <code>debugger</code> 方法</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><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="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>
</code></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><code><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[:id]
<span class="s2">"1"</span>
</code></pre></div>
</div>
<p>若想退出 <code>byebug</code>,继续执行应用,可以按 Ctrl-D 键。把 <code>show</code> 动作中的 <code>debugger</code> 方法删除,如<a class="xref-link" href="#listing-debugger-removed">代码清单 7.7</a> 所示。</p>
<div id="listing-debugger-removed" data-type="listing">
<h5><span class="title-label">代码清单 7.7</span>:删除 <code>debugger</code> 方法后的 <code>Users</code> 控制器</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><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>
</code></pre></div>
</div>
<p>只要你觉得 Rails 应用中哪部分有问题,就可以在可能导致问题的代码附近加上 <code>debugger</code> 方法。<code>byebug</code> gem 很强大,可以查看系统的状态,排查应用错误,还能交互式调试应用。</p>
<h5 id="exercises-debugger" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>像<a class="xref-link" href="#listing-debugger">代码清单 7.6</a> 那样在 <code>show</code> 动作中添加 <code>debugger</code> 方法,然后访问 /users/1。使用 <code>puts</code> 方法显示 <code>params</code> 散列的 YAML 形式。提示:参考 <a class="xref-link" href="#rails-environments">7.1.1 节</a>节的<a class="xref-link" href="#exercises-rails-environments">练习</a>。与网站模板中的 <code>debug</code> 方法相比,这样做显示的调试信息有什么不同?</p>
</li>
<li>
<p>把 <code>debugger</code> 方法添加到 <code>new</code> 动作中,然后访问 /users/new。<code>@user</code> 的值什么?</p>
</li>
</ol>
</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/" class="external-link">Gravatar</a>。<sup>[<a id="fn-ref-8" href="#fn-8">8</a>]</sup>这是一项免费服务,让用户上传图像,将其关联到自己的电子邮件地址上。使用 Gravatar 可以简化在网站中添加用户头像的过程,开发者不必分心去处理图像上传、剪裁和存储,只要使用用户的电子邮件地址构成头像的 URL 地址,用户的头像便能显示出来。(<a class="xref-link" href="chapter13.html#micropost-images">13.4 节</a>将说明如何处理图像上传。)</p>
<p>我们的计划是定义一个名为 <code>gravatar_for</code> 的辅助方法,返回指定用户的 Gravatar 头像,如<a class="xref-link" 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-erb"><pre><code><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>
</code></pre></div>
</div>
<p>默认情况下,所有辅助方法文件中定义的方法都自动在任意视图中可用,不过为了便于管理,我们会把 <code>gravatar_for</code> 方法放在 <code>Users</code> 控制器对应的辅助方法文件中。根据 <a href="http://en.gravatar.com/site/implement/hash/" class="external-link">Gravatar 的文档</a>,头像的 URL 地址中要使用用户电子邮件地址的 <a href="http://en.wikipedia.org/wiki/MD5" class="external-link">MD5 哈希值</a>。在 Ruby 中,MD5 哈希算法由 <code>Digest</code> 库中的 <code>hexdigest</code> 方法实现:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> email = "[email protected]"</span>
<span class="hll"><span class="go">>> Digest::MD5::hexdigest(email.downcase)</span></span>
<span class="p">=></span> <span class="s2">"1fda4469bcbec3badf5418269ffc5968"</span>
</code></pre></div>
</div>
<p>电子邮件地址不区分大小写,但是 MD5 哈希算法区分,所以我们要先调用 <code>downcase</code> 方法把电子邮件地址转换成小写形式,然后再传给 <code>hexdigest</code> 方法。(在<a class="xref-link" href="chapter6.html#listing-email-downcase">代码清单 6.32</a> 中的回调里我们已经把电子邮件地址转换成小写形式了,但这里最好也转换,以防电子邮件地址来自其他地方。)我们定义的 <code>gravatar_for</code> 辅助方法如<a class="xref-link" 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><code><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="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>
</code></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 class="xref-link" href="#fig-profile-with-gravatar">图 7.7</a> 所示,页面中显示的头像是 Gravatar 的默认图像,因为 <code>[email protected]</code> 不是真的电子邮件地址(访问这个网站你便会发现,example.com 这个域名是专门用来举例的)。</p>
<p>我们调用 <code>update_attributes</code> 方法(<a class="xref-link" href="chapter6.html#updating-user-objects">6.1.5 节</a>)更新一下数据库中的用户记录,然后就可以显示用户真正的头像了:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">$ rails console</span>
<span class="go">>> user = User.first</span>
<span class="go">>> user.update_attributes(name: "Example User",</span>
<span class="go">?> email: "[email protected]",</span>
<span class="go">?> password: "foobar",</span>
<span class="go">?> password_confirmation: "foobar")</span>
<span class="p">=></span> <span class="kp">true</span>
</code></pre></div>
</div>
<p>我们把用户的电子邮件地址改成 <code>[email protected]</code>。我已经把这个地址的头像设为了本书网站的徽标,修改后的结果如<a class="xref-link" href="#fig-profile-custom-gravatar">图 7.8</a> 所示。</p>
<p>我们还要添加一个侧边栏,这才能完成<a class="xref-link" 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 class="xref-link" 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><code><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">"col-md-4"</span><span class="nt">></span>
<span class="nt"><section</span> <span class="na">class=</span><span class="s">"user_info"</span><span class="nt">></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>
</code></pre></div>
</div>
<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>添加 HTML 结构和 CSS 类之后,我们再用 SCSS 为资料页面(包括侧边栏和 Gravatar 头像)定义一些样式,如<a class="xref-link" href="#listing-sidebar-css">代码清单 7.11</a> 所示。<sup>[<a id="fn-ref-9" href="#fn-9">9</a>]</sup>(注意:因为 Asset Pipeline 使用 Sass 预处理器,所以样式中才可以使用嵌套。)实现的效果如<a class="xref-link" 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.scss</div>
<div class="highlight language-scss"><pre><code><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="nl">margin-top</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</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">margin-top</span><span class="p">:</span> <span class="m">20px</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>
<span class="nc">.gravatar_edit</span> <span class="p">{</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
</div>
<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>
<h5 id="exercises-a-gravatar-image-and-a-sidebar" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>如果你还没为自己的电子邮件地址关联 Gravatar,现在关联一个。你的头像的 MD5 是多少?</p>
</li>
<li>
<p>确认<a class="xref-link" href="#listing-gravatar-option">代码清单 7.12</a> 中定义的 <code>gravatar_for</code> 辅助方法能接受可选的 <code>size</code> 参数,在视图中可以像这样调用:<code>gravatar_for user, size: 50</code>。(<a class="xref-link" href="chapter10.html#users-index">10.3.1 节</a>会使用这个改进的辅助方法。)</p>
</li>
<li>
<p>前一题中的 <code>options</code> 散列仍然经常使用,但是从 Ruby 2.0 开始,可以换成关键字参数(keyword argument)。确认<a class="xref-link" href="#listing-gravatar-keyword">代码清单 7.13</a> 中的代码可以代替<a class="xref-link" href="#listing-gravatar-option">代码清单 7.12</a>。二者有什么区别?</p>
</li>
</ol>
<div id="listing-gravatar-option" data-type="listing">
<h5><span class="title-label">代码清单 7.12</span>:为 <code>gravatar_for</code> 辅助方法添加一个可选的散列参数</h5>
<div class="source-file">app/helpers/users_helper.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">module</span> <span class="nn">UsersHelper</span>
<span class="c1"># 返回指定用户的 Gravatar</span>
<span class="hll"> <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">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">size: </span><span class="mi">80</span> <span class="p">})</span></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="hll"> <span class="n">size</span> <span class="o">=</span> <span class="n">options</span><span class="p">[</span><span class="ss">:size</span><span class="p">]</span></span>
<span class="hll"> <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">?s=</span><span class="si">#{</span><span class="n">size</span><span class="si">}</span><span class="s2">"</span></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>
</code></pre></div>
</div>
<div id="listing-gravatar-keyword" data-type="listing" class="pagebreak-before">
<h5><span class="title-label">代码清单 7.13</span>:在 <code>gravatar_for</code> 辅助方法中使用关键字参数</h5>
<div class="source-file">app/helpers/users_helper.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">module</span> <span class="nn">UsersHelper</span>
<span class="c1"># 返回指定用户的 Gravatar</span>
<span class="hll"> <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="ss">size: </span><span class="mi">80</span><span class="p">)</span></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">?s=</span><span class="si">#{</span><span class="n">size</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>
</code></pre></div>
</div>
</section>
</section>
<section data-type="sect1" id="signup-form">
<h1><span class="title-label">7.2</span> 注册表单</h1>
<p>用户资料页面已经可以访问了,但内容还不完整。下面我们要为网站创建一个注册表单。如<a class="xref-link" href="chapter5.html#fig-new-signup-page">图 5.11</a>(<a class="xref-link" href="#fig-blank-signup-page-recap">图 7.10</a> 再次展示)所示,注册页面还没有什么内容,无法注册新用户。本节将实现如<a class="xref-link" href="#fig-signup-mockup">图 7.11</a> 所示的注册表单,添加注册功能。</p>
<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>:注册页面(/signup)现在的样子</div></div>
<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>Users</code> 控制器的 <code>new</code> 动作处理(<a class="xref-link" href="chapter5.html#listing-signup-route">代码清单 5.43</a>)。首先,我们要创建传给 <code>form_for</code> 的用户对象,然后赋值给 <code>@user</code> 变量,如<a class="xref-link" href="#listing-new-action-with-user">代码清单 7.14</a> 所示。</p>
<div id="listing-new-action-with-user" data-type="listing">
<h5><span class="title-label">代码清单 7.14</span>:在 <code>new</code> 动作中添加 <code>@user</code> 变量</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><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="hll"> <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span></span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>表单的代码参见<a class="xref-link" href="#listing-signup-form">代码清单 7.15</a>。<a class="xref-link" href="#signup-form-html">7.2.2 节</a>会详细分析这个表单,现在我们先添加一些 SCSS,如<a class="xref-link" href="#listing-form-css">代码清单 7.16</a> 所示。(注意,这里重用了<a class="xref-link" href="#listing-mixin-and-debug">代码清单 7.2</a> 中的 <code>box_sizing</code> 混入。)添加样式后的注册页面如<a class="xref-link" href="#fig-signup-form">图 7.12</a> 所示。</p>
<div id="listing-signup-form" data-type="listing">
<h5><span class="title-label">代码清单 7.15</span>:用户注册表单</h5>
<div class="source-file">app/views/users/new.html.erb</div>
<div class="highlight language-erb"><pre><code><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">"col-md-6 col-md-offset-3"</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">email_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-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>
</code></pre></div>
</div>
<div id="listing-form-css" data-type="listing">
<h5><span class="title-label">代码清单 7.16</span>:注册表单的样式</h5>
<div class="source-file">app/assets/stylesheets/custom.scss</div>
<div class="highlight language-scss"><pre><code><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>
</code></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>
<h5 id="exercises-using-form-for" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>把<a class="xref-link" href="#listing-signup-form">代码清单 7.15</a> 中的 <code>:name</code> 换成 <code>:nome</code>,会看到什么错误消息?</p>
</li>
<li>
<p>把 <code>f</code> 换成 <code>foobar</code>,确认块变量的名称对结果没有影响。想想为什么使用 <code>foobar</code> 不好。</p>
</li>
</ol>
</section>
<section data-type="sect2" id="signup-form-html">
<h2><span class="title-label">7.2.2</span> 注册表单的 HTML</h2>
<p>为了更好地理解<a class="xref-link" href="#listing-signup-form">代码清单 7.15</a> 中定义的表单,我们将其分成几段来看。先看外层结构——开头的 <code>form_for</code> 方法和结尾的 <code>end</code>:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><code><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>
</code></pre></div>
</div>
<p>这段代码中有关键字 <code>do</code>,说明 <code>form_for</code> 方法可以接受一个块,而且有一个块变量 <code>f</code>(代表“form”)。</p>
<p>我们一般无需了解 Rails 辅助方法的内部实现,但是对 <code>form_for</code> 来说,我们要知道 <code>f</code> 对象的作用是什么:在这个对象上调用<a href="http://www.w3schools.com/html/html_forms.asp" class="external-link">表单字段</a>(例如,文本字段、单选按钮或密码字段)对应的方法,生成的字段元素可以用来设定 <code>@user</code> 对象的属性。也就是说:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><code><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>
</code></pre></div>
</div>
<p>生成的 HTML 是一个有标注(label)的文本字段,用于设定 <code>User</code> 模型的 <code>name</code> 属性。</p>
<p>在浏览器中按住 Ctrl 键再点击鼠标,然后选择“审查元素”,查看页面的源码,如<a class="xref-link" href="#listing-signup-form-html">代码清单 7.17</a> 所示。下面花点儿时间介绍一下表单的结构。</p>
<div id="listing-signup-form-html" data-type="listing">
<h5><span class="title-label">代码清单 7.17</span>:<a class="xref-link" href="#fig-signup-form">图 7.12</a> 中表单的源码</h5>
<div class="highlight language-html"><pre><code><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>
</code></pre></div>
</div>
<p>先看表单里的结构。比较一下<a class="xref-link" href="#listing-signup-form">代码清单 7.15</a> 和<a class="xref-link" href="#listing-signup-form-html">代码清单 7.17</a>,我们发现,下面的 ERb 代码</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><code><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>
</code></pre></div>
</div>
<p>生成的 HTML 是</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><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>
</code></pre></div>
</div>
<p>下面的 ERb 代码</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><code><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">email_field</span> <span class="ss">:email</span> <span class="cp">%></span>
</code></pre></div>
</div>
<p>生成的 HTML 是</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><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>
</code></pre></div>
</div>
<p>而下面的 ERb 代码</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><code><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>
</code></pre></div>
</div>
<p>生成的 HTML 是</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><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>
</code></pre></div>
</div>
<p>如<a class="xref-link" href="#fig-filled-in-form">图 7.13</a> 所示,文本字段和电子邮件地址字段(<code>type="text"</code> 和 <code>type="email"</code>)会直接显示填写的内容,而密码字段(<code>type="password"</code>)基于安全考虑会遮盖输入的内容。(把电子邮件地址字段的类型设为 <code>type="email"</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 class="xref-link" href="#successful-signups">7.4 节</a>会说明,之所以能创建用户,全靠 <code>input</code> 元素的 <code>name</code> 属性:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><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>
</code></pre></div>
</div>
<p><a class="xref-link" 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 class="xref-link" href="chapter4.html#constructors">4.4.1 节</a>),所以 Rails 知道 <code>@user</code> 所属的类是 <code>User</code>,而且,<code>@user</code> 是一个新用户,因此 Rails 知道要使用 <code>post</code> 方法——这正是创建新对象所需的 HTTP 请求(参见<a class="xref-link" href="chapter3.html#aside-get-etc">旁注 3.2</a>):</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><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>
</code></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><code><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>
</code></pre></div>
</div>
<p>这段代码不会在浏览器中显示,只在 Rails 内部有用,所以你并不需要知道它的作用。简单来说,这段代码首先使用 Unicode 字符 <code>&#x2713;</code>(对号 ✓)强制浏览器使用正确的字符编码提交数据;后面是一个真伪令牌(authenticity token),Rails 用它抵御跨站请求伪造(Cross-Site Request Forgery,简称 CSRF)攻击。<sup>[<a id="fn-ref-10" href="#fn-10">10</a>]</sup></p>
</section>
</section>
<section data-type="sect1" id="unsuccessful-signups">
<h1><span class="title-label">7.3</span> 注册失败</h1>
<p>虽然前一节大概介绍了<a class="xref-link" href="#fig-signup-form">图 7.12</a> 中表单的 HTML 结构(参见<a class="xref-link" href="#listing-signup-form-html">代码清单 7.17</a>),但并没涉及什么细节,其实注册失败时才能更好地理解这个表单的作用。本节,我们会在注册表单中填写一些无效的数据,然后提交表单,此时页面不会转向其他页面,而是重新渲染注册页面,并且列出错误消息,如<a class="xref-link" 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 class="xref-link" href="#a-users-resource">7.1.2 节</a>的内容,在 <code>routes.rb</code> 文件中添加 <code>resources :users</code> 之后(<a class="xref-link" href="#listing-users-resource">代码清单 7.3</a>),Rails 应用就可以响应<a class="xref-link" 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><code><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>
</code></pre></div>
</div>
<p><a class="xref-link" href="#signup-form-html">7.2.2 节</a>说过,这个表单会向 /users 地址发送 <code>POST</code> 请求。</p>
<p>为了让这个表单可用,首先我们要添加<a class="xref-link" href="#listing-first-create-action">代码清单 7.18</a> 中的代码。这段代码再次用到了 <code>render</code> 方法,上一次是在局部视图中(<a class="xref-link" href="chapter5.html#partials">5.1.3 节</a>),不过如你所见,在控制器的动作中也可以使用 <code>render</code> 方法。同时,我们在这段代码中介绍了 <code>if-else</code> 分支结构的用法:根据 <code>@user.save</code> 的返回值,分别处理用户存储成功和失败两种情况(<a class="xref-link" 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.18</span>:能处理注册失败的 <code>create</code> 动作</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><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="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">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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:user</span><span class="p">])</span> <span class="c1"># 不是最终的实现方式</span></span>
<span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">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>
</code></pre></div>
</div>
<p>留意上述代码中的注释,这不是最终的实现方式,但现在完全够用。最终版将在 <a class="xref-link" href="#strong-parameters">7.3.2 节</a>实现。</p>
<p>我们要实际操作一下,提交一些无效的注册数据,这样才能更好地理解<a class="xref-link" href="#listing-first-create-action">代码清单 7.18</a> 中代码的作用,结果如<a class="xref-link" href="#fig-signup-failure">图 7.15</a> 所示,底部完整的调试信息如<a class="xref-link" href="#fig-signup-failure-rails-debug">图 7.16</a> 所示。</p>
<div id="fig-signup-failure" class="figure"><img src="images/chapter7/signup_failure_4th_edition.png" alt="signup failure 4th 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_4th_edition.png" alt="signup failure debug 4th edition" /><div class="figcaption"><span class="title-label">图 7.16</span>:注册失败时显示的调试信息</div></div>
<p>下面我们来分析一下调试信息中请求参数散列的 <code>user</code> 部分(<a class="xref-link" href="#fig-signup-failure-rails-debug">图 7.16</a>),以便深入理解 Rails 处理表单的过程:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><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>
</code></pre></div>
</div>
<p>这个散列是 <code>params</code> 的一部分,会传给 <code>Users</code> 控制器。<a class="xref-link" 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 class="xref-link" href="chapter4.html#hashes-and-symbols">4.3.3 节</a>中使用控制台介绍 <code>params</code> 时用过。上面的调试信息说明,提交表单后,Rails 会构建一个名为 <code>user</code> 的散列,散列中的键是 <code>input</code> 标签 <code>name</code> 属性的值(<a class="xref-link" href="#listing-signup-form-html">代码清单 7.17</a>),键对应的值是用户在字段中填写的内容。例如:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><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>
</code></pre></div>
</div>
<p>其中,<code>name</code> 属性的值是 <code>user[email]</code>,对应于 <code>user</code> 散列中的 <code>email</code> 键。</p>
<p>虽然调试信息中的键是字符串形式,不过却以符号形式传给 <code>Users</code> 控制器。<code>params[:user]</code> 这个嵌套散列实际上就是 <code>User.new</code> 方法创建用户所需的参数。我们在 <a class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>介绍过 <code>User.new</code> 的用法,<a class="xref-link" href="#listing-first-create-action">代码清单 7.18</a> 也用到了。也就是说,下述代码:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><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="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:user</span><span class="p">])</span>
</code></pre></div>
</div>
<p>基本上等同于</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><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="p">(</span><span class="ss">name: </span><span class="s2">"Foo Bar"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"foo@invalid"</span><span class="p">,</span>
<span class="ss">password: </span><span class="s2">"foo"</span><span class="p">,</span> <span class="ss">password_confirmation: </span><span class="s2">"bar"</span><span class="p">)</span>
</code></pre></div>
</div>
<p>在旧版 Rails 中,使用 <code>@user = User.new(params[:user])</code> 就行了,但是这种用法并不安全,需要谨慎处理,以防恶意用户篡改应用的数据库。在 Rails 4.0 之后的版本中,这行代码会抛出异常(如<a class="xref-link" href="#fig-signup-failure">图 7.15</a> 和<a class="xref-link" 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 class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>提到过批量赋值,即使用一个散列初始化 Ruby 变量,如下所示:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><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="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:user</span><span class="p">])</span> <span class="c1"># 不是最终的实现方式</span>
</code></pre></div>
</div>
<p>上述代码中的注释在<a class="xref-link" href="#listing-first-create-action">代码清单 7.18</a> 中也有,说明这不是最终的实现方式。因为初始化整个 <code>params</code> 散列十分危险,会把用户提交的所有数据传给 <code>User.new</code> 方法。假设除了这几个属性,<code>User</code> 模型中还有一个 <code>admin</code> 属性,用于标识网站的管理员。(我们将在 <a class="xref-link" href="chapter10.html#administrative-users">10.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><code><span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">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>
</code></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><code><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="p">(</span><span class="n">user_params</span><span class="p">)</span>
</code></pre></div>
</div>
<p><code>user_params</code> 方法只会在 <code>Users</code> 控制器内部使用,不需要开放给外部用户,所以我们可以使用 Ruby 中的 <code>private</code> 关键字<sup>[<a id="fn-ref-11" href="#fn-11">11</a>]</sup>把这个方法的作用域设为“私有”,如<a class="xref-link" href="#listing-create-action-strong-parameters">代码清单 7.19</a> 所示。(我们会在 <a class="xref-link" href="chapter9.html#remember-me">9.1 节</a>进一步说明 <code>private</code>。)</p>
<div id="listing-create-action-strong-parameters" data-type="listing">
<h5><span class="title-label">代码清单 7.19</span>:在 <code>create</code> 动作中使用健壮参数</h5>
<div class="source-file">app/controller/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><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="n">create</span>
<span class="hll"> <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="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="p">.</span><span class="nf">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>