'
401 |
402 | # bin/rails test test/controllers/static_pages_controller_test.rb:14
403 | '
404 | ```
405 | 这里的错误信息是说未定义名为“static_pages_about_url”本地变量或者方法,这暗示我们需要给路由文件添加一行代码。我们依据代码清单3.5的模式完成它,如同代码清单3.16所示。
406 | ```ruby
407 | 代码清单3.16:添加关于页面的路由。红色
408 | # config/routes.rb
409 | Rails.application.routes.draw do
410 | get 'static_pages/home'
411 | get 'static_pages/help'
412 | get 'static_pages/about'
413 | .
414 | .
415 | .
416 | end
417 | ```
418 | 在代码清单3.16里显示的高亮的行告诉Rails路由URL/static_pages/about的GET请求到StaticPagesController的about动作。
419 |
420 | 再次运行测试,我们看见还是红色的,但是现在错误信息已经改变了:
421 | ```terminal
422 | 代码清单3.17:红色
423 | $ bundle exec rails test
424 | AbstractController::ActionNotFound:
425 | The action 'about' could not be found for StaticPagesController
426 | ```
427 | 错误信息提示在StaticPagesController里找不到**about**动作,我们可以照着代码清单3.6中**home**和**help**代码添加如代码清单3.18里一样的代码。
428 | ```ruby
429 | 代码清单3.18:在StaticPagesController里添加about动作。红色
430 | class StaticPagesController < ApplicationController
431 |
432 | def home
433 | end
434 |
435 | def help
436 | end
437 |
438 | def about
439 | end
440 | end
441 | ```
442 | 这次我们的测试通过了。
443 | ```terminal
444 | $ bundle exec rails test
445 | ...
446 | Finished in 0.380464s, 7.8851 runs/s, 7.8851 assertions/s.
447 | 3 runs, 3 assertions, 0 failures, 0 errors, 0 skips
448 | ```
449 | 但是,当我们运行服务器,然后访问about页面时,在命令行会提示:
450 | ```
451 | $ rails server
452 | => Booting Puma
453 | => Rails 5.0.0.beta1 application starting in development on
454 | http://localhost:3000
455 | => Run `rails server -h` for more startup options
456 | => Ctrl-C to shutdown server
457 |
458 | # 访问http://localhost:3000/about时
459 | Started GET "/static_pages/about" for ::1 at 2015-12-23 17:11:32 +0800
460 | Processing by StaticPagesController#about as HTML
461 | No template found for StaticPagesController#about, rendering head :no_content
462 | Completed 204 No Content in 11ms (ActiveRecord: 0.0ms)
463 | ```
464 | 服务器返回204状态码,这表示返回的HTML文件没有内容。而在终端里输出的提示说明缺少模板,在Rails的中,模板和视图是一回事。如同3.2.1节里面所描述的一样,**home**动作是和名为**home.html.erb**的视图相联系的,这个文件位于**# app/views/static_pages**目录,这意味着我们需要在同一个目录创建一个名为**about.html.erb**的文件。
465 |
466 | 创建一个文件根据系统有所不同,但是大多数文本编辑器会让你所在的目录按control-点击,然后弹出一个有“新文件”的菜单。你也可以使用“文件”菜单新建文件,然后选择正确的位置保存它。最后,你可以使用你最喜欢的小技巧,使用Unix的**touch**命令:
467 | ```terminal
468 | $ touch # app/views/static_pages/about.html.erb
469 | ```
470 | 尽管**touch**被用来设计成更新文件或目录的时间戳而不影响其他方面,副作用是假如不存在,就会创建一个新的文件。(假如使用云IDE,按照1.3.1节描述的方法刷新一下目录树。)
471 |
472 | 一旦你在正确的目录创建了**about.html.erb**文件以后,把代码清单3.19的代码复制进去。
473 | ```html
474 | 代码清单3.19: 关于页面的代码。绿色
475 | # # app/views/static_pages/about.html.erb
476 | About
477 |
478 | The Ruby on Rails
479 | Tutorial is a
480 | book and
481 | screencast series
482 | to teach web development with
483 | Ruby on Rails.
484 | This is the sample application for the tutorial.
485 |
486 | ```
487 |
488 | 当然,在浏览器里看看效果,确定我们的测试不是完全“疯了”,这从来不会是一个坏主意(图3.5)。
489 |
490 | ![图3.5]:新关于页面(/static_pages/about)。
491 |
492 | ### 3.3.4 重构
493 | 既然我们的测试已经变绿了,我们可以带着自信重构我们的代码了。当开发应用程序的时候,刚开始写的代码常常很“臭”,意思是丑陋,臃肿,或者充满了重复。计算机不管代码看上去怎样,当然,但是人类会,所以通过频繁的重构保持代码清洁是很重要的。尽管我们现在的示例程序对于重构来说太小了,但是代码的臭味从每个缝隙渗透出来,所以我们将从3.4.3节开始重构。
494 |
495 | ## 3.4 略微动态的网页
496 |
497 | 既然我们为几个静态页面创建了动作和视图,我们可以通过添加一些改变每个页面基础的内容让它们略微动态起来:我们将通过改变页面的标题反应它的内容。改变标题是不是真的动态内容,这点是有争议的,但是不管怎样,这为我们第7章毫不含糊地动态内容打下了基础。
498 |
499 | 我们的计划是编辑主页、帮助页和关于页面来改变每页的标题,也就是在我们的页面视图里使用**\**标签
500 | 。大部分的浏览器在浏览器的顶端显示标题标签里面的内容,它对搜索引擎优化也是重要的。我们将使用完整的“
501 | 红色,绿色,重构”循环:首先通过为我们的页面标题添加简单的测试(红色),然后给三个页面添加标题(绿色
502 | ),最后使用**layout**文件来消除重复(重构)。在本节结束,我们的三个静态页面有类似“<页面名称>| Ruby
503 | on Rails Tutorial Sample App”,这里标题的第一部分将根据页面变化(表3.2)。
504 |
505 | **rails new**命令(代码清单3.1)创建了一个默认的布局文件,但是我们先忽略它,这个对我们学习是有益的,
506 | 我们先重命名该文件:
507 | ```terminal
508 | $ mv # app/views/layouts/application.html.erb layout_file #temporary change
509 | ```
510 | 你平常不会在真正的应用程序里做这样的事,但是假如我们现在先不用它,我们就会更加容易地理解布局文件的用途。
511 |
512 | 页面 | URL | 基础标题 | 可变标题
513 | -----|-----|----------|------
514 | 主页 |/static_pages/home| "Ruby on Rails Tutorial Sample App" | "Home"
515 | 帮助 |/static_pages/help| "Ruby on Rails Turorial Sample App" | "Help"
516 | 关于| /static_pages/about| "Ruby on Rails Tutorial Sample App" |"About"
517 |
518 | 表3.2:示例应用(基本都是)静态的页面
519 |
520 | ### 3.4.1 测试标题(红色)
521 | 为了添加页面标题,我们需要学习(或复习)一个典型的网页的结构,如代码清单3.21所示。
522 | ```
523 | 代码清单3.21: 典型的网页的HTML结构
524 |
525 |
526 |
527 | Greeting
528 |
529 |
530 | Hello, world!
531 |
532 |
533 | ```
534 | 代码清单3.21里的结构包含**文档类型**,或**doctype**,在浏览器顶部的声明告诉浏览器我们使用的是那个版本的HTML(在这个例子里是HTML5);**head**部分,在这个例子里是**title**标签里面的“Greeting”;和**body**部分,这里是指**p**标签里的“Hello, world!”。(缩进是可选的--HTML对空白字符不敏感,忽略制表格和空格--但是这让文档的结构更清楚)
535 |
536 | 我们将为表3.2里的每个标题写一个简单的测试,合并代码清单3.13里的测试和**assert_select**方法,让我们测试某个HTML标签是不是存在(有时叫“选择器”,因此取了这个名字):
537 | ```ruby
538 | assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
539 | ```
540 | 以上代码检查包含 "Home | Ruby on Rails Tutorial Sample App"字符串的\标签是否存在。把这个思路应用到其他三个的静态页面,代码清单3.22显示了测试代码。
541 |
542 | ```ruby
543 | 代码清单 3.22: 静态页面控制器标题测试。红色
544 | # test/controllers/static_pages_controller_test.rb
545 | require 'test_helper'
546 |
547 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest
548 |
549 | test "should get home" do
550 | get static_pages_home_url
551 | assert_response :success
552 | assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
553 | end
554 |
555 | test "should get help" do
556 | get static_pages_help_url
557 | assert_response :success
558 | assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
559 | end
560 |
561 | test "should get about" do
562 | get static_pages_about_url
563 | assert_response :success
564 | assert_select "title", "About | Ruby on Rails Tutorial Sample App"
565 | end
566 | end
567 | ```
568 | (假如你觉得基础标题“Ruby on Rails Tutorial Sample App”啰嗦,看看3.6节的练习。)
569 |
570 | 代码清单3.22写好后,你应该验证一下测试集是红色的:
571 | ```
572 | 代码清单 3.23: 红色
573 | $ bundle exec rails test
574 | 3 tests, 6 assertions, 3 failures, 0 errors, 0 skips
575 | ```
576 |
577 | ### 3.4.2 添加页面标题(绿色)
578 | 现在我们将在每个页面增加一个标题,让3.4.1节缩写的测试通过。把代码清单3.21里基本的HTML结构从代码清单3.9生成代码清单3.24.
579 |
580 | ```
581 | 代码清单 3.24: Home页面完整的HTML文件。 红色
582 | # # app/views/static_pages/home.html.erb
583 |
584 |
585 |
586 | Home | Ruby on Rails Tutorial Sample App
587 |
588 |
589 | Sample App
590 |
591 | This is the home page for the
592 | Ruby on Rails Tutorial
593 | sample application.
594 |
595 |
596 |
597 | ```
598 | 对应的网页显示如图3.6
599 | 
600 |
601 | 参考这个模型,把帮助页面(代码清单3.10)和关于页面(代码清单3.19)改为代码清单3.25和代码清单3.26.
602 |
603 | ```
604 | 代码清单 3.25: 完整的Help页面HTML文件。 红色
605 | # app/views/static_pages/help.html.erb
606 |
607 |
608 |
609 | Help | Ruby on Rails Tutorial Sample App
610 |
611 |
612 | Help
613 |
614 | Get help on the Ruby on Rails Tutorial at the
615 | Rails Tutorial help
616 | section.
617 | To get help on this sample app, see the
618 | Ruby on Rails
619 | Tutorial book.
620 |
621 |
622 |
623 | ```
624 | ```
625 | 代码清单 3.26: 完整的About页面HTML文件。 绿色
626 | # app/views/static_pages/about.html.erb
627 |
628 |
629 |
630 | About | Ruby on Rails Tutorial Sample App
631 |
632 |
633 | About
634 |
635 | The Ruby on Rails
636 | Tutorial is a
637 | book and
638 | screencast series
639 | to teach web development with
640 | Ruby on Rails.
641 | This is the sample application for the tutorial.
642 |
643 |
644 |
645 | ```
646 | 到这一步,测试集应该变回绿色:
647 | ```
648 | 代码清单 3.27: 绿色
649 | $ bundle exec rails test
650 | 3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
651 | ```
652 | ###3.4.3 布局和内嵌Ruby(重构)
653 | 在这节,我们已经取得了许多成就,生成了三个使用Rails控制器和动作的有用的页面,但是他们是纯粹静态的HTML,因此没有显示出Rails得强大。而且,它们三个页面重复的可怕:
654 | * 页面标题几乎一模一样
655 | * “Ruby on Rails Tutorial Sample App"在三个标题里都有
656 | * 整个HTML框架在每个页面都重复了
657 |
658 | 这些重复的代码打破了“不要重复自己”(DRY,英语“Don't Repeat Yourself”的缩写)原则;在这节,我们将移除重复的代码,让我们的代码遵循DRY原则。最后,我们从3.4.2节开始重新运行测试确认标题正确。
659 |
660 | 矛盾地,我们先通过增加一些代码来消除重复:我们将创建页面标题,和当前的非常像是,完全匹配。这将使得一下子移除所有重复的代码变得更加容易。
661 |
662 | 这个技术将会在视图内使用内嵌的Ruby。因为主页、帮助页面、关于页面的标题有一个可变的部分,我们将使用一个Rails特有的函数,叫做**provide**来在每个页面上设置不同的标题。我们看看文件**home.html.erb**里的代码(代码清单3.28),通过替换**home.html.erb**视图文件中的“Home”实现了。
663 | ```
664 | 代码清单 3.28: 带嵌入代码的Home页面视图。绿色
665 | # # app/views/static_pages/home.html.erb
666 | <% provide(:title, "Home") %>
667 |
668 |
669 |
670 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
671 |
672 |
673 | Sample App
674 |
675 | This is the home page for the
676 | Ruby on Rails Tutorial
677 | sample application.
678 |
679 |
680 |
681 | ```
682 |
683 | 代码清单3.28是我们的第一个内嵌Ruby的例子,也叫ERB。(现在你知道为什么HTML视图文件的后缀是**.html.erb**.)ERb是网页中包含动态内容主要的模板系统。代码
684 | ```
685 | <% provide(:title, "Home") %>
686 | 使用<% ... %>Rails会调用**provide**函数,将字符串“Home”和标签**:title**联系在一起。然后,在标题中,我们使用类似的相关的标记<%= ... %>使用Ruby's**yield**函数插入模板中的标题:
687 |
688 | ```erb
689 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
690 | ```
691 | (两个内嵌Ruby标记的区别是<% ... %>执行里面的代码, 然而<%= ...
692 | %>执行代码,然后将结果插入模板。)页面的显示和之前的一模一样,只是现在标题可变的部分是由ERB动态生成的。
693 |
694 | 我们可以通过运行3.4.2节中的测试,确认所有的这些仍是绿色的:
695 |
696 | ```
697 | 代码清单 3.29: 绿色
698 | $ bundle exec rails test
699 | 3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
700 | ```
701 |
702 | 然后我们替换帮助页面和关于页面相应的代码(代码清单3.30和3.31)。
703 |
704 | ```
705 | 代码清单 3.30: 嵌入Ruby代码的Help页面。绿色
706 | # # app/views/static_pages/help.html.erb
707 | <% provide(:title, "Help") %>
708 |
709 |
710 |
711 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
712 |
713 |
714 | Help
715 |
716 | Get help on the Ruby on Rails Tutorial at the
717 | Rails Tutorial help
718 | section.
719 | To get help on this sample app, see the
720 | Ruby on Rails
721 | Tutorial book.
722 |
723 |
724 |
725 | ```
726 |
727 | ```
728 | 代码清单 3.31: 嵌入Ruby代码的About页面。绿色
729 | # # app/views/static_pages/about.html.erb
730 | <% provide(:title, "About") %>
731 |
732 |
733 |
734 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
735 |
736 |
737 | About
738 |
739 | The Ruby on Rails
740 | Tutorial is a
741 | book and
742 | screencast series
743 | to teach web development with
744 | Ruby on Rails.
745 | This is the sample application for the tutorial.
746 |
747 |
748 |
749 | ```
750 | 既然我们已经用ERB替换了页面标题的可变部分,我们每个页面看起来像:
751 | ```
752 | <% provide(:title, "The Title") %>
753 |
754 |
755 |
756 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
757 |
758 |
759 | Contents
760 |
761 |
762 | ```
763 | 换句话说,所有页面在结构上是相同的,包括标题标签的内容,除了**body**标签里的内容不同。
764 |
765 | 为了提炼出一致的结构,Rails自带一个特殊的布局文件,**application.html.erb**,我们在这节(3.4节)开始的时候重命名的那个文件,现在我们还原回去:
766 | ```terminal
767 | $ mv layout_file # app/views/layouts/application.html.erb
768 | ```
769 |
770 | 为了让布局工作,我们不得不用内嵌Ruby代码代替默认的标题:
771 | ```ruby
772 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
773 | ```
774 |
775 | 修改后的布局文件如代码清单3.32所示。
776 | ```
777 | 代码清单 3.32: 示例网站的布局(layout)文件。绿色
778 | # # app/views/layouts/application.html.erb
779 |
780 |
781 |
782 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
783 | <%= stylesheet_link_tag 'application', media: 'all',
784 | 'data-turbolinks-track' => true %>
785 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
786 | <%= csrf_meta_tags %>
787 |
788 |
789 | <%= yield %>
790 |
791 |
792 | ```
793 | 注意这里特殊的一行
794 | ```ruby
795 | <%= yield %>
796 | ```
797 | 这行代码是为了把每页的内容插入布局文件。现在想要确切地知道这是怎么回事还不重要;重要的是使用这个布局文件,确保,例如,访问网页/static_pages/home将**home.html.erb**转换成HTML文件,然后替换<%= yield %>。
798 |
799 | 默认的Rails布局文件也包含没有什么使用价值的几个文件:
800 |
801 | ```html
802 | <%= stylesheet_link_tag ... %>
803 | <%= javascript_include_tag "application", ... %>
804 | <%= csrf_meta_tags %>
805 | ```
806 |
807 | 这些代码用来将应用程序中的样式文件(CSS)和Javascript文件包含进来,它们是asset
808 | pipeline(5.2.1节)的一部分,**csrf_meta_tags**方法是防止[跨页面劫持](http://en.wikipedia.org/wiki/Cross-site_request_forgery)(CSRF),一种恶意的网络攻击。
809 |
810 | 当然,代码清单3.28、3.30和3.31中的视图现在还都是包含布局的HTML结构,所以我们不得不移除它,仅仅留下内部的内容。清理后的视图如代码清单3.33,3.34和3.35一样。
811 | ```erb
812 | 代码清单 3.33: 移除HTML结构的Home视图。 绿色
813 | # app/views/static_pages/home.html.erb
814 | <% provide(:title, "Home") %>
815 | Sample App
816 |
817 | This is the home page for the
818 | Ruby on Rails Tutorial
819 | sample application.
820 |
821 | ```
822 | ```erb
823 | 代码清单 3.34: 移除HTML结构的Help视图。 绿色
824 | # app/views/static_pages/help.html.erb
825 | <% provide(:title, "Help") %>
826 | Help
827 |
828 | Get help on the Ruby on Rails Tutorial at the
829 | Rails Tutorial help section.
830 | To get help on this sample app, see the
831 | Ruby on Rails Tutorial
832 | book.
833 |
834 | ```
835 |
836 | ```
837 | 代码清单 3.35: 移除HTML结构的About视图。 绿色
838 | # app/views/static_pages/about.html.erb
839 | <% provide(:title, "About") %>
840 | About
841 |
842 | The Ruby on Rails
843 | Tutorial is a
844 | book and
845 | screencast series
846 | to teach web development with
847 | Ruby on Rails.
848 | This is the sample application for the tutorial.
849 |
850 | ```
851 | 随着这些视图修改完毕,主页、帮助页面和关于页面和之前的一样,但是只有少量的代码是重复的。
852 |
853 | 经验显示即使相当简单的重构也容易犯错,也能轻易地偏离正确方向。这是为什么一个好的测试集是非常有价值的原因之一。不断重复地检查每个页面--早期还不难,但是随着项目的快速增长就会变得不好操作--我们可以简单的通过测试集确认仍然是绿色的:
854 | ```
855 | 代码清单 3.36: 绿色
856 | $ bundle exec rails test
857 | 3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
858 | ```
859 |
860 | 这不能证明我们的代码百分百正确,但是它大大地增加了它正确的可能性。因此它为我们提供了一个安全网,保护我们未来远离bug。
861 |
862 | ### 3.4.4 设置根路由
863 | 既然我们已经自定义了我们网站的页面,在测试集方面有了一个好的开始,在我们继续之前,让我们设置一下应用程序的根路由。正如1.3.4节和2.2.2节中描述的那样,需要修改**routes.rb**文件,连接到我们选择的页面,这里我们选择主页。(到这步,我也推荐从AppicationController中移除**hello**动作,假如你在章节3.1添加过的话)如同代码清单3.37,这意味着用以下代码替换代码清单3.5生成的**get**规则:
864 | ```
865 | root 'static_pages#home'
866 | ```
867 |
868 | 这改变了URL**static_pages/home**到“控制/动作对”static_pages#home的路由,确保了对“/”的GET请求路由到静态页面控制器的**home**动作。路由文件最后的结果如图3.7.(记住,代码清单3.37里,先前的路由**static_pages/home**将不再工作)
869 | ```
870 | 代码清单 3.37: 把根路由映射到static_pages/home页面。
871 | # config/routes.rb
872 | Rails.application.routes.draw do
873 | root 'static_pages#home'
874 | get 'static_pages/help'
875 | get 'static_pages/about'
876 | end
877 | ```
878 |
879 | 
880 |
881 | ## 3.5 结论
882 | 从浏览器的效果来看,这章几乎没有完成任何事情:我们从静态页面开始,然后用大部分都是静态的页面结束。但是表面是具有欺骗性的:通过用Rails控制器、动作和视图,该是我们添加任意数量的动态内容到我们网站的时候了。逐渐揭开这些神秘面纱,是本教程剩余章节的任务。
883 |
884 | 在我们继续前,让我们花几分钟提交我们主题分支的变化,然后把它们合并到主分支。回到3.2节中,为了创建静态页面,我们创建了一个Git分支。假如在我们一直开发的过程当中你没有提交,先提交一下,表示我们到达了一个停止点:
885 |
886 | ```
887 | $ git add -A
888 | $ git commit -m "Finish static pages"
889 | ```
890 |
891 | 然后使用和1.4.4节一样的技术,把变化合并到主分支。
892 |
893 | ```
894 | $ git checkout master
895 | $ git merge static-pages
896 | ```
897 |
898 | 一旦你到达了像这样的一个停止点,推送你的代码到远程仓库通常是一个好的主意(假如你按照1.4.3节的步骤,就是指Bitbucket):
899 | ```
900 | $ git push
901 | ```
902 |
903 | 我也推荐你将程序部署至Heroku:
904 |
905 | ```
906 | $ bundle exec rails test
907 | $ git push heroku
908 | ```
909 |
910 | 这里我们要在部署前先完整运行一下我们的测试集,确保它完全通过,这也是开发的一个好习惯。
911 |
912 | ### 3.5.1 本章我们学到了什么
913 | * 第三次,我们经历了从零开始创建了一个新的Rails应用,安装了必要的gem包,推送到远程仓库,最后部署到生产。
914 | * **rails**脚本使用**rails generate controller 控制器名称 <可选的动作名称参数>**生成了一个新的控制器。
915 | * 在文件**config/routes.rb**里定义了一个新路由。
916 | * Rails视图可以包含静态HTML或者内嵌Ruby(ERb)
917 | * 自动测试允许我们写测试集,驱动新特性的开发,允许有信心的重构,捕捉回溯。
918 | * 使用“红色,绿色,重构”循环测试驱动开发。
919 | * 因为Rails布局允许我们在应用程序里使用普通模板,所以消灭了重复。
920 |
921 | ## 3.6 练习
922 | 说明:练习答案手册,包括本书中每个练习的解决方案,购买本书可以免费从[www.railstutorial.org]网站获取。
923 |
924 | 从现在开始直到本教程结束,我推荐在一个单独的主题分支解决练习:
925 | ```
926 | $ git checkout static-pages
927 | $ git checkout -b static-pages-exercises
928 | ```
929 | 这样就会防止和教程出现冲突。
930 |
931 | 一旦你对自己的解决方案感到满意,你可以把练习的主题分支推送到远程仓库(假如你已经设置了):
932 |
933 | ```terminal
934 | <解决第一个练习>
935 | $ git commit -am "取消重复(解决练习3.1)"
936 | <解决第二个练习>
937 | $ git add -A
938 | $ git commit -m "添加一个联系页面(解决练习3.2)"
939 | $ git push -u origin static-pages-exercises
940 | $ git checkout master
941 | ```
942 | (作为为将来的开发准备,这里最后一步检出主分支,但是我们为了避免和教程剩余章节冲突,我们不会把变化合并至主分支。)在以后的章节中,分支和提交信息将会有所不同,当然,但是基本思路是一样的,
943 | 1. 你可能已经注意到在静态页面控制器测试(代码清单3.22)里有一些重复。具体来说,基本的标题,“Ruby
944 | on Rails Tutorial Sample App”,每个测试里有一样。使用特殊的函数**setup**,这个会在每次测试之前都自动运行,确认代码清单3.38里的测试仍是绿色的。(代码清单3.38使用一个*实例变量*,在2.2.2节中快速的看见过,在4.4.5节里会覆盖到,和插入字符串一起,都将在4.2.2节中介绍)
945 |
946 | 2. 为示例程序添加一个联系页面。模仿代码清单3.13,先写为了网页访问URL /static_pages/contact通过测试写一个测试标题“Contact | Ruby on Rails Tutorial Sample App”的测试。通过和在代码清单3.3.3中创建关于页面一样的步骤,包括添加代码清单3.39里的内容。(注意,为了保持练习独立,代码清单3.39不会合并代码清单3.38里的代码。)
947 |
948 | ```
949 | 代码清单 3.38: 有基本标题的静态页面控制器测试。 绿色
950 | # test/controllers/static_pages_controller_test.rb
951 | require 'test_helper'
952 |
953 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest
954 |
955 | def setup
956 | @base_title = "Ruby on Rails Tutorial Sample App"
957 | end
958 |
959 | test "should get home" do
960 | get static_pages_home_url
961 | assert_response :success
962 | assert_select "title", "Home | #{@base_title}"
963 | end
964 |
965 | test "should get help" do
966 | get static_pages_help_url
967 | assert_response :success
968 | assert_select "title", "Help | #{@base_title}"
969 | end
970 |
971 | test "should get about" do
972 | get static_pages_about_url
973 | assert_response :success
974 | assert_select "title", "About | #{@base_title}"
975 | end
976 | end
977 | ```
978 |
979 | ```
980 | 代码清单 3.39:Contact页面的内容。
981 | # app/views/static_pages/contact.html.erb
982 | <% provide(:title, "Contact") %>
983 | Contact
984 |
985 | Contact the Ruby on Rails Tutorial about the sample app at the
986 | contact page.
987 |
988 | ```
989 |
990 | ## 3.7 高级测试安装
991 | 这节是选修的,描述在本书配套的视频资源里使用的测试环境的安装。有三个注意的要素:一个是加强的通过/失败报告者(3.7.1节),还有一个是过滤失败测试回溯的工具(3.7.2节),和一个自动化测试运行器,监测文件变化,自动运行相应的测试(3.7.3节)。这节中的代码比较高级,仅仅是为了方便才在这里出现,目前还不要求你能理解他。
992 |
993 | **这节里的变化,应合并进主分支。**
994 | ```
995 | $ git checkout master
996 | ```
997 | ### 3.7.1 迷你测试报告者
998 | 为了默认的Rails测试在合适的时候显示红色和绿色,我推荐把代码清单3.40的代码加到你的测试帮助文件,从而可以使用**[minitest-reporters](https://github.com/kern/minitest-reporters)**gem。
999 |
1000 | ```
1001 | 代码清单 3.40: Configuring the tests to show 红色 and 绿色.
1002 | # test/test_helper.rb
1003 | ENV['RAILS_ENV'] ||= 'test'
1004 | require File.expand_path('../../config/environment', __FILE__)
1005 | require 'rails/test_help'
1006 | require "minitest/reporters"
1007 | Minitest::Reporters.use!
1008 |
1009 | class ActiveSupport::TestCase
1010 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical
1011 | # order.
1012 | fixtures :all
1013 |
1014 | # Add more helper methods to be used by all tests here...
1015 | end
1016 | ```
1017 | 如在云IDE里的显示(图3.8),出现了从红色到绿色的变化。
1018 |
1019 | 
1020 |
1021 | ### 3.7.2 回溯静默
1022 | 当遇见错误或者失败测试,测试运行者显示“堆栈追踪”或“追踪回溯”,从应用程序里追踪失败的原因。这个追踪回溯对于查找问题来说非常有用,在某些系统(包括云IDE)会通过应用程序代码进入各种gem依赖,包括Rails自己。回溯的结果常常太长,尤其因为问题原因通常是由程序的代码引起,而不是它的依赖引起的时候。
1023 |
1024 | 解决方案是过滤追踪信息消除不想要的信息。这需要**[mini_backtrace](https://github.com/metaskills/mini_backtrace)**,包含在代码清单3.2里,和*backtrace silencer*一起使用。在云IDE,多数不需要的信息包含字符串*rvm*(Ruby版本管理器),所以我推荐如代码清单3.41里显示的静默者把它们过滤掉。
1025 |
1026 | ```
1027 | 代码清单 3.41: 为RVM添加backtrace silencer。
1028 | # config/initializers/backtrace_silencers.rb
1029 | # Be sure to restart your server when you modify this file.
1030 |
1031 | # You can add backtrace silencers for libraries that you're using but don't
1032 | # wish to see in your backtraces.
1033 | Rails.backtrace_cleaner.add_silencer { |line| line =~ /rvm/ }
1034 |
1035 | # You can also remove all the silencers if you're trying to debug a problem
1036 | # that might stem from framework code.
1037 | # Rails.backtrace_cleaner.remove_silencers!
1038 | ```
1039 |
1040 | 如同在代码清单3.41里的一句评论显示,你应该在添加了静默者之后重启本地服务器。
1041 |
1042 | ### 3.7.3 使用Guard自动测试
1043 | 使用**rails test**命令有一个令人心烦的方面就是不得不切换到命令行,然后手动运行测试。为了避免这种不方便,我们可以使用Guard来自动运行测试。Guard监视文件系统的变化,例如,当我们改变了文件**static_pages_controller_test.rb**,仅仅这些测试运行。甚至更好,我们可以配置以便当**home.html.erb**改变了,**static_pages_controller_test.rb**自动运行。
1044 |
1045 | 在代码清单3.2的Gemfile里一句包含了*guard* gem, 所以我们只需要初始化它:
1046 | ```
1047 | $ bundle exec guard init
1048 | Writing new Guardfile to /home/ubuntu/workspace/sample_app/Guardfile
1049 | 00:51:32 - INFO - minitest guard added to Guardfile, feel free to edit it
1050 | ```
1051 |
1052 | 然后我们编辑**Guardfile**以便Guard当集成测试和视图更新时运行正确的测试(代码清单3.42)。(由于文件较长,而且属于高级特性,我推荐拷贝黏贴就好了)
1053 |
1054 | ```
1055 | 代码清单 3.42: A custom Guardfile.
1056 | # Defines the matching rules for Guard.
1057 | guard :minitest, spring: true, all_on_start: false do
1058 | watch(%r{^test/(.*)/?(.*)_test\.rb$})
1059 | watch('test/test_helper.rb') { 'test' }
1060 | watch('config/routes.rb') { integration_tests }
1061 | watch(%r{^app/models/(.*?)\.rb$}) do |matches|
1062 | "test/models/#{matches[1]}_test.rb"
1063 | end
1064 | watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|
1065 | resource_tests(matches[1])
1066 | end
1067 | watch(%r{^# app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
1068 | ["test/controllers/#{matches[1]}_controller_test.rb"] +
1069 | integration_tests(matches[1])
1070 | end
1071 | watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
1072 | integration_tests(matches[1])
1073 | end
1074 | watch('app/views/layouts/application.html.erb') do
1075 | 'test/integration/site_layout_test.rb'
1076 | end
1077 | watch('app/helpers/sessions_helper.rb') do
1078 | integration_tests << 'test/helpers/sessions_helper_test.rb'
1079 | end
1080 | watch('app/controllers/sessions_controller.rb') do
1081 | ['test/controllers/sessions_controller_test.rb',
1082 | 'test/integration/users_login_test.rb']
1083 | end
1084 | watch('app/controllers/account_activations_controller.rb') do
1085 | 'test/integration/users_signup_test.rb'
1086 | end
1087 | watch(%r{# app/views/users/*}) do
1088 | resource_tests('users') +
1089 | ['test/integration/microposts_interface_test.rb']
1090 | end
1091 | end
1092 |
1093 | # Returns the integration tests corresponding to the given resource.
1094 | def integration_tests(resource = :all)
1095 | if resource == :all
1096 | Dir["test/integration/*"]
1097 | else
1098 | Dir["test/integration/#{resource}_*.rb"]
1099 | end
1100 | end
1101 |
1102 | # Returns the controller tests corresponding to the given resource.
1103 | def controller_test(resource)
1104 | "test/controllers/#{resource}_controller_test.rb"
1105 | end
1106 |
1107 | # Returns all tests for the given resource.
1108 | def resource_tests(resource)
1109 | integration_tests(resource) << controller_test(resource)
1110 | end
1111 | ```
1112 | 这里
1113 | ```
1114 | guard :minitest, spring:true, all_on_start:false do
1115 | ```
1116 | 让Guard使用Rails提供的Spring服务加快加载时间,然而也阻止Guard重新开始运行完整测试
1117 | 为了防止Spring和Git使用Guard引起的冲突,你应该将**spring/目录加入到**.gitignore**中,当Git添加文件s时忽略它们。在云IDE里:
1118 | 1.点击右边的齿轮(图3.9)
1119 | 2.选择“显示隐藏文件在应用程序的根目录”显示**.gitignore**文件(图3.10)
1120 | 3.双击**.gitignore**文件(图3.11)来打开它,然后加入代码清单3.43的内容。
1121 |
1122 | 
1123 | 
1124 | 
1125 | ```
1126 | 代码清单 3.43: 把Spring添加到.gitignore文件中。
1127 | # See https://help.github.com/articles/ignoring-files for more about ignoring
1128 | # files.
1129 | #
1130 | # If you find yourself ignoring temporary files generated by your text editor
1131 | # or operating system, you probably want to add a global ignore instead:
1132 | # git config --global core.excludesfile '~/.gitignore_global'
1133 |
1134 | # Ignore bundler config.
1135 | /.bundle
1136 |
1137 | # Ignore the default SQLite database.
1138 | /db/*.sqlite3
1139 | /db/*.sqlite3-journƒal
1140 |
1141 | # Ignore all logfiles and tempfiles.
1142 | /log/*.log
1143 | /tmp
1144 |
1145 | # Ignore Spring files.
1146 | /spring/*.pid
1147 | ```
1148 | Spring服务仍然还有点难搞,有时Spring进场将累积,拖慢你的测试的表现。假如你的测试变的不同寻常的慢,检查系统进程,假如需要的话杀死进程(注:3.4)。
1149 |
1150 | 注3.4 Unix进程
1151 | 在类Unix系统,例如Linux和OS X,用户和系统任务都在一个定义的很好的,叫做进程的容器里运行。为了看见你的系统所有进程,你可以使用**ps**命令和**aux**参数:
1152 | $ ps aux
1153 | 为了依据类型过滤进程,你可以运行**ps**通过**grep**模式匹配器使用Unix管道|:
1154 | $ ps aux | grep spring
1155 | ubuntu 12241 0.3 0.5 589960 178416 ? Ssl Sep20 1:46
1156 | spring app | sample_app | started 7 hours ago
1157 | 结果显示进程的细节,但是最重要事情是第一个数字,它是进程ID,或者pid。为了消除不想要的进程,使用kill命令发送Unix杀死代码(正好是9)到那个pid:
1158 | ```
1159 | $ kill -9 12241
1160 | ```
1161 | 这是我推荐的杀死单个进程的技术,例如缓慢的Rails服务器(通过命令*ps aux | grep
1162 | server),但是有时杀死匹配某个模式的所有进程,例如当你想用杀死所有的*spring*进程。在这种情况,你应该首先试试用spring命令终止:
1163 | ```
1164 | $ sping stop
1165 | ```
1166 | 有时这不起作用,尽管,你能用以下命令啥事所有名为spring的进程:
1167 | ```
1168 | $ pkill -9 -f spring
1169 | ```
1170 | 任何时候有些进程行为和预料的不一样,或者冻结了,运行ps
1171 | aux命令看看什么情况,然后运行kill -9 或者pkill -9 -f <名字>清洁进程。
1172 |
1173 | 一旦Guard配置好,你应该新开一个终端(如果1.3.2节中介绍的一样)运行以下命令:
1174 |
1175 | ```
1176 | $ bundle exec guard
1177 | ```
1178 |
1179 | 这个代码清单3.42的规则是为本教程优化,当控制器的代码改变后自动运行(例如)集成测试。为了运行所有测试,在**guard>**点击回车键。(有时可能会给出一个和Spring相关的错误提示。解决这个问题,就是再点击一次回车键)
1180 |
1181 | 按Ctrl-D退出Guard。为了给Guard添加额外的匹配规则,参考代码清单3.42的例子,Guard的[读我](https://github.com/guard/guard),和Guard的[wiki](https://github.com/guard/guard/wiki).
1182 |
1183 |
--------------------------------------------------------------------------------
/chapter4_rails_flavored_ruby.md:
--------------------------------------------------------------------------------
1 | # 第四章 Rails风味的Ruby
2 | 经过第三章中示例程序的练习,这章我们将深入探索Ruby编程语言中的一些对Rails来说比较重要的内容。Ruby是一个块头较大得语言,但是幸运的是成为一个有效的Rails程序员需要的Ruby知识的子集相对比较小。和其他通常介绍Ruby的材料有些不同,这章的目的是不管你之前有没有相关经验,都会让你在Rails风味的Ruby知识方面打下牢固的基础。它覆盖了许多知识,如果第一次不能掌握也没有关系,我会在未来的章节里经常引用。
3 |
4 | ## 4.1 动机
5 | 如同我们在上一章看到的一样,就算我们没有掌握必要的Ruby知识,开发一个Rails应用程序的骨架,甚至测试它都是可能的。我们是依赖教程和错误信息的提示做到了这些。可是这种情形不可能一直持续,从这章开始,我们将直接面对我们在Ruby知识方面的软肋。
6 |
7 | 看看我们之前新写的应用程序,只是使用Rails布局更新了我们以静态为主的页面,消除了我们视图里的重复。如代码清单4.1所示(和代码清单3.32一样)。
8 |
9 | ```erb
10 | 代码清单 4.1: sample_app网站布局文件。
11 | # app/views/layouts/application.html.erb
12 |
13 |
14 |
15 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
16 | <%= stylesheet_link_tag 'application', media: 'all',
17 | 'data-turbolinks-track' => true %>
18 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
19 | <%= csrf_meta_tags %>
20 |
21 |
22 | <%= yield %>
23 |
24 |
25 | ```
26 | 让我们把焦点放在代码清单4.1里面的一行:
27 | ```erb
28 | <%= stylesheet_link_tag 'application', media: 'all',
29 | 'data-turbolinks-track' => true %>
30 | ```
31 | 这里使用了Rails内建的函数**stylesheet_link_tag**(你可以在[Rails
32 | API](http://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-stylesheet_link_tag)了解更多)包含为所有的[媒体类型](http://www.w3.org/TR/CSS2/media.html)(包括计算机屏幕和打印机)的**application.css**文件。对于一个有经验的Rails开发者,这行看起来很简单,但是起码有四个关于Ruby的用法让你感到迷惑:内建的Rails方法、没有圆括号、符号和hash。我们将在这章覆盖所有的这些概念。
33 |
34 | 另外,在视图中也包含其他大量的内建函数,而且Rails也允许创建新的函数。这样的函数称为*helper*。为了学习怎样开发一个自己的helper,让我们通过代码清单4.1里的代码来开始实验:
35 |
36 | ```ruby
37 | <%= yield(:title) %> | Ruby on Rails Tutorial Sample App
38 | ```
39 | 上面的代码依赖于在每个视图里网页标题的定义(使用provide),和这里
40 | ```
41 | <% provide(:title, "Home") %>
42 | Sample App
43 |
44 | This is the home page for the
45 | Ruby on Rails Tutorial
46 | sample application.
47 |
48 | ```
49 | 但是假如我们不提供标题呢?好的编程实践是在每个页面有一个基础标题,假如我们想要更具体一点,可以再加一个可选的标题。我们几乎完成了之前的布局文件,不过有个小问题:如你所见,假如你删除了视图里的**provide**函数,缺少详细页面标题的完整标题就变成下面这样:
50 | ```
51 | | Ruby on Rails Tutorial Sample App
52 | ```
53 | 换句话说,有一个合适的基础标题,但是在开头多了个字符“|”。
54 |
55 | 为了解决网页标题的问题,我们定义一个名为**full_title**的helper。假如没有定义页面标题,**full_title**helper返回基础标题,假如定义了的话,返回加了“|”的页面标题(代码清单4.2)。
56 | ```ruby
57 | 代码清单 4.2: 定义full_title helper.
58 | # app/helpers/application_helper.rb
59 | module ApplicationHelper
60 |
61 | # Returns the full title on a per-page basis.
62 | def full_title(page_title = '')
63 | base_title = "Ruby on Rails Tutorial Sample App"
64 | if page_title.empty?
65 | base_title
66 | else
67 | page_title + " | " + base_title
68 | end
69 | end
70 | end
71 | ```
72 |
73 | 现在我们定义了一个helper,我们可以用
74 | ```
75 | <%= full_title(yield(:title)) %>
76 | ```
77 | 替换
78 | <%= yield(:title) %> | Ruby on Rails Tutorial Samlple App
79 | 如代码清单4.3所见。
80 |
81 | ```
82 | 代码清单 4.3: 使用full_title helper的网站布局文件。绿色
83 | # app/views/layouts/application.html.erb
84 |
85 |
86 |
87 | <%= full_title(yield(:title)) %>
88 | <%= stylesheet_link_tag 'application', media: 'all',
89 | 'data-turbolinks-track' => true %>
90 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
91 | <%= csrf_meta_tags %>
92 |
93 |
94 | <%= yield %>
95 |
96 |
97 | ```
98 | 为了让我们的helper正常工作,我们可以从主页消除不必要的单词“Home”,允许它使用基础标题。我们首先用代码清单4.4的代码更新一下我们的测试:更新了先前的标题测试和增加了一条标题里缺少“home”字符串的测试。
99 | ```
100 | 代码清单 4.4: 更新测试主页标题的测试。红色
101 | # test/controllers/static_pages_controller_test.rb
102 | require 'test_helper'
103 |
104 | class StaticPagesControllerTest < ActionController::TestCase
105 | test "should get home" do
106 | get :home
107 | assert_response :success
108 | assert_select "title", "Ruby on Rails Tutorial Sample App"
109 | end
110 |
111 | test "should get help" do
112 | get :help
113 | assert_response :success
114 | assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
115 | end
116 |
117 | test "should get about" do
118 | get :about
119 | assert_response :success
120 | assert_select "title", "About | Ruby on Rails Tutorial Sample App"
121 | end
122 | end
123 | ```
124 | 让我们运行测试集来确认有一个测试失败:
125 | ```
126 | 代码清单 4.5: 红色
127 | $ bundle exec rails test
128 | 3 tests, 6 assertions, 1 failures, 0 errors, 0 skips
129 | ```
130 | 为了让测试集通过,我们先从主页视图移除**provide**行,如代码清单4.6所见。
131 | ```
132 | 代码清单 4.6: The Home page with no custom page title. 绿色
133 | # app/views/static_pages/home.html.erb
134 | Sample App
135 |
136 | This is the home page for the
137 | Ruby on Rails Tutorial
138 | sample application.
139 |
140 | ```
141 | 现在测试应该通过:
142 | ```
143 | 代码清单 4.7: 绿色
144 | $ bundle exec rails test
145 | ```
146 | (说明:先前的例子已经包含了运行**rails test**的部分输出,包括通过和失败测试的数量,但是为了简便起见,这些内容从这里开始就都忽略了。)
147 |
148 | 因为布局文件通过一行代码来引入了应用程序的样式表(CSS文件),代码清单4.2的代码可能对于有经验的Rails开发者来说很简单,但是它充满了重要的Ruby语法:模块、方法定义、可选的方法参数、注释、本地变量分配、布尔型、控制流、字符串连接以及返回值。这章也将覆盖所有这些内容。
149 |
150 | ## 4.2 字符串和方法
151 | 我们学习Ruby的主要工具是Rails控制台,如第一次在2.3.3节里看见的一样,用来和Rails应用程序交互的命令行工具。控制台是建立在交互Ruby(irb)基础之上的,因此可以使用Ruby语言的全部功能。(正如我们在4.4.4节中所见一样,控制台也可以进入Rails环境。)
152 |
153 | 假如你正使用云IDE,我建议你包含几个irb配置参数。使用简单的nano文本编辑器,在用户目录下创建.irbrc的文件,写入代码清单4.8里面的内容:
154 | ```
155 | $ namo ~/.irbrc
156 | ```
157 | 代码清单4.8里的内容的作用是简化irb提示,压制一些烦人的自动缩进行为。
158 | ```
159 | 代码清单 4.8: Adding some irb configuration.
160 | ~/.irbrc
161 | IRB.conf[:PROMPT_MODE] = :SIMPLE
162 | IRB.conf[:AUTO_INDENT_MODE] = false
163 | ```
164 |
165 | 无论你是否包含了代码清单4.8里面的配置,你都可以在命令行启动控制台:
166 | ```
167 | $ rails console
168 | >>
169 | ```
170 | 默认情况下,控制台开启的是开发环境,那是Rails定义的三个独立的环境之一(其他环境是测试和生产环境)。这点在本章来说不重要,我们将在7.1.1节学到更多的环境。
171 |
172 | 控制台是一个伟大的学习工具,你可以自由地开发它的潜力--别担心,你(可能)不会破坏任何东西。当你使用控制台时,假如你被卡主了,按Ctrl-C退出程序、或者Ctrl-D退出控制台。在本章剩余部分,你可能会发现查看Ruby API是很有帮助的,它包含了丰富的信息(可能过于丰富了)。例如,为了学习更多的关于Ruby字符串的之后,你可以看看Ruby API中字符串类一节。
173 |
174 | ### 4.2.1 注释
175 | Ruby注释前有个井号#(也叫“哈希标志”或者其他的),扩展到行末。Ruby忽略了注释,但是它对阅读代码者(也包括作者本人)有帮助。在代码里
176 | ```
177 | # 返回在页面基本标题的基础上返回完整的标题
178 | def full_title(page_title='')
179 | .
180 | .
181 | .
182 | end
183 | ```
184 | 第一行是注释,表明了后面的函数的功能。
185 |
186 | 你一般不用在使用控制台写注释,但是为了指导学习,我会在后面的代码里包含注释,像这样:
187 | ```
188 | $ rails console
189 | >> 17 + 42 #整数相加
190 | => 59
191 | ```
192 |
193 | 假如你跟随这节输入或者复制黏贴命令到自己的控制台,如果你想忽略注释的话,当然可以,反正控制台会忽略它们的。
194 |
195 | ### 4.2.2 字符串
196 | 字符串可能是网页应用程序里最重要的数据结构,因为网页基本上是由服务器发送至浏览的字符串组成。让我们开始在控制台探索字符串吧:
197 |
198 | ```ruby
199 | $ rails console
200 | >>"" #空字符串
201 | =>""
202 | >>"foo" #非空字符串
203 | =>"foo"
204 | ```
205 |
206 | 这些是一串字符(有趣的是,叫字符串),使用双引号"创建(译者注:输入时注意,是英文双引号,不是中文双引号)。控制台输出每行的值,在这里一串字符正是字符串本身。
207 |
208 | 我们也可以使用操作符+来连接字符串:
209 | ```
210 | >>"foo" + "bar" #字符串连接
211 | =>"foobar"
212 | ```
213 |
214 | 这里输出的是"foo"加"bar"的结果字符串"foobar"。
215 | 另一个方法是通过插值使用特殊的语法“#{}”:
216 | ```
217 | >> first_name="Michael" #变量赋值
218 | =>“Michael”
219 | >>"#{first_name} Hartl" #字符串插值
220 | => "Michael Hartl"
221 | ```
222 |
223 | 这里我们给变量first_name分配值“Michael”,然后把它插入字符串“#{first_name}
224 | Hartl"。我们也可以把它们两个都分配个变量:
225 |
226 | ```
227 | >> first_name = "Michael"
228 | => "Michael"
229 | >> last_name = "Hartl"
230 | => "Hartl"
231 | >> first_name + " " + last_name # Concatenation, with a space in between
232 | => "Michael Hartl"
233 | >> "#{first_name} #{last_name}" # The equivalent interpolation
234 | => "Michael Hartl"
235 | ```
236 | 注意到两个表达式结果是相同的,但是我偏好插值版的;必须要加进一个空格好像有点尴尬。
237 |
238 | #### 输出
239 | 最常用的输出字符串的Ruby函数是puts(发音:“put ess”,输出字符串):
240 | ```
241 | >> puts "foo" # put string
242 | foo
243 | => nil
244 | ```
245 | puts方法有个副作用:表达式puts
246 | "foo"把字符串输出到屏幕,然后返回空:nil是Ruby中特殊的值,表示什么也没有。(在后面的章节,为了简便,我有时会压制=>nil部分)
247 |
248 | 就像你在上个例子中看见的一样,使用puts自动第添加新行字符\n到输出。相关的print方法不会:
249 | ```
250 | >> print "foo" # 打印字符串 (和puts一样,但是结尾不换行)
251 | foo=> nil
252 | >> print "foo\n" # 和puts "foo"一样
253 | foo
254 | => nil
255 | ```
256 | ### 单引号字符串
257 | 到目前为止,所有的例子使用的是双引号括起来的字符串,但是Ruby也支持单引号括起来的字符串。在大部分时候,两种符号实际上是一样的:
258 | ```
259 | >> 'foo' # 单引号字符串
260 | => "foo"
261 | >> 'foo' + 'bar'
262 | => "foobar"
263 | ```
264 | 不过有一个重要的不同,Ruby不会插入单引号字符串:
265 | ```
266 | >> '#{foo} bar' # 单引号字符串不允许插值
267 | => "\#{foo} bar"
268 | ```
269 | 注意控制台使用双引号返回值,但是在#{前使用转义字符反斜杠。
270 |
271 | 假如双引号字符串能做单引号字符串能做的任何事情、还可以进行插值运算,那么单引号字符串的意义是什么?因为它们是真正的字符,包含的恰好是你输入的字符串,这点常常很有用。例如,反斜杠字符在大多数系统都是特殊字符,如换行字符\n。假如你想用一个包含反斜杠字符的变量,单引号让这个变得很容易:
272 | ```
273 | >> '\n' # 文字版的'\n'
274 | => "\\n"
275 | ```
276 |
277 | 在我们先前的组合“#{}”的例子里,Ruby用一个额外的反斜杠转义反斜杠,在双引号字符串里,一个反斜杠字符要用两个反斜杠来表示。像这个小例子,没有省下多少力气,但是假如有许多反斜杠,那就真的有帮助了:
278 | ```
279 | >> 'Newlines (\n) and tabs (\t) both use the backslash character \.'
280 | => "Newlines (\\n) and tabs (\\t) both use the backslash character \\."
281 | ```
282 | 最后,值得一提的是,在普通的情况下,单引号和双引号实际上是可以互换的,你将经常在源代码里看见两者的使用没有任何模式。关于这个真的没什么要做的了,除了说,“欢迎来到Ruby王国!”
283 |
284 | ### 4.2.3 对象和消息传输
285 | 在Ruby里,所有的一切都是对象。包括字符串、甚至nil,都是对象。我们将在4.4.2节里看到这个技术的意义,但是我认为任何人都不能通过看书里的定义就能理解,通过前面很多例子,我相信你已经有了一个直观的感受。
286 |
287 | 描述对象做什么很容易,它是对消息作出回应。像字符串这样的对象,例如,可以回应消息length,它会返回字符串的长度:
288 |
289 | ```
290 | >> "foobar".length # 把"length"当做消息传递给“foobar”
291 | => 6
292 | ```
293 |
294 | 典型地,传递到对象的消息叫方法,是定义在那些对象上的函数。字符串也有empty?方法:
295 |
296 | ```
297 | >> "foobar".empty?
298 | => false
299 | >> "".empty?
300 | => true
301 | ```
302 | 注意empty?方法末尾的问号。这是Ruby惯例表示返回值是逻辑型:true和false。逻辑型在控制流程方面尤其有用:
303 |
304 | ```
305 | >> s = "foobar"
306 | >> if s.empty?
307 | >> "The string is empty"
308 | >> else
309 | >> "The string is nonempty"
310 | >> end
311 | => "The string is nonempty"
312 | ```
313 |
314 | 为了包含几个语句,我们可以使用`elsif`(else + if):
315 |
316 | ```
317 | >> if s.nil?
318 | >> "The variable is nil"
319 | >> elsif s.empty?
320 | >> "The string is empty"
321 | >> elsif s.include?("foo")
322 | >> "The string includes 'foo'"
323 | >> end
324 | => "The string includes 'foo'"
325 | ```
326 | 逻辑型也可以通过使用&&(和)、||(或)和!(非)操作符:
327 | ```
328 | >> x = "foo"
329 | => "foo"
330 | >> y = ""
331 | => ""
332 | >> puts "Both strings are empty" if x.empty? && y.empty?
333 | => nil
334 | >> puts "One of the strings is empty" if x.empty? || y.empty?
335 | "One of the strings is empty"
336 | => nil
337 | >> puts "x is not empty" if !x.empty?
338 | "x is not empty"
339 | => nil
340 | ```
341 | 因为在Ruby中所有的东西都是对象,所以nil也是对象,所以它也有方法。另外一个例子是to_s方法能将任何对象都转换成字符串:
342 | ```
343 | >> nil.to_s
344 | => ""
345 | ```
346 | 这肯定显示是一个空的字符串,我们可以通过方法链接来传递给nil的消息来确认:
347 | ```
348 | >> nil.empty?
349 | NoMethodError: undefined method `empty?' for nil:NilClass
350 | >> nil.to_s.empty? # 方法串联
351 | => true
352 | ```
353 |
354 | 我们在这看见nil对象对empty?方法没有响应,但是nil.to_s响应了。
355 | 有一个特殊的测试值是nil的方法,你可能猜到了:
356 | ```
357 | >> "foo".nil?
358 | => false
359 | >> "".nil?
360 | => false
361 | >> nil.nil?
362 | => true
363 | ```
364 | 代码
365 | ```
366 | puts "x is not empty" if x.empty?
367 | ```
368 | 也显示了使用if关键词的变化:Ruby允许你这样写:仅仅当声明后面的if语句是true时才执行前面的代码。有个互补的关键词unless,工作方法一样:
369 | ```
370 | >> string = "foobar"
371 | >> puts "The string '#{string}' is nonempty." unless string.empty?
372 | The string 'foobar' is nonempty.
373 | => nil
374 | ```
375 | nil对象是特殊的,因为它是Ruby中所有对象里除了false之外,唯一值为false的对象。我们可以看见这个用法**!!**(读“bang bang”),这个否定一个对象两次,强迫一个变量转换为逻辑型值:
376 | ```
377 | >> !!nil
378 | => false
379 |
380 | 也就是说,所有别的Ruby对象都是true,甚至0也是。
381 |
382 | ### 4.2.4 定义方法
383 |
384 | 控制台允许我们和代码清单3.6的**home**动作或代码清单4.2中**full_title**辅助方法一样定义方法。(在控制台里定义方法有点啰嗦,通常你会用文件,但是为了方便说明,我们这里用控制台)例如,让我们定义函数**string_message**,这个函数有一个参数,最后根据是否参数是空返回信息:
385 | ```ruby
386 | >> def string_message(str = '')
387 | >> if str.empty?
388 | >> "It's an empty string!"
389 | >> else
390 | >> "The string is nonempty."
391 | >> end
392 | >> end
393 | => :string_message
394 | >> puts string_message("foobar")
395 | The string is nonempty.
396 | >> puts string_message("")
397 | It's an empty string!
398 | >> puts string_message
399 | It's an empty string!
400 | ```
401 |
402 | 如在最后的例子里所见,也可以不传递任何参数(这时我们可以忽略括号)。这是因为代码:
403 | ```ruby
404 | def string_message(str = '')
405 | ```
406 | 包含一个默认的参数,空字符串。这使得**str**参数可选,假如我们没有传输参数,就会使用默认值。
407 |
408 | 注意Ruby函数有隐含的返回,也就是说它会返回最后一条语句的值--在这里是依据方法的参数**str**是空还是非空,返回两个字符串之一。Ruby也可以用明示的方式返回;下面的函数和上面的是一样的:
409 | ```ruby
410 | >> def string_message(str = '')
411 | >> return "It's an empty string!" if str.empty?
412 | >> return "The string is nonempty."
413 | >> end
414 | ```
415 | (聪明的读者可能已经注意到第二个**return**实际上是不必要的--作为函数的表达式,字符串“The
416 | string is nonempty.”不管有没有关键词**return**都会返回值,但是在两个地方都使用**return**看起来有种对称美。)
417 |
418 | 理解函数参数的名称是和调用者关心的不相干的也是重要的。换句话说,上面的第一个例子能用任何有效的变量名替换**str**,例如**the_function_argument**,和之前的行为是一模一样的。
419 | ```ruby
420 | >> def string_message(the_function_argument = '')
421 | >> if the_function_argument.empty?
422 | >> "It's an empty string!"
423 | >> else
424 | >> "The string is nonempty."
425 | >> end
426 | >> end
427 | => nil
428 | >> puts string_message("")
429 | It's an empty string!
430 | >> puts string_message("foobar")
431 | The string is nonempty.
432 | ```
433 |
434 | ### 4.2.5 回到标题辅助函数
435 | 我们现在到了理解代码清单4.2中**full_title**辅助函数的时候了,代码清单4.9中用注释说明了每行代码的意义。
436 | ```ruby
437 | 代码清单 4.9: 注释好的title_helper。
438 | # app/helpers/application_helper.rb
439 | module ApplicationHelper
440 |
441 | # 返回基于每页基本标题的全标题 # 文档注释
442 | def full_title(page_title = '') # 方法 def,可选参数
443 | base_title = "Ruby on Rails Tutorial Sample App" # 变量分配
444 | if page_title.empty? # 逻辑测试
445 | base_title # 隐含返回
446 | else
447 | page_title + " | " + base_title # 字符串连接
448 | end
449 | end
450 | end
451 | ```
452 |
453 | 这些要素--函数定义(带可选参数)、变量分配、逻辑测试、控制流和字符串连接--一起为我们网站的布局文件组成了紧凑的辅助方法。最后的要素是**module ApplicationHelper**:模块(module)是让我们把相关的方法打包在一起的一种方法,这些方法然后可以使用**include**混合进Ruby类中。当写Ruby程序时,你常常会先写模块,然后显示地把它们包含进来,但是在这个例子中,辅助方法模块Rails为我们处理了包含动作,所以辅助方法full_title**在我们的所有视图里自动可用了。
454 |
455 | ## 4.3 其他数据结构
456 | 尽管网页应用归根到底是字符串,实际上创建那些字符串也需要别的数据结构。在这一节,我们将学习一些对Rails应用程序来说相对重要的Ruby数据结构。
457 |
458 | ### 4.3.1 数组和范围
459 | 数组就是按照一定顺序排列的列表。我们还没有在本书中讨论数组,但是理解它们会为理解哈希(4.3.3节)奠定良好的基础,为Rails的数据模块化(例如在2.3.3节里看见的**has_many**关联和在11.1.3节里将会有更多讨论)。
460 |
461 | 到目前为止我们已经花了许多时间理解字符串,有一个名为**split**的方法可以自然地把字符串转化为数组:
462 | ```ruby
463 | >> "foo bar baz".split #把字符串分成包含三个元素的数组
464 | => ["foo", "bar", "baz"]
465 | ```
466 | 这个操作的结果是包含三个字符串的数组。默认地,**split**使用空格作为分隔符,把字符串分成数组,但是你也可以用几乎任何字符作为分隔符:
467 | ```ruby
468 | >> "fooxbarxbazx".split('x')
469 | => ["foo", "bar", "baz"]
470 | ```
471 | 和许多其他计算机语言遵循的惯例一样,Ruby数组从0开始编号,这意味着在数组里第一个元素的索引是0,第二个是1,以此类推:
472 | ```ruby
473 | >> a = [42, 8, 17]
474 | => [42, 8, 17]
475 | >> a[0] # Ruby使用方括号取数组元素
476 | => 42
477 | >> a[1]
478 | => 8
479 | >> a[2]
480 | => 17
481 | >> a[-1] # 索引甚至可以是负的!
482 | => 17
483 | ```
484 |
485 | 我们看见Ruby使用方括号读取数组元素。另外,Ruby也为方括号提供了对称的方法取元素:
486 | ```
487 | >> a # 回忆一下'a' 是什么
488 | => [42, 8, 17]
489 | >> a.first
490 | => 42
491 | >> a.second
492 | => 8
493 | >> a.last
494 | => 17
495 | >> a.last == a[-1] # 使用==比较
496 | => true
497 | ```
498 |
499 | 最后一行介绍了等于比较符号*==*,Ruby和其他语言一样,沿用**!=**表示不相等,等等:
500 | ```
501 | >> x = a.length
502 | => 3
503 | >> x == 3
504 | => true
505 | >> x ==1
506 | => false
507 | >> x != 1
508 | => true
509 | >> x >= 1
510 | => true
511 | >> x < 1
512 | => false
513 | ```
514 | 除了**length**(上面第一行),数组还有很多其他方法:
515 | ```ruby
516 | >> a
517 | => [42, 8, 17]
518 | >> a.empty?
519 | => false
520 | >> a.include?(42)
521 | => true
522 | >> a.sort
523 | [8, 17, 42]
524 | >> a.reverse
525 | => [17, 8 . 42]
526 | >> a.shuffle
527 | => [17, 42, 8] #这个结果是随机,如果与此不同请不要担心
528 | a
529 | => [42, 8, 17]
530 | ```
531 |
532 | 注意:上面的方法都没有改变a本身。为了改变数组,要使用带感叹号的方法(在这里,感叹号的发音是“bang”):
533 | ```ruby
534 | >> a
535 | => [42, 8, 17]
536 | >> a.sort!
537 | => [8, 17, 42]
538 | >> a
539 | => [8, 17, 42]
540 | ```
541 | 你也可以使用**push**方法或效果相同的操作符, <<:
542 | ```ruby
543 | >> a.push(6) # 把6放进数组
544 | => [42, 8, 17, 6]
545 | >> a << 7 # 把7放进数组
546 | => [42, 8, 17, 6, 7]
547 | >> a << "foo" << "bar" # 链式压进数组
548 | => [42, 8, 17, 6, 7, "foo", "bar"]
549 | ```
550 |
551 | 最后例子显示你可以串联一起,将元素压进数组,而且也不像别的语言里的数组,Ruby中得数组可以包含不同类型的元素(在这个例子里,整数和字符串)。
552 | 之前我们看到**split**将字符串转为数组。我们也可以使用**join**方法把数组变成字符串:
553 | ```
554 | >> a
555 | => [42, 8, 17, 7, "foo", "bar"]
556 | >> a.join #什么也不用合并
557 | => "428177foobar"
558 | >> a.join(',') #用逗号合并
559 | => "42, 8, 17, 7, foo, bar"
560 | ```
561 |
562 | 和数组相关的另一个种数据结构是范围(rang),通过使用**to_a**方法把范围转换为数组可能最容易让人理解:
563 | ```ruby
564 | >> 0..9
565 | => 0..9
566 | >> 0..9.to_a #糟糕,在9上使用to_a函数
567 | NoMethodError: undefined method `to_a' for 9:Fixnum #没有定义此方法
568 | >> (0..9).to_a # 使用圆括号在范围上调用to_a
569 | => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
570 | ```
571 | 尽管0..9是有效的范围,上面第二个表达式显示我们在调用方法的时候需要使用圆括号。
572 |
573 | 范围对于去除数组元素很有用:
574 | ```
575 | >> a = %w[foo bar baz quux] #使用%w来创建一个字符串数组
576 | => ["foo", "bar", "baz", "quux"]
577 | >> a[0..2]
578 | => ["foo", "bar", "baz"]
579 | ```
580 |
581 | 一个尤其有用的技巧是在范围的末尾使用索引-1来选择从开始到结尾的每个要素,不需要显示第使用数组长度:
582 | ```
583 | >> a = (0..9).to_a
584 | => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
585 | >> a[2..(a.length-1)] # 显示地使用数组长度
586 | => [2, 3, 4, 5, 6, 7, 8, 9]
587 | >> a[2..-1] # 使用技巧索引-1
588 | => [2, 3, 4, 5, 6, 7, 8, 9]
589 | ```
590 | 范围对字符也有效:
591 | ```
592 | >> ('a'..'e').to_a
593 | => ["a", "b", "c", "d", "e"]
594 | ```
595 |
596 | ### 4.3.2 块(block)
597 | 数组和范围有许多方法接受块作为参数,块几乎是Ruby最强大,最有迷惑性的特性:
598 | ```ruby
599 | >> (1..5).each { |i| puts 2 * i }
600 | 2
601 | 4
602 | 6
603 | 8
604 | 10
605 | => 1..5
606 | ```
607 | 这行代码在范围**(1..5)**上调用**each**方法,然后把它传递给块**{ |i| puts 2 * i }。在**|i|**里,变量名称两边的竖线是Ruby中块变量的语法,决定了块对方法做什么。
608 | 在这个例子里,范围的**each**方法可以使用一个局部变量处理块,我们定义为**i**,它为范围里的每个值执行块。
609 |
610 | 大括号是声明块的方法之一,还有另外一种方法:
611 | >> (1..5).each do |i|
612 | ?> puts 2 * i
613 | >> end
614 | 2
615 | 4
616 | 6
617 | 8
618 | 10
619 | => 1..5
620 | ```
621 | 块中的代码可以多余一行,事实上常常多于一行。在本书中,我们将遵循惯例,如果块仅有一行代码,就使用大括号{},否则就用**do..end**语法:
622 | ```
623 | >> (1..5).each do |number|
624 | ?> puts 2 * number
625 | >> puts '--'
626 | >> end
627 | 2
628 | --
629 | 4
630 | --
631 | 6
632 | --
633 | 8
634 | --
635 | 10
636 | --
637 | => 1..5
638 | ```
639 | 这里我使用**number**替换**i**是为了强调我们可以用任何变量名。
640 |
641 | 除非你有很深的编程方面的背景,否则理解块是没有捷径的;你不得不看许多它们的例子,最终你将学会使用它们。幸运的是,人们擅长归纳;这里有几个使用**map**方法的例子:
642 | ```ruby
643 | >> 3.times { puts "Betelgeuse!" } # 3.times以一个没有变量的块作为参数。
644 | "Betelgeuse!"
645 | "Betelgeuse!"
646 | "Betelgeuse!"
647 | => 3
648 | >> (1..5).map { |i| i**2 } # The ** notation is for 'power'.
649 | => [1, 4, 9, 16, 25]
650 | >> %w[a b c] # Recall that %w makes string arrays.
651 | => ["a", "b", "c"]
652 | >> %w[a b c].map { |char| char.upcase }
653 | => ["A", "B", "C"]
654 | >> %w[A B C].map { |char| char.downcase }
655 | => ["a", "b", "c"]
656 | ```
657 | 正如你所见,**map**方法返回数组或范围里的每个元素执行块中的代码后的值。最后两个例子里,**map**里的块对块变量调用一个特别的方法,在这里有一个常用的简写:
658 | ```
659 | >> %w[A B C].map { |char| char.downcase }
660 | => ["a", "b", "c"]
661 | >> %w[A B C].map(&:downcase)
662 | => ["a", "b", "c"]
663 | ```
664 | (这个看上去奇怪,压缩的代码使用了符号(symbol),我们将在4.3.3节中讨论)关于这个结构有趣的一件事情是最初是Ruby
665 | on Rails加进来的,人们太喜欢它了,所以后来加进了Ruby内核中。
666 |
667 | 作为我们最后一个块例子,我们看一下代码清单4.4中的测试:
668 | ```
669 | test "should get home" do
670 | get :home
671 | assert_response :success
672 | assert_select "title", "Ruby on Rails Tutorial Sample App"
673 | end
674 | ```
675 |
676 | 理解细节不重要(实际上我不知道细节),但是我们可以从关键词**do**来推断测试的主体是块。**test**方法带一个字符串作为参数(描述)和一个块,然后执行块的主体,作为测试集的一部分。
677 |
678 | 顺便提一下,我们现在应该可以理解1.5.4节中生产随机子域名的方法了:
679 | ```
680 | ('a'..'z').to_a.shuffle[0..7].join
681 | ```
682 | 让我们一步步来:
683 | ```
684 | >> ('a'..'z').to_a # 字母表数组
685 | => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
686 | "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
687 | >> ('a'..'z').to_a.shuffle # 弄乱它
688 | => ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
689 | "b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
690 | >> ('a'..'z').to_a.shuffle[0..7] # 取出前8个元素。
691 | => ["f", "w", "i", "a", "h", "p", "c", "x"]
692 | >> ('a'..'z').to_a.shuffle[0..7].join # 把它们组合起来组成字符串
693 | => "mznpybuj"
694 | ```
695 |
696 | 注:shuffle方法返回的值是随机的,你最后得到的字符串可能和这里不一样。
697 |
698 | ### 4.3.3 哈希表和符号
699 | 哈希表本质上是数组。哈希的键,也叫做索引,几乎可以是任何对象。例如,我们可以使用字符串当作键:
700 |
701 | ```ruby
702 | >> user = {} # {}是空的哈希
703 | => {}
704 | >> user["first_name"] = "Michael" # 键“first_name”, 值“Michael”
705 | => "Michael"
706 | >> user["last_name"] = "Hartl“ # 键“last_name”, 值“Hartl”
707 | => "Hartl"
708 | >> user["first_name"] # Element access is like arrays.
709 | => "Michael"
710 | >> user # A literal representation of the hash
711 | => {"last_name"=>"Hartl", "first_name"=>"Michael"}
712 | ```
713 | 哈希用大括号声明,包含键-值对;没有键值的一对大括号表示空哈希。注意哈希使用的大括号和块使用的大括号没有任何关系。
714 | (是的,这有点让人困惑。)尽管和数组类似,一个重要的不同是哈希一般不能保证它的元素会保持一定的顺序。假如要用到顺序,就只能使用数组了。
715 | 通过方括号的方式可以每次定义一条Hash对,不过这种方式不太方便。使用=>定义Hash键值要方便的多,**=>**成为“哈希火箭(hashrocket)”:
716 | ```ruby
717 | >> user = { "first_name" => "Michael", "last_name" => "Hartl" }
718 | => {"last_name"=>"Hartl", "first_name"=>"Michael"}
719 | ```
720 |
721 | 这里我使用Ruby的通常做法:在哈希的两端添加额外的空格--会被控制台输出忽略的惯例。(不要问我为什么空格是惯例,可能一些早期有影响的Ruby程序员喜欢有额外空格的表示,然后就形成了惯例。)
722 |
723 | 到目前,我已经使用字符串作为哈希键值,但是在Rails里,更平常的是使用符号。符号看起来像是字符串,但是前面有个冒号,而不是引号。例如,:name是符号。你可以认为符号是基础的字符串,没有其他多余的包袱(方法):
724 | ```ruby
725 | >> "name".split('')
726 | => ["n", "a", "m", "e"]
727 | >> :name.split('')
728 | NoMethodError: undefined method `split' for :name:Symbol
729 | >> "foobar".reverse
730 | => "raboof"
731 | >> :foobar.reverse
732 | NoMethodError: undefined method `reverse' for :foobar:Symbol
733 | ```
734 | 符号是特殊的Ruby数据类型,很少可以和其他语言共享。所以刚开始可能觉得奇怪,但是Rails经常使用它们,所以你会很快学会使用它们。不像字符串,不是所有的字符都是有效的符号:
735 | ```ruby
736 | >> :foo-bar
737 | NameError: undefined local variable or method `bar' for main:Object
738 | >> :2foo
739 | SyntaxError
740 | ```
741 | 只要你用字母开始,然后使用普通的字符,你应该没事。
742 |
743 | 关于符号作为哈希键值,我们可以定义一个**user**哈希如下:
744 | ```ruby
745 | >> user = { :name => "Michael Hartl", :email => "michael@example.com" }
746 | => {:name=>"Michael Hartl", :email=>"michael@example.com"}
747 | >> user[:name] # 进入键:name对应的值
748 | => "Michael Hartl"
749 | >> user[:password] # 进入未定义的键返回nil
750 | => nil
751 | ```
752 | 我们从最后的例子看见,如果没有定义键,它的值是nil。
753 |
754 | 因为使用符号作为键值非常普遍,在版本1.9的Ruby为这种特例,支持一个新的语法:
755 | ```ruby
756 | >> h1 = { :name => "Michael Hartl", :email => "michael@example.com" }
757 | => {:name=>"Michael Hartl", :email=>"michael@example.com"}
758 | >> h2 = { name: "Michael Hartl", email: "michael@example.com" }
759 | => {:name=>"Michael Hartl", :email=>"michael@example.com"}
760 | >> h1 == h2
761 | => true
762 | ```
763 | 第二种语法用键名后面加一个冒号和值替换符号/哈希火箭组合的表示方法:
764 | ```ruby
765 | { name: "Michael Hartl", email: "michael@example.com" }
766 | ```
767 |
768 | 这种方法更接近其他语言的哈希声明(例如Javascript),欣赏这种方式的程序员在Rails社区里越来越多。因为两种哈希语法在使用上仍然很普遍,所以能认出这两种语法很有必要。
769 | 不幸的是,这可能有点让人困惑,尤其因为**:name**是有效地符号,但是**name:**本身没有任何意义。底线是**:name =>**和**name:**效率是一样的,因此
770 | ```ruby
771 | { :name => "Michael Hartl" }
772 | ```
773 | 和
774 | ```ruby
775 | { name: "Michael Hartl" }
776 | ```
777 | 是相同的,否则你需要使用**:name**(用冒号开头)来表明它是符号。
778 | 哈希的值可以是任何对象,甚至是其它哈希也可以,如同在代码清单4.10所示。
779 | ```
780 | 代码清单 4.10: 嵌套Hash
781 | >> params = {} # 定义一个名为'params'( 'parameters'的简写)的哈希。
782 | => {}
783 | >> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
784 | => {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
785 | >> params
786 | => {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
787 | >> params[:user][:email]
788 | => "mhartl@example.com"
789 | ```
790 |
791 | 这种哈希的哈希,或者叫嵌套哈希,在Rails中大量第使用,我们将在7.3节开始看见。
792 |
793 | 和数组和范围一样,哈希一样有**each**方法。例如,考虑一个名叫**flash**的哈希,有两组元素,**:success**和**:danger**:
794 | ```ruby
795 | >> flash = { success: "It worked!", danger: "It failed." }
796 | => {:success=>"It worked!", :danger=>"It failed."}
797 | >> flash.each do |key, value|
798 | ?> puts "Key #{key.inspect} has value #{value.inspect}"
799 | >> end
800 | Key :success has value "It worked!"
801 | Key :danger has value "It failed."
802 | ```
803 | 记住,数组的each方法是仅带一个变量的块,哈希的each方法有两个参数,键和值。这样,哈希的each方法遍历哈希一次一组键-值对。
804 |
805 | 最后的例子使用非常有用的**inspect**方法,它会返回字符串表示的对象:
806 | ```
807 | >> puts (1..5).to_a # 把数组当做字符串输出
808 | 1
809 | 2
810 | 3
811 | 4
812 | 5
813 | >> puts (1..5).to_a.inspect # 按我们输入的方式输出数组
814 | [1, 2, 3, 4, 5]
815 | >> puts :name, :name.inspect
816 | name
817 | :name
818 | >> puts "It worked!", "It worked!".inspect
819 | It worked!
820 | "It worked!"
821 | ```
822 | 顺便提一下,使用**puts**来输出对象非常普通,以至于它有一个简写,函数p:
823 | ```
824 | >> p :name
825 | :name
826 | ```
827 |
828 | ### 4.3.4 重访CSS
829 | 现在是时候重访代码清单4.1,在布局文件中包含层叠样式表(CSS):
830 | ```
831 | <%= stylesheet_link_tag 'application', media: 'all',
832 | 'data-turbolinks-track' => true %>
833 | ```
834 | 我们现在几乎可以理解这句了。如同在4.1节中简短地提到的一样,Rails定义了一个特殊函数来包含样式,如
835 | ```ruby
836 | stylesheet_link_tag 'application', media: 'all',
837 | 'data-turbolinks-track' => true
838 | ```
839 | 是调用这个函数。但是有几个迷题:首先,圆括号到哪里了?在Ruby中,它们是可选的,所以这两个是等价的:
840 | ```ruby
841 | # 函数调用时圆括号是可选的
842 | stylesheet_link_tag('application', media: 'all',
843 | 'data-turbolinks-track' => true)
844 | stylesheet_link_tag 'application', media: 'all',
845 | 'data-turbolinks-track' => true
846 | ```
847 | 其次,**media**参数看起来很像哈希,但是大括号去那里了?当哈希是函数最后的参数,大括号是可选的,所以它们两个是等价的:
848 | ```ruby
849 | # 如果函数最后的参数是哈希时,大括号是可选的
850 | stylesheet_link_tag 'application', { media: 'all',
851 | 'data-turbolinks-track' => true }
852 | stylesheet_link_tag 'application', media: 'all',
853 | 'data-turbolinks-track' => true
854 | ```
855 | 接下来,为什么**date-turbolinks-track**使用旧式的哈希火箭语法?这是因为使用新语法写
856 | ```ruby
857 | data-turbolinks-track: true
858 | ```
859 | 是无效的,因为连字符的关系,data-turbolinks-track不是有效的符号。(在4.3.3节提到过连字符不能用在符号中。)这迫使我们使用旧式语法,变成了
860 | ```ruby
861 | 'data-turbolinks-track' => true
862 | ```
863 | 最后,为什么Ruby正确地解释这行
864 | ```ruby
865 | stylesheet_link_tag 'application', media: 'all',
866 | 'data-turbolinks-track' => true
867 | ```
868 | 甚至在最后一行断开?答案是Ruby在这种环境下不区分换行符和别的空格。我选择把代码分成两行的原因是为了清晰。
869 | 我偏好把每行源代码的长度控制在80个字符内。
870 |
871 | 所以,我们继续往下看见这行
872 | ```
873 | stylesheet_link_tag 'application', media: 'all',
874 | 'data-turbolinks-track' => true
875 | ```
876 | 调用**stylesheet_link_tag**函数,带两个参数:字符串,表明样式表的路径,一个有两个元素的哈希表,表明媒体类型和告诉Rails使用**turbolinks**特性,它是在Rails4.0加进来的。
877 | 因为<%= %>括号,结果被插入ERb模板,假如你在浏览器里查看本页的源代码,你会看见HTML需要包含一个样式表(代码清单4.11)。(你肯定也看见其他的内容,如**?body=1**,跟在CSS文件名后面。这些是Rails插入的,用来确保当文件改变之后浏览器可以重新加载CSS文件。)
878 | ```
879 | 代码清单 4.11: 引入CSS函数输出的HTML代码。
880 |
882 | ```
883 | 假如你真的查看[http://localhost:3000/assets/application.css](http://localhost:3000/assets/application.css)这个文件,你会看见(除了注释)它是空的。我们将在第五章改变它。
884 |
885 | ## 4.4 Ruby类
886 | 我们之前说过在Ruby中所有的东西都是对象,在这节我们将在最后定义一些我们自己的类。Ruby,像其他许多面向对象的语言,使用类来组织方法;这些类然后被初始化,创建对象。假如你刚接触面向对象的编程语言,这听起来可能像胡扯,所以我们看一些具体的例子吧。
887 |
888 | ### 4.4.1 构造函数
889 | 我们已经看见过许多使用类初始化对象的例子了,但是我们仍然显示地实现。
890 | 例如,我们使用双引号初始化一个字符串,它就是字符串的构造函数:
891 | ```
892 | >> s = "foobar" # 双引号对于字符串来说是真的构造函。
893 | => "foobar"
894 | >> s.class
895 | => String
896 | ```
897 | 我们这里看见字符串响应方法**class**,只是返回它属于哪个类。
898 |
899 | 与使用字面的构造函数不同,我们使用等价的命名的构造函数,它在类名上调用**new**方法:
900 | ```
901 | >> s = String.new("foobar") #为字符串命名的构造函数。
902 | => "foobar"
903 | >> s.class
904 | => String
905 | >> s == "foobar"
906 | => true
907 | ```
908 |
909 | 这和字面构造函数是等价的,但是关于我们做得事更明显。
910 | 数组和字符串工作方法一样:
911 | ```
912 | >> a = Array.new([1, 3, 2])
913 | => [1, 3, 2]
914 | ```
915 | 哈希,相反地,是不同的。当数组构造函数**Array.new**为数组传递一个初始值,**Hash.new**为哈希传入一个默认的值,这个值是哈希为不存在的键的默认值:
916 | ```
917 | >> h = Hash.new
918 | => {}
919 | >> h[:foo]
920 | => nil
921 | >> h = Hash.new(0)
922 | => {}
923 | >> h[:foo]
924 | => 0
925 | ```
926 |
927 | 当类直接调用方法时,如这里的**new**,它在调用类方法。在类上调用**new**是类的对象,也叫类的实例。在实例上调用的方法,例如**length**,叫实例方法。
928 |
929 | ### 4.4.2 类继承
930 | 当学习使用类,使用**superclass**方法查明类的继承是很有用的:
931 | ```
932 | >> s = String.new("foobar")
933 | => "foobar"
934 | >> s.class # Find the class of s.
935 | => String
936 | >> s.class.superclass # Find the superclass of String.
937 | => Object
938 | >> s.class.superclass.superclass # Ruby 1.9 uses a new BasicObject base class
939 | => BasicObject
940 | >> s.class.superclass.superclass.superclass
941 | => nil
942 | ```
943 | 继承关系如图4.1显示。我们这里看见**String**的超级类是**Object**,**Object**的是**BasicObject**,但是**BasicObject**没有超类。这个模式对每个Ruby对象来说都是真的:回溯类继承关系足够远,每个Ruby类最终都继承于**BasicObject**,它本身没有超类。这是“Ruby中所有的都是对象”的技术意思。
944 |
945 | 
946 |
947 | 为了更深地理解类,创建我们自己的类是不二的选择。让我们创建一个**Word**类,类里有一个名为**palindrome?**的方法,该方法返回**true**,假如单词从前和从后写都是一样的话:
948 | ```
949 | >> class Word
950 | >> def palindrome?(string)
951 | >> string == string.reverse
952 | >> end
953 | >> end
954 | => :palindrome?
955 |
956 | ```
957 | 我们可以如下使用它:
958 | ```
959 | >> w = Word.new # Make a new Word object.
960 | => #
961 | >> w.palindrome?("foobar")
962 | => false
963 | >> w.palindrome?("level")
964 | => true
965 | ```
966 | 假如这个例子让你觉得有点牵强,挺好--这是故意设计的。创建一个只有一个带字符串为参数方法的类是有点奇怪。因为单词是字符串,**Word**继承于**String**更自然,如代码清单4.12所示。(你应该退出控制台然后重新进入,这样可以清除旧的**Word**定义。)
967 |
968 | ```
969 | 代码清单 4.12: 在控制台定义Word类。
970 | >> class Word < String # Word继承与字符串String。
971 | >> # 假如字符串是回文字符串则返回true
972 | >> def palindrome?
973 | >> self == self.reverse # self is the string itself.
974 | >> end
975 | >> end
976 | => nil
977 | ```
978 | 这里**Word < String**是Ruby继承类的语法(在3.2节中简断地讨论过),它确保了除了新的**palindrome?**方法,单词也有字符串一样的方法:
979 | ```
980 | >> s = Word.new("level") # Make a new Word, initialized with "level".
981 | => "level"
982 | >> s.palindrome? # Words have the palindrome? method.
983 | => true
984 | >> s.length # Words also inherit all the normal string methods.
985 | => 5
986 | ```
987 |
988 | 因为**Word**类继承了**String**类,我们用控制台显示地看看类的继承等级:
989 | ```
990 | >> s.class
991 | => Word
992 | >> s.class.superclass
993 | => String
994 | >> s.class.superclass.superclass
995 | => Object
996 | ```
997 |
998 | 图4.2阐明了继承。
999 | 
1000 | Rubyye 允许我们使用**self**关键词:在**Word**类里,**self**是对象自己,这意味着我们可以用
1001 | ```ruby
1002 | self == self.reverse
1003 | ```
1004 |
1005 | 来检查是否单词是回文。实际上,在字符串类中在方法和属性上使用**self.**是可选的,(除非我们创建一个赋值),所以
1006 | ```ruby
1007 | self = reverse
1008 | ```
1009 | 一样工作。
1010 |
1011 | ### 修改内建类
1012 |
1013 | 虽然继承是一个有力的方式,在回文的例子中,可能把方法直接加入**String**类可能更自然,以便(在其他情况)我们可以在字符串上调用**palindrome?**。现在我们做不到这个:
1014 | ```
1015 | >> "level".palindrome?
1016 | NoMethodError: undefined method `palindrome?' for "level":String
1017 | ```
1018 |
1019 | 令人惊奇的是,Ruby允许你这样做;Ruby类是开放类,可以修改,允许普通人,例如我们给它们添加方法:
1020 | ```ruby
1021 | >> class String
1022 | >> # Returns true if the string is its own reverse.
1023 | >> def palindrome?
1024 | >> self == self.reverse
1025 | >> end
1026 | >> end
1027 | => nil
1028 | >> "deified".palindrome?
1029 | => true
1030 | ```
1031 | (我不知道那个更酷:是Ruby让你添加方法到内建类,还是**deified**是回文。)
1032 |
1033 | 修改内建类是强大的技术,但是能力越大,责任越大,如果没有非常好的理由,那么给内建类添加方法是非常不推荐的。Rails确实有一些好的理由;例如,在Web应用程序里,我们常常想要阻止变量是空得--例如,用户名不能是空的--所以Rails加了一个**blank?**方法到Ruby。因为Rails控制台自动包含了Rails扩展,我们可以看看这里的例子(这在irb下不工作):
1034 | ```ruby
1035 | >> "".blank?
1036 | => true
1037 | >> " ".empty?
1038 | =>false
1039 | >> " ".blank?
1040 | =>true
1041 | >> nil.blank?
1042 | => true
1043 | ```
1044 |
1045 | 我们看见空格字符串不是空的,但是它是空白的。注意**nil**是空的;因为**nil**不是字符串,这暗示Rails实际上把**blank?**到**String**的基类,就是(就像我们在这节开始看到的一样)是**Object**。我们将在8.4节看到Rails其他给Ruby内建类添加方法的例子。
1046 |
1047 | ### 4.4.4 控制器类
1048 | 所有这些关于类和继承可能已经激发了认知的闪光,因为之前我们已经看见两者了,在类StaticPagesController中(代码清单3.18):
1049 | ``` Ruby
1050 | class StaticPagesController < ApplicationController
1051 |
1052 | def home
1053 | end
1054 |
1055 | def help
1056 | end
1057 |
1058 | def about
1059 | end
1060 | end
1061 | ```
1062 | 你现在可以理解它了,起码模糊地,代码的意义:**StaticPagesController**是一个继承于**ApplicationController**类,它有**home,help,和about**几个方法。
1063 | 因为每个Rails控制台的session加载了本地的Rails环境,我们甚至可以显示地创建一个控制器,然后检查它的继承:
1064 | ```
1065 | >> controller = StaticPagesController.new
1066 | => #"text/html"}, @_status=200,
1068 | @_request=nil, @_response=nil>
1069 | >> controller.class
1070 | => StaticPagesController
1071 | >> controller.class.superclass
1072 | => ApplicationController
1073 | >> controller.class.superclass.superclass
1074 | => ActionController::Base
1075 | >> controller.class.superclass.superclass.superclass
1076 | => ActionController::Metal
1077 | >> controller.class.superclass.superclass.superclass.superclass
1078 | => AbstractController::Base
1079 | >> controller.class.superclass.superclass.superclass.superclass.superclass
1080 | => Object
1081 | ```
1082 | 继承结构如图4.3所示。
1083 | 
1084 | 我们甚至可以在控制台里调用控制器动作,它们只是方法:
1085 | ```
1086 | >> controller.home
1087 | => nil
1088 | ```
1089 |
1090 | 这里因为**home**动作是空的所以返回**nil**。
1091 |
1092 | 但是等等--动作没有返回值,起码没有返回值得关心的值。**home**动作,正如我们在第三章看见的一样,渲染了一个网页,而不是返回一个值。而且我确定不记得曾经在任何地方调用过**StaticPagesController.new**。发生什么了?
1093 |
1094 | 发生了什么是Rails是用Ruby写的,但是Rails不是Ruby。有些Rails类像普通的Ruby对象,但是有些只是Rails魔法山的材料。Rails是自成一格的,应该独立于Ruby学习和理解。
1095 |
1096 |
1097 | ### 4.4.5 User类
1098 | 我们通过编写一个完整的类来结束我们的Ruby之旅,**User**类和第六章的User模型的先驱。
1099 |
1100 | 到目前,我们都是在控制台进行类的定义,但是你应该很快就感觉很烦了。所以,我们现在直接在应用程序的根目录下创建文件**example_user.rb**,然后写入代码清单4.13里面的代码:
1101 | ```
1102 | 代码清单 4.13: User类的代码
1103 | # example_user.rb
1104 | class User
1105 | attr_accessor :name, :email
1106 |
1107 | def initialize(attributes = {})
1108 | @name = attributes[:name]
1109 | @email = attributes[:email]
1110 | end
1111 |
1112 | def formatted_email
1113 | "#{@name} <#{@email}>"
1114 | end
1115 | end
1116 | ```
1117 | 这里有点长,所以我们一步步来。第一行,
1118 | ```ruby
1119 | attr_accessor :name, :email
1120 | ```
1121 | 声明了可以读取属性:name和:email。这条语句创建了“getter”和“setter”方法,允许我们读取和设置@name和@email实例变量,我们在2.2.2节和3.6节中简单地提到过。
1122 | 在Rails里,实例变量最主要的特性是它们自动就可以在视图中使用了。但是通常来说,它们是在Ruby类中可随意调用的变量。(关于这个还有更多知识需要讲。)实例变量总是以**@**开始,当定义时默认的值为**nil**。
1123 |
1124 | 接下来我们再看看类里面的第一个方法,**initialize**。它在Ruby中是特殊的一个方法:它是我们在执行**User.new**时执行的第一个方法。这个特殊的**initialize**方法有一个参数,**arrtributes**:
1125 |
1126 | ```ruby
1127 | def initialize(attributes = {})
1128 | @name = attributes[:name]
1129 | @email = attributes[:email]
1130 | end
1131 | ```
1132 | 这里**attrbiuttes**变量是默认值为空的哈希,以便我们能在没有传递name和email的情况下定义一个用户。(回忆4.3.3节,哈希为不存在的键返回**nil**,所以假如没有设置**attributes[:name]**的话,**attributes[:name]**值将默认为时**nil**,**attributes[:email]**也类似。)
1133 |
1134 | 最后,我们的类定义了一个名为**formatted_email**的方法,使用字符串插值的方法用@name和@email(4.2.2节)建立了一个格式化好得用户名、email地址的版本:
1135 |
1136 | ```
1137 | def formatted_email
1138 | "#{@name} < #{@email}>"
1139 | end
1140 | ```
1141 | 因为@name和@email两个都是实例变量(用@标记表明的),它们在**formatted_email**方法里是自动可用的。
1142 |
1143 | 让我们发动控制台,**require**用户例子的代码,然后把我们的用户类取出来溜溜:
1144 | ```ruby
1145 | >> require './example_user'
1146 | => true
1147 | >> example = User.new
1148 | >> #
1149 | >> example.name
1150 | => nil
1151 | >> example.name = "Example User"
1152 | => "Example User"
1153 | >> example.email = "user@example.com"
1154 | => "user@example.com"
1155 | >> example.formatted_email
1156 | => "Example User < user@example.com>"
1157 | ```
1158 |
1159 | 这里**‘.’**是Unix表示“当前目录”,**'/example_user'**告诉Ruby在相对位置查找例子用户文件。随后的代码创建了一个空得用户例子,然后填入名字和email地址通过直接给相应的属性赋值(通过代码清单4.13里的**attr_accessor**,赋值变得可能)。当我们写
1160 | ```ruby
1161 | example.name = "Example User"
1162 | ```
1163 | Ruby正给变量**@name**赋值**"Example User"**(**email**属性也是一样的),我们然后会在**formatted_email**方法里使用。
1164 |
1165 | 回忆4.3.4节,我们可以忽略最后的哈希参数的大括号,然后我们通过直接给**initialize**方法传递哈希参数创建一个预先定义属性的用户:
1166 | ```ruby
1167 | >> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
1168 | => #
1169 | >> user.formatted_email
1170 | => "Michael Hartl "
1171 | ```
1172 |
1173 | 我们在第七章会看到使用哈希参数初始化对象--在普通的Rails应用程序里常见的技术--就做集中赋值。
1174 |
1175 |
1176 | ## 4.5 结语
1177 | 这里结束我们Ruby语言的概览。在第五章,我们准备开始在开发示例程序时好好使用这些技能。
1178 |
1179 | 我们不会再使用4.4.5节用过的**example_user.rb**文件了,所以我建议删除它:
1180 | ```
1181 | $ rm example_user.rb
1182 | ```
1183 |
1184 | 然后把其他改变提交到主要的源代码仓库,推送到Bitbucket, 然后部署到Heroku:
1185 | ```
1186 | $ git status
1187 | $ git commit -am "Add a full_title helper"
1188 | $ git push
1189 | $ bundle exec rails test
1190 | $ git push heroku
1191 | ```
1192 |
1193 | ### 4.5.1 我们在这章学到了什么
1194 | * Ruby有许多操作字符串的方法
1195 | * 在Ruby里所有的都是对象
1196 | * Ruby使用**def**定义方法
1197 | * Ruby使用**class**定义类
1198 | * Rails视图可以包含静态HTML和内嵌Ruby(ERb)
1199 | * 内建的Ruby数据结构包含数组,范围和哈希
1200 | * Ruby块是易用的结构,允许自然遍历可遍历的数据结构
1201 | * 符号式标签,像没有额外结构的字符串
1202 | * Ruby支持对象继承
1203 | * 打开和修改Ruby内建类是可能的
1204 | * 单词“deified”是回文
1205 |
1206 | ## 4.6 练习
1207 | 1. 通过用合适的方法替换代码清单4.14里的问号,联合**split,shuffle和join**写一个生成给定字符串的随机字符。
1208 | 2. 使用代码清单4.15为向导,把**shuffle**方法添加到**String**类。
1209 | 3. 创建三个哈希:person1, person2, person3。用键:first, :last表示名字和姓。然后创建一个参数哈希,以便params[:father]是person1,
1210 | params[:mother]是person2,params[:child]是person3。确认,例如,params[:fater][:first]有正确的值。
1211 | 4. 查看在线版的Ruby API,阅读关于哈希方法**merge**。下面表达式的值是什么?
1212 | ```ruby
1213 | { "a" => 100, "b" => 200}.merge({ "b" => 300 })
1214 | ```
1215 |
1216 | ```ruby
1217 | 代码清单 4.14: string shuffle函数的框架
1218 | >> def string_shuffle(s)
1219 | >> s.?('').?.?
1220 | >> end
1221 | >> string_shuffle("foobar")
1222 | => "oobfra"
1223 | ```
1224 |
1225 | ```ruby
1226 | 代码清单 4.15: 添加到String类的shuffle函数的框架
1227 | >> class String
1228 | >> def shuffle
1229 | >> self.?('').?.?
1230 | >> end
1231 | >> end
1232 | >> "foobar".shuffle
1233 | => "borafo"
1234 | ```
1235 |
1236 |
--------------------------------------------------------------------------------
/chapter5_filling_in_the_layout.md:
--------------------------------------------------------------------------------
1 | # 第五章 填充布局
2 |
3 | 在第四章短暂的Ruby旅行中,我们学到了包含网页的样式表(CSS文件)进入示例应用程序(4.1节),但是(如4.3.4节里说明的)样式表文件里还不包含
4 | 任何CSS。在这章里我们将开始通过纳入一个CSS框架进入我们的应用程序,然后我们添加一些自定义格式。我们也将开始用到目前我们创建的链接填写布局(例如主页和关于页面),(5.1节)。沿着这条路,我们将学习部件(partial),Rails路由,和资源管线(asset pipeline),包括Sass的介绍(5.2节)我们将通过让用户在我们网站注册(5.4节),向前迈出重要的第一步。
5 |
6 | 在这章大部分的变化是在示例应用程序的网站布局文件添加和编辑文档,(依据注3.3里的指导原则)是确定一般不需要测试驱动的,或者甚至根本不需要测试。因此,我们将花费大部分时间在我们的文本编辑器和浏览器里,使用TDD仅仅是为了添加一个联系页面(5.3.1节)。不过,我们将
7 | 添加一个重要的新测试,编写我们第一个集成测试来检查最后的布局文件是正确的(5.3.4节)。
8 |
9 | ## 5.1 添加一些结构
10 | 本书是一本网页开发的书,不是网页设计,但是工作在一个看上去非常难看的应用程序上将是很令人沮丧的,所以在这节我们会给布局文件添加一些样式。另外,使用自定义CSS规则,我们将使用Bootstrap,这个Twitter公司开发的一个开源的网页设计框架。我们也会给我们的代码增加一些样式,可以说,一旦布局文件变得凌乱不堪,就使用partial来整理它。
11 |
12 | 当构建网页应用程序时,尽可能早得对用户接口有总体的规划常常很有用,相关内容遍及本书剩余的部分。因此我将使用mockup(在网页环境,常常叫框架图),它就是最终应用将看起来什么样的。在这章,我们将主要开发3.2节介绍的静态网页,包括网站的标志、导航栏、网站的底部。
13 | 这些页面里最重要的框架图,即主页,如图5.1。你能看见最终的结果如图5.7。你将注意到在细节上有些不同--例如,我们将通过在网页上添加一个Rails标志结束--没有关系,因为框架图不需要一模一样。
14 |
15 | 
16 |
17 | 和平常一样,假如你正使用Git作为版本控制,现在正是创建新分支的好时候:
18 | ```
19 | $ git checkout master
20 | $ git checkout -b filling-in-layout
21 | ```
22 |
23 | ### 5.1.1 网站导航
24 | 作为朝示例程序添加链接和样式的第一步,我们将更新网站布局文件**application.html.erb**(上一次看见是在代码清单4.3中)用另外的HTML结构。这包含一些另外的分割,一些CSS类,和我们网站导航的开始。在代码清单5.1里的是所有的文件内容;后面紧跟着就解释各种各样的片段。假如你迫不及待,你可以看看图5.2里的效果。(说明:仍然不是很满意。)
25 |
26 | ```ruby
27 | 代码清单 5.1: 添加了结构的网站布局文件。
28 | # app/views/layouts/application.html.erb
29 |
30 |
31 |
32 | <%= full_title(yield(:title)) %>
33 | <%= stylesheet_link_tag 'application', media: 'all',
34 | 'data-turbolinks-track' => true %>
35 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
36 | <%= csrf_meta_tags %>
37 |
41 |
42 |
43 |
55 |
56 | <%= yield %>
57 |
58 |
59 |
60 | ```
61 | 让我们仔细看看代码清单5.1里面的新内容,如在3.4.1节简单提过的,Rails默认使用HTML5(如文档类型
62 | ****);因为HTML5标准相对较新,一些浏览器(尤其旧版的Internet
63 | Explore)不能很好的支持他,所以我们包含进一些Javascript代码(著名的“HTML5 shim(或者shiv)”),解决这个问题:
64 |
65 | ```ruby
66 |
70 | ```
71 | 有点奇怪的语法
72 | ```ruby
73 |
425 | ```
426 |
427 | 相似地,我们可以把头部的内容移入片段,如代码清单5.10所示,然后用**render**方法把它插入布局文件。(对于Partial文件,通常来说你只能使用文本编辑器手动创建)。
428 |
429 | ```html
430 | 代码清单 5.10: 网站头部的Partial
431 | # app/views/layouts/_header.html.erb
432 |
444 | ```
445 |
446 | 现在我们知道怎么创建片段,让我们和头部一起,给站点添加底部。我知道现在你可能已经猜出来我们会给它起什么名字了,是的,就是**_footer.html.erb**,然后把它放到布局目录里(代码清单5.11)。
447 |
448 | ```html
449 | 代码清单 5.11:网站底部的Partial
450 | # app/views/layouts/_footer.html.erb
451 |
464 | ```
465 | 和头部一样,在底部我们使用**link_to**链接“关于”和“联系”页面,现在用‘#’。(和**header**一样,**footer**也是HTML5新加的)
466 |
467 | 我们可以通过和样式表和头部Partial一样的模式的在布局文件里渲染底部Partial,(代码清单5.12)
468 |
469 | ```html
470 | 代码清单 5.12:使用了底部Partial的网站布局文件。
471 | # app/views/layouts/application.html.erb
472 |
473 |
474 |
475 | <%= full_title(yield(:title)) %>
476 | <%= stylesheet_link_tag "application", media: "all",
477 | "data-turbolinks-track" => true %>
478 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
479 | <%= csrf_meta_tags %>
480 | <%= render 'layouts/shim' %>
481 |
482 |
483 | <%= render 'layouts/header' %>
484 |
485 | <%= yield %>
486 | <%= render 'layouts/footer' %>
487 |
488 |
489 |
490 | ```
491 |
492 | 当然,因为底部还没有样式,所以自然有点丑陋(代码清单5.13)。效果如图5.7所示。
493 |
494 | ```scss
495 | 代码清单 5.13: 为网站底部添加CSS样式。
496 | # app/assets/stylesheets/custom.css.scss
497 | .
498 | .
499 | .
500 | /* footer */
501 |
502 | footer {
503 | margin-top: 45px;
504 | padding-top: 5px;
505 | border-top: 1px solid #eaeaea;
506 | color: #777;
507 | }
508 |
509 | footer a {
510 | color: #555;
511 | }
512 |
513 | footer a:hover {
514 | color: #222;
515 | }
516 |
517 | footer small {
518 | float: left;
519 | }
520 |
521 | footer ul {
522 | float: right;
523 | list-style: none;
524 | }
525 |
526 | footer ul li {
527 | float: left;
528 | margin-left: 15px;
529 | }
530 | ```
531 |
532 | 
533 |
534 | ## 5.2 Sass和资源管线(asset pipline)
535 | 最近的Rails版本值得额外注意的是资源管线(asset pipeline),它显著地提高了静态资源,例如CSS、Javascript和图片资源的产生和管理。这节首先给我们一个资源管线的高度概述,然后显示怎样使用Sass--一个编写CSS强有力的工具。
536 |
537 | ### 5.2.1 资源管线
538 | 资源管线加入了许多Rails帽子下的特性,但是从典型的Rails开发者的角度来审视的话,主要有三个特性需要理解:资产目录、说明文件、预处理引擎。让我们依次说明。
539 |
540 | ### 资产目录
541 | 在Rails 3以及之前的版本,静态资产位于**public**目录,如下:
542 |
543 | * public/stylesheets
544 | * public/javascripts
545 | * public/images
546 |
547 | 在这些目录里的文件(甚至在3.0以后)自动通过到http://www.example.com/stylesheet等等的请求服务。
548 |
549 | 在最近的Rails版本,为静态资源提供了三个显而易见的目录,每一个有它自己的目的:
550 | * app/assets: 目前应用程序专用的资源
551 | * lib/assets:你的开发团队写的库的资源
552 | * vendor/assets: 来自第三方的资源
553 |
554 | 你可能猜到了,这些目录每个都有三个子目录,如:
555 | ```terminal
556 | $ ls app/assets/
557 | # images/ javascripts/ stylesheets/
558 | ```
559 | 到了现在,我们到了理解5.1.2节里自定义CSS文件位置背后隐藏的动机的时候了:**custom.css.scss**是范例程序专用的,所以把它放到**app/assets/stylesheets**。
560 |
561 | ### 清单文件
562 |
563 | 一旦你把资产放在了它们的逻辑位置,你可以使用清单文件告诉Rails(通过[Sprockets](https://github.com/sstephenson/sprockets) gem)怎样把它们组合成一个文件。(这是针对CSS和Javascript文件而言,不适用于图片资源)。正如例子所示,让我们看看范例应用程序的默认的CSS文件的代码清单(代码清单5.14)
564 |
565 | ```css
566 | /*
567 | * This is a manifest file that'll be compiled into application.css, which
568 | * will include all the files listed below.
569 | *
570 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets,
571 | * vendor/assets/stylesheets, or vendor/assets/stylesheets of plugins, if any,
572 | * can be referenced here using a relative path.
573 | *
574 | * You're free to add application-wide styles to this file and they'll appear
575 | * at the bottom of the compiled file so the styles you add here take
576 | * precedence over styles defined in any styles defined in the other CSS/SCSS
577 | * files in this directory. It is generally better to create a new file per
578 | * style scope.
579 | *
580 | *= require_tree .
581 | *= require_self
582 | */
583 | ```
584 |
585 | 关键的行实际上是CSS注释,但是Sprockets用它来包含正确的文件:
586 |
587 | ```css
588 | /*
589 | .
590 | .
591 | .
592 | *= require_tree .
593 | *= require_self
594 | */
595 | ```
596 |
597 | 这里
598 | ```css
599 | *= require_tree .
600 | ```
601 | 确保所有的在**app/assets/stylesheets**目录里的CSS文件(包含树子目录)被包含进入应用程序的CSS文件。这行
602 | ```css
603 | *=require_self
604 | ```
605 | 在加载后续CSS文件时,把自身也包含进来。
606 |
607 | Rails创建了合理的默认文件,在本书里我们不需要做任何修改,但是假如你需要修改它,[Rails指南里关于资源管线](http://guides.rubyonrails.org/asset_pipeline.html)有更多详细信息。
608 |
609 | ### 预处理引擎
610 | 你放置好网站的静态资源以后,Rails会通过运行几个预处理引擎和使用清单文件把它们组合起来,为输出到浏览器做准备。我们通过文件扩展名告诉Rails使用那个处理器处理,最常用的三个是Sass的**.scss**,Coffeescript的**.coffee**和内嵌Ruby的**.erb**(ERb)。我们在3.4.3节第一次介绍了ERb的知识,在5.2.2节介绍了Sass的知识。在本书中我们不需要CoffeeScript,它是一个可以编译成Javascript的小语言。([RailsCast](http://railscasts.com/episodes/267-coffeescript-basics)是学习CoffeeScript的好地方。)
611 |
612 | 预处理引擎可以链接起来,所以foobar.js.coffee会运行CoffeeScript处理器,foobar.js.erb.coffee会依次运行CoffeeScript和ERb(按照从左到右的顺序运行,例如,先运行CoffeeScript)
613 |
614 | ###生产中的效率问题
615 |
616 | 资源管线最好的东西之一是它会自动在生产环境的应用程序里优化资源。CSS和Javascript文件组织的传统方法是将按照功能将代码分别存放到不同的文件中,在这些文件中使用漂亮的代码格式(通过代码缩进)。虽然对程序员来说比较方便,但是会导致程序在生产环境中效率低下。加载多个文件会显著地拖慢页面加载的时间,这是影响网络用户体验的最主要因素之一。有了资源管线,我们不再需要在速度和便利之间作出选择:我们可以在开发环境使用几个格式化好的文件,然后在生产环境下通过资源管线实现文件有效加载。具体来说,资源管线会把应用程序所有的CSS文件汇总至(application.css),把所有的Javascript程序合并至javascript文件(application.js),然后移除文件内不必要的空格和缩进以及其他影响资源文件大小的因素,再通过压缩合并后的文件,进一步减小文件体积。结果是最好的两个世界:程序员感觉友好的开发环境和用户体验很好的生产环境。
617 |
618 | ### 5.2.2 句法超赞的CSS文件
619 |
620 | Sass是为了编写CSS而发明的语言,在许多方面提高了CSS。本节我们学习最重要的两个语法提升:嵌套和变量。(另一个技术,混入(mixin),将在7.1.1节介绍)。
621 |
622 | 如在5.1.2节简单介绍的一样,Sass是名为SCSS的格式(用**.scss**文件扩展名表示),它是CSS的严格的超集。即,SCSS仅仅是为CSS增强了一些特性,而不是重新定义新的语法。这意味着每个有效的CSS文件也是有效的SCSS文件,对于
623 | 已有的CSS的项目文件很方便。在我们的例子中,我们使用SCSS是为了使用Bootstrap。因为Rails资源管线会自动使用Sass处理**.scss**扩展,所以应用程序先运行Sass处理器将**custome.css.scss**文件处理成标准的CSS文件,然后打包发送到浏览器。
624 |
625 | #### 嵌套
626 | 样式表经常会嵌套定义元素的样式,例如,在代码清单5.5中,我们有两个规则,**.center**和**.center h1**:
627 |
628 | ```css
629 | .center {
630 | text-align: center;
631 | }
632 |
633 | .center h1 {
634 | margin-bottom: 10px;
635 | }
636 |
637 | ```
638 | 我们可以用Sass替换为
639 | ```css
640 | .center {
641 | text-align: center;
642 | h1 {
643 | margin-bottom: 10px;
644 | }
645 | }
646 |
647 | ```
648 |
649 | 这里嵌套的**h1**规则自动继承了**.center**上下文。
650 |
651 | 另一种嵌套语法有一点点不同。在代码清单5.7里,我们有以下代码:
652 |
653 | ```css
654 | #logo {
655 | float: left;
656 | margin-right: 10px;
657 | font-size: 1.7em;
658 | color: #fff;
659 | text-transform: uppercase;
660 | letter-spacing: -1px;
661 | padding-top: 9px;
662 | font-weight: bold;
663 | }
664 |
665 | #logo:hover {
666 | color: #fff;
667 | text-decoration: none;
668 | }
669 |
670 | ```
671 |
672 | 这里CSS ID “#logo”显示了两次,一次是单独出现、一次是和**hover**属性一起出现(当鼠标悬在问题里的要素控制它的表现)。为了嵌套第二个样式,我们需要引用父级元素**#logo**;在SCSS里,用连接符“&”:
673 | ```css
674 | #logo {
675 | float: left;
676 | margin-right: 10px;
677 | font-size: 1.7em;
678 | color: #fff;
679 | text-transform: uppercase;
680 | letter-spacing: -1px;
681 | padding-top: 9px;
682 | font-weight: bold;
683 | &:hover {
684 | color: #fff;
685 | text-decoration: none;
686 | }
687 | }
688 |
689 | ```
690 |
691 | Sass把**&:hover**编译为**#logo:hover**,作为从SCSS转化为CSS的一部分。
692 |
693 | 把这两种嵌套技术应用到代码清单5.13中footer的CSS代码中,它变成:
694 | ```css
695 | footer {
696 | margin-top: 45px;
697 | padding-top: 5px;
698 | border-top: 1px solid #eaeaea;
699 | color: #777;
700 | a {
701 | color: #555;
702 | &:hover {
703 | color: #222;
704 | }
705 | }
706 | small {
707 | float: left;
708 | }
709 | ul {
710 | float: right;
711 | list-style: none;
712 | li {
713 | float: left;
714 | margin-left: 15px;
715 | }
716 | }
717 | }
718 |
719 | ```
720 |
721 | 手动转换一下代码清单5.13是掌握Sass语法的好方法,你应该确认转化后的CSS仍然工作正常。
722 |
723 | ### 变量
724 |
725 | Sass允许我们通过定义变量来写出更多富有表达力的代码。例如,在代码清单5.6和5.13中,我们看见有重复的颜色代码:
726 | ```css
727 | h2 {
728 | .
729 | .
730 | .
731 | color: #777;
732 | }
733 | .
734 | .
735 | .
736 | footer {
737 | .
738 | .
739 | .
740 | color: #777;
741 | }
742 |
743 |
744 | ```
745 | 在这里,**#777**是浅灰,然后我们可以通过定义变量,给它一个名字,如下:
746 | ```css
747 | $light-gray: #777;
748 |
749 | ```
750 |
751 | 允许我们像这样一样重写SCSS:
752 |
753 | ```css
754 | $light-gray: #777;
755 | .
756 | .
757 | .
758 | h2 {
759 | .
760 | .
761 | .
762 | color: $light-gray;
763 | }
764 | .
765 | .
766 | .
767 | footer {
768 | .
769 | .
770 | .
771 | color: $light-gray;
772 | }
773 |
774 |
775 | ```
776 |
777 | 把Sass嵌套和变量定义应用到整个SCSS文件里,最后的文件如代码清单5.15所示。这里使用了Sass变量(参考Bootstrap
778 | Less变量定义)和内建的已命名的颜色(如,white为#fff)。通过这些手段**footer**标签里的样式文件可读性有了显著的提高。
779 |
780 | ```css
781 | 代码清单 5.15:使用嵌套和变量的SCSS文件。
782 | # app/assets/stylesheets/custom.css.scss
783 | @import "bootstrap-sprockets";
784 | @import "bootstrap";
785 |
786 | /* mixins, variables, etc. */
787 |
788 | $gray-medium-light: #eaeaea;
789 |
790 | /* universal */
791 |
792 | body {
793 | padding-top: 60px;
794 | }
795 |
796 | section {
797 | overflow: auto;
798 | }
799 |
800 | textarea {
801 | resize: vertical;
802 | }
803 |
804 | .center {
805 | text-align: center;
806 | h1 {
807 | margin-bottom: 10px;
808 | }
809 | }
810 |
811 | /* typography */
812 |
813 | h1, h2, h3, h4, h5, h6 {
814 | line-height: 1;
815 | }
816 |
817 | h1 {
818 | font-size: 3em;
819 | letter-spacing: -2px;
820 | margin-bottom: 30px;
821 | text-align: center;
822 | }
823 |
824 | h2 {
825 | font-size: 1.2em;
826 | letter-spacing: -1px;
827 | margin-bottom: 30px;
828 | text-align: center;
829 | font-weight: normal;
830 | color: $gray-light;
831 | }
832 |
833 | p {
834 | font-size: 1.1em;
835 | line-height: 1.7em;
836 | }
837 |
838 |
839 | /* header */
840 |
841 | #logo {
842 | float: left;
843 | margin-right: 10px;
844 | font-size: 1.7em;
845 | color: white;
846 | text-transform: uppercase;
847 | letter-spacing: -1px;
848 | padding-top: 9px;
849 | font-weight: bold;
850 | &:hover {
851 | color: white;
852 | text-decoration: none;
853 | }
854 | }
855 |
856 | /* footer */
857 |
858 | footer {
859 | margin-top: 45px;
860 | padding-top: 5px;
861 | border-top: 1px solid $gray-medium-light;
862 | color: $gray-light;
863 | a {
864 | color: $gray;
865 | &:hover {
866 | color: $gray-darker;
867 | }
868 | }
869 | small {
870 | float: left;
871 | }
872 | ul {
873 | float: right;
874 | list-style: none;
875 | li {
876 | float: left;
877 | margin-left: 15px;
878 | }
879 | }
880 | }
881 |
882 |
883 | ```
884 |
885 | Sass为我们提供了甚至更多的方法去简化我们的样式表,但是代码清单5.15里的使用的最重要的特性,给我们开了个好头。你可以去[Sass官网](http://sass-lang.com/)查看更多细节。
886 |
887 | ## 5.3 布局链接
888 | 既然我们已经为网站的布局定义了得体的样式,是时候开始用真实的链接来替换占位符#代表的链接了。当然,我们可以用硬编码的方式实现:
889 | ```html
890 | About
891 |
892 | ```
893 |
894 | 但是这不是Rails的方式。一者,“关于”页面的URL是/about而不是/static_pages/about;再者,依据Rails的惯例会使用具名路由(named route),代码看起来像:
895 | ```ruby
896 | <%= link_to "About", about_path %>
897 | ```
898 |
899 | 这种方法的代码有更直白的意思,而且富有弹性。因为我们可以通过改变**about_path**的定义即可实现所有使用**about_path**的URL都被改变。
900 |
901 | 下表是我们规划的链接列表,如表5.1所示,和它们映射的URL、路由一起。我们在3.4.4节认真看过第一个路由,我们在这章结束将实现除最后一个以外的所有路由(我们在第八章创建最后一个路由)。
902 |
903 | Page | URL | Namedroute
904 | ----|----|----
905 | Home |/ |root_path
906 | About |/about_path|about_path
907 | Help|/help|help_path
908 | Contact|/contact|contact_path
909 | Sign up|/signup|signup_path
910 | Log in|/login|login_path
911 |
912 | 表5.1: 路由和网站链接映射的URL
913 |
914 | ### 5.3.1 “联系”页面
915 | 为了补全网站信息,我们再添加一个“联系(Contact)”页面,我们在第三章的时候曾布置过这个作业。测试代码如代码清单5.16,它简单的模仿了代码清单3.22里内容。
916 |
917 | ```ruby
918 | 代码清单 5.16: Contact的页面的测试。红色
919 | # test/controllers/static_pages_controller_test.rb
920 | require 'test_helper'
921 |
922 | class StaticPagesControllerTest < ActionController::TestCase
923 |
924 | test "should get home" do
925 | get static_pages_home_url
926 | assert_response :success
927 | assert_select "title", "Ruby on Rails Tutorial Sample App"
928 | end
929 |
930 | test "should get help" do
931 | get static_pages_help_url
932 | assert_response :success
933 | assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
934 | end
935 |
936 | test "should get about" do
937 | get static_pages_about_url
938 | assert_response :success
939 | assert_select "title", "About | Ruby on Rails Tutorial Sample App"
940 | end
941 |
942 | test "should get contact" do
943 | get static_pages_contact_url
944 | assert_response :success
945 | assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
946 | end
947 | end
948 | ```
949 |
950 | 到这点,在代码清单5.16里的测试应该是红色的:
951 |
952 | ```terminal
953 | $ bundle exec rails test
954 | ```
955 | 应用程序的代码几乎和3.3节的“关于”页面差不多:首先我们更新了路由(代码清单5.18),然后我们给静态页面控制器加了一个**contact**动作,最后我们创建了一个“联系”视图(代码清单5.20)。
956 |
957 | ```ruby
958 | 代码清单 5.18: 添加了到“联系(Contact)”页面的路由。红色
959 | # config/routes.rb
960 | Rails.application.routes.draw do
961 | root 'static_pages#home'
962 | get 'static_pages/help'
963 | get 'static_pages/about'
964 | get 'static_pages/contact'
965 | end
966 | ```
967 |
968 | ```ruby
969 | 代码清单 5.19:在控制器中添加了“Contact”动作。红色
970 | # app/controllers/static_pages_controller.rb
971 | class StaticPagesController < ApplicationController
972 | .
973 | .
974 | .
975 | def contact
976 | end
977 | end
978 | ```
979 |
980 | ```ruby
981 | 代码清单 5.20: 为Contact页面添加视图。绿色
982 | # app/views/static_pages/contact.html.erb
983 | <% provide(:title, 'Contact') %>
984 | Contact
985 |
986 | Contact the Ruby on Rails Tutorial about the sample app at the
987 | contact page.
988 |
989 | ```
990 |
991 | 现在确保测试是绿色的:
992 | ```terminal
993 | 代码清单 5.21: 绿色
994 | $ bundle exec rails test
995 | ```
996 |
997 | ### 5.3.2 Rails 路由
998 | 为了给Sample App的静态页面添加具名路由,我们通过编辑路由文件**config/routes.rb**来实现,Rails使用它来定义URL映射。我们通过回顾主页的路由(在3.4.4节定义的)开始,它是特殊的例子,然后为其余的静态页面定义一套路由。
999 |
1000 | 到目前为止,我们已经见过了三个定义根路由的例子,从Hello应用开始(代码清单1.10)代码:
1001 | ```ruby
1002 | root ‘application#hello’
1003 | ```
1004 | 玩具应用代码(代码清单2.3)
1005 | ```ruby
1006 | root 'users#index'
1007 | ```
1008 | 和Sample APP程序代码(代码清单3.37)
1009 | ```ruby
1010 | root 'static_pages#home'
1011 | ```
1012 | 在每个例子中,**root**方法为根路径“/”路由到控制器和指定的动作建立了映射。这样定义跟路由有另一个重要的影响,就是创建了具名路由,允许我们通过名字而不是原始的URL查找路由。在这里,形成了两个路由,分别是**root_path**和**root_url**,不同之处在于后面的路由包含整个URL:
1013 |
1014 | ```
1015 | root_path -> '/'
1016 | root_url -> 'http://www.example.com/'
1017 | ```
1018 |
1019 | 在本书中,我们将遵循一般的惯例,使用_path形式,除了重定向的时候我们使用_url形式。(这是因为HTTP标准的技术重定向后需要全URL,尽管在大部分浏览器两种方法都工作。)
1020 |
1021 | 为了为“帮助”、“关于”和“联系”页面定义具名路由,我们需要改变代码清单5.18的get规则,把:
1022 | ```ruby
1023 | get 'static_pages/help'
1024 | ```
1025 | 改为
1026 | ```ruby
1027 | get ‘help’ => 'static_pages#help'
1028 | ```
1029 | 这些模式的第二个路由GET请求URL /help到静态页面控制器里面help动作,以便我们可以使用URL /help代替啰嗦的/static_pages/help。和根路由一样,这创建两个具名路由,help_path和help_url:
1030 |
1031 | ```ruby
1032 | help_path -> '/help'
1033 | help_url -> 'http://www.example.com/help'
1034 | ```
1035 |
1036 | 应用这个规则到剩下的静态页面路由,从代码清单5.18转换为代码清单5.22.
1037 |
1038 | ```ruby
1039 | 代码清单 5.22: Routes for static pages.
1040 | # config/routes.rb
1041 | Rails.application.routes.draw do
1042 | root 'static_pages#home'
1043 | get 'help' => 'static_pages#help'
1044 | get 'about' => 'static_pages#about'
1045 | get 'contact' => 'static_pages#contact'
1046 | end
1047 | ```
1048 |
1049 | ### 5.3.3 使用具名路由
1050 |
1051 | 使用代码清单5.22里定义的路由,该是我们在网站布局文件里使用具名路由的时候了。只是需要简单的把正确的路由填充到第二个参数。例如,我们把
1052 | ```ruby
1053 | <%= link_to "About", '#' %>
1054 | ```
1055 | 修改为
1056 | ```ruby
1057 | <%= link_to "About", about_path %>
1058 | ```
1059 | 等等。
1060 |
1061 | 我们将从网站头部Partial文件_header.html.erb开始(代码清单5.23),这里有主页和帮助页面的链接。我们将遵循网页设计原理,把网站标志链接到主页。
1062 |
1063 | ```html
1064 | 代码清单 5.23: 带链接的网站头部Partial文件。
1065 | # app/views/layouts/_header.html.erb
1066 |
1078 | ```
1079 |
1080 | 到第八章我们才会为“登陆”添加命名路由,所以我们现在仍然使用占位符“#”。
1081 |
1082 | 另一个有链接的地方是网站底部Partial,_footer.html.erb的“关于”和“联系”页面(代码清单5.24)
1083 |
1084 | ```ruby
1085 | 代码清单 5.24: 带链接的网站底部Partial文件。
1086 | # app/views/layouts/_footer.html.erb
1087 |
1100 | ```
1101 | 现在,我们的布局文件已经有了第三章创建的所有静态页面的链接,即,例如,/about映射到关于页面(图5.8)。
1102 |
1103 | 
1104 |
1105 | ### 5.3.4 布局文件链接测试
1106 |
1107 | 既然我们经用实际的链接替换了临时链接,测试一下这些链接,确保它们可以正确地工作是个好主意。我们可以通过手动通过浏览器来完成这个任务。首先访问根路径,然后手动检查一下每个链接,但是很快你就会变得麻烦。因此我们将使用集成测试来模拟同样的步骤。集成测试允许我们写端到端的应用程序行为测试。我们可以通过生成集成测试模板开始生成一个叫site_layout的测试:
1108 |
1109 | ```terminal
1110 | $ rails generate integration_test site_layout
1111 | invoke test_unit
1112 | create test/integration/site_layout_test.rb
1113 | ```
1114 |
1115 | 注意Rails生成器自动给测试文件添加了“test”后缀。
1116 |
1117 | 我们为布局文件制定的测试计划是检查我们网站的HTML结构:
1118 | 1.访问根路径(主页)
1119 | 2.确认渲染正确的页面
1120 | 3.检查主页、帮助页面、关于页面和联系页面链接正确
1121 |
1122 | 代码清单5.25显示了我们怎么通过使用Rails集成测试把这些步骤翻译称代码,用**assert_template**方法来确认主页渲染了正确的视图。为了使用**assert_template**我们需要先在Gemfile文件里添加**rails-controller_testing**
1123 | Gem。
1124 | ```ruby
1125 | # Gemfile
1126 | ...
1127 |
1128 | group :test do
1129 | ...
1130 | gem 'rails-controller-testing' #添加这两个GEM到:test组
1131 | gem 'rails-dom-testing'
1132 | ...
1133 | end
1134 |
1135 | ...
1136 | ```
1137 | 然后运行
1138 | ```
1139 | $ bundle install
1140 | ```
1141 | 然后修改测试文件:
1142 | ```ruby
1143 | 代码清单 5.25: A test for the links on the layout. 绿色
1144 | # test/integration/site_layout_test.rb
1145 | require 'test_helper'
1146 |
1147 | class SiteLayoutTest < ActionDispatch::IntegrationTest
1148 |
1149 | test "layout links" do
1150 | get root_path
1151 | assert_template 'static_pages/home'
1152 | assert_select "a[href=?]", root_path, count: 2
1153 | assert_select "a[href=?]", help_path
1154 | assert_select "a[href=?]", about_path
1155 | assert_select "a[href=?]", contact_path
1156 | end
1157 | end
1158 | ```
1159 |
1160 | 代码清单5.25使用了一些高级的assert_select方法,之前在代码清单3.22和5.16里见过。在这个例子中,允许我们通过使用标签名a和属性herf测试指定链接组合,如
1161 | ```ruby
1162 | assert_select "a[href=?]", about_path
1163 | ```
1164 |
1165 | 这里,Rails自动插入about_path的值,代替问号(假如需要转义任何特殊字符),然后检查表格的HTML标签
1166 | ```html
1167 | ...
1168 | ```
1169 |
1170 | 注意根路径的断言确认有两个这样的链接(一个是网站标志一个是导航菜单中的内容):
1171 | ```ruby
1172 | asset_select "a[href=?]", root_path, count: 2
1173 | ```
1174 |
1175 | 这个断言确保代码清单5.23里定义的主页两个链接都存在。
1176 | 更多的assert_select用法在表5.2里。assert_select是非常灵活,功能很强(比这里的选项还多)。经验显示通过仅仅测试HTML元素(例如网站布局文件链接)这样的轻度测试是明智的,不会为测试增加很多时间。
1177 |
1178 | Code | Matching HTML
1179 | ---|---
1180 | assert_select "div" | foobar
1181 | assert_select "div", "foobar" | foobar
1182 | assert_select "div.nav" | foobar
1183 | assert_select "div#profile" | foobar
1184 | assert_select "div[name=yo]" | hey
1185 | assert_select "a[href=?]", ’/’, count: 16 | foo
1186 | assert_select "a[href=?]", ’/’, text: "foo" | foo
1187 |
1188 | 表5.2: 一些assert_select用法
1189 |
1190 | 为了确保代码清单5.25的测试可以通过,我们可以仅运行集成测试,使用下面的命令:
1191 | ```terminal
1192 | 代码清单 5.26: 绿色
1193 | $ bundle exec rails test:integration
1194 | ```
1195 | 假如一切正常,你应该运行完整测试集来确认所有的测试都是绿色的:
1196 | ```terminal
1197 | 代码清单 5.27: 绿色
1198 | $ bundle exec rails test
1199 | ```
1200 | 随着为布局文件链接加了集成测试,我们该是使用测试来快速捕捉回溯了。
1201 |
1202 | ## 5.4 用户登陆: 第一步
1203 | 作为我们在布局文件和路由的最难处,这节我们将为登陆页面创建一个路由,然后接着创建第二个控制器。这是允许用户注册我们网站的重要一步,接下来模块化用户。在第六章,我们将完成第七章里的一部分任务。
1204 |
1205 | ### 5.4.1 用户控制器
1206 | 在3.2节中我们创建了第一个控制器--静态页面控制器(StaticPagesController)。是时候创建另一个了--用户控制器(UsersController)。和之前一样,我们使用generate来创建一个简单的控制器,为新用户提供注册页面。遵循Rails偏爱的REST架构的惯例,我们命名创建新用户的动作为new,我们可以通过传递new作为generate的参数来实现。结果如代码清单5.28所示。
1207 | ```terminal
1208 | 代码清单 5.28:生成Users控制器(包含一个动作:new)。
1209 | $ rails generate controller Users new
1210 | create app/controllers/users_controller.rb
1211 | route get 'users/new'
1212 | invoke erb
1213 | create app/views/users
1214 | create app/views/users/new.html.erb
1215 | invoke test_unit
1216 | create test/controllers/users_controller_test.rb
1217 | invoke helper
1218 | create app/helpers/users_helper.rb
1219 | invoke test_unit
1220 | invoke assets
1221 | invoke coffee
1222 | create app/assets/javascripts/users.js.coffee
1223 | invoke scss
1224 | create app/assets/stylesheets/users.css.scss
1225 | ```
1226 | 如同我们想要的结果,代码清单5.28创建了一个用户控制器(UsersController),并为这个控制器添加了动作new,还创建了一个用户视图(app/views/users/new.html.erb,详见代码清单5.31)。它也为新用户页面创建了一个最小化的测试(代码清单5.32),现在测试应该通过:
1227 |
1228 | ```terminal
1229 | 代码清单 5.29: 绿色
1230 | $ bundle exec rails test
1231 | ```
1232 | ```ruby
1233 | 代码清单 5.30: 初始化的UsersController,包含名为new的动作。
1234 | # app/controllers/users_controller.rb
1235 | class UsersController < ApplicationController
1236 | def new
1237 | end
1238 | end
1239 | ```
1240 | ```ruby
1241 | 代码清单 5.31:初始化用户控制器的new视图。
1242 | # app/views/users/new.html.erb
1243 | Users#new
1244 | Find me in app/views/users/new.html.erb
1245 | ```
1246 |
1247 | ```ruby
1248 | 代码清单 5.32:控制器测试视图。绿色
1249 | # test/controllers/users_controller_test.rb
1250 | require 'test_helper'
1251 |
1252 | class UsersControllerTest < ActionController::TestCase
1253 |
1254 | test "should get new" do
1255 | get users_new_url
1256 | assert_response :success
1257 | end
1258 | end
1259 | ```
1260 |
1261 | ### 5.4.2 注册URL
1262 | 有了5.4.1节里的代码,新用户就可以通过/users/new页面在我们的网站注册了。但是回忆表5.1,我们想要用户通过/signup来注册。我们模仿代码清单5.22里的例子,为用户注册地址添加 **get ‘/signup’**的路由,如代码清单5.33所示。
1263 | ```ruby
1264 | 代码清单 5.33:注册页面的路由。
1265 | # config/routes.rb
1266 | Rails.application.routes.draw do
1267 | root 'static_pages#home'
1268 | get 'help' => 'static_pages#help'
1269 | get 'about' => 'static_pages#about'
1270 | get 'contact' => 'static_pages#contact'
1271 | get 'signup' => 'users#new'
1272 | end
1273 | ```
1274 |
1275 | 接下来,我们使用新定义的具名路由把正确的链接按钮添加到主页。和其他路由一样,get 'signup'自动为我们生成了两个具名路由signup_path和signup_url。
1276 | 我们把它放到代码清单5.34里,为注册页面添加测试这个任务当做练习(5.6节)留给你来完成。
1277 |
1278 | ```ruby
1279 | 代码清单 5.34:为按钮添加“注册”链接。
1280 | # app/views/static_pages/home.html.erb
1281 |
1282 |
Welcome to the Sample App
1283 |
1284 |
1285 | This is the home page for the
1286 | Ruby on Rails Tutorial
1287 | sample application.
1288 |
1289 |
1290 | <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
1291 |
1292 |
1293 | <%= link_to image_tag("rails.png", alt: "Rails logo"),
1294 | 'http://rubyonrails.org/' %>
1295 | ```
1296 |
1297 | 最后,我们再为注册页面添加一个自定义临时视图(代码清单5.35)。
1298 |
1299 | ```erb
1300 | 代码清单 5.35:初始化临时注册视图。
1301 | # app/views/users/new.html.erb
1302 | <% provide(:title, 'Sign up') %>
1303 | Sign up
1304 | This will be a signup page for new users.
1305 | ```
1306 |
1307 | 有了这个视图,我们完成了链接和命名路由,指定我们将在第八章会添加得登陆路由。新用户注册页面的结果(URL地址/signup)显示如图5.9。
1308 |
1309 | 
1310 |
1311 | ## 5.5 结论
1312 |
1313 | 在这章,我们把应用程序布局文件锤炼成形,然后润色了一下路由。本书剩余部分致力于充实应用程序的内容:
1314 | 首先,实现用户注册、登陆和退出;接下来添加用户微博功能;然后,添加关注其他用户的能力。
1315 |
1316 | 到了这里,假如你使用Git,你应该把你的变化合并到主分支:
1317 |
1318 | ```terminal
1319 | $ bundle exec rails test
1320 | $ git add -A
1321 | $ git commit -m "Finish layout and routes"
1322 | $ git checkout master
1323 | $ git merge filling-in-layout
1324 | ```
1325 | 然后推送到Bitbucket
1326 | ```terminal
1327 | $ git push
1328 | ```
1329 | 最后,部署到Heroku
1330 | ```
1331 | $ git push heroku
1332 | ```
1333 | 部署的结果应该是在生产环境服务器工作Sample App程序(图5.10)。
1334 |
1335 | 
1336 |
1337 | ### 5.5.1 这章我们学到了什么
1338 | * 使用HTML5,我们可以用网站标志、头部、底部和主体内容定义网站的布局文件
1339 | * Rails片段(Partial)常用来为在单独的文件里放置模板提供便利
1340 | * CSS允许我们依据CSS类和id样式化站点布局
1341 | * Bootstrap框架使得快速开发一个漂亮的网站变得很容易
1342 | * Sass和资源管线允许我们消除我们CSS里的重复,然后为生产环境提供打包好的高效的资源文件
1343 | * Rails允许我们自定义路由规则,提供具名路由
1344 | * 集成测试高效地模拟浏览器在页面间转换
1345 |
1346 |
1347 | ## 5.6 练习
1348 |
1349 | 1. 如5.2.2节建议的那样,学习代码清单5.13到5.15例子,手动把footer部分的CSS转换成SCSS。
1350 | 2. 在代码清单5.25的集成测试里,添加代码使用get方法访问注册页面,然后确认结果页的标题是正确的。
1351 | 3. 通过包含Application Helper,在测试中使用full_title辅助方法。Ruby代码清单5.36所示。然后使用Ruby代码清单5.37的代码(它是从前面的练习扩展的解决方案)测试标题是否正确。这个测试是易碎的,不过,因为现在任何在基础标题的错误(例如“Ruby on Rails Totoial”)不会被测试集捕获。
1352 | 通过编写一个直接对full_title进行测试的辅助方法来解决这个问题。需要创建一个文件来测试应用程序辅助方法,然后在代码清单5.38里FILL_IN的地方填入正确的代码。(代码清单5.38用操作符==来确认assert_equal <想要的值>,<实际的值>,期待的结果和实际的值匹配)。
1353 |
1354 | ```ruby
1355 | 代码清单 5.36:在测试中包含Application辅助方法。
1356 | # test/test_helper.rb
1357 | ENV['RAILS_ENV'] ||= 'test'
1358 | .
1359 | .
1360 | .
1361 | class ActiveSupport::TestCase
1362 | fixtures :all
1363 | include ApplicationHelper
1364 | .
1365 | .
1366 | .
1367 | end
1368 | ```
1369 |
1370 | ```ruby
1371 | 代码清单 5.37:在测试中使用full_title辅助方法。 绿色
1372 | # test/integration/site_layout_test.rb
1373 | require 'test_helper'
1374 |
1375 | class SiteLayoutTest < ActionDispatch::IntegrationTest
1376 |
1377 | test "layout links" do
1378 | get root_path
1379 | assert_template 'static_pages/home'
1380 | assert_select "a[href=?]", root_path, count: 2
1381 | assert_select "a[href=?]", help_path
1382 | assert_select "a[href=?]", about_path
1383 | assert_select "a[href=?]", contact_path
1384 | get signup_path
1385 | assert_select "title", full_title("Sign up")
1386 | end
1387 | end
1388 | ```
1389 | ```ruby
1390 | 代码清单 5.38:直接测试full_title辅助方法。
1391 | # test/helpers/application_helper_test.rb
1392 | require 'test_helper'
1393 |
1394 | class ApplicationHelperTest < ActionView::TestCase
1395 | test "full title helper" do
1396 | assert_equal full_title, FILL_IN
1397 | assert_equal full_title("Help"), FILL_IN
1398 | end
1399 | end
1400 | ```
1401 |
--------------------------------------------------------------------------------
/chapter6_modeling_users.md:
--------------------------------------------------------------------------------
1 | # 第六章 用户模型
2 |
3 | 在第五章,我们做了个临时的注册页面(5.4节)。在接下来的五章,我们将完成用户注册页面里暗示的承诺。本章我们将通过创建用户模型开始艰难的第一步,包括数据存储。在第七章,我们也会为在我们网站注册的用户创建一个用户信息页面。一旦用户可以注册,我们也准备实现用户登陆和退出的功能(第八章),在第九章(9.2.1节)我们将学习怎样防止未未授权用户登陆。最后,在第十章,我们将添加账户激活功能(通过确认有效的email地址)和密码重置功能。综合起来,从第六章到第十章我们开发了一个完整的Rails登陆和授权系统。正如你可能知道的,在Rails社区有许多用户验证解决方案,注6.1解释了为什么,起码在刚学Rails的时候,建立自己的一套用户登陆验证系统可能是更好的想法。
4 |
5 | 注6.1 建立自己的授权系统
6 |
7 | 实际上,几乎所有的网站都需要用户登陆和授权系统。所以大多数WEB框架都有很多这样的系统可供选择,Rails也不例外。认证和授权系统包括Clearance、Authlogic、Devise和CanCanCan(和建立在OpenID和OAuth之上的非Rails专用方案)。你肯定想问为什么我们应该重新发明轮子?为什么不使用现成的方案而是创建自己的用户认证和授权系统?
8 |
9 | 这是因为实际的经验显示授权系统在大多数网站都有自定义扩展的需求。修改第三方产品常常需要比从零开始构建系统花费更多的工作。另外,现成的系统可能是“黑盒”,有潜在的迷一样的内部结构。当你在编写自己的系统时,你更可能充分理解它。而且,近来的Rails(6.3节)让写一个自定义授权系统更加容易。最后,假如你最终还是使用了第三方系统,但是一旦你曾经自己创建过一个类似的系统,你会更好的理解第三方系统,并在必要的时候可以修改它。
10 |
11 | ## 6.1 用户模型
12 | 尽管接下来三章的终极目标是为我们的网站创建注册页面(原型如图6.1),但是现在接受新用户信息几乎还没什么用:现在还没有存放用户数据的地方。因此,用户注册的第一步是创建接收和储存用户信息的数据结构。
13 |
14 | 
15 |
16 | 在Rails里,数据模型默认的数据结构叫模型(Model,MVC中的M),足够自然。默认的Rails解决持久性问题是用数据库来长期保存数据,和数据库交互默认的库是Activ Record。ApplicationRecord有许多用来创建、保存和查找数据对象的方法。所有的查找都不需要使用关系数据库使用的结构化查询语句(SQL),而且Rails有一个特性叫migration,允许用纯Ruby写数据定义,所以也不必学习SQL数据定义语言(DDL)。结果是Rails把和数据存储细节几乎隔离开来。
17 | 本书在开发环境使用SQLite,生产环境用PostgreSQL(通过Heroku)。这个主题我们已经跨的很远了,跨到我们几乎不得不考虑Rails怎样储存数据,甚至需要考虑生产环境的应用程序怎样储存数据。
18 |
19 | 和往常一样,假如你一直使用Git做版本控制,现在是时候为模型化用户创建一个主题分支:
20 |
21 | ```terminal
22 | $ git checkout master
23 | $ git checkout -b modeling-users
24 | ```
25 |
26 | ### 6.1.1 数据库迁移
27 | 你可能回忆起在4.4.5节我们已经自定义过一个User类,它有name和email两个属性。那个例子作为一个有用的例子缺少严格的持续性的属性:当我们在控制台创建User对象后,当我们退出时这个对象就消失了。我们这节的目标是为用户创建模型,让它不会如此容易就消失。
28 |
29 | 和4.4.5节里的User类一样,我们将通过两个属性name和email来模型化用户,email将作为用户唯一的用户名。(我们在6.3节加入密码属性)在代码清单4.13里,我们用attr_accessor方法实现了:
30 | ```ruby
31 | class User
32 | attr_accessor :name, :email
33 | .
34 | .
35 | .
36 | end
37 |
38 | ```
39 | 相反,当使用Rails来模型化用户时,我们不需要严格地识别属性的读写权限。如同上面简单提到过的,默认使用关系数据库储存数据,它包含由数据行组成的表,每行由属性组成列构成。例如,为了储存用户的name和emai,我们使用name和email列创建一个用户表(每行对应一个用户)。这种的表的例子在图6.2里显示,对应的数据模型显示在图6.3里。(图6.3只是草图,完整的数据模型如图6.4)。通过命名列name和emai,我们让ApplicationRecord为我们查找User对象的属性。
40 |
41 | 
42 |
43 | 
44 |
45 | 你可能想起代码清单5.28,我们创建了一个User控制器(带动作new),使用命令
46 | ```
47 | $ rails generate controller Users new
48 | ```
49 | 自动生成控制器代码。生成模型的命令类似,是`rails generate model`,我们用来生成用户模型,具有name和email属性,如代码清单6.1所示。
50 |
51 | ```terminal
52 | 代码清单 6.1: 生成用户模型。
53 | $ rails generate model User name:string email:string
54 | invoke active_record
55 | create db/migrate/20151224010738_create_users.rb
56 | create # app/models/user.rb
57 | invoke test_unit
58 | create test/models/user_test.rb
59 | create test/fixtures/users.yml
60 | ```
61 | (注意,和使用复数的控制器惯例不同,模型名字是单数的:用户们的控制器,但是用户的模型。)通过传递可选参数name:string和email:string,我们告诉Rails我们想要添加那些属性、这些属性应该是那种数据类型(这里是String)。比较这个和代码清单3.4和5.28里的动作名称)
62 |
63 | 代码清单6.1里generate命令的创建了一个叫做迁移(migration)的新文件。迁移提供了一种增量改变数据结构的方法,我们所有的数据模型都可以使用它来根据需求改变。在用户模型这个例子里,迁移是通过模型生成脚本自动创建的;它创建了用户表,有两列:name和email,如代码清单6.2所示。(我们将在6.2.5节开始学习怎样从零开始创建数据迁移)
64 | ```ruby
65 | 代码清单 6.2: 为User模型创建的数据迁移(为了创建users表)。
66 | # db/migrate/[timestamp]_create_users.rb
67 | class CreateUsers < ActiveRecord::Migration
68 | def change
69 | create_table :users do |t|
70 | t.string :name
71 | t.string :email
72 |
73 | t.timestamps null: false
74 | end
75 | end
76 | end
77 | ```
78 |
79 | 注意迁移文件名称有一个时间戳前缀,依据是迁移生成的时间。在早期的迁移,文件名是递增的数据,但是假如多个程序员合作开发时容易引起冲突。摒弃不太可能发生的在同一秒生成迁移的场景,使用时间戳避免了这种冲突。
80 |
81 | 迁移自身包含了change方法,决定了对数据库所作的改变。在代码清单6.2里,change使用Rails create_table的方法,在数据库里创建了一个表储存用户数据。create_table方法接受块(4.3.2节)作为参数,带一个块变量,在这里是t。在块内部,create_table方法使用t对象在数据库里创建了name和email列,两个都是string类型。这里表名是复数(users),即使模型名称是单数(User)。它反映了Rails遵循的语法惯例:模型代表单个用户,然而数据表包含了许多用户。在块里,最后的一行, t.timestamps null: false,是特殊的命令,创建两个魔法列叫做created_at和updated_at,是自动记录指定用户创建和更新的时间。(我们将在6.1.3节里看见魔法列的实际例子)。在代码清单6.2里迁移代表的完整数据模型如图5.6.4所示。(注意额外的魔法列,我们在图6.3里没有显示)
82 |
83 | 
84 |
85 | 我们可以运行迁移,知名的“迁移起来”,使用rails命令(注3.1)如下:
86 |
87 | ```
88 | $ bundle exec rails db:migrate
89 | ```
90 | (你可能回忆起来我们在2.2节类似的环境运行过这个命令)db:migrate首次运行,它创建了一个名为db/development.sqlite3的文件,它是SQLite数据库。我们可以用[SQLite数据库浏览器](http://sqlitebrowser.org/)打开development.sqlite3文件。
91 | (假如你正使用云IDE,你应该先把数据库文件下载到本地,如图6.5所示)。结果如图6.6所示);和图6.4比较,你可能注意到在图6.6有一列,在迁移文件里却没有:id列。如同在2.2节简单提过的这个列是自动创建的,Rails用它来识别每一行数据。
92 |
93 | 
94 |
95 | 
96 |
97 | 许多迁移(包括本书中所有的)是可逆的,这意味着我们可以“迁移回去”,用一个Rails任务还原它们,叫做db:rollback:
98 |
99 | ```
100 | $ bundle exec rails db:rollback
101 | ```
102 | (参考注3.1查看另一个有用的技术,逆迁移。)在后台,这个命令执行drop_table从数据库移除用户表。这样工作的原理是change方法指导drop_table是create_table的逆操作,这意味着回滚操作可以容易地被推理出来。在这个不可逆转的迁移例子里,例如移除数据库的一列,这样就有必要分别定义up和down方法替代唯一的change方法。阅读[Rails Guides](http://guides.rubyonrails.org/migrations.html)获得更多信息。
103 |
104 | 假如你回滚了数据库,在继续学习前再次迁移起来:
105 |
106 | ```
107 | $ bundle exec rails db:migrate
108 | ```
109 |
110 | ### 6.1.2 模型文件
111 |
112 | 我们已经在代码清单6.1生成了User模型、生成了一个迁移文件(代码清单6.2),我们在图6.6里看见了运行这个迁移的结果:它更新了development.sqlite3的文件,通过创建用户表,表里有列id,name,email,created_at和updated_at。代码清单6.1也创建了模型本身。这节剩余部分用来理解它。
113 |
114 | 我们看看User模型的代码,它# app/models目录里的user.rb中。它是,委婉地说,很紧凑的(代码清单6.3)。
115 |
116 | ···ruby
117 | 代码清单 6.3: 全新的User 模型。
118 | # # app/models/user.rb
119 |
120 | class User < ApplicationRecord
121 | end
122 |
123 | ```
124 | 回忆在4.4.2节,语法class User < ApplicationRecord的意思是User类继承于ApplicationRecord,以便User模型自动有了ApplicationRecord类的所有功能。当然,这个学问出发我们知道ApplicationRecord包含什么才有用,所有让我们开始一些实际的例子。
125 |
126 | ### 6.1.3 创建User对象
127 |
128 | 如在第四章里,我们用Rails控制台探索数据模型。因为我们不想在交互的时候修改数据库,所以我们在沙盒里运行控制台:
129 |
130 | ```terminal
131 | $ rails console --sandbox #简写为:rails c -s
132 | Loading development environment in sandbox
133 | Any modifications you make will be rolled back on exit
134 | >>
135 | ```
136 |
137 | 正如帮助信息表明的“你所做的任何改变退出时将会回滚”。当在沙盒里与数据库交互后,在退出控制台的时候控制台会“回滚”(还原)期间发生的任何数据库变化。
138 |
139 | 在4.4.5节的控制台session里,我们用User.new创建了一个用户对象,那时我们必须包含代码清单4.13里的sampel_user.rb文件后才可以。对于模型来说,情形有点不同。如果你回忆在4.4.4节中,Rails控制台自动加载了Rails环境,其中就包含了模型。这意味着我们可以无需其他操作就可以创建新用户对象:
140 | ```terminal
141 | >> User.new
142 | => #
143 | ```
144 |
145 | 我们看见了控制台输出的User对象。
146 |
147 | 当我们不带参数调用new函数时,User.new返回一个所有属性都是nil的对象。在4.4.5节,我们设计了示例User类,用初始化的哈希来设置对象属性;那个设计是受ApplicationRecord启发,它允许对象以同样的方法初始化:
148 |
149 | ```
150 | >> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
151 | => #
153 | ```
154 |
155 | 这里我们看见name和email属性已经和设想的那样设置好了。
156 |
157 | 有效性的概念对于理解ApplicationRecord模型对象是重要的。我们将在6.2节里更加深入地探索这个主题。但是目前对刚刚初始化的User对象来说有效性还没什么意义,我们可以通过调用逻辑函数valid?方法来验证一下:
158 |
159 | ```
160 | >> user.valid?
161 | true
162 | ```
163 |
164 | 到目前为止,我们还没有真正接触数据库:User.new仅在内存里创建了一个对象,然而user.valid?只检查对象是否有效。为了把User对象保存到数据库,我们需要在user变量上调用save方法:
165 |
166 | ```
167 | >> user.save
168 | (0.2ms) begin transaction
169 | User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users".
170 | "email") = LOWER('mhartl@example.com') LIMIT 1
171 | SQL (0.5ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at)
172 | VALUES (?, ?, ?, ?) [["created_at", "2014-09-11 14:32:14.199519"],
173 | ["email", "mhartl@example.com"], ["name", "Michael Hartl"], ["updated_at",
174 | "2014-09-11 14:32:14.199519"]]
175 | (0.9ms) commit transaction
176 | => true
177 | ```
178 |
179 | save方法假如成功返回true,否则返回false。(现在,保存应该成功,因为仍然没有有效性验证;我们将在6.2节里看见有时会失败。)为了提供参考,Rails控制台也显示和user.save相对应的SQL命令(即,INSERT INTO "users" ...)。我们在本书里几乎不需要使用原始的SQL,我将从现在开始忽略讨论SQL命令,但是你可以通过阅读ApplicationRecord相应的SQL学到很多相关的知识。让我们看看我们的save做了些什么:
180 |
181 | ```
182 | >> user
183 | => #
185 |
186 | ```
187 | 我们看见已经为user的ID赋值为1,魔法列也已经被赋值为当前的时间和日期。现在,创建和更新时间戳是一样的,我们在6.1.5节会看见它们有时也会不同。
188 |
189 | 正如4.4.5节里User类一样,User模型的实例允许通过“.”访问它们的属性:
190 |
191 | ```
192 | >> user.name
193 | => "Michael Hartl"
194 | >> user.email
195 | => "mhartl@example.com"
196 | >> user.updated_at
197 | => Thu, 24 Jul 2014 00:57:46 UTC +00:00
198 | ```
199 |
200 | 正如我们将在第七章看见的一样,前面的例子一样分创建和保存两步来保存数据模型,但是ApplicationRecord也让我们一步完成,使用User.create:
201 |
202 | ```
203 | >> User.create(name: "A Nother", email: "another@example.org")
204 | #
206 | >> foo = User.create(name: "Foo", email: "foo@bar.com")
207 | #
209 | ```
210 |
211 | 注意User.create,不是返回true和false,而是返回User对象本身,我们也可以选择分配一个变量(例如上面的第二个例子赋值给foo)。
212 | 与create相对应的是destroy:
213 |
214 | ```
215 | >> foo.destroy
216 | => #
218 | ```
219 | 像create,destroy返回对象,尽管我回忆不起来曾经用过destroy的返回值。另外,删除的对象仍会在内存里存在:
220 |
221 | ```
222 | >> foo
223 | => #
225 | ```
226 | 那么我们怎么知道是否已经删除了?而且对于已经保存的和没有删除的对象,我们怎样才能从数据库里撤回用户?为了回答这些问题,我们需要学习怎样使用ApplicationRecord查找用户对象。
227 |
228 | ### 6.1.4 查找用户
229 | ApplicationRecord提供了几个查找用户的方法。让我们查找第一个用户,然后确认第三个用户(foo)已经删除了。我们从存在的用户开始:
230 |
231 | ```
232 | >> User.find(1)
233 | => #
235 | ```
236 | 这里我们给User.find方法传递了第一个用户的id;ApplicationRecord返回id为1的用户。
237 |
238 | 让我们看看id为3的用户是否仍然在数据库里:
239 | ```
240 | >> User.find(3)
241 | ActiveRecord::RecordNotFound: Couldn't find User with ID=3
242 | ```
243 |
244 | 因为我们在6.1.3节删除了我们的第三个用户,Active Record在数据库里找不到他。因此find抛出异常,这是程序里表示例外事件的一种方式--在这里一个不存在的Active Record id引起find抛出ActiveRecord::RecordNotFound异常。
245 |
246 | 除了普通的find,Active Record也允许我们通过特殊的属性查找用户:
247 |
248 | ```
249 | >> User.find_by(email: "mhartl@example.com")
250 | => #
252 | ```
253 |
254 | 因为我们后面会使用email地址作为用户登陆时使用的用户名,因此当我们学习怎样让用户登陆我们网站时,这类型的find将会有用(第七章)。假如你担心用户量很多时find_by效率比较低,你已经领先了。我们后面会讨论这些问题,在6.2.5节中我们通过为数据库构建索引来解决。
255 |
256 | 我们将通过几个更普通的查找用户的方法来结束本小结。首先,是first:
257 | ```ruby
258 | >> User.first
259 | => #
261 | ```
262 | 自然,first返回数据库里第一个用户;还有all:
263 | ```ruby
264 | >> User.all
265 | => #, #]>
270 | ```
271 | 正像你从控制台看见的,User.all以数组形式返回数据库里所有的用户数据。它们都是类ActiveRecord::Relation的实例。
272 |
273 | ### 6.1.5 更新用户
274 |
275 | 一旦我们创建了用户,我们可能经常需要更新他们的信息。有两个基础的方法实现这个。首先,我们可以单独为某个属性赋值,正如我们在4.4.5节所做的:
276 |
277 | ```ruby
278 | >> user # 只是为了回忆一下user的属性
279 | => #
281 | >> user.email = "mhartl@example.net"
282 | => "mhartl@example.net"
283 | >> user.save
284 | => true
285 | ```
286 |
287 | 注意最后把变化写入数据库是必须的。我们能通过使用reload看看有没有保存,reload会依据数据库信息重新加载对象:
288 |
289 | ```
290 | >> user.email
291 | => "mhartl@example.net"
292 | >> user.email = "foo@bar.com"
293 | => "foo@bar.com"
294 | >> user.reload.email
295 | => "mhartl@example.net"
296 | ```
297 |
298 | 既然我们已经通过运行user.save更新了用户,魔法列的值也自动改变了,如我们在6.1.3节说过的:
299 |
300 | ```ruby
301 | >> user.created_at
302 | => "2014-07-24 00:57:46"
303 | >> user.updated_at
304 | => "2014-07-24 01:37:32"
305 | ```
306 | 另外一个可以同时更新几个属性的主要方法是使用update_attributes:
307 |
308 | ```ruby
309 | >> user.update_attributes(name: "The Dude", email: "dude@abides.org")
310 | => true
311 | >> user.name
312 | => "The Dude"
313 | >> user.email
314 | => "dude@abides.org"
315 | ```
316 |
317 | update_attributes方法以哈希作为参数,假如成功的话会依次执行update和save(保存成功则返回true)。注意假如无论哪个属性验证失败,例如当需要密码才能保存记录(如6.3), update_attributes都会失败,返回false。假如我们只需要更新其中某个属性时,可以使用update_attribute来绕过这个限制:
318 |
319 | ```ruby
320 | >> user.update_attribute(:name, "The Dude")
321 | => true
322 | >> user.name
323 | => "The Dude"
324 | ```
325 |
326 | ## 6.2 用户验证
327 | 我们在6.1节创建的用户模型拥有name和email两个属性。但是它们是非常普通的:现在任何字符串(包含一个空字符串)对它们来说都是有效的。但是无论是name还是email都不应该是任意字符串。例如,name应该非空,email应该匹配一定的格式。而且因为我们准备使用email作为用户登陆时唯一的用户名,
328 | 我们不允许数据库里有重复的email。
329 |
330 | 简而言之,我们要求name和email两个属性要满足一定的限制。ApplicationRecord允许我们使用验证(validates,在2.3.2节里提过)来强制为某些属性添加限制。本节我们将学习几个最普通的验证:验证属性不能为空(NOT NULL)、长度、属性格式和唯一性。在6.3.2节,我们再添加最后一个验证。我们将在7.3节会看到当属性不满足条件时,验证会为我们提供一些方便调试的信息。
331 |
332 | ### 6.2.1 有效性测试
333 | 如注3.3里提到的,测试驱动开发并不是总是正确的,但是模型验证(Model validates)是TDD适用的完美场景。要确保validates实现了我们想要的功能,如果不通过测试来验证,我们是不会放心的。
334 |
335 | 我们的方法是从验证模型对象开始,把这个对象的属性设置为无效属性,然后测试它实际上是无效的。作为一道安全网,我们首先写一个测试来确保初始化模型对象是有效的。这样,当验证测试失败时我们就知道什么是真正的原因(而不是因为刚开始初始对象无效)。
336 |
337 | 为了让我们开始,代码清单6.1的命令产生了一个测试用户的初始化测试,尽管在这里,它是实际是空得(代码清单6.4)。
338 |
339 | ```
340 | 代码清单 6.4: The practically blank default User test.
341 | # test/models/user_test.rb
342 | require 'test_helper'
343 |
344 | class UserTest < ActiveSupport::TestCase
345 | # test "the truth" do
346 | # assert true
347 | # end
348 | end
349 |
350 | ```
351 |
352 | 为了为有效的对象写测试,我们先通过特殊的setup方法创建一个有效的User模型对象@user,(在第三章练习里讨论过),它会自动在每个测试运行前运行。因为@user是一个实例变量,它会自动在所有测试里都可以使用,我们通过“valid?”方法来测试它的有效性(6.1.3节)。如代码清单6.5显示的一样。
353 |
354 | ```
355 | 代码清单 6.5: 测试user是否有效。绿色
356 | # test/models/user_test.rb
357 | require 'test_helper'
358 |
359 | class UserTest < ActiveSupport::TestCase
360 |
361 | def setup
362 | @user = User.new(name: "Example User", email: "user@example.com")
363 | end
364 |
365 | test "should be valid" do
366 | assert @user.valid?
367 | end
368 | end
369 | ```
370 | 代码清单6.5使用普通的assert方法,在这里假如@user.valid?返回true,测试就成功,返回false意味着失败。
371 |
372 | 因为我们的User模型现在还没有添加任何validates,因此初始化的测试应该通过:
373 | ```
374 | 代码清单 6.6: 绿色
375 | $ bundle exec rails test:models
376 | ```
377 | 这里我们使用rails test:models来只运行模型测试(和5.3.4节的rails test:integration比较)
378 |
379 | ### 6.2.2 非空验证
380 | 可能最基础的验证就是验证属性非空,它只是验证所给的属性非空。例如,本节我们确保name和email列在用户保存到数据库前是非空的。在7.3.3节,我们将看见怎样把这种要求求添加到新用户的注册表格。
381 |
382 | 我们将通过在代码清单6.5里的测试开始来测试@user对象的name属性非空。如同代码清单6.7所示,我们需要做的就是设置@user 的name属性为空白的字符串(在这个例子,一个空格字符串),然后检查(使用assert_not方法)User对象是无效的。
383 |
384 | ```
385 | require 'test_helper'
386 |
387 | class UserTest < ActiveSupport::TestCase
388 |
389 | def setup
390 | @user = User.new(name: "Example User", email: "user@example.com")
391 | end
392 |
393 | test "should be valid" do
394 | assert @user.valid?
395 | end
396 |
397 | test "name should be present" do
398 | @user.name = " "
399 | assert_not @user.valid?
400 | end
401 | end
402 | ```
403 |
404 | 到这里,模型测试应该是红色的:
405 | ```
406 | 代码清单 6.8: 红色
407 | $ bundle exec rails test:models
408 | ```
409 |
410 | 正如我们在第二章练习中看到的一样,验证name属性存在的方法是使用validates方法,参数为presence: true,如在代码清单6.9里显示的一样。
411 | presence: true参数是仅包含一个元素的可选哈希;回忆4.3.4节讲到在方法中如果最后的参数是哈希,大括号可以省略。(如5.1.1节提到的,在Rails里可选哈希是循环主题。)
412 |
413 | ```
414 | 代码清单 6.9: 验证name属性非空。绿色
415 | # app/models/user.rb
416 | class User < ApplicationRecord
417 | validates :name, presence: true
418 | end
419 | ```
420 |
421 | 代码清单6.9可能看起来像魔术,但是validates其实是个普通的方法。使用圆括号的等价的形式如代码清单6.9所示:
422 |
423 | ```
424 | class User < ApplicationRecord
425 | validates(:name, presence: true)
426 | end
427 | ```
428 |
429 | 让我们顺便进入控制台来看看把验证加入到我们的User模型后的效果:
430 |
431 | ```
432 | $ rails console --sandbox
433 | >> user = User.new(name: "", email: "mhartl@example.com")
434 | >> user.valid?
435 | => false
436 | ```
437 | 这里我们用valid?检查user变量的有效性,当对象没有通过一个或多个有效性验证就返回false,当所有的有效性验证都通过就返回true。在这个例子里,我们仅有一个有效性验证,所以我们很容易猜到那个测试没有通过,但是通过检查有效性验证失败后生成的errors对象可以让我们明确的知道违反了那条:
438 | (错误信息暗示Rails使用blank?方法验证属性的存在,我们在4.4.3节看见过)
439 |
440 | 因为用户无效,企图把用户保存到数据库自动失败了:
441 | ```
442 | >> user.save
443 | => false
444 | ```
445 |
446 | 因此,代码清单6.7的测试应该是绿色的:
447 |
448 | ```
449 | 代码清单 6.10: 绿色
450 | $ bundle exec rails test:models
451 | ```
452 |
453 | 模仿代码清单6.7,写一个测试email属性非空的测试(代码清单6.11),应用程序代码应该可以通过(代码清单9.13)。
454 |
455 | ```
456 | 代码清单 6.11:验证email属性非空的测试。红色
457 | # test/models/user_test.rb
458 | require 'test_helper'
459 |
460 | class UserTest < ActiveSupport::TestCase
461 |
462 | def setup
463 | @user = User.new(name: "Example User", email: "user@example.com")
464 | end
465 |
466 | test "should be valid" do
467 | assert @user.valid?
468 | end
469 |
470 | test "name should be present" do
471 | @user.name = ""
472 | assert_not @user.valid?
473 | end
474 |
475 | test "email should be present" do
476 | @user.email = " "
477 | assert_not @user.valid?
478 | end
479 | end
480 | ```
481 |
482 | ```
483 | 代码清单 6.12:验证email属性非空。 绿色
484 | # app/models/user.rb
485 | class User < ApplicationRecord
486 | validates :name, presence: true
487 | validates :email, presence: true
488 | end
489 | ```
490 | 到这里,属性非空的验证结束了,测试集应该是绿色的:
491 | ```ruby
492 | 代码清单 6.13: 绿色
493 | $ bundle exec rails test
494 | ```
495 |
496 | ### 6.2.3 长度验证
497 |
498 | 我们已经限制每个用户都需要一个非空的名字,但是我们应该更进一步:用户名需要在Sample App上显示,所以我们应该在长度上再加一些限制。随着6.2.2节我们所做的工作,这一步很容易。
499 |
500 | 没有关于名字长度最长是多长的科学依据,我们只是以50作为合理的上限,这意味着字符长度为51的名字太长了。另外,尽管这不是问题,有的用户email地址可能超过字符串最大长度,对于许多数据库来说是255是合理的上限。在6.2.4节的格式验证不会强加这样一个限制,所以我们将在这节添加一个长度验证。代码清单6.14显示了测试运行的结果。
501 |
502 | ```ruby
503 | 代码清单 6.14:测试name长度的有效性。 红色
504 | # test/models/user_test.rb
505 | require 'test_helper'
506 |
507 | class UserTest < ActiveSupport::TestCase
508 |
509 | def setup
510 | @user = User.new(name: "Example User", email: "user@example.com")
511 | end
512 | .
513 | .
514 | .
515 | test "name should not be too long" do
516 | @user.name = "a" * 51
517 | assert_not @user.valid?
518 | end
519 |
520 | test "email should not be too long" do
521 | @user.email = "a" * 244 + "@example.com"
522 | assert_not @user.valid?
523 | end
524 | end
525 | ```
526 | 为了方便,我们使用"字符串相乘”在代码清单6.14里来创建51个字符的字符串。我们可以通过控制台来看看怎么工作的:
527 |
528 | ```ruby
529 | >> "a" * 51
530 | => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
531 | >> ("a" * 51).length
532 | => 51
533 | ```
534 | email长度验证创建字符太长的有效的email地址:
535 |
536 | ```ruby
537 | >> "a" * 244 + "@example.com"
538 | => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
539 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
540 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
541 | aaaaaaaaaaa@example.com"
542 | >> ("a" * 244 + "@example.com").length
543 | => 256
544 | ```
545 |
546 | 到这点,代码清单6.14应该是红色的:
547 |
548 | ```
549 | 代码清单 6.15: 红色
550 | $ bundle exec rails test
551 | ```
552 | 为了让它们通过,我们需要使用验证参数来限制长度,就是length,和maximum参数一起强迫上边界(代码清单6.16)。
553 |
554 | ```ruby
555 | 代码清单 6.16:为name属性添加长度(length)验证。绿色
556 | # app/models/user.rb
557 | class User < ApplicationRecord
558 | validates :name, presence: true, length: { maximum: 50 }
559 | validates :email, presence: true, length: { maximum: 255 }
560 | end
561 | ```
562 | 现在我们的测试集再次通过,我们可以前往更具有挑战性验证:email格式验证。
563 |
564 | ### 6.2.3 格式化验证
565 |
566 | 我们对name属性的有效性验证仅仅有最低的限制--非空和不超过51个字符--然而email属性必须满足更严格的要求。到目前为止,我们仅仅拒绝空email地址和长度超过255个字符的email地址;本节我们将要求email地址遵从大家熟知的user@example.com模式。
567 |
568 | 无论是测试还是有效性验证都不能彻底的让我们接受有效的email,拒绝所有无效的email地址。我们将通过几个有效的和几个无效的email地址开始学习。为了创建这些集合,我们使用%w[]来创建字符串数组,这个技术很值得学习。我们通过控制台来学习一下:
569 | ```ruby
570 | >> %w[foo bar baz]
571 | => ["foo", "bar", "baz"]
572 | >> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
573 | => ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
574 | >> addresses.each do |address|
575 | ?> puts address
576 | >> end
577 | USER@foo.COM
578 | THE_US-ER@foo.bar.org
579 | first.last@foo.jp
580 | ```
581 | 这里我们使用each方法(4.2.3节)遍历了addresses数组的元素。学会了这个,我们就可以写一些基础的email格式验证的测试。
582 |
583 | 因为email格式验证很麻烦而且很容易出错,我们将通过一些有效的email地址测试来在验证中捕捉可能出现的任何错误。换句话说,我们想要确保不只是无效的地址如“user@example,com”被拒绝,而且有效的地址,像“user@example.com”必须被接受。(现在,当然,因为所有非空email地址目前都是有效的。)有效email地址的代表性例子显示在代码清单6.18里。
584 |
585 | ```
586 | 代码清单 6.18: 测试有效的email地址。绿色
587 | # test/models/user_test.rb
588 | require 'test_helper'
589 |
590 | class UserTest < ActiveSupport::TestCase
591 |
592 | def setup
593 | @user = User.new(name: "Example User", email: "user@example.com")
594 | end
595 | .
596 | .
597 | .
598 | test "email validation should accept valid addresses" do
599 | valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
600 | first.last@foo.jp alice+bob@baz.cn]
601 | valid_addresses.each do |valid_address|
602 | @user.email = valid_address
603 | assert @user.valid?, "#{valid_address.inspect} should be valid"
604 | end
605 | end
606 | end
607 |
608 | ```
609 |
610 | 注意这次我们给assert传递了第二项可选的参数,用来在测试失败时输出提示信息。在这个例子中,可以通过它来辨别是那个地址引起测试失败:
611 |
612 | ```
613 | assert @user.valid?, "#{valid_address.inspect} should be valid"
614 | ```
615 | (这使用插值inspect方法,在4.3.3节提过)。在输出的调试信息中包含引起测试失败的地址在测试里尤其有用。否则无论是那个email地址引起测试失败只输出行号,这对于所有email地址来说都一样,对于识别问题的根源来说不够明显。
616 |
617 | 接下来我们将为无效的email地址添加有效性验证测试,例如“user@example,com”(句号替换成逗号),和user_at_foo.org(没有‘@’标志)。在代码清单6.18和6.19里包含一个自定义错误信息来输出引起测试失败的确切地址。
618 | ```ruby
619 | 代码清单 6.19:email格式有效性验证测试。 红色
620 | # test/models/user_test.rb
621 | require 'test_helper'
622 |
623 | class UserTest < ActiveSupport::TestCase
624 |
625 | def setup
626 | @user = User.new(name: "Example User", email: "user@example.com")
627 | end
628 | .
629 | .
630 | .
631 | test "email validation should reject invalid addresses" do
632 | invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
633 | foo@bar_baz.com foo@bar+baz.com]
634 | invalid_addresses.each do |invalid_address|
635 | @user.email = invalid_address
636 | assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
637 | end
638 | end
639 | end
640 | ```
641 |
642 | 现在测试应该是红色的:
643 | ```
644 | 代码清单 6.20: 红色
645 | $ bundle exec rails test
646 | ```
647 | 为验证email格式的应用程序代码使用format验证,它的形式是这样的:
648 | ```ruby
649 | validates :email, format: { with: /<正则表达式>/ }
650 | ```
651 | 给定的正则表达式用来验证属性。正则表达式是非常强大的(难解的)匹配字符串的语言。这意味着我们需要构建正则表达式来匹配有效的email地址,不匹配无效的email地址。
652 |
653 | 实际上email官方标准有一个完整的匹配email地址的正则表达式,但是它太庞大了,而且非常晦涩,最后可能适得其反。本书中我们采用更具可编程特色的正则表达式,实践中证明这个表达式更加实用,即:
654 | ```ruby
655 | VALID_EMAIL_REGEX = /\A[\w\d\-.]+@[a-z]\d\-.]+\.[a-z]+\z/i
656 | ```
657 |
658 | 为了理解它,表6.1把它分解成一小块一小块。
659 |
660 | 表达式 | 意思
661 | ----|----
662 | /\A[\w\d\-.]+@[a-z]\d\-.]+\.[a-z]+\z/i | 完整的正则表达式
663 | / | 正则表达式开始
664 | \A | 匹配字符串开始
665 | [\w+\-.]+ | 起码一个单词、加号、短线或点
666 | @ | 字符@
667 | [a-z\d\-.]+ | 起码一个字母、数字、短线或者点
668 | \. | 转义.
669 | [a-z]+ | 起码一个字符
670 | \z | 匹配字符串结尾
671 | / | 正则表达式结尾
672 | i| 忽略大小写
673 |
674 | 表6.1: 分解有效email正则表达式
675 |
676 | 尽管你通过表6.1可以学到很多知识,要真正的理解正则表达式我认为使用交互的正则表达式匹配器如[Rubular](http://www.rubular.com/)是非常有必要
677 | 的(图6.7)。Rubular网站为创建正则表达式制作了一个漂亮的交互界面,还提供了很顺手的正则表达式规则参考。我鼓励你用浏览器打开Rubular学习表6.1的知识--眼过千遍不如手过一遍。(注意:假如你在Rubular中使用表6.1的正则表达式,我推荐不要加\A和\z字符,这样你就可以同时匹配几个email地址)
678 |
679 | 
680 |
681 | 将表6.1的正则表达式添加到email格式有效性验证,如代码清单6.21:
682 |
683 | ```ruby
684 | 代码清单 6.21:带正则表达式的email格式有效性验证。绿色
685 | # app/models/user.rb
686 | class User < ApplicationRecord
687 | validates :name, presence: true, length: { maximum: 50 }
688 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
689 | validates :email, presence: true, length: { maximum: 255 },
690 | format: { with: VALID_EMAIL_REGEX }
691 | end
692 | ```
693 |
694 | 这里正则表达式VALID_EMAIL_REGEX是一个常量,在Ruby里常量用首字母大写的变量名来表示。代码:
695 | ```ruby
696 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
697 | validates :email, presence: true, length: { maximum: 255 },
698 | format: { with: VALID_EMAIL_REGEX }
699 | ```
700 | 确保仅仅和模式匹配的email地址才被当做是有效的。(上面的表达式有一个值得关注的弱点:它也接受一些无效的地址如包含连续的点,例如“foo@bar..com.”这类的email地址。解决这个问题需要更复杂的正则表达式,我把这个留下来作为作业(6.5节)。
701 |
702 | 到了这里,测试应该是绿色的:
703 | ```terminal
704 | 代码清单 6.22: 绿色
705 | $ bundle exec rails test:models
706 | ```
707 |
708 | 这也意味着仅仅剩下一个限制性条件:要求email地址唯一。
709 |
710 | ### 6.2.5 唯一性验证
711 |
712 | 为了限制email地址唯一的性(以便我们能使用它们当做用户名),我们为validates方法传递:uniqueness参数。但是警告一下:这里有个重要的预告,所以不要只是掠过这节--最好仔细读读。
713 |
714 | 我们先通过一些简短的测试开始学习。在我们先前的模型测试中主要使用User.new,它只是在内存创建了一个Ruby对象。但是对唯一性测试来说,我们实际需要先在数据库中存储一个数据。唯一性验证的初始化email测试显示在代码清单6.23里。
715 |
716 | ```ruby
717 | 代码清单6.23:重复email的测试。红色
718 | # test/models/user_test.rb
719 |
720 | require 'test_helper'
721 |
722 | class UserTest < ActiveSupport::TestCase
723 |
724 | def setup
725 | @user = User.new(name: "Example User", email: "user@example.com")
726 | end
727 | .
728 | .
729 | .
730 | test "email addresses should be unique" do
731 | duplicate_user = @user.dup
732 | @user.save
733 | assert_not duplicate_user.valid?
734 | end
735 | end
736 |
737 | ```
738 |
739 | 这里使用的方法是通过@user.dup创建了一个和@user一模一样的用户,它们的email地址也一样。因为我们后来保存了@user,所以数据库里已经有了克隆的user的email地址,所以导致duplicate_user无效。
740 |
741 | 我们通过在代码清单6.23里email有效性验证中添加参数“uniqueness: true”,让新的测试通过,如代码清单6.24所示。
742 |
743 | ```ruby
744 | 代码清单 6.24:验证email地址的唯一性。 绿色
745 | # app/models/user.rb
746 | class User < ApplicationRecord
747 | validates :name, presence: true, length: { maximum: 50 }
748 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
749 | validates :email, presence: true, length: { maximum: 255 },
750 | format: { with: VALID_EMAIL_REGEX },
751 | uniqueness: true
752 | end
753 | ```
754 |
755 | 不过,我们还没完全结束。email地址通常是忽略大小写的,例如foo@bar.com和FOO@BAR.COM,
756 | 或者FoO@BAr.coM是一样的,所以我们的验证应该也纳入这个。测试忽略大小写是很重要的,我们在代码清单6.25里实现:
757 |
758 | ```ruby
759 | 代码清单 6.25:测试email唯一性对大小写不敏感。 红色
760 | # test/models/user_test.rb
761 | require 'test_helper'
762 |
763 | class UserTest < ActiveSupport::TestCase
764 |
765 | def setup
766 | @user = User.new(name: "Example User", email: "user@example.com")
767 | end
768 | .
769 | .
770 | .
771 | test "email addresses should be unique" do
772 | duplicate_user = @user.dup
773 | duplicate_user.email = @user.email.upcase
774 | @user.save
775 | assert_not duplicate_user.valid?
776 | end
777 | end
778 | ```
779 |
780 | 这里我们在字符串上使用upcase方法(4.3.2节提过)。这个测试和email唯一性验证测试刚开始时一样,但是用的是大写的email地址。假如这个测试看起
781 | 来有点抽象,让我们到控制台试试:
782 |
783 | ```ruby
784 | $ rails console --sandbox
785 | >> user = User.create(name: "Example User", email: "user@example.com")
786 | >> user.email.upcase
787 | => "USER@EXAMPLE.COM"
788 | >> duplicate_user = user.dup
789 | >> duplicate_user.email = user.email.upcase
790 | >> duplicate_user.valid?
791 | => true
792 | ```
793 | 当然,duplicate_user.valid?现在是true,因为唯一性验证是对字母的大小写敏感的,但是我们希望它的结果是false。幸运的是,:uniqueness接受一个
794 | 选项,:case_sensitive,仅仅为这个目的服务(代码清单6.26)。
795 |
796 | ```ruby
797 | class User < ApplicationRecord
798 | validates :name, presence: true, length: { maximum: 50 }
799 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
800 | validates :email, presence: true, length: { maximum: 255 },
801 | format: { with: VALID_EMAIL_REGEX },
802 | uniqueness: { case_sensitive: false }
803 | end
804 | ```
805 |
806 | 注意在代码清单6.26里,我们只是用case_sensitive: false替换了true。(Rails也推理uniqueness是true)。
807 |
808 | 到这里,我们的应用程序--随着重要的预告--已经实现了强制email属性唯一,我们的测试集应该通过:
809 |
810 | ```terminal
811 | 代码清单 6.27: 绿色
812 | $ bundle exec rails test
813 | ```
814 | 不过还有个小问题,ApplicationRecord唯一性验证并不能保证数据库水平的唯一性。这里有一个场景,可以解释为什么:
815 |
816 | 1. Alice注册了示例应用网站,使用地址alice@wonderland.com
817 | 2. Alice不小心点了两次“提交”按钮,一下发送了两个请求
818 | 3. 接下来发生了:请求1在内存里创建了用户,通过了验证,请求2也一样,所以请求1保存了,请求2也保存了
819 | 4. 数据库中有了email地址重复的两条用户记录,尽管我们有唯一性验证。
820 |
821 | 上面的一系列场景听起来不合情理。相信我,这种情况非常可能出现:这种事情可以发生在任意一个流量较大的Rails网站(我曾经艰难的学会了)。幸运地是,解决方案是很简单的:我们需要强制数据库水平和模型水平一样唯一性。我们的方法是早email列创建一个数据库索引(注6.2),然后要求索引是唯一的。
822 |
823 | 注6.2 数据库索引
824 |
825 | 当为数据库表添加一列,考虑一下是否我们需要依靠这一列来查找记录是很重要的。例如,考虑一下在代码清单6.2里通过数据迁移创建的email属性。当我们允许用户登录到Sample App时(第七章),我们需要查找和用户提交的email地址相对应的用户记录。不幸地是,依靠幼稚的数据模型,通过email地址查找用户的唯一方法是浏览数据库里的每个用户,然后比较它的email属性和所给的email是否一致--这意味着我们可能不得不检查每一行记录(因为用户可能是数据库里最后一位)。在数据库业务里,这是著名的全表扫描,对于一个有着成千上万的用户的真实网站来说是一件[坏事](http://catb.org/jargon/html/B/Bad-Thing.html)。
826 |
827 | 在email列上添加索引可以解决这个问题。为了理解数据库索引,我们可以想想书的索引,这对我们理解数据库索引是有帮助。在一本书中,为了查找所给的字符串,如“foobar”,你不得不扫描每页,纸版的全表扫描。假如我们换个方式,通过书的目录来查找,你可以通过在目录里查找“foobar”来找到所有包含“foobar”的页面。数据库索引和它的道理基本一样。
828 |
829 | 为email列添加索引代表我们数据模型需求的更新,(如6.1.1节里讨论过的)在Rails中我们通过使用迁移来实现。在6.1.1节我们看到生成User模型时Rails为我们自动创建了一个新迁移(代码清单6.2);在当前的例子,我们是添加一个结构到一个已经存在的模型,所以我们需要直接使用migration生成器来创建迁移:
830 |
831 | ```terminal
832 | $ rails generate migration add_index_to_users_email
833 | ```
834 |
835 | 不像用户的迁移,email唯一性迁移没有预定义,所以我们需要把代码清单6.28里的内容填充进去。
836 |
837 | ```
838 | 代码清单 6.28:强制email唯一的数据迁移。
839 | # db/migrate/[timestamp]_add_index_to_users_email.rb
840 | class AddIndexToUsersEmail < ActiveRecord::Migration
841 | def change
842 | add_index :users, :email, unique: true
843 | end
844 | end
845 | ```
846 |
847 | 这使用了Rails名为add_index的方法来在users表里的email列添加一个索引。索引本身不强制唯一,但是选项unique: true会实现强制索引唯一。
848 |
849 | 最后一步是迁移数据库:
850 |
851 | ```
852 | $ bundle exec rails db:migrate
853 | ```
854 | (假如运行该命令失败了,试试退出其他正在运行的沙盒控制台会话,因为它会锁住数据库,阻止数据迁移。)
855 |
856 | 到这里,测试集应该是红色的,因为fixture中的数据破坏了数据库索引的唯一性,fixture包含了为测试数据库准备的示例数据。user fixture是在代码清单6.1里自动生成的,如代码清单6.29所示,email地址不是唯一的。(它们也不是有效的,但是fixture数据不必通过验证。)
857 |
858 | ```ruby
859 | 代码清单 6.29:默认的user fixture。红色
860 | # test/fixtures/users.yml
861 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
862 | # FixtureSet.html
863 |
864 | one:
865 | name: MyString
866 | email: MyString
867 |
868 | two:
869 | name: MyString
870 | email: MyString
871 | ```
872 |
873 | 因为直到第八章我们才需要fixture,所以现在我们直接移除它们,留一个空的fixture文件(代码清单6.30)。
874 |
875 | ```ruby
876 | 代码清单 6.30:空的fixture文件。 绿色
877 | # test/fixtures/users.yml
878 | # empty
879 | ```
880 |
881 | 要确保email地址唯一还有一件事情:一些数据库的索引区分大小写,例如数据库会认为字符串“Foo@ExAMPle.CoM”和“foo@example.com”是不同的索引。但是我们的应用程序认为这些地址是一样的。为了避免这种不兼容,我们将标准化email列,统一使用小写的email。因此在把数据保存到数据前我们需要把“Foo@ExAMPle.CoM”转换为“foo@example.com”。我们通过callback来实现这个功能。callback是一种在ApplicationRecord对象生命周期的某个点被唤醒的特殊函数。在当前例子中,这个点就是对象被保存到数据库前。代码显示在代码清单6.31。(这是我们的第一次实现。我们会在8.4节再次讨论这个主题,在那里我们会使用Rails偏好的办法来定义callback函数。)
882 |
883 | ```ruby
884 | class User < ApplicationRecord
885 | before_save { self.email = email.downcase }
886 | validates :name, presence: true, length: { maximum: 50 }
887 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
888 | validates :email, presence: true, length: { maximum: 255 },
889 | format: { with: VALID_EMAIL_REGEX },
890 | uniqueness: { case_sensitive: false }
891 | end
892 | ```
893 |
894 | 代码清单6.31里的代码是把块传递给before_save这个callback函数,然后使用downcase字符串方法把用户的email地址设置为小写。(为验证email小写写一个测试,这个任务当作练习(6.5节)留给你完成。)
895 |
896 | 在代码清单6.31里,我们可以通过如下方式为变量赋值,如
897 | ```ruby
898 | self.email = self.email.downcase
899 | ```
900 | (self指当前用户),但是在User模型里,右边的关键词self是可选的:
901 |
902 | ```ruby
903 | self.eamil = email.downcase
904 | ```
905 |
906 | 我们在palindrome方法里reverse的上下文里简短地介绍过这个方法(4.4.2节),也要注意的左边的self在赋值时是不可选的,所以
907 | ```ruby
908 | email = email.downcase
909 | ```
910 | 不会起作用。(我们将在8.4节更深入地讨论这个主题)
911 |
912 | 到这里,即便遇到上面提到的Alice的遭遇的场景程序也会很好的运行:数据库响应第一个请求,保存用户记录;当第二个请求到达时,因为它打破了数据库索引的唯一性限制,因此数据库拒绝保存它。(在Rails日志里会出现错误提示,但是不会对应用程序有任何伤害。)同时,在email属性上添加索引达成了我们的另一个目标:在6.1.4节里间接提到过的,如在注6.2节里说的,在email属性上的索引也解决了潜在的效率问题,当通过email地址查找用户时会避免对数据库进行全表扫描。
913 |
914 | ## 6.2 添加安全的密码
915 |
916 | 既然我们已经为name和email属性定义了有效性验证,我们以及准备好添加最后一个很基础的用户属性:安全的密码。实现的方法是要求每个用户拥有一个密码(还有密码确认),然后在数据库里储存哈希版的密码。(这里可能有些读者会产生困惑。在当前的环境,哈希不是指4.3.3节中提到的Ruby的数据结构哈希,而是指应用不可逆的哈希函数对输入数据处理的结果。)我们也依据所给的密码进行用户验证。我们将在第八章通过用户验证允许用户登陆我们的网站。
917 |
918 | 为了验证用户名和用户提交的密码是否匹配,我们的方法是通过对用户提交的密码进行哈希运算,然后用我们得到的值和数据库里储存的值进行比较。假如它们两个相等则说明用户提交的密码是正确的,用户通过验证,否则要求用户重新输入密码。通过比较哈希值而不是直接比较原始密码的方式,我们就不需要保存用户的原始密码来验证用户。这意味着,即使我们的数据库被脱库(指数据库被黑客下载)我们网站的用户的密码仍然是安全的。
919 |
920 | ### 6.3.1 用哈希算法处理过的密码
921 | 大部分的密码安全机制都是通过Rails中的一个方法,has_secure_password,实现的。我们在User模型里添加如下代码:
922 |
923 | ```ruby
924 | class User < ApplicationRecord
925 | .
926 | .
927 | .
928 | has_secure_password
929 | end
930 |
931 | ```
932 |
933 | 在模型里包含这个方法会添加以下功能:
934 | * 会把被哈希过的安全的password_digest添加到数据库
935 | * 生成一对虚拟的属性(password和password_confirmation),包含要求创建用户对象时要求它们一致的有效性验证
936 | * 当密码正确时authenticate方法返回user对象,否则返回false
937 |
938 | 为了让has_secure_password正常工作,仅要求相应的模型中有一个名为password_digest的(密码摘要)属性。(digest是来自于cryptographic
939 | 哈希函数的术语。这里,哈希过的密码和密码digest是一样的。)在User模型的例子中,生成如图6.8显示的数据模型。
940 |
941 | 
942 |
943 | 为了实现图6.8的数据模型,我们需要为user模型添加password_digest属性。我们先生成一个数据迁移来实现这个目标。我们可以为数据迁移文件起任何我们想要的名称,但是用to_users结尾会方便一些。因为Rails通过识别它会自动构建为users表添加列的数据迁移。因此我们为数据迁移文件起名为add_password_digest_to_users,显示如下:
944 | ```ruby
945 | $ rails generate migration add_password_digest_to_users password_digest:string
946 | ```
947 |
948 | 这里我们也提供了参数password_digest:string,我们想要添加的属性的名称和类型。(比较这个和代码清单6.1里生成的创建users表的数据迁移,我们为Rails提供了足够的信息来创建整个数据迁移,如代码清单6.32所示:
949 |
950 | ```ruby
951 | 代码清单 6.32:为用户表添加password_digest列的数据迁移。
952 | # db/migrate/[timestamp]_add_password_digest_to_users.rb
953 | class AddPasswordDigestToUsers < ActiveRecord::Migration
954 | def change
955 | add_column :users, :password_digest, :string
956 | end
957 | end
958 | ```
959 |
960 | 代码清单6.32使用add_column方法来添加password_digest列到users表。应用它,我们将迁移数据库:为了创建密码摘要,has_secure_password使用最先进技术的哈希函数叫做[bcrypt](http://en.wikipedia.org/wiki/Bcrypt)。通过用bcrypt哈希密码,我们确保攻击者不会登进网站,及时它们拥有一份数据库备份。为了在示例网站使用bcrypt,我们需要把bcrypt gem添加到Gemfile(代码清单6.33)。
961 |
962 | ```
963 | 代码清单 6.33: Adding bcrypt to the Gemfile.
964 | source 'https://rubygems.org'
965 |
966 | gem 'rails', '5.0.0'
967 | gem 'bcrypt', '3.1.7'
968 | .
969 | .
970 | .
971 | ```
972 | 然后运行bundle install,如往常一样:
973 | ```
974 | $ bundle install
975 | ```
976 |
977 | ### 6.3.2 安全密码(has secure password)
978 |
979 | 既然我们已经为User模型添加了has_secure_password方法要求的password_digest属性并且安装了bcrypt Gem,我们现在把has_secure_password添加到User模型,如代码清单6.34所示:
980 |
981 | ```ruby
982 | 代码清单 6.34:为User模型添加has_secure_password。 红色
983 | # app/models/user.rb
984 | class User < ApplicationRecord
985 | before_save { self.email = email.downcase }
986 | validates :name, presence: true, length: { maximum: 50 }
987 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
988 | validates :email, presence: true, length: { maximum: 255 },
989 | format: { with: VALID_EMAIL_REGEX },
990 | uniqueness: { case_sensitive: false }
991 | has_secure_password
992 | end
993 | ```
994 | 如在代码清单6.34说明的,红色表示现在测试失败。你可以通过命令行确认
995 |
996 | ```terminal
997 | 代码清单 6.35: 红色
998 | $ bundle exec rails test
999 | ```
1000 |
1001 | 原因是,如在6.3.1节里提到的,has_secure_password要求虚拟属性password和password_confirmation的值非空并且一致。但是在代码清单6.25里的测试创建的@user变量还没有这些属性:
1002 |
1003 | ```ruby
1004 | def setup
1005 | @user = User.new(name: "Example User", email: "user@example.com")
1006 | end
1007 | ```
1008 | 所以,为了再次让测试集通过,我们需要添加password和password_confirmation,如代码清单6.36显示的。
1009 | ```
1010 | 代码清单 6.36:添加password和password_confirmation。 绿色
1011 | # test/models/user_test.rb
1012 | require 'test_helper'
1013 |
1014 | class UserTest < ActiveSupport::TestCase
1015 |
1016 | def setup
1017 | @user = User.new(name: "Example User", email: "user@example.com",
1018 | password: "foobar", password_confirmation: "foobar")
1019 | end
1020 | .
1021 | .
1022 | .
1023 | end
1024 | ```
1025 |
1026 | 现在我们看到了添加has_secure_password的用处(6.3.4节)了,但是现在我们还需要添加密码长度的限制。
1027 |
1028 | ### 6.3.3 密码长度
1029 |
1030 | 一般来说限制密码的最小长度可以让密码更难破译,这对程序设计来说是好的实践方法。在Rails里有许多[强制密码强度](http://lmgtfy.com/?q=rails+enforce+password+strength)的选择,但是简单起见,我们只是通过限制密码的最短长度和要求密码非空来加强我们的密码强度。要求密码不能少于6为是合理的,密码长度有效性验证测试如代码清单6.38所示:
1031 |
1032 | ```ruby
1033 | 代码清单 6.38: 测试密码的最小长度。 红色
1034 | # test/models/user_test.rb
1035 | require 'test_helper'
1036 |
1037 | class UserTest < ActiveSupport::TestCase
1038 |
1039 | def setup
1040 | @user = User.new(name: "Example User", email: "user@example.com",
1041 | password: "foobar", password_confirmation: "foobar")
1042 | end
1043 | .
1044 | .
1045 | .
1046 |
1047 | test "password should be present (nonblank)" do
1048 | @user.password = @user.password_confirmation = " " * 6
1049 | assert_not @user.valid?
1050 | emd
1051 |
1052 | test "password should have a minimum length" do
1053 | @user.password = @user.password_confirmation = 'a' * 5
1054 | assert_not @user.valid?
1055 | end
1056 | end
1057 | ```
1058 |
1059 | 注意在代码清单6.38里同时赋值的使用:
1060 | ```ruby
1061 | @user.password = @user.password_confirmation = 'a' * 5
1062 | ```
1063 | 这条语句同时给password和password_confirmation赋值,(在这个例子,字符串长度为5,使用和代码清单6.14一样的乘法构建)。
1064 |
1065 | 你可能猜出来通过使用和maximum相应的minimum长度限制来验证用户名的长度的代码(代码清单6.16):
1066 | ```ruby
1067 | validates :password, length: { minimum: 6 }
1068 | ```
1069 |
1070 | 把这个和presence验证组合(6.2.2节)来阻止空密码,User模型如代码清单6.39所示。(它证明了has_secure_password方法虽然包含存在验证,但是它仅适用于新纪录,当更新用户信息时会有问题。)
1071 | ```ruby
1072 | 代码清单 6.39: 完整的安全的密码实现。绿色
1073 | # app/models/user.rb
1074 | class User < ApplicationRecord
1075 | before_save { self.email = email.downcase }
1076 | validates :name, presence: true, length: { maximum: 50 }
1077 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
1078 | validates :email, presence: true, length: { maximum: 255 },
1079 | format: { with: VALID_EMAIL_REGEX },
1080 | uniqueness: { case_sensitive: false }
1081 | has_secure_password
1082 | validates :password, presence: true, length: { minimum: 6 }
1083 | end
1084 | ```
1085 |
1086 | 在这里,测试应该是绿色的:
1087 | ```
1088 | 代码清单 6.40: 绿色
1089 | $ bundle exec rails test:models
1090 | ```
1091 |
1092 | ### 6.3.4 用户注册和验证
1093 |
1094 | 既然基本的用户模型创建完毕,我们现在开始在数据库里添加一个用户,这样我们就可以着手创建显示用户信息的页面(7.1节);我们也将通过实例来验证一下在User模型中添加了has_secure_password方法后的效果,包括authenticate方法的作用。
1095 |
1096 | 因为用户还不能通过网页来注册Sample App--这是第七章的目标--我们先通过Rails控制台来手动创建一个新用户。简单起见,我们将使用在6.1.3节讨论过的create方法。不过我们现在不启用沙盒了,这样用户就可以被保存到数据库中。也就是说我们开启一个普通的rails console会话,然后用有效的name和email,有效的password和password_confirmation来创建一个用户:
1097 |
1098 | ```ruby
1099 | $ rails console
1100 | >> User.create(name: "Michael Hartl", email: "mhartl@example.com",
1101 | ?> password: "foobar", password_confirmation: "foobar")
1102 | => #
1105 | ```
1106 |
1107 | 为了检查上面的命令确实在数据库中添加了一条新用户记录,让我们用SQLite数据库浏览器来看看开发数据库里的users表,如图6.9所示。(假如你正使用云IDE,你应该先下载数据库文件图6.5)。注意在图6.8里的各列对应了数据模型相应的属性。
1108 |
1109 | 
1110 |
1111 | 返回控制台,我们能从代码清单6.39里看到has_secure_password的效果,即password_digest属性的值:
1112 |
1113 | ```ruby
1114 | >> user = User.find_by(email: "mhartl@example.com")
1115 | >> user.password_digest
1116 | => "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"
1117 | ```
1118 |
1119 | 它的值就是我们新建的用户的哈希版的密码(“foobar”)。因为它是通过bcrypt gem构建,因此想使用摘要来还原密码估计是不太现实的。
1120 |
1121 | 如6.3.1节提到的,has_secue_password自动添加为相应的数据模型添加了authenticate方法。这个方法决定是否所给的密码对某个特定的对象(这里是指用户对象)是有效的,它是通过计算这个对象密码的摘要和数据库里的password_digest结果是否一致。通过我们刚创建的用户我们可以先试试几个无效的密码:
1122 |
1123 | ```ruby
1124 | >> user.authenticate("not_the_right_password")
1125 | false
1126 | >> user.authenticate("foobaz")
1127 | false
1128 | ```
1129 | 这里user.authenticate对无效的密码返回false。假如我们使用正确的密码,authenticate会返回用户自身:
1130 | ```
1131 | >> user.authenticate("foobar")
1132 | => #
1135 | ```
1136 |
1137 | 在第八章,我们将使用authenticate方法来验证用户登陆。实际上,它也证明authenticate是否返回用户本身并不重要,我们主要关心的是它返回了逻辑值为true的值。因为用户对象不是nil或false,所以它也符合我们的要求:
1138 |
1139 | ```ruby
1140 | >> !!user.authenticate("foobar")
1141 | => true
1142 | ```
1143 | ## 6.4 结语
1144 |
1145 | 在这章我们从零开始,创建了一个包含name、email和password属性的用户(User)模型,并添加了几个重要的强制有效的验证规则。另外我们现在也有了安全地通过密码验证用户的方法。这里我们仅仅通过十二行代码就取得了大量的功能。
1146 |
1147 | 在下一章,第七章,我们将为新用户创建一个注册表和显示用户信息的页面。在第八章,我们使用6.3节的验证机制来让用户登陆网站。
1148 |
1149 | 假如你正使用Git,假如你还没有提交,现在是提交的好时候:
1150 |
1151 | ```ruby
1152 | $ bundle exec rails test
1153 | $ git add -A
1154 | $ git commit -m "Make a basic User model (including secure passwords)"
1155 | ```
1156 |
1157 | 然后合并进主分支,推送至远程仓库:
1158 |
1159 | ```terminal
1160 | $ git checkout master
1161 | $ git merge modeling-users
1162 | $ git push
1163 | ```
1164 |
1165 | 为了让用户模型在生产环境也可以工作,我们需要先在Heroku上运行数据库迁移,我们通过heroku run完成:
1166 |
1167 | ```
1168 | $ bundle exec rails test
1169 | $ git push heroku
1170 | $ heroku run rails db:migrate
1171 | ```
1172 |
1173 | 我们可以通过生产环境里的控制台来确认这个工作:
1174 | ```ruby
1175 | $ heroku run console --sandbox
1176 | >> User.create(name: "Michael Hartl", email: "michael@example.com",
1177 | ?> password: "foobar", password_confirmation: "foobar")
1178 | => #
1181 | ```
1182 |
1183 | ### 6.4.1 这章我们学到了什么
1184 | * 数据迁移允许我们修改应用程序的数据模型
1185 | * ApplicationRecord有创建和操作数据模型的大量方法
1186 | * ApplicationRecord validates方法允许我们在模型里放置数据限制规则
1187 | * 普通的validates包括非空、长度和格式
1188 | * 正则表达式是晦涩的,但是很强大
1189 | * 当允许数据库水平的属性值唯一,定义数据库索引提高了查询效率
1190 | * 我们可以使用内建的has_secure_password方法为模型添加安全的密码
1191 |
1192 | ## 6.5 练习
1193 |
1194 | 1. 从代码清单6.31里添加一个email小写化测试,如代码清单6.41所示。这个测试使用reload方法为了从数据库里加载一个方法和assert_equal方法。为了确认代码清单6.41测试了正确的需求,先注释before_save行,让它变红,然后去掉注释让它变绿。
1195 |
1196 | 2. 通过运行测试集,确认before_save回叫能使用“bang”方法email.downcase!来直接修改email属性,如代码清单6.42所示。
1197 | 3. 如在6.2.4节提到的,在代码清单6.21里的email正则表达式允许在域名里有连续的点的无效email地址,例如“foo@bar..com”格式的地址。把这个地址加入到代码清单6.19里来得到失败测试,然后使用在代码清单6.43里更复杂的正则表达式来让测试通过。
1198 |
1199 | ```ruby
1200 | 代码清单 6.41:以代码清单6.31为基础,添加email小写化测试。
1201 | # test/models/user_test.rb
1202 | require 'test_helper'
1203 |
1204 | class UserTest < ActiveSupport::TestCase
1205 |
1206 | def setup
1207 | @user = User.new(name: "Example User", email: "user@example.com",
1208 | password: "foobar", password_confirmation: "foobar")
1209 | end
1210 | .
1211 | .
1212 | .
1213 | test "email addresses should be unique" do
1214 | duplicate_user = @user.dup
1215 | duplicate_user.email = @user.email.upcase
1216 | @user.save
1217 | assert_not duplicate_user.valid?
1218 | end
1219 |
1220 | test "email addresses should be saved as lower-case" do
1221 | mixed_case_email = "Foo@ExAMPle.CoM"
1222 | @user.email = mixed_case_email
1223 | @user.save
1224 | assert_equal mixed_case_email.downcase, @user.reload.email
1225 | end
1226 |
1227 | test "password should have a minimum length" do
1228 | @user.password = @user.password_confirmation = "a" * 5
1229 | assert_not @user.valid?
1230 | end
1231 | end
1232 | ```
1233 |
1234 | ```ruby
1235 | 代码清单 6.42:before_save回叫函数的另一种实现方法。绿色
1236 | # app/models/user.rb
1237 | class User < ApplicationRecord
1238 | before_save { email.downcase! }
1239 | validates :name, presence: true, length: { maximum: 50 }
1240 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
1241 | validates :email, presence: true, length: { maximum: 255 },
1242 | format: { with: VALID_EMAIL_REGEX },
1243 | uniqueness: { case_sensitive: false }
1244 | has_secure_password
1245 | validates :password, presence: true, length: { minimum: 6 }
1246 | end
1247 | ```
1248 |
1249 | ```ruby
1250 | 代码清单 6.43:不允许域名中包括两个及以上连续的点。绿色
1251 | # app/models/user.rb
1252 | class User < ApplicationRecord
1253 | before_save { email.downcase! }
1254 | validates :name, presence: true, length: { maximum: 50 }
1255 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
1256 | validates :email, presence: true,
1257 | format: { with: VALID_EMAIL_REGEX },
1258 | uniqueness: { case_sensitive: false }
1259 | has_secure_password
1260 | validates :password, presence: true, length: { minimum: 6 }
1261 | end
1262 | ```
1263 |
--------------------------------------------------------------------------------
/rendering_with_a_flash_message.md:
--------------------------------------------------------------------------------
1 | ### 8.1.4带闪现信息渲染
2 |
3 | 回忆7.3.3节,我们使用用户模型的错误信息显示注册错误。这些错误是和特殊的Active
4 | Record对象联系的,但是这个策略在这里不工作,因为回话不是Active
5 | Record模型。相反,我们将在失败的登陆上显示一个闪现的消息。首先,有点不正确,的企图显示在代码清单8.6.
6 |
7 | ```
8 | 代码清单 8.6: An (unsuccessful) attempt at handling failed login.
9 | # app/controllers/sessions_controller.rb
10 | class SessionsController < ApplicationController
11 |
12 | def new
13 | end
14 |
15 | def create
16 | user = User.find_by(email: params[:session][:email].downcase)
17 | if user && user.authenticate(params[:session][:password])
18 | # Log the user in and redirect to the user's show page.
19 | else
20 | flash[:danger] = 'Invalid email/password combination' # Not quite right!
21 | render 'new'
22 | end
23 | end
24 |
25 | def destroy
26 | end
27 | end
28 | ```
29 |
30 | 因为显示在站点布局里的闪现消息(代码清单7.25),flash[:danger]信息自动显示;因为Bootstrap的CSS,它自动得到好看的样式化(图8.5)。
31 |
32 | 
33 |
34 | 不幸的是,如在文本和在代码清单8.6里的注释里提到的,这段代码不是十分正确。页面看上去很好,不过,发生什么了?问题是flash的内容是持续一个请求,但是--不像重定向,我们在代码清单7.24里使用的--用render重新渲染模板不算请求。结果是flash信息显示的比我们想要的一个请求长。例如,假如我们提交无效的登陆信息,然后点击主页,flash又显示了(图8.6)。解决这个瑕疵是8.1.5的任务。
35 |
36 | 
37 |
38 | ### 8.1.5 flash测试
39 | 不正确的flash行为是我们应用程序的小bug。根据测试指导(旁注3.3),这恰好属于我们应该写测试来捕捉错误以便它不再发生的情况。我们继续前因此写为登陆表单写一个短的集成测试。另外,记录bug,然后防止回归,这也给我们好的基础,为将来进一步测试登陆和退出。
40 |
41 | 我们通过为我们的应用程序的登陆行为生成集成测试:
42 | ```
43 | $ rails generate integration_test users_login
44 | invoke test_unit
45 | create test/integration/users_login_test.rb
46 | ```
47 |
48 | 接下来,我们需要测试捕捉显示在图8.5和8.6的序列。基本步骤如下:
49 | 1.访问登陆路径
50 | 2.确认新的会话表单正确的渲染
51 | 3.用一个无效的params哈希POST到会话路径
52 | 4.确认新会话表单再次渲染,flash信息显示
53 | 5.访问另一个页面(例如主页)
54 | 6.确认flash信息不显示在新页面。
55 |
56 | 以上步骤的测试实现显示在在代码清单8.7。
57 |
58 | ```
59 | 代码清单 8.7: A test to catch unwanted flash persistence. 红色
60 | # test/integration/users_login_test.rb
61 | require 'test_helper'
62 |
63 | class UsersLoginTest < ActionDispatch::IntegrationTest
64 |
65 | test "login with invalid information" do
66 | get login_path
67 | assert_template 'sessions/new'
68 | post login_path, session: { email: "", password: "" }
69 | assert_template 'sessions/new'
70 | assert_not flash.empty?
71 | get root_path
72 | assert flash.empty?
73 | end
74 | end
75 | ```
76 | 添加测试到代码清单8.7以后,登陆测试应该是红色的:
77 |
78 | ```
79 | 代码清单 8.8: 红色
80 | $ bundle exec rails test TEST=test/integration/users_login_test.rb
81 | ```
82 | 这显示了怎样运行一个(仅一个)测试文件,使用完整的文件路径。
83 |
84 | 让在列表8.7里的失败测试通过的方法是用flash.now来代替flash,flash.now是是设计用来在渲染过的页面上显示flash消息的。不像flash的内容,flash.now的内容只要有有一个另外的请求就消失了,这就是我们在列表8.7里要测试的行为。随着提交,相应的应用程序代码如列表8.9所示。
85 |
86 | ```
87 | 代码清单 8.9: Correct code for failed login. 绿色
88 | # app/controllers/sessions_controller.rb
89 | class SessionsController < ApplicationController
90 |
91 | def new
92 | end
93 |
94 | def create
95 | user = User.find_by(email: params[:session][:email].downcase)
96 | if user && user.authenticate(params[:session][:password])
97 | # Log the user in and redirect to the user's show page.
98 | else
99 | flash.now[:danger] = 'Invalid email/password combination'
100 | render 'new'
101 | end
102 | end
103 |
104 | def destroy
105 | end
106 | end
107 | ```
108 | 我们然后能确认登陆集成测试和完整的测试集是绿色的:
109 |
110 | ```
111 | 代码清单 8.10: 绿色
112 | $ bundle exec rails test TEST=test/integration/users_login_test.rb
113 | $ bundle exec rails test
114 | ```
115 |
116 | ## 8.2 登陆
117 |
118 | 既然我们的登陆表单很处理无效的提交,下一步是通过实际的用户登陆正确处理有效的提交。在这节,我们将用临时的会话cookie来登陆用户,当浏览器关闭时,会话自动失效。在8.4节,我们将添加持续性的会话,即使浏览器关闭也影响。
119 |
120 | 实现会话将需要定义大量的相关的函数,为使用多个控制器和视图。你可能回忆4.2.5节,Ruby提供了module工具,把这些函数打包在一个地方。方便地,当生成会话控制器时(8.1.1节)会话辅助方法模块也自动生成了。恶气,这样的辅助方法会自动包含在Rails视图里;通过包含模块进入所有控制器的基类(ApplicationController),我们准备让它们在我们的控制器也工作(列表8.11)。
121 |
122 | ```
123 | 代码清单 8.11: Including the Sessions helper module into the Application
124 | controller.
125 | # app/controllers/application_controller.rb
126 | class ApplicationController < ActionController::Base
127 | protect_from_forgery with: :exception
128 | include SessionsHelper
129 | end
130 | ```
131 | 随着配置结束,我们现在准备写登陆用户的代码。
132 |
133 | ### 8.2.1 log_in方法
134 |
135 | 有了Rails定义的session方法的帮助,让用户登陆是简单的。(这个方法是分开的,不同于8.1.1节生成的会话控制器)我们能对待session为哈希,可以使用如下方法赋值:
136 |
137 | ```
138 | session[:user_id]=user.id
139 | ```
140 | 这在用户的浏览器放置了临时的cookie,包含加密了的用户的id,它允许我们在后续的网页使用session[:user_id]来获取id。相比cookie方法创建的持续性cookie(8.4节),session方法创建的cookie当浏览器关闭是立即过期。
141 |
142 | 因为我们想要使用同样的登陆技术在几个不同的地方,我将定义一个名为log_in的方法,在Session
143 | helper里,如列表8.12显示。
144 |
145 | ```
146 | 代码清单 8.12: The log_in function.
147 | # app/helpers/sessions_helper.rb
148 | module SessionsHelper
149 |
150 | # Logs in the given user.
151 | def log_in(user)
152 | session[:user_id] = user.id
153 | end
154 | end
155 | ```
156 |
157 | 因为临时cookie创建使用了session方法自动加密,列表8.12里的代码是安全的,攻击者没有办法来使用登陆用户的会话信息。这仅仅应用到session方法初始化的临时会话,不过,不是使用cookies方法创建的持久性会话。持久性cookie是容易收到会话劫持攻击的,所以在8.4节,我们将不得不是更小心关于我们放在用户浏览器的信息。
158 |
159 | 随着我们在列表8.12里定义的log_in方法,我们现在准备完成会话create动作,通过登陆用户,然后重定向到用户的简介页面。结果如列表8.13显示。
160 |
161 | ```
162 | 代码清单 8.13: Logging in a user.
163 | # app/controllers/sessions_controller.rb
164 | class SessionsController < ApplicationController
165 |
166 | def new
167 | end
168 |
169 | def create
170 | user = User.find_by(email: params[:session][:email].downcase)
171 | if user && user.authenticate(params[:session][:password])
172 | log_in user
173 | redirect_to user
174 | else
175 | flash.now[:danger] = 'Invalid email/password combination'
176 | render 'new'
177 | end
178 | end
179 |
180 | def destroy
181 | end
182 | end
183 | ```
184 | 注意简短的重定向
185 | ```
186 | redirect_to user
187 | ```
188 | 我们在7.4.1节之前见过。Rails自动把它转化称用户的简介页面的路由:
189 | ```
190 | user_url(user)
191 | ```
192 | 随着在列表8.13里定义了create动作,在8.2节定义了登陆表单现在应该工作了。它在应用程序的显示上没有任何效果,不过,所有直接查看浏览器会话的缺点,没有方法分辨你登陆了。作为长更多可见变化的第一步,在8.2.2节我们将使用用户的id从数据库里获取当前用户的信息。在8.2.3节,我们将改变应用程序布局文件的链接,包括到当前用户的简介的URL。
193 |
194 | ### 8.2.2 当前用户
195 |
196 | 在临时会话里安全的放置了用户的id,我们现在到了在后续的网页获取它的时候了,我们将通过定义current_user方法来在数据库里找到与会话id相应的用户。current_user的目的是允许例如
197 | ```
198 | <%= current_user.name %>
199 | ```
200 | 的结构和
201 | ```
202 | redirect_to current_user
203 | ```
204 | 为了查找当前用户,一个可能性是使用find方法,如在用户简介页面上的(列表7.5):
205 | ```
206 | User.find(session[:user_id])
207 | ```
208 |
209 | 但是回忆6.1.4节,假如用户不存在,find抛出了异常。这个行为在用户简介页面是合适的,因为当id是无效的时候它才会发生,但是目前的情况是session[:user_id]将常常是nil(例如,没有登陆的用户)。为了处理这个可能性,我们将使用相同的find_by方法,过去在create动作里用来通过email地址查找用户的的find_by方法,用id代替email:
210 |
211 | ```
212 | User.find_by(id: session[:user_id])
213 | ```
214 | 不是抛出例外,这个方法当id是无效的时候返回nil(表示没有这样的用户)。
215 |
216 | 我们现在能定义current_user方法如下:
217 | ```
218 | def current_user
219 | User.find_by(id: session[:user_id])
220 | end
221 | ```
222 | 这个将会很好的工作,但是它会使用几次数据库,假如,current_user在一个页面显示几次。相反,我们将使用常见的Ruby惯例,通过储存User.find_by的结果到一个实例变量,第一次它使用数据库,但是在后续的调用立即返回实例变量。
223 | ```
224 | if @current_user.nil?
225 | @current_user = User.find_by(id: session[:user_id])
226 | else
227 | @current_user
228 | end
229 | ```
230 |
231 | 回忆在4.2.3节里见过的或操作付||,我们也能重写为:
232 | ```
233 | @current_user = @current_user || User.find_by(id: session[:user_id])
234 | ```
235 | 因为User对象是在逻辑上下文里是true,对find_by的调用仅仅当@current_user仍然没有被赋值是执行。
236 | 尽管之前的代码将会工作,它不符合正确的Ruby的语言习惯;相反,正确给@current_user赋值的语法像这样:
237 |
238 | ```
239 | @current ||= User.find_by(id: session[:user_id])
240 | ```
241 | 这里使用了容易造成困惑的,但是经常使用的||=(“或相等”)操作符(旁注8.1)
242 |
243 | 旁注8.1 *$@!是||=什么?
244 |
245 |
--------------------------------------------------------------------------------