-
Notifications
You must be signed in to change notification settings - Fork 0
/
action_controller_overview.html
1328 lines (1187 loc) · 82.3 KB
/
action_controller_overview.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-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Action Controller 概覽 — Ruby on Rails 指南</title>
<meta name="description" content="Ruby on Rails 指南:系統學習 Rails(Rails 4.2 版本)" >
<meta name="keywords" content="Ruby on Rails Guides 指南 中文 學習 免費 網路 Web 開發" >
<meta name="author" content="http://git.io/G_R1sA">
<meta property="fb:admins" content="1340181291">
<meta property="og:title" content="Action Controller 概覽 — Ruby on Rails 指南" >
<meta property="og:site_name" content="Ruby on Rails 指南">
<meta property="og:image" content="http://rails.ruby.tw/images/rails_guides_cover.jpg">
<meta property="og:url" content="http://rails.ruby.tw/">
<meta property="og:type" content="article">
<meta property="og:description" content="Ruby on Rails 指南:系統學習 Rails(Rails 4.2 版本)">
<link rel="stylesheet" href="stylesheets/application.css">
<link href="http://fonts.googleapis.com/css?family=Noto+Sans:400,700|Noto+Serif:700|Source+Code+Pro" rel="stylesheet">
<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon">
</head>
<body class="guide">
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/zh-TW/sdk.js#xfbml=1&appId=837401439623727&version=v2.0";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<script type="text/javascript">
window.twttr=(function(d,s,id){var t,js,fjs=d.getElementsByTagName(s)[0];if(d.getElementById(id)){return}js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);return window.twttr||(t={_e:[],ready:function(f){t._e.push(f)}})}(document,"script","twitter-wjs"));
</script>
<div id="topNav">
<div class="wrapper">
<strong class="more-info-label">更多內容 <a href="http://rubyonrails.org/">rubyonrails.org:</a></strong>
<span class="red-button more-info-button">
更多內容
</span>
<ul class="more-info-links s-hidden">
<li class="more-info"><a href="http://rubyonrails.org/">綜覽</a></li>
<li class="more-info"><a href="http://rubyonrails.org/download">下載</a></li>
<li class="more-info"><a href="http://rubyonrails.org/deploy">部署</a></li>
<li class="more-info"><a href="https://github.com/rails/rails">原始碼</a></li>
<li class="more-info"><a href="http://rubyonrails.org/screencasts">影片</a></li>
<li class="more-info"><a href="http://rubyonrails.org/documentation">文件</a></li>
<li class="more-info"><a href="http://rubyonrails.org/community">社群</a></li>
<li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
</ul>
</div>
</div>
<div id="header">
<div class="wrapper clearfix">
<h1><a href="index.html" title="回首頁">Guides.rubyonrails.org</a></h1>
<ul class="nav">
<li><a class="nav-item" href="index.html">首頁</a></li>
<li class="guides-index guides-index-large">
<a href="index.html" id="guidesMenu" class="guides-index-item nav-item">指南目錄</a>
<div id="guides" class="clearfix" style="display: none;">
<hr>
<dl class="L">
<dt>起步走</dt>
<dd><a href="getting_started.html">Rails 起步走</a></dd>
<dt>Models</dt>
<dd><a href="active_record_basics.html">Active Record 基礎</a></dd>
<dd><a href="active_record_migrations.html">Active Record 遷移</a></dd>
<dd><a href="active_record_validations.html">Active Record 驗證</a></dd>
<dd><a href="active_record_callbacks.html">Active Record 回呼</a></dd>
<dd><a href="association_basics.html">Active Record 關聯</a></dd>
<dd><a href="active_record_querying.html">Active Record 查詢</a></dd>
<dt>Views</dt>
<dd><a href="layouts_and_rendering.html">Rails 算繪與版型</a></dd>
<dd><a href="form_helpers.html">Action View 表單輔助方法</a></dd>
<dt>Controllers</dt>
<dd><a href="action_controller_overview.html">Action Controller 綜覽</a></dd>
<dd><a href="routing.html">Rails 路由:深入淺出</a></dd>
</dl>
<dl class="R">
<dt>深入了解</dt>
<dd><a href="active_support_core_extensions.html">Active Support 核心擴展</a></dd>
<dd><a href="i18n.html">Rails 國際化 API</a></dd>
<dd><a href="action_mailer_basics.html">Action Mailer 基礎</a></dd>
<dd><a href="active_job_basics.html">Active Job 基礎</a></dd>
<dd><a href="security.html">Rails 安全指南</a></dd>
<dd><a href="debugging_rails_applications.html">除錯 Rails 應用程式</a></dd>
<dd><a href="configuring.html">Rails 應用程式設定</a></dd>
<dd><a href="command_line.html">Rake 任務與 Rails 命令列工具</a></dd>
<dd><a href="asset_pipeline.html">Asset Pipeline</a></dd>
<dd><a href="working_with_javascript_in_rails.html">在 Rails 使用 JavaScript</a></dd>
<dd><a href="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</a></dd>
<dt>擴充 Rails</dt>
<dd><a href="rails_on_rack.html">Rails on Rack</a></dd>
<dd><a href="generators.html">客製與新建 Rails 產生器</a></dd>
<dd><a href="rails_application_templates.html">Rails 應用程式模版</a></dd>
<dt>貢獻 Ruby on Rails</dt>
<dd><a href="contributing_to_ruby_on_rails.html">貢獻 Ruby on Rails</a></dd>
<dd><a href="api_documentation_guidelines.html">API 文件準則</a></dd>
<dd><a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南準則</a></dd>
<dt>維護方針</dt>
<dd><a href="maintenance_policy.html">維護方針</a></dd>
<dt>發佈記</dt>
<dd><a href="upgrading_ruby_on_rails.html">升級 Ruby on Rails</a></dd>
<dd><a href="4_2_release_notes.html">Ruby on Rails 4.2 發佈記</a></dd>
<dd><a href="4_1_release_notes.html">Ruby on Rails 4.1 發佈記</a></dd>
<dd><a href="4_0_release_notes.html">Ruby on Rails 4.0 發佈記</a></dd>
<dd><a href="3_2_release_notes.html">Ruby on Rails 3.2 發佈記</a></dd>
<dd><a href="3_1_release_notes.html">Ruby on Rails 3.1 發佈記</a></dd>
<dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 發佈記</a></dd>
<dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 發佈記</a></dd>
<dd><a href="2_2_release_notes.html">Ruby on Rails 2.2 發佈記</a></dd>
<dt>Rails 指南翻譯術語</dt>
<dd><a href="translation_terms.html">翻譯術語</a></dd>
</dl>
</div>
</li>
<li><a class="nav-item" href="//github.com/docrails-tw/guides">貢獻翻譯</a></li>
<li><a class="nav-item" href="contributing_to_ruby_on_rails.html">貢獻</a></li>
<li><a class="nav-item" href="credits.html">致謝</a></li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">指南目錄</option>
<optgroup label="起步走">
<option value="getting_started.html">Rails 起步走</option>
</optgroup>
<optgroup label="Models">
<option value="active_record_basics.html">Active Record 基礎</option>
<option value="active_record_migrations.html">Active Record 遷移</option>
<option value="active_record_validations.html">Active Record 驗證</option>
<option value="active_record_callbacks.html">Active Record 回呼</option>
<option value="association_basics.html">Active Record 關聯</option>
<option value="active_record_querying.html">Active Record 查詢</option>
</optgroup>
<optgroup label="Views">
<option value="layouts_and_rendering.html">Rails 算繪與版型</option>
<option value="form_helpers.html">Action View 表單輔助方法</option>
</optgroup>
<optgroup label="Controllers">
<option value="action_controller_overview.html">Action Controller 綜覽</option>
<option value="routing.html">Rails 路由:深入淺出</option>
</optgroup>
<optgroup label="深入了解">
<option value="active_support_core_extensions.html">Active Support 核心擴展</option>
<option value="i18n.html">Rails 國際化 API</option>
<option value="action_mailer_basics.html">Action Mailer 基礎</option>
<option value="active_job_basics.html">Active Job 基礎</option>
<option value="security.html">Rails 安全指南</option>
<option value="debugging_rails_applications.html">除錯 Rails 應用程式</option>
<option value="configuring.html">Rails 應用程式設定</option>
<option value="command_line.html">Rake 任務與 Rails 命令列工具</option>
<option value="asset_pipeline.html">Asset Pipeline</option>
<option value="working_with_javascript_in_rails.html">在 Rails 使用 JavaScript</option>
<option value="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</option>
</optgroup>
<optgroup label="擴充 Rails">
<option value="rails_on_rack.html">Rails on Rack</option>
<option value="generators.html">客製與新建 Rails 產生器</option>
<option value="rails_application_templates.html">Rails 應用程式模版</option>
</optgroup>
<optgroup label="貢獻 Ruby on Rails">
<option value="contributing_to_ruby_on_rails.html">貢獻 Ruby on Rails</option>
<option value="api_documentation_guidelines.html">API 文件準則</option>
<option value="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南準則</option>
</optgroup>
<optgroup label="維護方針">
<option value="maintenance_policy.html">維護方針</option>
</optgroup>
<optgroup label="發佈記">
<option value="upgrading_ruby_on_rails.html">升級 Ruby on Rails</option>
<option value="4_2_release_notes.html">Ruby on Rails 4.2 發佈記</option>
<option value="4_1_release_notes.html">Ruby on Rails 4.1 發佈記</option>
<option value="4_0_release_notes.html">Ruby on Rails 4.0 發佈記</option>
<option value="3_2_release_notes.html">Ruby on Rails 3.2 發佈記</option>
<option value="3_1_release_notes.html">Ruby on Rails 3.1 發佈記</option>
<option value="3_0_release_notes.html">Ruby on Rails 3.0 發佈記</option>
<option value="2_3_release_notes.html">Ruby on Rails 2.3 發佈記</option>
<option value="2_2_release_notes.html">Ruby on Rails 2.2 發佈記</option>
</optgroup>
<optgroup label="Rails 指南翻譯術語">
<option value="translation_terms.html">翻譯術語</option>
</optgroup>
</select>
</li>
</ul>
</div>
</div>
</div>
<hr class="hide">
<div id="feature">
<div class="wrapper">
<h2>Action Controller 概覽</h2><p>本篇介紹 Controller 的工作原理、Controller 如何與應用程式的請求(Request)週期結合在一起。</p><p>讀完本篇,您將了解:</p>
<ul>
<li>如何透過 Controller 了解請求流程。</li>
<li>如何限制傳入 Controller 的參數。</li>
<li>資料存在 Session 或 Cookie 裡的應用場景。</li>
<li>如何在處理請求時,使用 Filters 來執行程式。</li>
<li>如何使用 Action Controller 內建的 HTTP 認證機制。</li>
<li>如何用串流方式將資料直接傳給使用者。</li>
<li>如何過濾應用程式 Log 裡的敏感資料。</li>
<li>如何在 Request 生命週期裡,處理可能拋出的異常。</li>
</ul>
<div id="subCol">
<h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />Chapters</h3>
<ol class="chapters">
<li><a href="#controller-%E7%9A%84%E5%B7%A5%E4%BD%9C">Controller 的工作</a></li>
<li><a href="#controller-%E5%91%BD%E5%90%8D%E6%85%A3%E4%BE%8B">Controller 命名慣例</a></li>
<li><a href="#%E5%8B%95%E4%BD%9C%E5%8D%B3%E6%96%B9%E6%B3%95">動作即方法</a></li>
<li>
<a href="#%E5%8F%83%E6%95%B8">參數</a>
<ul>
<li><a href="#hash-%E8%88%87%E9%99%A3%E5%88%97%E5%8F%83%E6%95%B8">Hash 與陣列參數</a></li>
<li><a href="#json-%E5%8F%83%E6%95%B8">JSON 參數</a></li>
<li><a href="#%E8%B7%AF%E7%94%B1%E5%8F%83%E6%95%B8">路由參數</a></li>
<li><a href="#default-url-options"><code>default_url_options</code></a></li>
<li><a href="#strong-parameters">Strong Parameters</a></li>
</ul>
</li>
<li>
<a href="#session">Session</a>
<ul>
<li><a href="#%E5%AD%98%E5%8F%96-session">存取 Session</a></li>
<li><a href="#%E6%8F%90%E7%A4%BA%E8%A8%8A%E6%81%AF">提示訊息</a></li>
</ul>
</li>
<li><a href="#cookies">Cookies</a></li>
<li><a href="#%E7%AE%97%E7%B9%AA-xml-%E8%88%87-json-%E8%B3%87%E6%96%99">算繪 XML 與 JSON 資料</a></li>
<li>
<a href="#%E6%BF%BE%E5%8B%95%E5%99%A8">濾動器</a>
<ul>
<li><a href="#%E5%BE%8C%E7%BA%8C%E6%BF%BE%E5%8B%95%E5%99%A8%E8%88%87%E5%89%8D%E5%BE%8C%E6%BF%BE%E5%8B%95%E5%99%A8">後續濾動器與前後濾動器</a></li>
<li><a href="#%E6%BF%BE%E5%8B%95%E5%99%A8%E7%9A%84%E5%85%B6%E5%AE%83%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F">濾動器的其它使用方式</a></li>
</ul>
</li>
<li><a href="#request-%E5%81%BD%E9%80%A0%E4%BF%9D%E8%AD%B7">Request 偽造保護</a></li>
<li>
<a href="#%E8%AB%8B%E6%B1%82%E8%88%87%E9%9F%BF%E6%87%89">請求與響應</a>
<ul>
<li><a href="#request-%E7%89%A9%E4%BB%B6"><code>request</code> 物件</a></li>
<li><a href="#response-%E7%89%A9%E4%BB%B6"><code>response</code> 物件</a></li>
</ul>
</li>
<li>
<a href="#http-%E8%AA%8D%E8%AD%89">HTTP 認證</a>
<ul>
<li><a href="#http-%E5%9F%BA%E7%A4%8E%E8%AA%8D%E8%AD%89">HTTP 基礎認證</a></li>
<li><a href="#http-%E6%91%98%E8%A6%81%E8%AA%8D%E8%AD%89">HTTP 摘要認證</a></li>
</ul>
</li>
<li>
<a href="#%E4%B8%B2%E6%B5%81%E8%88%87%E6%AA%94%E6%A1%88%E4%B8%8B%E8%BC%89">串流與檔案下載</a>
<ul>
<li><a href="#%E5%82%B3%E9%80%81%E6%AA%94%E6%A1%88">傳送檔案</a></li>
<li><a href="#restful-%E9%A2%A8%E6%A0%BC%E7%9A%84%E4%B8%8B%E8%BC%89">RESTful 風格的下載</a></li>
<li><a href="#%E5%8D%B3%E6%99%82%E4%B8%B2%E6%B5%81%E4%BB%BB%E4%BD%95%E8%B3%87%E6%96%99">即時串流任何資料</a></li>
</ul>
</li>
<li>
<a href="#%E9%81%8E%E6%BF%BE-log">過濾 Log</a>
<ul>
<li><a href="#%E9%81%8E%E6%BF%BE%E5%8F%83%E6%95%B8">過濾參數</a></li>
<li><a href="#%E9%81%8E%E6%BF%BE%E8%BD%89%E5%9D%80">過濾轉址</a></li>
</ul>
</li>
<li>
<a href="#%E6%8B%AF%E6%95%91%E7%95%B0%E5%B8%B8">拯救異常</a>
<ul>
<li><a href="#%E5%85%A7%E5%BB%BA%E7%9A%84-500%E3%80%81404-%E8%88%87-422-%E6%A8%A1%E7%89%88">內建的 500、404 與 422 模版</a></li>
<li><a href="#rescue-from"><code>rescue_from</code></a></li>
<li><a href="#%E8%87%AA%E8%A8%82%E9%8C%AF%E8%AA%A4%E9%A0%81%E9%9D%A2">自訂錯誤頁面</a></li>
</ul>
</li>
<li><a href="#%E5%BC%B7%E5%88%B6%E4%BD%BF%E7%94%A8-https-%E5%8D%94%E5%AE%9A">強制使用 HTTPS 協定</a></li>
</ol>
</div>
</div>
</div>
<div id="container">
<div class="wrapper">
<div id="mainCol">
<h3 id="controller-的工作">1 Controller 的工作</h3><p>Action Controller 是 MVC 的 C,Controller。一個請求進來,路由決定是那個 Controller 的工作後,便把工作指派給 Controller,Controller 負責處理該請求,給出適當的回應。幸運的是,Action Controller 把大部分的苦差事都辦好了,只需遵循一些簡單的規範來寫程式,事情便豁然開朗。</p><p>對多數按照 <a href="http://en.wikipedia.org/wiki/Representational_state_transfer">REST</a> 規範來編寫的應用程式來說,Controller 的工作便是接收請求(開發者看不到),去 Model 讀或寫資料,再使用 View 來產生出 HTML。若 Controller 要處理別的事情,沒有問題,上面不過是 Controller 的主要功能。</p><p>Controller 因此可以想成是 Model 與 View 的中間人。負責替 Model 將資料傳給 View,讓 View 可以顯示資料給使用者。Controller 也將使用者更新或儲存的資料,存回 Model。</p><p>路由的詳細過程可以查閱 <a href="/routing.html">Rails 路由:由表入裡</a>。</p><h3 id="controller-命名慣例">2 Controller 命名慣例</h3><p>Rails Controller 的命名慣例是<strong>最後一個單字以複數形式結尾</strong>,但是也有例外,比如 <code>ApplicationController</code>。舉例來說:偏好 <code>ClientsController</code> 勝過 <code>ClientController</code>。偏好 <code>SiteAdminsController</code> 勝過 <code>SitesAdminsController</code> 等。</p><p>遵循慣例便可享受內建 Rails Router 的功能,如:<code>resources</code>、<code>resource</code> 路由等,而無需特地傳入 <code>:path</code>、<code>:controller</code> 選項,便可保持 URL 與路徑輔助方法的一致性。詳細內容請參考 <a href="/layouts_and_rendering.html">Rails 算繪與版型</a>一篇。</p><div class="note"><p>Controller 的命名慣例與 Model 的命名慣例不同,Model 命名慣例是<strong>單數形式</strong>。</p></div><h3 id="動作即方法">3 動作即方法</h3><p>Controller 是從 <code>ApplicationController</code> 繼承而來的類別,但 Controller 其實和 Ruby 的類別相同,擁有許多動作(即 Ruby 的方法)。當應用程式收到請求時,Rails 的 Router 會決定這要交給那個 Controller 的那個 Action 來處理,接著 Rails 新建該 Controller 的實體,呼叫與動作同名的方法。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
def new
end
end
</pre>
</div>
<p>舉個例子,假設應用程式的使用者到 <code>/clients/new</code>,想要新建一位 <code>client</code>,Rails 會新建 <code>ClientsController</code> 的實體,並呼叫 <code>new</code> 來處理。注意 <code>new</code> 雖沒有內容,但 Rails 的預設行為會算繪(render) <code>new.html.erb</code>,除非 <code>new</code> 動作裡指定要做別的事。<code>new</code> 動作可透過 <code>Client.new</code>,為 View 提供實體變數 <code>@client</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def new
@client = Client.new
end
</pre>
</div>
<p>詳情請參考 <a href="/layouts_and_rendering.html">Rails 算繪與版型</a>一篇。</p><p><code>ApplicationController</code> 繼承自 <code>ActionController::Base</code>,<code>ActionController::Base</code> 定義了許多有用的方法。本篇會提到一些,若是好奇到底有什麼方法可用,請參考 <a href="http://edgeapi.rubyonrails.org/classes/ActionController/Base.html">ActionController::Base 的 API 文件</a>,或是閱讀 <a href="https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/base.rb">ActionController::Base 的原始碼</a>。</p><p>只有公有方法,才可以被外部作為“動作”呼叫。所以輔助方法、濾動方法(Filter Methods),最好用 <code>protected</code> 或 <code>private</code> 隱藏起來。</p><h3 id="參數">4 參數</h3><p>通常會想在 Controller 裡取得使用者傳入的資料,或是其他的參數。Web 應用程式有兩種參數。第一種是由 URL 的部份組成,這種叫做 “Query String 參數”。Query String 是 URL <code>?</code> 號後面的任何字串,通常是透過 HTTP <code>GET</code> 傳遞。第二種參數是 “POST 資料”。通常來自使用者在表單所填寫的資料。叫做 POST 資料的原因是,這種參數只能作為 HTTP POST 請求的一部分來傳遞。Rails 並不區分 Query String 參數或 POST 參數,兩者皆可在 Controller 裡取用,而它們都存在 <code>params</code> Hash:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
# 使用了 Query String 參數,因為 Request 用的是
# HTTP GET。URL 看起來會像是: /clients?status=activated
def index
if params[:status] == "activated"
@clients = Client.activated
else
@clients = Client.inactivated
end
end
# 使用了 POST 參數,參數很可能是從使用者送出的表單而來。
# URL 看起來會像是: "/clients" (遵循 RESTful 慣例)。
# 資料會放在請求的 Body 裡再送過來。
def create
@client = Client.new(params[:client])
if @client.save
redirect_to @client
else
# 覆寫預設的 `render` 行為,預設是 `render "create"`。
render "new"
end
end
end
</pre>
</div>
<h4 id="hash-與陣列參數">4.1 Hash 與陣列參數</h4><p><code>params</code> Hash 不侷限於一維的 Hash,可以是嵌套結構,裡面可存陣列或嵌套的 Hash。</p><p>若想以陣列形式來傳遞參數,在鍵的名稱後方附加 <code>[]</code> 即可,如下所示:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
GET /clients?ids[]=1&ids[]=2&ids[]=3
</pre>
</div>
<p>注意:上例 URL 會編碼為 <code>"/clients?ids%5B%5D=1&ids%5B%5D=2&ids%5B%5D=3"</code>,因為 <code>[]</code> 對 URL 來說是非法字元。多數情況下,瀏覽器會檢查字元是否合法,會自動對非法字元做編碼。Rails 收到時再自己解碼。但若是要手動發請求給伺服器時,要記得自己處理好這件事。</p><p><code>params[:ids]</code> 現在會是 <code>["1", "2", "3"]</code>。注意!參數的值永遠是字串類型。Rails 不會試著去臆測或轉換類型。</p><div class="note"><p><code>params</code> 裡像是 <code>[]</code>、<code>[nil]</code> 或是 <code>[nil, nil, ...]</code> 基於安全考量,會自動替換成 <code>nil</code>。詳情請參考 <a href="/security.html#unsafe-query-generation">Rails 安全指南:產生不安全的查詢</a>一節。</p></div><p>要送出 Hash 形式的參數,在中括號裡指定鍵的名稱:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/clients" method="post">
<input type="text" name="client[name]" value="Acme" />
<input type="text" name="client[phone]" value="12345" />
<input type="text" name="client[address][postcode]" value="12345" />
<input type="text" name="client[address][city]" value="Carrot City" />
</form>
</pre>
</div>
<p>這個表單送出時,<code>params[:client]</code> 的值為:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{
"name" => "Acme",
"phone" => "12345",
"address" => {
"postcode" => "12345", "city" => "Carrot City"
}
}`
</pre>
</div>
<p>注意 <code>params[:client][:address]</code> 是嵌套的 Hash 結構。</p><p><code>params</code> Hash 其實是 <code>ActiveSupport::HashWithIndifferentAccess</code> 的實體。<code>ActiveSupport::HashWithIndifferentAccess</code> 與一般 Hash 類似,不同之處是取出 Hash 的值時,鍵可以用字串與符號,即 <code>params[:foo]</code> 等同於 <code>params["foo"]</code>。</p><h4 id="json-參數">4.2 JSON 參數</h4><p>在寫 Web 服務的應用程式時,處理 JSON 格式的參數比其他種類的參數更好。若請求的 <code>"Content-Type"</code> 標頭檔(header)是 <code>"application/json"</code>,Rails 會自動將收到的 JSON 參數轉換好(將 JSON 轉成 Ruby 的 Hash),存至 <code>params</code> 裡。用起來與一般 Hash 相同。</p><p>舉個例子,若傳送的 JSON 參數如下:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
{ "company": { "name": "acme", "address": "123 Carrot Street" } }
</pre>
</div>
<p>則獲得的參數會是:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params[:company] => { "name" => "acme", "address" => "123 Carrot Street" }
</pre>
</div>
<p>除此之外,如果開啟了 <code>config.wrap_parameters</code> 選項,或是在 Controller 呼叫了 <code>wrap_parameters</code>,則可忽略 JSON 參數的根元素。Rails 會以 Contorller 的名稱另起新鍵,將 JSON 內容轉換好存在這個鍵下面。所以上面的 JSON 參數可以這樣寫就好:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
{ "name": "acme", "address": "123 Carrot Street" }
</pre>
</div>
<p>傳給 <code>CompaniesController</code> 時,轉換好的參數會存在 <code>params[:company]</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
</pre>
</div>
<p>關於如何鍵名稱的客製化,或針對某些特殊的參數執行 <code>wrap_parameters</code>,請查閱 <a href="http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html">ActionController::ParamsWrapper 的 API 文件</a>。</p><div class="note"><p>XML 的功能現已抽成 <a href="https://github.com/rails/actionpack-xml_parser">actionpack-xml_parser</a> 這個 RubyGem。</p></div><h4 id="路由參數">4.3 路由參數</h4><p><code>params</code> Hash 永遠會有兩個鍵:<code>:controller</code> 與 <code>:action</code>,分別是當下呼叫的 Controller,與動作的名稱。但若想知道當下的 Controller 與動作名稱時,請使用 <code>controller_name</code> 與 <code>action_name</code>,不要直接從 <code>params</code> 裡取:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
controller.controller_name %>
controller.action_name %>
</pre>
</div>
<p>路由裡定義的參數也會放在 <code>params</code> 裡,像是 <code>:id</code>。</p><p>假設有一張 <code>Client</code> 的清單,<code>Client</code> 有兩種狀態,分別為啟用與停用兩種狀態。我們可以加入一條路由,來捕捉 <code>Client</code> 的狀態:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
get '/clients/:status' => 'clients#index', foo: 'bar'
</pre>
</div>
<p>這個情況裡,當使用者打開 <code>/clients/active</code> 這一頁,<code>params[:status]</code> 便會被設成 <code>"active"</code>,<code>params[:foo]</code> 也會被設成 <code>"bar"</code>,就像是我們原本透過 Query String 傳進去那樣。同樣的,<code>params[:action]</code> 也會被設成 <code>index</code>。</p><h4 id="default-url-options">4.4 <code>default_url_options</code>
</h4><p>可以設定預設用來產生 URL 的參數。首先在 Controller 定義一個叫做 <code>default_url_options</code> 的方法。這個方法必須回傳一個 Hash。鍵必須是 <code>Symbol</code> 類型,值為需要的內容:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
def default_url_options
{ locale: I18n.locale }
end
end
</pre>
</div>
<p>產生 URL 時會採用 <code>default_options</code> 所定義的選項,作為預設值。不過還是可以用 <code>url_for</code> 覆寫掉。</p><p>如果在 <code>ApplicationController</code> 定義 <code>default_url_options</code>,如上例。則產生所有 URL 的時候,都會傳入 <code>default_url_options</code> 內所定義的參數。<code>default_url_options</code> 也可以在特定的 Controller 裡定義,如此一來便只會影響該 Controller 所產生的 URL。</p><h4 id="strong-parameters">4.5 Strong Parameters</h4><p>原先大量賦值是由 Active Model 來處理,透過白名單來過濾不可賦值的參數。也就是得明確指定那些屬性可以賦值,避免掉不該被賦值的屬性被賦值了。有了 Strong Parameter 之後,這件工作交給 Action Controller 負責。</p><p>除此之外,還可以限制必須傳入那些參數。若是沒給入這些必要參數時,Rails 預先定義好的 <code>raise</code>/<code>rescue</code> 會處理好,回傳 400 Bad Request。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class PeopleController < ActionController::Base
# 會拋出 ActiveModel::ForbiddenAttributes 異常。
# 因為做了大量覆值卻沒有明確的說明允許賦值的參數有那些。
def create
Person.create(params[:person])
end
# 若沒有傳入 :id,會拋出 ActionController::ParameterMissing 異常。
# 這個異常會被 ActionController::Base 捕捉,並轉換成 400 Bad Request。
def update
person = current_account.people.find(params[:id])
person.update!(person_params)
redirect_to person
end
private
# 使用 private 方法來封裝允許大量賦值的參數
# 這麼做的好處是這個方法可以在 create 與 update 重複使用。
# 同時可以這個方法也很容易擴展。
def person_params
params.require(:person).permit(:name, :age)
end
end
</pre>
</div>
<h5 id="允許使用的純量值">4.5.1 允許使用的純量值</h5><p>給定:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.permit(:id)
</pre>
</div>
<p>若 <code>params</code> 有 <code>:id</code>,並且 <code>:id</code> 有允許使用的純量值。便可以通過白名單檢查,否則 <code>:id</code> 就會被過濾掉。這也是為什麼陣列、Hash 或任何其他的物件無法被注入。</p><p>允許的純量類型有:</p><p><code>String</code>、<code>Symbol</code>、<code>NilClass</code>、<code>Numeric</code>、<code>TrueClass</code>、<code>FalseClass</code>、<code>Date</code>、<code>Time</code>、<code>DateTime</code>、<code>StringIO</code>、<code>IO</code>、<code>ActionDispatch::Http::UploadedFile</code> 以及
<code>Rack::Test::UploadedFile</code>。</p><p><code>params</code> 裡需要允許賦值的參數是陣列形式怎麼辦?</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.permit(id: [])
</pre>
</div>
<p>允許整個 Hash 裡的參數可以賦值,使用 <code>permit!</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.require(:log_entry).permit!
</pre>
</div>
<p><code>params</code> 裡的 <code>:log_entry</code> hash 以及裡面所有的子 Hash 此時都允許做大量賦值。<strong>使用 <code>permit!</code> 要非常小心</strong>,因為這允許了 Model 所有的屬性,都可以做大量賦值,要是之後 Model 新增了 <code>admin</code> 屬性而沒注意到 <code>permit!</code>,可能就會出問題了。</p><h5 id="嵌套參數">4.5.2 嵌套參數</h5><p>要允許嵌套參數做大量賦值,比如:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.permit(:name, { emails: [] },
friends: [ :name,
{ family: [ :name ], hobbies: [] }])
</pre>
</div>
<p>上面的宣告允許:<code>name</code>、<code>emails</code> 以及 <code>friends</code> 屬性。且 <code>emails</code> 會是陣列形式、<code>friends</code> 會是由 resource 組成的陣列,需要有 <code>name</code>、<code>hobbies</code> (必須是陣列形式)、以及 <code>family</code> (只允許有 <code>name</code>)。</p><h5 id="更多例子">4.5.3 更多例子</h5><p>可能也想在 <code>new</code> 動作裡使用允許的屬性。但這帶出了一個問題,無法對根元素使用 <code>require</code>。因為呼叫 <code>new</code> 的時候,資料根本還不存在,這時可以用 <code>fetch</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 使用 `fetch` 你可以設定預設值,並使用
# Strong Parameters 的 API 來取出
params.fetch(:blog, {}).permit(:title, :author)
</pre>
</div>
<p><code>accepts_nested_attributes_for</code> 允許基於 <code>id</code> 與 <code>_destroy</code> 參數,來 <code>update</code> 與 <code>destroy</code> 相關的記錄:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 允許 :id 與 :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
</pre>
</div>
<p>當 Hash 的鍵是整數時,處理的方式不大一樣。可以宣告屬性是子 Hash。在 <code>has_many</code> 的關聯裡使用 <code>accepts_nested_attributes_for</code> 時會得到以下類型的參數:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 白名單過濾下列資料
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
params.require(:book).permit(:title, chapters_attributes: [:title])
</pre>
</div>
<h5 id="strong-parameters-處理不了的問題">4.5.4 Strong Parameters 處理不了的問題</h5><p>Strong Parameter API 不是銀彈,無法處理所有關於白名單的問題。但可以簡單地將 Strong Parameter API 與你的程式混合使用,來對付不同的需求。</p><p>假想看看,想要給某個屬性加上白名單,該屬性可以包含一個 Hash,裡面可能有任何鍵。使用 Strong Parameter 無法允許有任何 key 的 Hash,但可以這麼做:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def product_params
params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end
</pre>
</div>
<h3 id="session">5 Session</h3><p>應用程式為每位使用者都準備了一個 Session,可以儲存小量的資料,資料在請求之間都會保存下來。Session 僅在 Controller 與 View 裡面可以使用,Session 儲存機制如下:</p>
<ul>
<li>
<code>ActionDispatch::Session::CookieStore</code> ─ 所有資料都存在用戶端。</li>
<li>
<code>ActionDispatch::Session::CacheStore</code> ─ 資料存在 Rails 的 Cache。</li>
<li>
<code>ActionDispatch::Session::ActiveRecordStore</code> ─ 資料使用 Active Record 存在資料庫(需要 <code>activerecord-session_store</code> RubyGem)。</li>
<li>
<code>ActionDispatch::Session::MemCacheStore</code> ─ 資料存在 memcached(這是遠古時代的實作方式,考慮改用 CacheStore 吧)。</li>
</ul>
<p>所有的 Session 儲存機制都會使用一個 Cookie。在 Cookie 裡為每個 Session 存一個獨立的 Session ID。Session ID 必須要存在 Cookie 裡,因為 Rails 不允許在 URL 傳遞 Session ID(不安全)。</p><p>多數的儲存機制使用 Session ID 到伺服器上查詢 Session 資料,譬如到資料庫裡查詢。但有個例外,會把 Session 資料全部存在 Cookie,即 CookieStore 的儲存方式。優點是非常輕量,完全不用設定。存在 Cookie 的資料經過加密簽署,防止有心人士竄改。即便是擁有 Session 資料存取權的人,也無法讀取內容(內容經過加密)。如果 Cookie 的資料遭到修改,Rails 也不會使用這個資料。</p><p>CookieStore 大約可以存 4KB 的資料,其他儲存機制可以存更多,但通常 4KB 已經夠用了。不管用的是那種儲存機制,不建議在 Session 裡存大量資料。特別要避免將複雜的物件儲存在 Session 裡(除了 Ruby 基本物件之外的東西都不要存,比如 Model 的實體)。因為伺服器可能沒辦法在請求之間重新將物件還原,便會導致錯誤發生。</p><p>若使用者的 Session 沒有儲存重要的資料,或存的是短期的資料(比如只是用來顯示提示訊息)。可以考慮使用 <code>ActionDispatch::Session::CacheStore</code>。這會將 Session 存在應用程式所設定的快取裡。優點是利用現有的快取架構來儲存,不用額外管理,或是設定 Session 的儲存機制。缺點是生命週期短、隨時可能會消失。</p><p>關於如何安全地儲存 Session,請閱讀 <a href="/security.html#session">Rails 安全指南:Session</a> 一節。</p><p>如需不同的 Session 儲存機制,可以在 <code>config/initializers/session_store.rb</code> 裡設定:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 使用資料庫來存 Session,而不是使用預設的 Cookie 來存。
# 注意,不要存任何高度敏感的資料在 Session。
# (建立 Session 資料表:"rails g active_record:session_migration")
# Rails.application.config.session_store :active_record_store
</pre>
</div>
<p>簽署 Session 資料時,Rails 設了一個 Session 鍵(為 Cookie 的名字),這個名字可在 <code>config/initializers/session_store.rb</code> 裡修改:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 修改此文件時記得重新啟動 Server
Rails.application.config.session_store :cookie_store, key: '_your_app_session'
</pre>
</div>
<p>也可以傳入 <code>:domain</code> key,來指定 cookie 的 domain name:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 修改此文件時記得重新啟動 Server
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
</pre>
</div>
<p>Rails 替 CookieStore 設了一個 secret key,用來簽署加密 Session 資料。這個 key 可以在 <code>config/secrets.yml</code> 裡修改。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 修改此文件時記得重新啟動 Server
# Secret Key 用來簽署與認證 Cookie。
# Key 變了先前的 cookie 都會失效!
# 確保 Secret 至少有 30 個隨機字元,沒有一般的單字(防禦字典查表攻擊)。
# 可以使用 `rake secret` 來產生一個安全的 Secret Key.
# 如果要將程式碼公開,
# 不要公開這個檔案裡的 Secret。
development:
secret_key_base: a75d...
test:
secret_key_base: 492f...
# Repository 裡不要放 Production 的 Secret。
# 應該把 Secret 放在環境變數裡讀進來。
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
</pre>
</div>
<div class="note"><p>更改 <code>secret_key_base</code> 之後,先前簽署的 Session 都會失效。</p></div><h4 id="存取-session">5.1 存取 Session</h4><p>在 Controller 可以透過 <code>session</code> 這個實體方法來存取 Session。</p><p>**注意:Session 是惰性加載的。若動作沒用到 Session,便不會載入 Session。若是不想用 Session,無需關掉 Session,不要用就好了。</p><p>Session 以類似於 Hash 的方式儲存(鍵值對):</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
private
# 用存在 Session 的 :current_user_id 來找到 User。
# 這是 Rails 常見處理使用者登入的手法;
# 登入時將使用者的 ID 存在 Session,登出時再清掉。
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
end
</pre>
</div>
<p>要在 Session 裡存值,給 Hash 的鍵賦值即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
# 建立“登入”,也就是“登入使用者”
def create
if user = User.authenticate(params[:username], params[:password])
# 將使用者的 ID 存在 Session,供之後的 Request 使用。
session[:current_user_id] = user.id
redirect_to root_url
end
end
end
</pre>
</div>
<p>要從 Session 裡移掉數值,給想移除的鍵賦 <code>nil</code> 值即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
def destroy
# 將 user id 從 session 裡移除
@_current_user = session[:current_user_id] = nil
redirect_to root_url
end
end
</pre>
</div>
<p>要將整個 session 清掉,使用 <code>reset_session</code> 方法。</p><h4 id="提示訊息">5.2 提示訊息</h4><p>提示訊息(Flash Message)是 Session 特殊的一部分,可以從一個請求傳遞(錯誤、提示)訊息到下個請求,下個請求結束後,便會自動清除提示訊息。</p><p><code>flash</code> 的使用方式與 <code>session</code> 雷同,和操作一般的 Hash 一樣(實際上 <code>flash</code> 是 <a href="http://edgeapi.rubyonrails.org/classes/ActionDispatch/Flash/FlashHash.html">FlashHash</a> 的實體)。</p><p>用登出作為例子,Controller 可以傳一個訊息,用來給下個請求顯示:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
def destroy
session[:current_user_id] = nil
flash[:notice] = "成功登出了"
redirect_to root_url
end
end
</pre>
</div>
<p>注意也可以直接在 <code>redirect_to</code> 設定提示訊息:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }
</pre>
</div>
<p>上面的 <code>destroy</code> 動作會導向到應用程式的 <code>root_url</code>,導回到 <code>root_url</code> 後會顯示<code>"成功登出了"</code>的訊息。注意到提示訊息永遠在上個動作裡設定。</p><p>通常都會用 Flash 來顯示錯誤、提示訊息等,通常會在應用程式的版型檔案 <code>app/views/layout/application.html.erb</code>,加入提示訊息所需的 View:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<html>
<!-- <head/> -->
<body>
<% flash.each do |name, msg| -%>
<%= content_tag :div, msg, class: name %>
<% end -%>
<!-- more content -->
</body>
</html>
</pre>
</div>
<p>如此一來,若動作有設定 <code>:notice</code> 或 <code>:alert</code> 訊息,View 便會自動顯示。</p><p>提示訊息的種類不侷限於 <code>:notice</code>、<code>:alert</code> 或 <code>:flash</code>,可以自己定義:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<% if flash[:just_signed_up] %>
<p class="welcome">Welcome to our site!</p>
<% end %>
</pre>
</div>
<p>若想要提示訊息在請求之間保留下來,使用 <code>keep</code> 方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class MainController < ApplicationController
# 假設這個動作會回應 root_url
# 但想要所有的請求都導到 UsersController#index
# 若在此設定了提示訊息,接著 redirect,則無法保存提示訊息。
# 可以用 flash.keep 將 flash 的值保存下來,供別的請求使用。
def index
# 保留整個 flash
flash.keep
# 也可以只保留提示訊息的 :notice 部分
# flash.keep(:notice)
redirect_to users_url
end
end
</pre>
</div>
<h5 id="flash-now">5.2.1 <code>flash.now</code>
</h5><p>預設情況下,加入值至 <code>flash</code>,只能在下次請求可以取用,但有時會想在同個請求裡使用這些訊息。舉例來說,如果 <code>create</code> 動作無法儲存,想要直接 <code>render</code> <code>new</code>,這不會發另一個請求,但仍需要顯示訊息,這時候便可以使用 <code>flash.now</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
if @client.save
# ...
else
flash.now[:error] = "無法儲存 Client"
render action: "new"
end
end
end
</pre>
</div>
<h3 id="cookies">6 Cookies</h3><p>應用程式可以在客戶端儲存小量的資料,這種資料稱為 Cookie。Cookie 在請求之間是不會消失,可以用來存 Session。Rails 裡存取 Cookies 的非常簡單,<code>cookies</code>,用起來跟 <code>session</code> 類似,和 Hash 用法相同:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class CommentsController < ApplicationController
def new
# 若是 Cookie 裡有存留言者的名字,自動填入。
@comment = Comment.new(author: cookies[:commenter_name])
end
def create
@comment = Comment.new(params[:comment])
if @comment.save
flash[:notice] = "感謝您的意見!"
if params[:remember_name]
# 選擇記住名字,則記下留言者的名稱。
cookies[:commenter_name] = @comment.author
else
# 選擇不記住名字,刪掉 Cookie 裡留言者的名稱。
cookies.delete(:commenter_name)
end
redirect_to @comment.article
else
render action: "new"
end
end
end
</pre>
</div>
<p><strong>注意 Session 是用賦 <code>nil</code> 值來清空某個鍵的值;Cookie 則要使用 <code>cookies.delete(:key)</code> 刪掉。</strong></p><p>Rails 也提供簽署 Cookie 與加密 Cookie,用來儲存敏感資料。簽署 Cookie 裡的數值會附上加密過的簽名,確保值沒有被竄改。加密 Cookie 不僅會在值附加簽名的基礎上再次加密,讓用戶端使用者無法讀取。詳細資料請閱讀 <a href="http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html">Action Dispatch 的 API 文件</a></p><p>這兩種特殊的 Cookie 使用一個 Serializer,將數值序列化成字串,讀取時再反序列化回來。</p><p>指定使用的 Serializer:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Rails.application.config.action_dispatch.cookies_serializer = :json
</pre>
</div>
<p>Rails 新版的預設 Serializer 是 <code>:json</code>。但為了與舊版應用程式裡的 Cookie 相容,沒特別指定 Serializer 時,會使用 <code>:marshal</code>。</p><p>也可以設成 <code>:hybrid</code>。讀到以 <code>Marshal</code> 序列化的 Cookie 時,會用 <code>:marshal</code> 來反序列化。並重新使用 <code>JSON</code> 格式寫回去。這在將現有應用程式的 Serializer 升級到 <code>:json</code> 時很有用。</p><p>使用自訂的 Serializer 也可以(必須要實作 <code>load</code> 與 <code>dump</code>):</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
</pre>
</div>
<p>在使用 <code>:json</code> 或 <code>hybrid</code> Serializer 時,應該要注意到,不是所有的 Ruby 物件,都可以轉成 JSON。舉個例子,<code>Date</code> 與 <code>Time</code> 物件會被序列化成字串,Hash 的鍵也會被序列化成字串。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class CookiesController < ApplicationController
def set_cookie
cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
redirect_to action: 'read_cookie'
end
def read_cookie
cookies.encrypted[:expiration_date] # => "2014-03-20"
end
end
</pre>
</div>
<p>建議 Cookie 裡只存放簡單的資料(像是數字與字串)。</p><p>若必須存放複雜的物件,需要自己在接下來的請求裡手動轉換。</p><p>如果 Session 採用的是 CookieStore 儲存機制,則上面的規則, <code>session</code> 與 <code>flash</code> 同樣適用。</p><h3 id="算繪-xml-與-json-資料">7 算繪 XML 與 JSON 資料</h3><p>在 <code>ActionController</code> 裡算繪 <code>XML</code> 或是 <code>JSON</code> 真是再簡單不過了,看看下面這個用鷹架所產生出來的 Controller:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class UsersController < ApplicationController
def index
@users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render xml: @users}
format.json { render json: @users}
end
end
end
</pre>
</div>
<p>注意這裡 <code>render</code> XML 的時候是寫 <code>render xml: @users</code>,而不是 <code>render xml: @users.to_xml</code>。如果 <code>render</code> 的物件不是字串的話,Rails 會自動呼叫 <code>to_xml</code>。</p><h3 id="濾動器">8 濾動器</h3><p>濾動器(Filter)是可在 Controller 動作執行前、後、之間所執行的方法。</p><p>濾動器可被 Controller 繼承,也就是在 <code>ApplicationController</code> 定義的濾動器,在整個應用程式裡都會執行該濾動器。</p><p>前置濾動器(Before Filter)可能會終止請求週期。常見的前置濾動器,像是執行某個動作需要使用者登入。則可以這麼定義濾動器方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
before_action :require_login, only: [:admin]
def admin
# 管理員才可使用的...
end
private
def require_login
unless logged_in?
flash[:error] = "這個區塊必須登入才能存取"
redirect_to new_login_url # 終止請求週期
end
end
end
</pre>
</div>
<p>這個方法非常簡單,當使用者沒有登入時,將錯誤訊息存在 <code>flash</code> 裡,並轉向到登入頁。若前置濾動器執行了 <code>render</code> 或是 <code>redirect_to</code>,便不會執行 <code>admin</code> 動作。要是 before 濾動器之間互相有依賴,一個取消了,另一個也會跟著取消。</p><p>剛剛的例子裡,濾動器加入至 <code>ApplicationController</code>,所以在應用程式裡,只要是繼承 <code>ApplicationController</code> 的所有動作,都會需要登入才能使用。但使用者還沒註冊之前,怎麼登入?所以一定有方法可以跳過濾動器,<code>skip_before_action</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
</pre>
</div>
<p>現在 <code>LoginsController</code> 的 <code>new</code> 與 <code>create</code> 動作如先前一般工作,無需使用者登入。<code>:only</code> 選項用來決定這個濾動器只需要檢查那幾個動作,而 <code>:except</code> 選項則是決定這個濾動器不需要檢查那幾個動作。</p><h4 id="後續濾動器與前後濾動器">8.1 後續濾動器與前後濾動器</h4><p>除了有前置濾動器,也可以在動作結束後執行(後置濾動器,after filter),或者是動作前後之間執行(前後濾動器,around filter)。</p><p>後置濾動器與前置濾動器類似,但因為 <code>action</code> 已經執行完畢,所以後置濾動器可以存取即將要回給使用者的響應(Response)。後置濾動器無法終止請求週期,因為動作已經執行完畢,無法終止。不像前置濾動可以透過 <code>render</code> 或是 <code>redirect_to</code>,來終止動作的執行。</p><p>前後濾動器主要透過 <code>yield</code> 來負責執行相關的動作,跟 Rack 中間件的工作原理類似。</p><p>舉例來說,要給某個網站提交改動時,必須先獲得管理員同意,改動才會生效。管理員會需要某種類似預覽功能的操作,將此操作包在交易即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
</pre>
</div>
<p>注意前後濾動器包含了 <code>render</code>。需要特別說明的是,假設 View 會從資料庫讀取資料來顯示,在交易裡也會這麼做,如此一來便可達到預覽的效果。</p><p>響應也可以自己生,不需要用 <code>yield</code>。若是沒使用 <code>yield</code>,則 <code>show</code> 動作便不會被執行。</p><h4 id="濾動器的其它使用方式">8.2 濾動器的其它使用方式</h4><p>濾動器一般的使用方式是,先建立一個 <code>private</code> 方法,在使用 <code>*_action</code> 來針對是要在特定 <code>action</code> 前、後、之間執行該 <code>private</code> 方法。除了寫個方法,還有兩種方式可以達到濾動器的效果。</p><p>第一種是直接對 <code>*_action</code> 使用區塊。區塊接受 <code>controller</code> 作為參數,上面的 <code>require_login</code> 例子可以改寫為:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
before_action do |controller|
unless controller.send(:logged_in?)
flash[:error] = flash[:error] = "這個區塊必須登入才能存取"
redirect_to new_login_url
end
end
end
</pre>
</div>
<p>注意到這裡使用了 <code>send</code>,因為 <code>logged_in?</code> 方法是 <code>private</code>,濾動器不在 Controller 的作用域下執行。這種實作濾動器的方式不推薦使用,但在非常簡單的情況下可能有用。</p><p>第二種方式是使用類別,實際上使用任何物件都可以,只要物件有回應對的方法即可。用類別實作的好處是提高可讀性、重用性。舉個例子,上例可以改寫為:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
before_action LoginFilter
end
class LoginFilter
def self.before(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "這個區塊必須登入才能存取"
controller.redirect_to controller.new_login_url
end
end
end
</pre>
</div>
<p>同樣這不是這種濾動器的好例子,因為不在 Controller 的作用域下執行,需要傳入 Controller 作為參數。濾動器類別必須實作與濾動器同名的方法,所以 <code>before_filter</code> 便需要實作 <code>before</code> 方法,以此類推。<code>around</code> 方法則必須 <code>yield</code>,來執行該動作。</p><h3 id="request-偽造保護">9 Request 偽造保護</h3><p>跨站偽造請求(CSRF, Cross-site request forgery)是利用 A 站的使用者,給 B 站發送請求的一種攻擊手法,比如利用 A 站的梁山伯,去新增、修改、刪除 B 站祝英台的資料。</p><p>防範的第一動是確保所有破壞性的動作,如:<code>create</code>、<code>update</code> 與 <code>destroy</code> 只可以透過 <strong>非 GET</strong> 請求來操作。若遵循 RESTful 的慣例,則這已經解決了。但惡意站點仍可發送非 GET 請求至你的網站,這時便是請求偽造防護(Request Forgery Protection)派上用場的時刻了,請求偽造防護如其名:偽造請求防禦。</p><p>防護的手法是每次請求時,加上一個猜不到的 token。如此一來,沒有正確 token 的請求便會被拒絕存取。.</p><p>假設有下列表單:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @user do |f| %>
<%= f.text_field :username %>
<%= f.text_field :password %>
<% end %>
</pre>
</div>
<p>token 如何加到隱藏欄位:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
value="67250ab105eb5ad10851c00a5621854a23af5489"
name="authenticity_token"/>
<!-- username & password fields -->
</form>
</pre>
</div>
<p>Rails 自動給所有使用了<a href="/form-helpers.html">表單輔助方法</a> 的表單加上這個 token,所以不用擔心怎麼處理。若是手寫表單可以透過 <code>form_authenticity_token</code> 方法來加上 token。</p><p><a href="http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html#method-i-form_authenticity_token"><code>form_authenticity_token</code></a> 產生一個有效的驗證 token。這在 Rails 沒有自動加上 token 的場景下很有用,像是自定的 Ajax 請求,<code>form_authenticity_token</code> 很簡單,就是設定了 Session 的 <code>_csrf_token</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def form_authenticity_token
session[:_csrf_token] ||= SecureRandom.base64(32)
end
</pre>
</div>
<p>參閱 <a href="/security.html">Rails 安全指南</a>來了解此議題,以及開發 Web 應用程式所需要了解的安全性問題。</p><h3 id="請求與響應">10 請求與響應</h3><p>請求生命週期裡,每個 Controller 都有兩個存取器方法,<code>request</code> 與 <code>response</code>。<code>request</code> 方法包含了 <code>AbstractRequest</code> 的實體。<code>response</code> 方法則是即將回給客戶端的 <code>response</code> 物件。</p><h4 id="request-物件">10.1 <code>request</code> 物件</h4><p><code>request</code> 物件帶有許多從客戶端而來的有用資訊。關於所有可用的方法,請查閱 <a href="http://api.rubyonrails.org/classes/ActionDispatch/Request.html">ActionDispatch::Request API 文件</a>。而所有可存取的特性有:</p>
<table>
<thead>
<tr>
<th>
<code>request</code> 的 property</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>host</td>
<td>請求所使用的 hostname。</td>
</tr>
<tr>
<td>domain(n=2)</td>
<td>主機名稱的前 <code>n</code> 個區段,從 TLD 右邊開始算起。</td>
</tr>
<tr>
<td>format</td>
<td>請求所使用的 content type。</td>
</tr>
<tr>
<td>method</td>
<td>請求所使用的 HTTP 動詞。</td>
</tr>
<tr>
<td>get?, post?, patch?, put?, delete?, head?</td>
<td>HTTP 動詞為右列其一時,返回真。 GET/POST/PATCH/PUT/DELETE/HEAD。</td>
</tr>
<tr>
<td>headers</td>
<td>返回請求的標頭檔(Hash)。</td>
</tr>
<tr>
<td>port</td>
<td>請求使用的埠號。</td>
</tr>
<tr>
<td>protocol</td>
<td>返回包含 <code>"://"</code> 的字串,如 <code>"http://"</code>。</td>
</tr>
<tr>
<td>query_string</td>
<td>URL 的 Query String 部分。也就是 "?" 之後的字串。</td>
</tr>
<tr>
<td>remote_ip</td>
<td>客戶端的 IP 位址。</td>
</tr>
<tr>
<td>url</td>
<td>請求所使用的完整 URL 位址。</td>
</tr>
</tbody>
</table>
<h5 id="path-parameters、query-parameters-以及-request-parameters">10.1.1 <code>path_parameters</code>、<code>query_parameters</code> 以及 <code>request_parameters</code>
</h5><p>Rails 將所有與請求一起送來的參數,不管是 Query String 還是 POST body 而來的參數,都蒐集在 <code>params</code> Hash 裡。</p><p><code>request</code> 物件有三個存取器,可以取出這些參數,分別是 <code>query_parameters</code>、<code>request_parameters</code> 以及 <code>path_parameters</code>,它們都是 Hash。</p>
<ul>
<li><p><code>query_parameters</code>: Query String 參數(via GET)。</p></li>
<li><p><code>request_parameters</code>: POST 而來的參數。</p></li>
<li><p><code>path_parameters</code>: Controller 與動作名稱:</p></li>
</ul>
<div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{ 'action' => 'my_action', 'controller' => 'my_controller' }
</pre>
</div>
<h4 id="response-物件">10.2 <code>response</code> 物件</h4><p><code>response</code> 物件通常不會直接使用,會在執行動作時,與算繪即將送回給使用者的資料時,建立出 <code>response</code> 物件。需要先處理響應,處理完再回給 User 的場景下有用,比如在後置濾動器處理這件事。此時便可以存取到 <code>response</code>,甚至可透過 Setters 來改變 <code>response</code> 部分的值。</p>
<table>
<thead>
<tr>
<th>
<code>response</code> 的 property</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>body</td>
<td>傳回給客戶端的字串,通常是 HTML。</td>
</tr>
<tr>
<td>status</td>
<td>響應的狀態碼,比如成功回 200,找不到回 404。</td>
</tr>
<tr>
<td>location</td>
<td>轉址的 URL(如果有的話)。</td>
</tr>
<tr>
<td>content_type</td>
<td>響應的 Content-Type。</td>
</tr>
<tr>
<td>charset</td>
<td>響應使用的編碼集,預設是 "UTF-8"。</td>
</tr>
<tr>
<td>headers</td>
<td>響應使用的標頭檔。</td>
</tr>
</tbody>
</table>
<h5 id="自訂標頭檔">10.2.1 自訂標頭檔</h5><p>若是想給響應自定標頭檔,修改 <code>response.headers</code>。<code>headers</code> 是一個 Hash,將響應標頭檔的名稱與值關連起來,某些值 Rails 已經設定好了。假設 API 需要回一個特殊的 Header,<code>X-TOP-SECRET-HEADER</code>,在 Controller 便可以這麼寫:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
response.headers["X-TOP-SECRET-HEADER"] = '123456789'
</pre>
</div>
<p>若是要設定每個響應預設的標頭檔,可在 <code>config/application.rb</code> 裡設定,詳情參考 <a href="/configuring.html#configuring-action-dispatch">Rails 設定應用程式 - 3.8 設定 Action Dispatch</a> 一節。</p><h3 id="http-認證">11 HTTP 認證</h3><p>Rails 內建了兩種 HTTP 認證方法:</p>
<ul>
<li>Basic Authentication(基礎認證)</li>
<li>Digest Authentication(摘要認證)</li>
</ul>
<h4 id="http-基礎認證">11.1 HTTP 基礎認證</h4><p>「HTTP 基礎認證」是一種主流瀏覽器與 HTTP 客戶端皆支援的認證方式。舉個例子,假設有一段管理員才能瀏覽的區塊,必須在瀏覽器的 HTTP 基本會話視窗輸入 <code>username</code> 與 <code>password</code>,確保身分是管理員才可瀏覽。</p><p>在 Rails 裡只要使用一個方法:<code>http_basic_authenticate_with</code> 即可。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class AdminsController < ApplicationController
http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end
</pre>
</div>
<p>有了這行程式碼之後,可以從 <code>AdminsController</code> 切出命名空間,讓要管控的 Controller 繼承 <code>AdminsController</code>。</p><h4 id="http-摘要認證">11.2 HTTP 摘要認證</h4><p>HTTP 摘要認證比 HTTP 基礎認證高級一些,不需要使用者透過網路傳送未加密的密碼(但採用 HTTPS 的情況下,HTTP 基礎認證是安全的)。使用摘要認證也只需要一個方法:<code>authenticate_or_request_with_http_digest</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class AdminsController < ApplicationController
USERS = { "lifo" => "world" }
before_action :authenticate