├── README.md ├── chapter0_forward.md ├── chapter10_account_activation_and_password_reset.md ├── chapter11_user_microposts.md ├── chapter12_following_users.md ├── chapter1_from_zero_to_deploy.md ├── chapter2_a_toy_app.md ├── chapter3_mostly_static_pages.md ├── chapter4_rails_flavored_ruby.md ├── chapter5_filling_in_the_layout.md ├── chapter6_modeling_users.md ├── chapter7_sign_up.md ├── chapter8_log_in_log_out.md ├── chapter9_updating_showing_and_deleting_users.md └── rendering_with_a_flash_message.md /README.md: -------------------------------------------------------------------------------- 1 | # Rails 5 教程 2 | 3 | 4 | 这个教程是根据Michael Hartl的Rails Tutorial (3rd)的在线版翻译的, 5 | 部分章节(应该是10章-12章)只是初译,欢迎Pull Request。 6 | 7 | 代码已经更新至Rails 5. 8 | 9 | # 贡献力量 10 | 11 | 如果想做出贡献的话,你可以: 12 | 13 | - 帮忙校对,挑错别字、病句等等 14 | - 提出修改建议 15 | - 提出术语翻译建议 16 | 17 | # 翻译建议 18 | 19 | 如果你愿意一起校对的话,请仔细阅读: 20 | 21 | - 使用markdown进行翻译,文件名必须使用英文,因为中文的话gitbook编译的时候会出问题 22 | - 翻译后的文档请放到source文件夹下的对应章节中,然后pull request即可,我会用gitbook编译成网页 23 | - 工作分支为gh-pages,用于GitHub的pages服务 24 | - fork过去之后新建一个分支进行翻译,完成后pull request这个分支,没问题的话我会合并到gh-pages分支中 25 | - 有其他任何问题都欢迎发issue,我看到了会尽快回复 26 | 27 | 谢谢! 28 | 29 | # 关于术语 30 | 31 | 翻译术语的时候请参考这个流程: 32 | 33 | - 尽量保证和已翻译的内容一致 34 | - 尽量先搜索,一般来说编程语言的大部分术语是一样的,可以参考[微软官方术语搜索](http://www.microsoft.com/Language/zh-cn/Search.aspx) 35 | - 如果以上两条都没有找到合适的结果,请自己决定一个合适的翻译或者直接使用英文原文,后期校对的时候会进行统一 36 | 37 | # 参考流程 38 | 39 | 有些朋友可能不太清楚如何帮忙翻译,我这里写一个简单的流程,大家可以参考一下: 40 | 41 | 1. 首先fork我的项目 42 | 2. 把fork过去的项目也就是你的项目clone到你的本地 43 | 3. 在命令行运行 `git branch develop` 来创建一个新分支 44 | 4. 运行 `git checkout develop` 来切换到新分支 45 | 5. 运行 `git remote add upstream https://github.com/numbbbbb/the-swift-programming-language-in-chinese.git` 把我的库添加为远端库 46 | 6. 运行 `git remote update`更新 47 | 7. 运行 `git fetch upstream gh-pages` 拉取我的库的更新到本地 48 | 8. 运行 `git rebase upstream/gh-pages` 将我的更新合并到你的分支 49 | 50 | 这是一个初始化流程,只需要做一遍就行,之后请一直在develop分支进行修改。 51 | 52 | 如果修改过程中我的库有了更新,请重复6、7、8步。 53 | 54 | 修改之后,首先push到你的库,然后登录GitHub,在你的库的首页可以看到一个 `pull request` 按钮,点击它,填写一些说明信息,然后提交即可。 55 | 56 | 有任何问题可以发邮件wangqsh999 at icloud dot com 57 | 58 | 本文所有版权归原作者所有,遵循原作者的版权协议。 59 | 60 | 禁止将本翻译教程用于任何商业用途。 61 | -------------------------------------------------------------------------------- /chapter0_forward.md: -------------------------------------------------------------------------------- 1 | #前言 2 | 3 | 我之前创立的公司(CD Baby)就是首批高调地转到使用Ruby on Rails的公司之一,然后甚至更加高调地返回了PHP阵营(可以谷歌搜索我的名字去阅读相关戏剧性的事件)。Michael Hartl 写的这本书一发布就得到很多人的推荐,因此我决定再尝试一下.正是这本Ruby on Rails 教程让我再次回到Rails阵营。 4 | 虽然我曾经读过很多Rails相关书籍,并以我自己的方式应用到实际工作中,但是这本教程才让我觉得我终于真正地掌握了Rails。这本书所有的东西都是以“Rails之道“完成,刚开始我感觉很不自然,读完这本书后却发现一切都是最自然不过了。这本书也是唯一的一本从始至终都遵循着TDD(测试驱动开发)模式的Rails书籍,TDD模式一直以来被各类专家所推崇,但是从来没有像这本教程一样讲的简单、透彻。另外,在教程里把Git,GitHub还有Heroku等都包含进来,让我们体验到了真实项目开发是如何进行的。本书中的代码例程不是孤立存在的。 5 | 线性叙述方式是一种很棒的写作方法。我个人怀着极强的动力花费了整整三天的时间阅读了一遍这本教程,并把每一章节后的例程和挑战题目都做了一遍。从头到尾地完整地做一遍,不跳过任何内容,最终你定会受益匪浅。 6 | 欣赏吧! 7 | [Derek Sivers](sivers.org) CD Baby 创始人 8 | 9 | 10 | #感谢 11 | 12 | 本Ruby on Rails 教程借鉴了我早期所写的Rails书籍《RailsSpace》,同时还有我的合著作者Aurelius Prochazka。在此我想感谢Aure对于《RailsSpace》的工作付出以及对本教程的大力支持。我也想感谢Debra Williams Cauley,他是《RailsSpace》和《Ruby on Rails 教程》的编辑;只要她经常带我去打棒球,我将会一直为她写书。 13 | 我要感谢这些年来在Ruby开发上给我指导和启发的那些Ruby开发者们,他们的名单如下:David Heinemeier Hansson(Rails创始人), Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Mark Bates, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest,Pivotal 实验室的好人们,Heroku团队,thoughtbot团队,还有GitHub团队。最后,还有很多很多的读者——名单太长了——他们在我写这本书的过程中贡献了大量的bug报告,以及很多很好的建议。我非常诚挚的感谢他们对我提供的这些帮助,同时也让这本书变得尽可能的完美。 14 | 15 | #关于作者 16 | 17 | Michael Hartl 是这本书《Ruby on Rails 教程》的作者,网页应用开发的倡导者之一,并且是Softcover自出版平台的联合创始人。他早期的从业经历包括编写和开发RailsSpace,一本年代有些久远的Rails教程书籍,以及开发Insoshi(建站软件),一款曾经很流行但是现在已经过时的基于Ruby on Rails开发的社交网络平台。在2011年,因为他对Ruby社区的杰出贡献,Michael获得了Ruby英雄奖章。他在哈佛大学获取了学士学位,在加利福尼亚理工学院拿到了物理学博士学位,同时也是美国著名的创业孵化器Y Combinator创业者课程的毕业生。 18 | 19 | #版权和许可 20 | Ruby on Rails Tutorial: Learn Web Development withe Rails. 版权归Michael Hartl所有。本教程内所有的源代码遵守MIT License和Bearware License。 21 | 22 | -------------------------------------------------------------------------------- /chapter2_a_toy_app.md: -------------------------------------------------------------------------------- 1 | #2 玩具网站 2 | 3 | 这章我们将用一个名字叫做玩具(toy)的应用程序来炫耀一下Rails的强大。通过使用**scaffold**快速生成应用程序, 目的是让我们对Ruby on Rails编程(和网页开发的基本原理)有个总体的认知。它会自动创建大量函数。如同我们在[注1.2(#b1.2)]中讨论过的一样,从第三章开始我们将使用跟第二章完全相反的方式来开发网站,也就是我们不再使用脚手架来生成代码,而是从零开始,循序渐进地开发网站,每一个新概念都会有详细的讲解,但是为了能够快速浏览(还有创作的成就感),脚手架在这些方面还是有着无可替代的优势。玩具网站允许我们通过URL与它进行交互,还让我们直观地了解到Rails应用的内在结构,也包括Rails偏好的REST架构的首次演示。 4 | 5 | 和本书后续的网站相同,玩具网站将包含用户和用户发布的微博(类似于迷你版的新浪微博)。功能模块还需要在后续开发,而且很多步骤看起来像变魔术,不过不用担心:后面的完整的示例程序(sample网站)将会从零开始开发和这个网站功能差不多的新的网站,从[第三章](#3)开始,而且我还会提供大量的资料供后续参考。而现在,你所需要的是耐心和一点信心——这本书的重点在于带你透过现象看本质,通过脚手架这个例子对Rails有更深入的理解。 6 | 7 | ##2.1 前期准备 8 | 9 | 在这一节,我们对玩具网站做一个概要设计。如同在[1.3](#1.3)中一样,我们将从创建这个应用的骨架入手,还是用`rails new`命令,同时要记得加上Rails的版本号: 10 | 11 | ``` 12 | $ cd ~/workspace 13 | $ rails _5.0.0_ new toy_app 14 | $ cd toy_app/ 15 | 16 | ``` 17 | 18 | 如果运行了以上的命令出现类似这样的错误“Could not find 'railties'”(找不到rails相关资源),那说明你没有安装好正确的Rails版本,这时你要好好确认一下是否严格按照[命令清单1.1](#l1.1)的步骤来安装Rails。(如果你使用的是我们推荐的云IDE([1.2.1](#1.2.1))注意这第二个应用项目是可以和上一个应用建立在同一个工作空间里的,不需要再重新创建一个新的工作空间。有时候你新建了一个新的项目但是看不到相关文档,这时候你需要点击右上角的齿轮状图标,选择“Refresh File Tree”(刷新文件目录树)。) 19 | 20 | 下一步,我们用文本编辑器将Gemfile文档更新如[代码清单2.1](#l2.1),以便Bundler命令使用。 21 | 22 | 代码清单2.1:toy app使用的Gemfile文档。 23 | 24 | ``` 25 | source 'https://rubygems.org' 26 | 27 | gem 'rails', '5.0.0' 28 | gem 'sass-rails', '5.0.2' 29 | gem 'uglifier', '2.5.3' 30 | gem 'coffee-rails', '4.1.0' 31 | gem 'jquery-rails', '4.0.3' 32 | gem 'turbolinks', '2.3.0' 33 | gem 'jbuilder', '2.2.3' 34 | gem 'sdoc', '0.4.0', group: :doc 35 | 36 | group :development, :test do 37 | gem 'sqlite3', '1.3.9' 38 | gem 'byebug', '3.4.0' 39 | gem 'web-console', '2.0.0.beta3' 40 | gem 'spring', '1.1.3' 41 | end 42 | 43 | group :production do 44 | gem 'pg', '0.17.1' 45 | gem 'rails_12factor', '0.0.2' 46 | end 47 | ``` 48 | 注意,[代码清单2.1](#l2.1)其实和[代码清单1.14](#l1.14)是一样的。 49 | 50 | 如同[1.5](#1.5)一样,接下来我们将使用`--without production`选项,只安装本地开发所需的gem,而不安装生产环境所需的gem: 51 | 52 | ``` 53 | $ bundle install --without production 54 | 55 | ``` 56 | 57 | 最后,我们将玩具网站纳入Git版本管理中: 58 | 59 | ``` 60 | $ git init 61 | $ git add -A 62 | $ git commit -m "Initialize repository" 63 | 64 | ``` 65 | 66 | 点击Bitbucket([图2.1](#p2.1))上的“Create”按钮,新建一个[新仓库](https://bitbucket.org/repo/create),然后将代码推送到这个远端仓库中。 67 | 68 | ``` 69 | $ git remote add origin git@bitbucket.org:/toy_app.git 70 | $ git push -u origin --all # 第一次推送代码到远程仓库 71 | 72 | ``` 73 | 74 | ![create_demo_repo_bitbucket](http://i.imgur.com/3swZBtG.png) 75 | 图2.1 在Bitbucket上新建一个toy app所需的托管仓库 76 | 77 | 最后,来部署验证一下,我建议完全按照之前的例程“hello,world!”中的步骤操作,请回去参考[代码清单1.8](#l1.8)和[代码清单1.9](#l1.9)。然后将改动提交,并推送到Heroku上: 78 | 79 | ``` 80 | $ git commit -am "Add hello" 81 | $ heroku create 82 | $ git push heroku master 83 | 84 | ``` 85 | 86 | (如同在[1.5](#1.5)一样,你可能会看到很多警告信息,现在你完全可以无视他们。我们会在[7.5](#7.5))处理这些信息。)除了Heroku 应用的地址不一样之外,其他的应该都和[图1.18](#p1.18)一样。 87 | 88 | 现在我们已经准备好开始开发这个玩具网站了。按照正常流程,开发Web应用的第一步应该是创建数据模型(data model),它是应用程序所需结构的体现。在我们这个项目中,玩具网站将会是一个微博,只有用户和微博。所以,我们将从用户模型开始开发我们的app([2.1.1](#2.1.1)),然后再添加一个微博模型([2.1.2](#2.1.2))。 89 | 90 | 91 | ###2.1.1 玩具网站的用户模型 92 | 93 | 在网上有多少种类型的注册表单,就对应有多少种类型的用户数据模型;我们将从最简单的开始。我们的网据网站用户将会有一个身份验证号(id),用整数类型(integer)来表示,一个公共可见的用户名(name),用字符串类型(string)来表示,还有一个email地址(也是字符串string类型),email地址也可作为用户名使用。用户数据模型可以总结为[图2.2](#p2.2)所示: 94 | 95 | ![demo_user_model](http://i.imgur.com/gihl57a.png) 96 | 图2.2 用户数据模型 97 | 98 | 我们在[6.1.1](#6.1.1)中将会发现,[图2.2](#p2.2)中的“users”标签对应数据库中的表单名,而“id”,“name”,“email”将对应表中的列名。 99 | 100 | ###2.1.2 玩具网站的微博数据模型 101 | 102 | 微博数据模型的核心甚至比用户数据模型还要简单:微博数据模型只需一个id号和存放微博内容的content字段(类型为“text”)。但是我们还需要一个额外的复杂一些的字段:为了把每一篇微博和它的拥有者联系起来,我们要用到“user_id”字段,如[图2.3](#p2.3)所示。 103 | 104 | ![demo_micropost_model](http://i.imgur.com/0zVNzN3.png) 105 | 图2.3 微博数据模型 106 | 107 | 我们将在[2.3.3](#2.3.3)中了解到“user_id”字段的作用([11](#11)中的介绍更详细),这个字段可以让我们很容易实现一个用户拥有多个关联微博的想法。 108 | 109 | ##2.2 用户资源 110 | 111 | 在这一节里,我们将实现[2.1.1](#2.1.1)描述的用户数据模型,同时还要为这个模型实现一个web接口界面。这两者的结合就组成了我们的用户资源(Users resource),我们可以把用户(users)想象成一个对象(object),这个对象可以被创建,读取,更新,以及删除等操作,这些操作都可以通过[HTTP协议](http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol)在web页面上实现。正如在前面的简介中提到的,我们的用户资源将由Rails脚手架程序创建。我劝你现在还是不要太深究这些由脚手架生成的代码,因为在这个阶段,它只会让你感到更迷惑。 112 | 113 | 通过传递**scaffold**到**rails generate**脚本程序里生成Rails脚手架。**scaffold**命令的参数是资源名的单数形式(在这里,是“User”),同时还可以加上一些数据模型的属性的作为参数: 114 | 115 | ``` 116 | $ rails generate scaffold User name:string email:string 117 | invoke active_record 118 | create db/migrate/20140821011110_create_users.rb 119 | create app/models/user.rb 120 | invoke test_unit 121 | create test/models/user_test.rb 122 | create test/fixtures/users.yml 123 | invoke resource_route 124 | route resources :users 125 | invoke scaffold_controller 126 | create app/controllers/users_controller.rb 127 | invoke erb 128 | create app/views/users 129 | create app/views/users/index.html.erb 130 | create app/views/users/edit.html.erb 131 | create app/views/users/show.html.erb 132 | create app/views/users/new.html.erb 133 | create app/views/users/_form.html.erb 134 | invoke test_unit 135 | create test/controllers/users_controller_test.rb 136 | invoke helper 137 | create app/helpers/users_helper.rb 138 | invoke test_unit 139 | create test/helpers/users_helper_test.rb 140 | invoke jbuilder 141 | create app/views/users/index.json.jbuilder 142 | create app/views/users/show.json.jbuilder 143 | invoke assets 144 | invoke coffee 145 | create app/assets/javascripts/users.js.coffee 146 | invoke scss 147 | create app/assets/stylesheets/users.css.scss 148 | invoke scss 149 | create app/assets/stylesheets/scaffolds.css.scss 150 | ``` 151 | 152 | 命令里包含了`name:string`和`email:string`,这样我们就可以实现[图2.2](#p2.2)中的用户模型结构了。(注意我们不需要将`id`字段包含在命令中,因为Rails会我们自动生成**id**,并把**id**作为数据表的主键(primary key)) 153 | 154 | 为了运行玩具网站,首先我们要使用Rake来迁移(migrate)数据库(详见[注2.1](#b2.1)): 155 | 156 | ``` 157 | $ bundle exec rake db:migrate 158 | == CreateUsers: migrating ==================================================== 159 | -- create_table(:users) 160 | -> 0.0017s 161 | == CreateUsers: migrated (0.0018s) =========================================== 162 | ``` 163 | 164 | 这是使用我们新的users数据模型来更新数据库。(我们会在[6.1.1](#6.1.1)学习更多关于数据库迁移(database migration)的知识。)注意,为了确保所使用的Rake命令版本与我们的Gemfile文件中的一致,我们需要使用`bundle exec`来运行``rake`。在很多系统里,包括我们的云IDE,都可以省略`bundle exec`,但是在有些系统里这个命令是必须的,所以为了完整性,我会一直使用`bundle exec`。 165 | 166 | 接下来重新打开一个终端([图1.7](#p1.7))使用以下的命令启动Web服务: 167 | ``` 168 | $ rails server -b $IP -p $PORT # 如果运行在本机上只需要输入`rails sever` 169 | 170 | ``` 171 | 172 | 现在我们的玩具网站应该可以在本地运行起来了,如同[1.3.2](#1.3.2)描述的情况一样。(如果你使用云IDE,请记住要在一个新的浏览器窗口里运行我们的服务器,而不是在IDE里运行。) 173 | 174 | #### 注2.1 Rake 命令 175 | 176 | 在传统的Unix系统里,“[make](http://en.wikipedia.org/wiki/Make_(software))”命令在将源代码编译成可执行程序的过程中扮演着非常重要的角色,很多计算机黑客甚至已经做到肌肉记忆了。 177 | 178 | ``` 179 | $ ./configure && make && sudo make install 180 | ``` 181 | 182 | 在Unix系统里,这行命令一般用来编译代码(适用于Linux系统和Mac OS X系统)。 183 | 184 | “Rake”就相当于Ruby语言的“make”,用Ruby写的类make语言。Rails使用Rake的范围很广,尤其在开发基于数据库的web应用要做的那些无数的小型管理任务时。`rake db:migrate`命令应该是最普遍的一条命令,但是还有其他类似的普遍命令;你可以看到一长串的数据库任务使用`-T db`: 185 | 186 | ``` 187 | $ bundle exec rake -T db 188 | ``` 189 | 190 | 要查看所有可用的Rake任务,运行 191 | 192 | ``` 193 | $ bundle exec rake -T 194 | ``` 195 | 196 | 这个列表看起来可能非常庞大,但是别担心,你不需要知道所有的内容(甚至也不需要知道太多)。在结束本书的阅读后,你将会了解到其中最重要的那些。 197 | 198 | 为了不让Rails初学者感到困惑,Rails 5已经把rake命令和rails命令整合为rails命令。因此,以上命令中的rake都可以用rails来替代。后面我们将统一使用rails命令,如果你要使用Rails 5之前的版本,请查阅相应的文档。 199 | 200 | ###2.2.1 用户演示 201 | 202 | 如果我们在“/”(读做“斜杠”,[1.3.4](#1.3.4)有说明)访问根URL(http://localhost:3000/),我们还是会得到如[图1.9](#p1.9)所示的默认Rails页面,但是使用用户资源脚手架,我们同时也创建了很多用来操作用户的页面。例如,用来列出所有用户的页面是“[/users](http://localhost:3000/users)”,以及添加新用户的页面是“[/users/new](http://localhost:3000/users/new)”。这节剩余的部分让我们通过这些用户相关的页面来场旋风之旅。我们继续,[表格2.1](#t2.1)中的内容对我们接下来的学习应该有帮助,他显示了页面和URL之间的关系。 203 | 204 | | URL | 动作 | 作用 | 205 | | :-------- | :----- | :---- | 206 | | /users | index | 用来显示所有用户的页面| 207 | | /users/1 | show |显示用户id为1的用户页面| 208 | | /users/new | new |添加新用户的页面| 209 | | /users/1/edit | edit|用来修改用户id为1的用户的页面| 210 | 211 | 表格2.1 用户资源的页面和URL之间的关联 212 | 213 | 在这个应用中,我们将从显示所有用户信息的页面开始入手,也称为[index](http://localhost:3000/users)页面;正如你所意料的,初始化的页面里根本没有用户([图2.4](#p2.4))。 214 | 215 | ![demo_blank_user_index_3rd_edition](http://i.imgur.com/Ukit2hH.png) 216 | 217 | 图2.4 用户资源的初始index页面([/users](http://localhost:3000/users)) 218 | 219 | 为了添加一个新用户,我们要访问[new](http://localhost:3000/users/new)页面,如[图2.5](#p2.5)所示。(因为我们在本地开发时,其地址要么是http://localhost:3000,或者是云端IDE分配的地址,接下来我就不再重复啰嗦这些地址了。)在[7](#7),这个页面将会成为我们的注册页面。 220 | 221 | ![demo_new_user_3rd_edition](http://i.imgur.com/rznX6DR.png) 222 | 223 | 图2.5 用户的new页面([/users/new](http://localhost:3000/users/new)) 224 | 225 | 我们在指定的方框内填写新用户的用户名,email地址后,再按一下“Create User”(新建用户)按钮,就可以新建一个新用户了。得到的结果就是用户详细信息([show](http://localhost:3000/users/1))页面,如[图2.6](#p2.6)所示。(绿色的欢迎信息是使用flash显示的(译者注:这里的flash和我们平常说的flash不是一回事,这里的flash只是Rails里面一个特殊的变量),我们将在[7.4.2](#7.4.2)学习它。)注意这里的URL是“[/users/1](http://localhost:3000/users/1)”;可能你已经注意到了,数字“1”就是来自[图2.2](#p2.2)中的用户id字段。在[7.1](#7.1),这个页面将变成用户资料页面。 226 | 227 | ![demo_show_user_3rd_edition](http://i.imgur.com/lxj9ZpY.png) 228 | 229 | 图2.6 显示用户信息的页面([/users/1](http://localhost:3000/users/1)) 230 | 231 | 要改变用户信息,我们需要访问“[edit](http://localhost:3000/users/1/edit)”(编辑)页面,如[图2.7](#p2.7)所示。修改了所需的内容后,我们单击“Update User”(更新用户)按钮,这样就可以更新玩具网站的用户信息了([图2.8](#p2.8))。(从[6](#6)开始,我们将看到这部分内容的更多细节,这个用户数据将会存储在后端的数据库中。)在[9.1](#9.1)我们将会为sample应用了添加用户信息的“edit/update”(编辑/更新)功能。 232 | 233 | ![demo_edit_user_3rd_edition](http://i.imgur.com/QXMYkH4.png) 234 | 图2.7 用户信息编辑页面([/users/1/edit](http://localhost:3000/users/1/edit)) 235 | 236 | 237 | ![demo_update_user_3rd_edition](http://i.imgur.com/AxExfLh.png) 238 | 图2.8 用户信息更新后的页面 239 | 240 | 接下来我们重新访问“[new](http://localhost:3000/users/new)”页面来添加第二个用户,同时提交第二个用户的用户信息。得到的用户[index](http://localhost:3000/users)页面将如[图2.9](#p2.9)所示。在[7.1](#7.1)我们将会美化这个用户主页,用来显示所有的用户信息。 241 | 242 | ![demo_user_index_two_3rd_edition](http://i.imgur.com/9oB73yb.png) 243 | 图2.9 用户index页面显示第二个用户信息([/users](http://localhost:3000/users)) 244 | 245 | 在经过了创建,显示,编辑这些步骤以后,接下来我们要删除用户([图2.10](#p2.10))。你需要用[图2.10](#p2.10)里的“destroy”按钮来删除第二个用户,刷新页面,只剩下一个用户了。(如果你删不掉,你得查看一下你的浏览器是否支持JavaScript;Rails使用JaveScript来发出删除用户的请求。)[9.4](#9.4)将会在我们的sample应用中添加用户删除功能,这个功能只有特殊的管理组用户才能使用。 246 | 247 | 248 | ![demo_destroy_user_3rd_edition](http://i.imgur.com/SOwTXWy.png) 249 | 图2.10 删除一个用户 250 | 251 | ###2.2.2 使用MVC 252 | 253 | 既然我们已经快速浏览了用户资源的内容,接下来让我们用[1.3.3](#1.3.3)中提到的MVC架构(模型-视图-控制器)来验证一下用户资源里的一些特定部分。我们的想法是,通过一个典型的浏览器访问过程——访问用户主页([/users](http://localhost:3000/users)),来了解一下MVC([图2.11](#p2.11))。 254 | 255 | ![mvc_detailed](http://i.imgur.com/1oeWxYU.png) 256 | 图2.11 Rails中的MVC详细图解 257 | 258 | 以下是[图2.11](#p2.11)的步骤总结: 259 | 1. 浏览器向“/users”发送一个URL请求; 260 | 2. Rails将“/users”路由到“Users controller”里的“index”动作; 261 | 3. “index”动作请求“User model”检索所有的用户(`User.all`); 262 | 4. “User model”从数据库中拉取所有的用户信息; 263 | 5. “User model”将用户列表返回给控制器; 264 | 6. 控制器将用户赋值给`@users`变量,然后将这个变量传到index视图; 265 | 7. 视图使用内嵌的Ruby程序将页面渲染成HTML格式; 266 | 8. 控制器将HTML文件传回浏览器。 267 | 268 | 现在让我们深入研究一下以上的步骤。我们从浏览器发出请求——例如,在浏览器地址栏里输入URL或直接点击一个链接(图2.11的步骤1)。这个请求传送到Rails的路由(步骤2),路由基于URL决定将这个请求发送给某个合适的控制器动作来处理([注3.2](#b3.2)列出了请求的类型)。[代码清单2.2](#l2.2)用来为用户资源生成从URL到控制器动作的映射;这份代码有效地建立起了[表格2.1](#t2.1)中URL和动作的对应关系。(这个奇怪的表示法**:users**是symbol,我们将在[4.3.3](#4.3.3)中进一步了解)。 269 | 270 | 代码清单2.2:Rails路由,其中定义了用户资源的路由规则。 271 | `config/routes.rb` 272 | ``` 273 | Rails.application.routes.draw do 274 | resources :users 275 | . 276 | . 277 | . 278 | end 279 | 280 | ``` 281 | 282 | 既然我们已经在查看路由文件了,那让我们把根路由指向用户主页,这样当访问“/”时就会显示“/users”页面。回忆一下,在[代码清单1.10](#l1.10)中,我们将下面的代码 283 | 284 | ``` 285 | # root 'welcome#index' 286 | 287 | ``` 288 | 289 | 改成 290 | 291 | ``` 292 | root 'application#hello' 293 | 294 | ``` 295 | 296 | 所以根路由指向了app控制器的hello动作。在现在的这个例子里,我们想要使用用户控制器(Users controller)的index动作(action),我们可以使用[代码清单2.3](#l2.3)来实现。(此时此刻,如果你再这一节开始的时候添加了hello动作,我建议将其从app控制器中删除。) 297 | 298 | 代码清单2.3:为users添加根路由。 299 | `config/routes.rb` 300 | ```ruby 301 | Rails.application.routes.draw do 302 | resources :users 303 | root 'users#index' 304 | . 305 | . 306 | . 307 | end 308 | 309 | ``` 310 | 311 | [2.2.1](#2.2.1)中浏览的页面,对应于用户控制器里的不同动作,这是相关动作的集合。使用脚手架生成的控制器显示在[代码清单2.4](#l2.4)中。注意`class UsersController < ApplicationController`这种写法,这是Ruby类继承的表示方法。(在[2.3.4](#2.3.4)我们会简要说明,在[4.4](#4.4)会详细说明) 312 | 313 | 代码清单2.4:用户控制器代码结构示意。 314 | `app/controllers/users_controller.rb` 315 | 316 | ``` 317 | class UsersController < ApplicationController 318 | . 319 | . 320 | . 321 | def index 322 | . 323 | . 324 | . 325 | end 326 | 327 | def show 328 | . 329 | . 330 | . 331 | end 332 | 333 | def new 334 | . 335 | . 336 | . 337 | end 338 | 339 | def edit 340 | . 341 | . 342 | . 343 | end 344 | 345 | def create 346 | . 347 | . 348 | . 349 | end 350 | 351 | def update 352 | . 353 | . 354 | . 355 | end 356 | 357 | def destroy 358 | . 359 | . 360 | . 361 | end 362 | end 363 | ``` 364 | 365 | 你可能注意到了动作(action)的数量要多于页面的数量;`index`,`show`,`new`和`edit`等动作都对应于[2.2.1](#2.2.1)提到的页面,但是同时还有`create`,`update`,`destroy`等其他动作。这些动作一般不直接用来渲染页面(虽然ta们有这个能力);实际上,他们主要的作用在于修改数据库中的用户信息。在[表格2.2](#t2.2)中列出了控制器的所有动作,Rails就是用这些动作来实现REST架构([注2.2](#b2.2)),REST架构的意思是“表现层状态转移”(representational state transfer),他是由计算机科学家[ Roy Fielding](https://en.wikipedia.org/wiki/Roy_Fielding)提出来的。注意在[表格2.2](#t2.2)中在一些URL中有重叠;例如,用户`show`和`update`动作都对应到“/users/1”URL地址。他们之间的区别在于他们使用的[HTTP请求方法](http://en.wikipedia.org/wiki/HTTP_request#Request_methods)不同。我们将在[3.3](#3.3)学习关于HTTP请求方法的内容。 366 | 367 | 368 | | HTTP请求 | URL地址 | 动作 |作用| 369 | | :-------- | :----- | :---- | :----| 370 | | GET | /users | index| 显示所有用户的页面| 371 | | GET | /users/1 |show| 显示id为1的用户的页面| 372 | | GET | /users/new |new| 创建新用户的页面 | 373 | | POST | /users |create|创建一个新用户 | 374 | | GET | /users/1/edit|edit| 修改id为1的用户的页面| 375 | | PATCH | /users/1 | update| 更新id为1的用户| 376 | | DELETE | /users/1 | destroy| 删除id为1的用户| 377 | 378 | 表格2.2 [代码清单2.2](#l2.2)生成的RESTful架构路由表 379 | 380 | #### 注2.2 表现层状态转移(REST) 381 | 382 | 如果你阅读过一些关于Ruby on Rails的web开发资料,你会看到很多提放都提到“REST”架构,这是“表现层状态转移”(REpresentational State Transfer)的缩写。REST是一个用来做分布式的网络系统和软件应用的架构,例如我们熟悉的万维网(www)和web应用开发。虽然REST的概念听起来比较抽象,但是在Rails应用环境中,REST意味着大部分的应用组件(例如用户和微博)都会被模型化成为资源,这些资源可被生成(create),可读取(read),可更新(update),并且也可删除(delete)——这些操作对应于[关系型数据库的CRUD操作](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete)和[HTTP请求方法](https://en.wikipedia.org/wiki/HTTP_request#Request_methods)的四个基本操作:POST,GET,PATCH,DELETE。(我们将在[3.3](#3.3)学习HTTP请求的知识,尤其是在[注3.2](#b3.2)中更详细。) 383 | 384 | 作为一个Rails的开发者,RESTful风格的开发方法将会帮你选择应该写哪个控制器和动作:你只需要使用可以建立,读取,更新,删除的资源来构建应用就可以了。在这个例子里,“用户”和“微博”就很直观,因为他们都很自然地会被当成资源来对待。在[12](#12)中,我们将会看到如何使用REST基本准则对一个微秒的问题进行建模,“跟随用户”,使用一种自然而便利的方法来实现。 385 | 386 | 387 | 为了验证用户控制器和用户模型之间的关系,让我们将注意力集中在一个简化后的index动作版本上,如同[代码清单2.5](#l2.5)所示。(脚手架代码又难看又不容易理解,所以我省略了一些不需要的细节。) 388 | 389 | 代码清单2.5:toy app简化版的用户index动作。 390 | `app/controllers/users_controller.rb` 391 | 392 | ``` 393 | class UsersController < ApplicationController 394 | . 395 | . 396 | . 397 | def index 398 | @users = User.all 399 | end 400 | . 401 | . 402 | . 403 | end 404 | 405 | ``` 406 | 407 | 这个index动作里有一行代码` @users = User.all`([图2.11](https://www.railstutorial.org/book/toy_app#fig-mvc_detailed)的步骤3),这行代码的作用是请求用户模型从数据库中返回所有用户信息列表([图2.11](https://www.railstutorial.org/book/toy_app#fig-mvc_detailed)的步骤4),然后将这个列表赋值给变量`@users`([图2.11](https://www.railstutorial.org/book/toy_app#fig-mvc_detailed)的步骤5)。用户模型的代码在[代码清单2.6](#l2.6)里;虽然代码看起来很少,但是其实功能很强大,应该这里使用了类的继承机制([2.3.4](#2.3.4)和[4.4](#4.4))。具体来说,我们通过调用Rails的库“Active Record”,User.all就可以从数据库中返回所有的用户信息了。 408 | 409 | 代码清单2.6:toy app的用户模型。 410 | `app/models/user.rb` 411 | 412 | ``` 413 | class User < ActiveRecord::Base 414 | end 415 | 416 | ``` 417 | 418 | 一旦`@users`变量定义之后,控制器将会调用视图(view)(步骤6),如[代码清单2.7](#l2.7)所示。以`@`符号为开头的变量,我们称为实例变量(instance variables),在视图中的自动可用的变量;在这个例子中,[代码清单2.7](#l2.7)中的`index.html.erb`视图将会遍历`@users`列表,然后为每个用户生成一条HTML代码。 419 | (请记住,现在你不需要完全明白这些代码的意思。这里是演示一下。) 420 | 421 | 代码清单2.7:用户index页面视图HTML代码。 422 | `app/models/user.rb` 423 | 424 | ``` 425 |

代码清单 users

426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | <% @users.each do |user| %> 437 | 438 | 439 | 440 | 441 | 442 | 444 | 445 | <% end %> 446 |
NameEmail
<%= user.name %><%= user.email %><%= link_to 'Show', user %><%= link_to 'Edit', edit_user_path(user) %><%= link_to 'Destroy', user, method: :delete, 443 | data: { confirm: 'Are you sure?' } %>
447 | 448 |
449 | 450 | <%= link_to 'New User', new_user_path %> 451 | 452 | ``` 453 | 454 | 视图将代码转换为HTML(步骤7),然后控制器将其回传给浏览器显示(步骤8)。 455 | 456 | 457 | ###2.2.3 用户资源的弊端 458 | 459 | 虽然使用脚手架可以很直观的看到Rails的结构,但是由脚手架生成的用户资源也有一些弊端: 460 | 461 | * **没有数据验证功能。**我们的用户模型会无差别的接受任何数据,可能有些空的名字或者无效的email地址等。 462 | * **没有认证机制。**我们没有登录或注销操作,任何用户都可以进行操作。 463 | * **没有测试。**技术上来讲这个可能不对——脚手架里包含了一些基本测试——但是这些测试没有包含数据有效性,认证等测试,更不用说其他客户要求的测试了。 464 | * **没有页面样式和布局。**没有网站样式和导航。 465 | * **没有真正理解代码。**如果你能读懂脚手架的代码,那你可以不用阅读本书了。 466 | 467 | 468 | ##2.3 微博资源 469 | 470 | 我们已经生成和浏览了用户资源,接下来要把微博资源关联进来。在这一节的学习过程中,我建议你将本节的微博资源相关元素与之前[2.2](#2.2)里的用户资源相关元素对比一下;你将会发现这两种资源在很多内容上很相似。通过这种重复的形式,我们可以更好地理解Rails应用的REST的架构——确实,在现在比较早的阶段就观察用户资源和微博资源的异同也是本章的主要目的之一。 471 | 472 | ###2.3.1 微博资源的微旅行 473 | 474 | 如同用户资源一般,我们也要为微博资源生成脚手架代码,使用的命令是`rails generate scaffold`,在这里,我们使用[图2.3](#p2.3)所示来生成数据模型: 475 | 476 | ``` 477 | $ rails generate scaffold Micropost content:text user_id:integer 478 | invoke active_record 479 | create db/migrate/20140821012832_create_microposts.rb 480 | create app/models/micropost.rb 481 | invoke test_unit 482 | create test/models/micropost_test.rb 483 | create test/fixtures/microposts.yml 484 | invoke resource_route 485 | route resources :microposts 486 | invoke scaffold_controller 487 | create app/controllers/microposts_controller.rb 488 | invoke erb 489 | create app/views/microposts 490 | create app/views/microposts/index.html.erb 491 | create app/views/microposts/edit.html.erb 492 | create app/views/microposts/show.html.erb 493 | create app/views/microposts/new.html.erb 494 | create app/views/microposts/_form.html.erb 495 | invoke test_unit 496 | create test/controllers/microposts_controller_test.rb 497 | invoke helper 498 | create app/helpers/microposts_helper.rb 499 | invoke test_unit 500 | create test/helpers/microposts_helper_test.rb 501 | invoke jbuilder 502 | create app/views/microposts/index.json.jbuilder 503 | create app/views/microposts/show.json.jbuilder 504 | invoke assets 505 | invoke coffee 506 | create app/assets/javascripts/microposts.js.coffee 507 | invoke scss 508 | create app/assets/stylesheets/microposts.css.scss 509 | invoke scss 510 | identical app/assets/stylesheets/scaffolds.css.scss 511 | 512 | ``` 513 | 514 | (如果看到Spring相关的错误,只需重新运行代码即可。)为了更新数据库以便使用新数据模型,我们需要运行如[2.2](#2.2)中的迁移命令: 515 | 516 | ``` 517 | $ bundle exec rails db:migrate 518 | == CreateMicroposts: migrating =============================================== 519 | -- create_table(:microposts) 520 | -> 0.0023s 521 | == CreateMicroposts: migrated (0.0026s) ====================================== 522 | 523 | ``` 524 | 525 | 现在我们所处的创建微博资源阶段和在[2.2.1](#2.2.1)中建立用户资源阶段是一样的。你可能已经猜到了,脚手架生成器已经将微博资源的Rails路由规则做了更新,如同[代码清单2.8](#l2.8)中所示。如同用户资源一样,`resources :microposts`路由规则将微博URL地址映射到微博控制器对应的动作上,如[表格2.3](#t2.3)所示。 526 | 527 | 代码清单2.8:Rails路由,有一条微博资源的新规则。 528 | `config/routes.rb` 529 | 530 | ```ruby 531 | Rails.application.routes.draw do 532 | resources :microposts 533 | resources :users 534 | . 535 | . 536 | . 537 | end 538 | ``` 539 | 540 | | HTTP请求 | URL地址 | 动作 |作用| 541 | | :-------- | :----- | :---- | :----| 542 | | GET | /microposts | index| 显示所有微博的页面| 543 | | GET | /microposts/1 |show| 显示id为1的微博页面| 544 | | GET | /microposts/new |new| 创建新微博页面 | 545 | | POST | /microposts |create|创建一篇新微博 | 546 | | GET | /microposts/1/edit|edit| 修改id为1的微博页面| 547 | | PATCH | /microposts/1 | update| 更新id为1的微博| 548 | | DELETE | /microposts/1 | destroy| 删除id为1的微博| 549 | 表格2.3 微博资源[代码清单2.8](#l2.8)生成的RESTful架构路由表 550 | 551 | 微博控制器(MicropostsController)代码摘要如[代码清单2.9](#l2.9)所示。请注意,除了名字不一样之外,[代码清单2.9](#l2.9)和[代码清单2.4](#l2.4)其实没什么区别。这是REST架构反映在两种资源之间的共同之处。 552 | 553 | 代码清单2.9:微博控制器代码摘要。 554 | `app/controllers/microposts_controller.rb` 555 | 556 | ``` 557 | class MicropostsController < ApplicationController 558 | . 559 | . 560 | . 561 | def index 562 | . 563 | . 564 | . 565 | end 566 | 567 | def show 568 | . 569 | . 570 | . 571 | end 572 | 573 | def new 574 | . 575 | . 576 | . 577 | end 578 | 579 | def edit 580 | . 581 | . 582 | . 583 | end 584 | 585 | def create 586 | . 587 | . 588 | . 589 | end 590 | 591 | def update 592 | . 593 | . 594 | . 595 | end 596 | 597 | def destroy 598 | . 599 | . 600 | . 601 | end 602 | end 603 | 604 | ``` 605 | 606 | 现在我们来新建几篇真正的微博,在新的微博页面上输入微博内容,如[图2.12](#p2.12)显示的[/microposts/new](http://localhost:3000/microposts/new)页面。 607 | 608 | ![demo_new_micropost_3rd_edition](http://i.imgur.com/cBUsgoR.png) 609 | 图2.12 新微博建立页面([/microposts/new](http://localhost:3000/microposts/new)) 610 | 611 | 接下来让我们继续前行,再创建一到两篇微博,注意其中一篇微博的用户id`user_id`应该为“1”,这是为了和之前[2.2.1](#2.2.1)中创建的用户一致。得到的结果应该和[图2.13](#p2.13)接近。 612 | 613 | ![demo_micropost_index_3rd_edition](http://i.imgur.com/bLmgUrC.png) 614 | 图2.13 微博index页面([/microposts](http://localhost:3000/microposts)) 615 | 616 | ###2.3.2 微博重在“微” 617 | 618 | 微博,顾名思义,就是其内容应该不能太长。在Rails中使用**validation**功能就可以轻松实现这种限制;要使微博的长度限制在140个字以内(如同Twitter一样),我们使用长度验证(length validation)。现在,你需要打开文件`app/models/micropost.rb`,然后将[代码清单2.10](#l2.10)中的代码输入。 619 | 620 | 代码清单2.10:微博内容限制在140个字节以内。 621 | `app/models/micropost.rb` 622 | 623 | ```ruby 624 | class Micropost < ActiveRecord::Base 625 | validates :content, length: { maximum: 140 } 626 | end 627 | 628 | ``` 629 | 630 | [代码清单2.10](#l2.10)中的代码可能看起来让人有点迷惑,在[6.2](#6.2)我们有更多的内容来介绍“validation”验证功能,但是还有什么方法比直接测试更有效呢,让我们直接在微博页面上输入超过140个字节,看看有什么结果。就像[图2.14](#p2.14)所示,Rails错误信息提示微博的内容太长了。(我们将在[7.3.3](#7.3.3)了解到更多错误信息。) 631 | 632 | ![micropost_length_error_3rd_edition](http://i.imgur.com/H9RoLMT.png) 633 | 图2.14 微博发布失败显示的错误信息 634 | 635 | ###2.3.3 用户有许多(has_many)微博 636 | 637 | Rails最强大的功能之一就是可以在不同数据模型之间建立**关联**(association)。在我们用户模型的例子中,一个用户可以有多篇微博。我们可以通过更新用户和微博模型的代码来实现这种关联,如下所示,用户模型[代码清单2.11](l2.11)和微博模型[代码清单2.12](l2.12)。 638 | 639 | ####代码清单2.11 单用户多微博 640 | `app/models/user.rb` 641 | 642 | ```ruby 643 | class User < ActiveRecord::Base 644 | has_many :microposts 645 | end 646 | 647 | ``` 648 | 649 | ####代码清单2.12 微博关联到用户 650 | `app/models/micropost.rb` 651 | 652 | ```ruby 653 | class Micropost < ActiveRecord::Base 654 | belongs_to :user 655 | validates :content, length: { maximum: 140 } 656 | end 657 | 658 | ``` 659 | 660 | 我们可以从[图2.15][#p2.15]中直观地看到关联结果。因为“microposts”表格中有“user_id”这一栏,所以Rails(通过使用Active Record)可以将微博和各个用户关联起来。 661 | 662 | ![micropost_user_association](http://i.imgur.com/8IAotYy.png) 663 | 664 | 图2.15 用户和微博之间的关联表格 665 | 666 | 在[11](#11)和[12](#12)中,我们将会把用户和微博关联起来,并将他们用类似Twitter网站的方式显示在页面上。现在,我们可以用控制台(console)来检查用户和微博的关联结果,控制台是Rails应用开发中非常强大的交互工具。首先我们在命令行中输入`rails console`来调出控制台(console),然后使用`User.first`命令从数据库中检索第一个用户(将检索到的内容放入变量“first_user”中): 667 | 668 | ``` 669 | $ rails console 670 | >> first_user = User.first 671 | => # 673 | >> first_user.microposts 674 | => [#, #] 678 | >> micropost = first_user.microposts.first # Micropost.first would also work. 679 | => # 681 | >> micropost.user 682 | => # 684 | >> exit 685 | 686 | ``` 687 | 688 | (在最后一行我加上了`exit`命令,这是用来退出控制台的命令。在其他系统中,你也可以使用“Ctrl+D”来退出)。现在我们已经使用代码`first_user.microposts`来访问“first_user”用户所有的微博。通过这个代码,Active Record会自动返回“user_id”和“first_user”的id一样的所有微博(在这个例子里,id="1")。在[11](#11)和[12](#12)中我们将会学习更多关于Active Record中的这种关联工具。 689 | 690 | 691 | ###2.3.4 继承体系结构 692 | 693 | 接下来,我们将简要介绍Rails的控制器和模型类继承,以此作为这章关于玩具网站学习的结尾。如果你对面向对象编程(object-oriented programming,简称OOP)有过相关经验,那我们这一节的讨论你将更容易理解;但是如果你没有学习过OOP,你可以随时跳过这一节。实际上,如果你对“类”(class)的概念不是很熟悉([4.4](#4.4)会学习),我建议你到时候回过头来看看这一节。 694 | 695 | 我们从模型的继承开始入手。对比[代码清单2.13](l2.13)和[代码清单2.14](l2.14)的差异,我们发现,其实用户模型和微博模型都是从`ActiveRecord::Base`中继承而来(使用符号“<”表示继承),`ActiveRecord::Base`是由ActiveRecord提供的模型的基类;[图2.16][#p2.16]将这种关系用框图的形式表现出来。通过继承这个模型基类,我们的模型对象才能拥有与数据库通信,将数据库的列作为Ruby属性来处理的能力,还有其他很多功能,这里就不一一列举了。 696 | 697 | ####代码清单2.13 用户User类,高亮的地方表示继承 698 | `app/models/user.rb` 699 | 700 | ```ruby 701 | class User < ActiveRecord::Base 702 | . 703 | . 704 | . 705 | end 706 | 707 | ``` 708 | 709 | ####代码清单2.14 微博Micropost类,高亮的地方表示继承 710 | `app/models/micropost.rb` 711 | 712 | ```ruby 713 | class Micropost < ActiveRecord::Base 714 | . 715 | . 716 | . 717 | end 718 | 719 | ``` 720 | 721 | ![demo_model_inheritance](http://i.imgur.com/TJFM1rM.png) 722 | 图2.16 用户模型和微博模型的继承体系结构 723 | 724 | 控制器的继承结构要稍微复杂一点。对比[代码清单2.15](l2.15)和[代码清单2.16](l2.16)的差异,我们可以看到用户控制器(User controller)和微博控制器(Microposts controller)都是从应用控制器(ApplicationController)继承来的。然后再看[代码清单2.17](l2.17),我们将发现,其实**ApplicationController**也是从其他地方继承而来,具体来说就是从**ActionController::Base**继承来的;这是由Rails的Action Pack库提供的控制器基类。在[图2.17][#p2.17]的框图中显示了这些类之间的关系。 725 | 726 | ####代码清单2.15 用户控制器UsersController类,高亮的地方表示继承 727 | `app/controllers/users_controller.rb` 728 | 729 | ```ruby 730 | class UsersController < ApplicationController 731 | . 732 | . 733 | . 734 | end 735 | 736 | ``` 737 | 738 | ####代码清单2.16 微博控制器MicropostsController类,高亮的地方表示继承 739 | `app/controllers/microposts_controller.rb` 740 | 741 | ```ruby 742 | class UsersController < ApplicationController 743 | . 744 | . 745 | . 746 | end 747 | 748 | ``` 749 | 750 | ####代码清单2.17 ApplicationController类,高亮的地方表示继承 751 | `app/controllers/application_controller.rb` 752 | 753 | ```ruby 754 | class ApplicationController < ActionController::Base 755 | . 756 | . 757 | . 758 | end 759 | 760 | ``` 761 | 762 | ![demo_controller_inheritance](http://i.imgur.com/4P5S2RK.png) 763 | 图2.17 用户控制器和微博控制器的继承体系结构图 764 | 765 | 通过这种模型的继承,用户模型和微博模型可以从基类获得很多功能和属性(这个例子中的基类就是** ActionController::Base**),同时也包括对模型对象的操作,HTTP请求滤波,以及将视图渲染为HTML等等功能。由于Rails的所有控制器都从基类`ActionController`继承而来,因此在 766 | 应用控制器中定义的规则将自动适用于应用的每个动作。例如,在[8.4](#8.4)我们将看到如何引入辅助功能,为终极例程中的所有应用控制器添加登录和注销功能。 767 | 768 | ###2.3.5 部署toy app 769 | 770 | 既然我们都已经完成了微博资源,现在是时候将代码推送到Bitbucket仓库中了: 771 | 772 | ``` 773 | $ git status 774 | $ git add -A 775 | $ git commit -m "Finish toy app" 776 | $ git push 777 | 778 | ``` 779 | 780 | 一般情况下,你需要养成“小规模改动,高频率提交”的代码管理习惯,但是本章为了方便给大家做演示,只需在现在,作为最后阶段一次性提交。 781 | 782 | 现在,你也可以将toy app部署到Heroku上,如同[1.5](#1.5)中的步骤: 783 | 784 | ``` 785 | $ git push heroku 786 | 787 | ``` 788 | 789 | (在这里我们假设你已经在[2.1](#2.1)中新建了一个Heroku应用,否则,你需要先运行`heroku create`命令,然后运行`git push heroku master`命令。) 790 | 791 | 为了使应用程序的数据库工作,你同时也需要迁移生产环境数据库: 792 | 793 | ``` 794 | $ heroku run rails db:migrate 795 | 796 | ``` 797 | 798 | 这个命令将使用必要的用户和微博数据模型来更新Heroku的数据库。在运行迁移命令之后,你应该可以在生产环境中运行玩具网站了,同时这个应用是基于实际的PostgreSQL数据库,如[图2.18](#p2.18)所示。 799 | 800 | ![toy_app_production](http://i.imgur.com/VV4d45k.png) 801 | 图2.18 在生产环境中运行toy app 802 | 803 | 804 | ##2.4 本章小结 805 | 806 | 现在我们已经从一个终极高度来了解了Rails应用的结构。本章讲述的玩具网站有几个优点也有挺多缺点。 807 | 808 | #####优点 809 | 810 | * 全局视角来了解Rails 811 | * 引出了MVC的概念 812 | * 第一次了解REST架构(表现层状态转移) 813 | * 开始接触数据模型 814 | * 一个在生产环境中运行的基于数据库的web应用 815 | 816 | #####缺点 817 | 818 | * 没有用户自定义的布局和样式 819 | * 没有静态页面(例如“首页”和“关于”) 820 | * 没有用户密码 821 | * 没有用户头像 822 | * 没有登录界面 823 | * 没有安全系统 824 | * 没有自动化的用户/微博关联功能 825 | * 没有“关注”和“被关注”功能 826 | * 没有微博列表 827 | * 没有实际的测试 828 | * **没有真正理解开发过程** 829 | 830 | 本书的后续将专注于扬长避短。 831 | 832 | 2.4.1 本章我们学到了什么 833 | 834 | * 实用脚手架自动生成数据模型的代码,并通过页面和应用交互 835 | * 脚手架适合快速上手,但不适合深入了解web开发 836 | * Rails实用MVC架构(Model-View-Controller)开发web应用 837 | * 由Rails我们了解到,为了和数据模型交互,REST架构定义了一个标准的URL动作和控制器动作 838 | * Rails支持数据校验,在数据模型属性里可以添加约束条件 839 | * Rails内含定义不同的数据模型之间的关联关系的功能 840 | * 使用Rails控制台可以使用命令行的方式与应用进行交互 841 | 842 | ##2.5 本章练习题 843 | 844 | 注意:练习题答案手册包含了本书的所有练习题详细解答,当购买本书时会免费赠送。详情请查看官网www.railstutorial.org。 845 | 846 | 1. [代码清单2.18](l2.18)用来为当前微博内容添加一个有效性验证功能,以此来保证该微博不是空的,是有内容的有效微博。确保能实现[如图2.19][#p2.19]显示的功能。 847 | 2. 更新[代码清单2.19](l2.19),将`FILL_IN`修改未合适的代码,用来验证当前用户模型中用户的名字和email地址是否已经存在?([如图2.20][#p2.20]) 848 | 849 | 850 | ####代码清单2.18 验证当前微博是否有效的代码 851 | `app/models/micropost.rb` 852 | 853 | ```ruby 854 | class Micropost < ActiveRecord::Base 855 | belongs_to :user 856 | validates :content, length: { maximum: 140 }, 857 | presence: true 858 | end 859 | 860 | ``` 861 | 862 | ![micropost_content_cant_be_blank](http://i.imgur.com/8f1wri2.png) 863 | 864 | 图2.19 微博有效性验证效果图 865 | 866 | 867 | ####代码清单2.19 给用户模型添加存在验证 868 | `app/models/user.rb` 869 | 870 | ```ruby 871 | class User < ActiveRecord::Base 872 | has_many :microposts 873 | validates FILL_IN, presence: true 874 | validates FILL_IN, presence: true 875 | end 876 | 877 | ``` 878 | 879 | ![user_presence_validations](http://i.imgur.com/mp3xjCs.png) 880 | 图2.20 用户模型存在性检测效果图 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | -------------------------------------------------------------------------------- /chapter3_mostly_static_pages.md: -------------------------------------------------------------------------------- 1 | ## 第三章 2 | #主要是静态的页面 3 | 4 | 这章,我们将开始开发专业级的示例程序,这个程序将贯穿本教程剩余的章节。尽管示例应用有用户、微博以及完整的登陆和授权框架,我们从一个好像非常有限的主题开始:创建静态页面。尽管它是显而易见的简单,制作静态页面是一个高建设度的练习,内涵丰富--对我们初生的应用程序来说是一个完美的开始。 5 | 6 | 尽管Rails被设计成制作数据库支持的动态网站,它在制作原始的HTML文件的静态页面也是非常出色的。实际上,使用Rails甚至对静态页面的生成一个显著的优势:我们能容易第添加一小部分动态的内容。在这章我们将学习怎样添加动态内容。沿着这个思路,我们将首次尝尝自动测试的味道,这将有助于增强我们对自己的代码的信心。而且,有一个好的测试将允许我们自信地重构代码,在不改变功能的情况下改变代码。 7 | 8 | ### 3.1 示例程序安装 9 | 和第二章一样,在我们开始前我们需要创建一个新的Rails工程,这次叫做**sample_app**,如同列表3.1所示。假如列表3.1的命令返回类似“Could not find 'railties”错误,这意味着你没有安装正确的Rails版本,你应该好好第检查一下是不是完全按照列表1.1的命令操作。 10 | ``` 11 | 代码清单 3.1: 生成新的sample app 12 | $ cd ~/workspace 13 | $ rails _4.2.2_ new sample_app 14 | $ cd sample_app/ 15 | ``` 16 | 17 | (和章节2.1一样,云IDE的读者可以在和前两章同样的工作空间创建工程。重新创建一个新的工作空间是没有必要的) 18 | 19 | 和章节2.1一样,我们下一步是使用文本编辑器更新Gemfile,将我们需要的gem加入我们的应用程序里。列表3.2**test**组里的gem是和列表1.5、列表2.1完全一样的,它们对于安装可选的高级测试是需要的(章节3.7)。记住:假如你愿意为示例程序安装所有的gem,你这次应该使用列表11.67的代码。 20 | 21 | ``` 22 | 代码清单 3.2: A Gemfile for the sample app. 23 | source 'https://rubygems.org' 24 | 25 | gem 'rails', '5.0.0' 26 | gem 'sass-rails', '5.0.2' 27 | gem 'uglifier', '2.5.3' 28 | gem 'coffee-rails', '4.1.0' 29 | gem 'jquery-rails', '4.0.3' 30 | gem 'turbolinks', '2.3.0' 31 | gem 'jbuilder', '2.2.3' 32 | gem 'sdoc', '0.4.0', group: :doc 33 | 34 | group :development, :test do 35 | gem 'sqlite3', '1.3.9' 36 | gem 'byebug', '3.4.0' 37 | gem 'web-console', '2.0.0.beta3' 38 | gem 'spring', '1.1.3' 39 | end 40 | 41 | group :test do 42 | gem 'minitest-reporters', '1.0.5' 43 | gem 'mini_backtrace', '0.1.3' 44 | gem 'guard-minitest', '2.3.1' 45 | end 46 | 47 | group :production do 48 | gem 'pg', '0.17.1' 49 | gem 'rails_12factor', '0.0.2' 50 | end 51 | ``` 52 | 和前两章一样,我们运行**bundle install**安装Gemfile里面指定的gem,使用**--without production**跳过生产环境才需要安装的gem. 53 | 54 | ``` 55 | $ bundle install --without production 56 | ``` 57 | 这样就在开发环境跳过了PostSQL使用的*pg* gem,在开发和测试环境使用SQLite。Heroku不推荐在开发环境和生产环境使用不同的数据库,但是对于我们这个示例程序来说没什么不同,而且SQLite与PostSQL相比,在本地安装和配置非常容易。万一你之前安装过gem的其他版本而不是**Gemfile**指定的版本,用**bundle update**命令*更新*gem是个不错的主意: 58 | ``` 59 | $ bundle update 60 | ``` 61 | 做完这个,剩下的就是初始化Git仓库: 62 | ``` 63 | $ git init 64 | $ git add -A 65 | $ git commit -m "Initialize repository" 66 | ``` 67 | 和第一个应用程序一样,我建议更新一下**README**文件(位于应用程序的根目录),让它变得更加有帮助和更具描述性。添加列表3.3的内容: 68 | ``` 69 | 列表 3.3: 为sample app修饰过的README文件 70 | # Ruby on Rails Tutorial: sample application 71 | 72 | This is the sample application for the 73 | [*Ruby on Rails Tutorial: 74 | Learn Web Development with Rails*](http://www.railstutorial.org/) 75 | by [Michael Hartl](http://www.michaelhartl.com/). 76 | ``` 77 | 最后,提交变化 78 | ``` 79 | $ git commit -am "Improve the README" 80 | ``` 81 | 你可以回忆一下在1.4.4我们使用Git命令**git commit -a -m "Message"**,用标记(-a)表示“所有变化”,(-m)表示信息。如同在第二个命令显示的那样,Git也允许我们把两个标记合在一起使用**git commit -am "Message"**。 82 | 83 | 因为我们将在本书剩余的部分都使用这个例子,因此在[Bitbucket创建一个新的仓库](https://bitbucket.org/repo/create)并且推送至远程仓库是个好主意: 84 | ``` 85 | $ git remote add origin git@bitbucket.org:/sample_app.git 86 | $ git push -u origin --all #首次推送仓库和它的参考 87 | ``` 88 | 为了避免后续集成时令人头痛的问题,在初期就将应用部署至Heroku也是个好想法。如同在第一章和第二章一样,我建议跟随列表1.8和1.9里的“hello, world!”完成。然后提交变化,推送至Heroku: 89 | ``` 90 | $ git commit -am "Add hell" 91 | $ heroku create 92 | git push heroku master 93 | ``` 94 | (如同在1.5节一样,你可能会看见一些警告信息,你现在应该忽视它们。我们将在7.5节消除它们)除了Heroku应用的地址,结果应该和图1.18一样。 95 | 96 | 当你进一步深入本书剩余的部分,我建议你规律地推送和部署应用程序,它会自动远程备份,让你尽可能早地发现程序错误。假如你在Heroku上碰到问题,确定看一看产品日志尽力诊断问题: 97 | ``` 98 | $ heroku logs 99 | ``` 100 | 注:假如你要把真实的应用程序部署至Heroku,请确定按照章节7.5描述的那样配置你的Web服务器。 101 | 102 | ## 3.2 静态页面 103 | 随着在3.1里的所有准备结束,我们准备开始开发示例应用程序。在这节,首先通过创建一套Rails动作(actions)和仅包含静态HTML的视图(views)的动态页面迈出我们的第一步。Rails动作把里面的控制器(controllers,就是1.3.3节中介绍的MVC中的C)联系在一起,包含常用目的的一套动作。我们在第二章领略了控制器,一旦我们更全面地揭开REST架构(从第6章开始)的神秘面纱,将会有一个更加深入的理解。为了找到感觉,回忆一下在1.3节(图1.4)介绍的Rails的目录结构会对我们有帮助的。在这节,我们将开始主要在**app/controllers**和**# app/views**目录工作。 104 | 105 | 回忆一下1.4.4节,当我们使用Git时,在一个单独的主题分支修改代码比起直接在主分支修改来说是一个更好的软件开发实践。假如你正用Git做版本控制,你应该运行以下命令,为静态页面检出(checkout)主题分支: 106 | ``` 107 | $ git checkout master 108 | $ git checkout -b static-pages 109 | ``` 110 | (这里第一行只是确定你从主分支开始,以便**static-pages**主题分支是基于主分支的。假如你已经在主分支了,你可以跳过第一条命令。) 111 | 112 | ### 3.2.1 生成静态页面 113 | 从静态页面开始,和第二章一样的,我们同样使用Rails的**generate**生成控制器的命令来生成脚手架。因为我们要开始处理静态页面了,所以我们就叫它静态页面控制器,使用[驼峰式命名法](https://en.wikipedia.org/wiki/CamelCase)命名为**StaticPages**。我们也计划添加一个主页,一个帮助页面,和一个关于页面,使用小写的动作名称**home、help、about**。**generate**脚本以动作名列表作为参数,所以我们将主页、帮助页面直接加在命令行上,故意不把“关于”页面的动作名加上,以便在后面看看怎么添加(3.3节)。命令的结果是生成如代码清单3.4所示的静态页面控制器。 114 | ``` 115 | 代码清单 3.4: Generating a Static Pages controller. 116 | $ rails generate controller StaticPages home help 117 | create app/controllers/static_pages_controller.rb 118 | route get 'static_pages/help' 119 | route get 'static_pages/home' 120 | invoke erb 121 | create # app/views/static_pages 122 | create # app/views/static_pages/home.html.erb 123 | create # app/views/static_pages/help.html.erb 124 | invoke test_unit 125 | create test/controllers/static_pages_controller_test.rb 126 | invoke helper 127 | create app/helpers/static_pages_helper.rb 128 | invoke test_unit 129 | create test/helpers/static_pages_helper_test.rb 130 | invoke assets 131 | invoke coffee 132 | create app/assets/javascripts/static_pages.coffee 133 | invoke css 134 | create app/assets/stylesheets/static_pages.css 135 | ``` 136 | 顺便提一下,虽然不值得一提,**rails g**是**rails generate**的缩写,是Rails支持的几个缩写之一(表3.1)。声明一下,本教程总是使用全拼的命令,但是在真实的开发过程中,Rails开发者常常使用表3.1所示的一个或多个缩写。 137 | 138 | Full command | Shortcut 139 | -----------------|--------- 140 | $ rails server | $ rails s 141 | $ rails console | $ rails c 142 | $ rails generate | $ rails g 143 | $ bundle install | $ bundle 144 | $ rails test | $ rake 145 | 146 | 【表3.1:一些Rails命令的缩写 147 | 148 | 在继续之前,假如你正使用Git,把我们的静态控制器加入到远程仓库是个很好的主意。 149 | ``` 150 | $ git status 151 | $ git add -A 152 | $ git commit -m "添加一个静态页面控制器" 153 | $ git push -u origin static-pages 154 | ``` 155 | 最后一行命令把**static-pages**主题分支推送至Bitbucket。 156 | 随后推送时可以忽略别的参数,只写 157 | ``` 158 | $ git push 159 | ``` 160 | 提交和推送是我平常在真实开发过程中遵循的模式,但是为了简化,从现在开始我会忽略类似这种中间提交。 161 | 162 | 在代码清单3.4里,记住我们已经按照驼峰命名法传递过控制器的名字,它会创建一个[蛇形命名法](https://en.wikipedia.org/wiki/Snake_case)的控制器文件,所以StaticPages控制器生成文件名为**static_pages_controller.rb**的文件。这只是个惯例,实际上使用蛇形命名法也是一样的:命令 163 | ``` 164 | $ rails generate controller static_pages ... 165 | ``` 166 | 也是生成**static_pages_controller.rb**文件。因为Ruby用驼峰命名法为类命名(章节4.4),我的偏好是使用控制器的驼峰命名法,但是这只是个人习惯问题。(因为Ruby文件名一贯使用蛇形命名法,Rails生成器会使用**underscore**将驼峰命名法的单词转为蛇形命名法。) 167 | 168 | 顺便提一下,假如你生成代码时犯了错误,知道怎么取消操作是很有用的。关于Rails里是怎么取消操作的,请参考注3.1。 169 | 170 | 注3.1 取消操作 171 | 即使你非常小心,当你开发Rails应用程序时你也搞错一些事情。令人高兴的是,Rails有一些帮你恢复的工具。 172 | 173 | 一个常见的场景是想要还原生成的代码--例如,当你想改变你之前生成的控制器的名称,先要删除已经生成的文件。因为Rails和控制器一道创建了数量可观的辅助文件(如代码清单3.4所示), 174 | 这不是和移除控制器文件本身一样容易;取消生成文件意味着取消主要的文件,而且也包括辅助的文件。(实际上,正如我们再章节2.2和2.3中看到的一样, 175 | **rails generate**可以自动编辑**routes.rb**文件,这个我们也需要自动还原。)在Rails里,可以通过rails destroy加上生成的要素的名称 176 | 完成。在这里,这两个命令取消了彼此的输出: 177 | ``` 178 | $ rails generate controller StaticPages home help 179 | $ rails destroy controller StaticPages home help 180 | ``` 181 | 类似地,在第6章,我们按照如下方式生成模型**model**: 182 | ``` 183 | $ rails generate model User name:string email:string 184 | ``` 185 | 可以使用以下命令还原 186 | ``` 187 | $ rails destroy model User 188 | ``` 189 | (在这个例子里,证明了我们可以省略其他命令行参数。等到了第6章,看看你是不是能弄明白为什么。) 190 | 另一个和模型相关的技术是还原迁移(migrations),我们在第2章简单的介绍过,在第6章将会经常用到。迁移使用以下命令改变了数据的状态 191 | ``` 192 | $ bundle exec rails db:migrate 193 | ``` 194 | 我们可以使用以下命令还原一步 195 | ``` 196 | $ bundle exec rails db:rollback 197 | ``` 198 | 还原到初始状态,我们可以用 199 | ``` 200 | $ bundle exec rails db:migrate VERSION=0 201 | ``` 202 | 正如你所猜到的,用其他数字代替0会迁移到数字对应的迁移,版本数字来自原列在迁移后面的数字。 203 | 204 | 掌握了这些技术,我们已经准备好了应对开发过程中的不可避免的混乱局面了。 205 | 206 | 为了理解这个页面来自于哪里,让我们打开文本编辑器,看看静态页面控制器。应该看上去和代码清单3.6一样。你可能注意到了,和第2章用户和微博控制器不一样,静态页面控制器没有使用标准的REST动作。这对于一个静态页面的集合是正常的:REST架构不适用于每个问题。 207 | 208 | ``` 209 | 代码清单 3.6: 代码清单3.4生成的静态页面控制器 210 | # app/controllers/static_pages_controller.rb 211 | class StaticPagesController < ApplicationController 212 | 213 | def home 214 | end 215 | 216 | def help 217 | end 218 | end 219 | ``` 220 | 221 | 我们看见在static_pages_controller.rb文件里,如代码清单3.6所示,使用class定义了一个类(class),在这里,这个类名为**StaticPagesController**。类是一种组织函数(也叫方法)很方便的途径,就像用**def**定义的home和help动作一样。正如2.3.4节讨论的那样,尖括号**<**表示StaticPagesController继承于Rails的ApplicationController类;就像我们将要看见的一样,这意味着我们的页面已经具有了大量的Rails指定的功能。(我们将在章节4.4中学习更多类和继承的知识。) 222 | 223 | 在这个静态页面控制器的例子里,它的两个方法均是空的: 224 | ```ruby 225 | def home 226 | end 227 | 228 | def help 229 | end 230 | ``` 231 | 在Ruby中,这些方法什么也不做。在Rails中,情况却步相同。因为StaticPagesController是一个Ruby类,但是因为它继承于ApplicationController,所以这些方法的行为对于Rails来说是有特定意义的:当访问URL /static_pages/home, Rails在静态页面控制类里查找和执行home动作,然后渲染视图(1.3.3节中MVC中的V),回应这个动作。在目前的情况,home动作是空的,所以所有访问/static_pages/home所做得就是渲染视图。那么视图是什么样的,怎么找到它? 232 | 233 | 假如你再看看代码清单3.4里的输出,你可能可以猜出来在动作和视图之间的响应:像home动作有一个对应的视图,叫做home.html.erb。我们将在章节3.4里学习到**.erb**的意思;**.html**你可能不惊奇,因为看起来像HTML(代码清单3.7)。 234 | ``` 235 | 代码清单 3.7: 为Home页面生成的视图 236 | # app/views/static_pages/home.html.erb 237 |

StaticPages#home

238 |

Find me in # app/views/static_pages/home.html.erb

239 | ``` 240 | 241 | **help**动作的视图类似(代码清单3.8) 242 | ``` 243 | 代码清单 3.8: 为Help页面生成的视图 244 | # app/views/static_pages/help.html.erb 245 |

StaticPages#help

246 |

Find me in # app/views/static_pages/help.html.erb

247 | ``` 248 | 249 | 这些视图只是占位符:它们有个一级的标题,(在标签**h1**里)和一段(标签**p**)包含对应文件完整路径的段落。 250 | 251 | ## 3.2.2 自定义静态页面 252 | 在3.4节我们开始加入一点点动态的内容,但是如代码清单3.7和3.8说明的一样,强调了一点:Rails视图可以仅包含静态的HTML文件。这意味着即使我们没有Rails的相关知识,我们可以开代码清单始自定义主页和帮助页面,如同代码清单3.9和3.10所示。 253 | ``` 254 | 代码清单 3.9: 为Home页面自定义HTML。 255 | # app/views/static_pages/home.html.erb 256 |

Sample App

257 |

258 | This is the home page for the 259 | Ruby on Rails Tutorial 260 | sample application. 261 |

262 | ``` 263 | ``` 264 | 代码清单 3.10: 为Help页面自定义HTML。 265 | # app/views/static_pages/help.html.erb 266 |

Help

267 |

268 | Get help on the Ruby on Rails Tutorial at the 269 | Rails Tutorial help section. 270 | To get help on this sample app, see the 271 | Ruby on Rails Tutorial 272 | book. 273 |

274 | ``` 275 | 代码清单3.9和3.10的结果如图3.3和3.4. 276 | ![图3.3:自定义主页](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/custom_home_page.png) 277 | ![图3.4:自定义帮助y](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/custom_help_page_3rd_edition.png) 278 | 279 | ## 3.3 从测试开始 280 | 既然我们已经创建并且为我们的示例程序的主页和帮助页面添加了内容(3.2.2节),现在我们准备再添加一个关于页面。当我们进行这一类的变化,好的开发实践是先写自动化测试来验证是否正确地实现了这个特性。在创建应用程序的过程中,测试套件就像安全网,像一套可执行的文档。当这些都正确实施时,即便编写测试要写一些额外的代码,也可以加快我们开发的速度,因为我们追踪bug的时间变少了。一旦我们擅长写测试时这就是真理,尽可能早地开始实践是很重要的,这也是其中的原因之一。 281 | 282 | 尽管所有的Rails开发者都同意测试是好主意,但是在细节方面也有不少分歧。关于测试驱动开发(TDD)有一个尤其活跃的争论,TDD是一种测试技术,程序员先写失败的测试,然后在编写能让测试通过的代码。本教程采取轻量级的、简便的方法去测试,当需要的时候才使用测试驱动开发(TDD),而不是像教条主义一样的(注3.3)。 283 | 284 | 285 |

注3.3 何时需要测试

286 | 当决定何时和怎样测试时,理解为什么测试是很有帮助的。在我来看,写自动测试有三个主要的好处: 287 | 1. 测试可以有效防止功能特性因为某些原因不工作而造成开发回溯。 288 | 2. 测试允许重构代码(例如,不改变功能的情况下重构),而且更有信心。 289 | 3. 测试同时扮演应用程序客户的角色,因此有助于决定系统设计以及系统其他部分的接口。 290 | 291 | 尽管以上的好处没有一个是要求先写测试的,有许多情况测试驱动开发(TDD)是你工具包里有价值的工具之一。决定何时以及怎样测试部分依赖于你写的测试是多么舒服;许多开发者发现当他们越擅长写测试,他们越倾向于先写测试。同时,它也依赖于和应用程序代码相关的测试编写难度有多大、想要的特性是否清楚,而且未来这个特性被更改的可能性有多大。 292 | 293 | 在这篇教程里,有一套关于何时我们应该先写测试(或者根本不写测试)的指导标准是有帮助的。基于我个人的经验,这里我有一些建议: 294 | * 当测试非常短或者和它测试的应用代码相比很简单,倾向于先写测试。 295 | * 当想要的行为还不是特别清楚,倾向于先写代码,然后针对结果写测试。 296 | * 因为安全是顶级优先的,安全模型的错误测试先写。 297 | * 无论何时发现bug,写一个重现bug的测试,优先防止开发回溯,然后编写补丁。 298 | * 未来可能改变的(例如HTML结构的细节)倾向于不写测试。 299 | * 重构代码前先写测试,专注于可能导致程序崩溃的测试。 300 | 301 | 在实践中,以上的指导思路意味着我们通常先写控制器和模型测试,集成测试(测试连贯模型、视图和控制器的)次之,而且当我们正在编写的代码是不太容易崩溃的、或者不太容易出错的应用程序代码、或者可能会变化的(常发生在视图)代码时,我们常常跳过测试。 302 | 303 | 304 | 我们主要的测试工具将是控制器测试(从本节开始)、模型测试(从第6章开始),还有集成测试(从第7章开始)。集成测试尤其强大,因为它允许我们模拟用户使用网页浏览器和我们的应用程序进行交互。集成测试最终是我们主要的测试技术,但是控制器测试会让我们从容易的地方着手。 305 | 306 | ### 3.3.1 我们的第一个测试 307 | 现在是给我们的应用程序添加关于页面的的时候了。正如我们将看见的一样,这个测试很简单,所以我们根据注3.3的指导方针先写测试,然后我们使用失败的测试来驱动我们编写应用程序代码。 308 | 309 | 从测试开始有点挑战性,涉及到大量的Rails和Ruby知识。在我们目前的阶段,写测试可能让人感到歇斯底里的胆怯。幸运的是,Rails已经帮我们完成了最艰难的部分,因为**rails generate controller**(代码清单3.4)自动生成了测试文件,我们可以从这里开始: 310 | ``` 311 | $ ls test/controllers/ 312 | static_pages_controller_test.rb 313 | ``` 314 | 315 | 让我们看看它(代码清单3.11) 316 | ``` 317 | 代码清单 3.11: The default tests for the StaticPages controller. 绿色 318 | # test/controllers/static_pages_controller_test.rb 319 | require 'test_helper' 320 | 321 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest 322 | test "should get home" do 323 | get static_pages_home_url 324 | assert_response :success 325 | end 326 | 327 | test "should get help" do 328 | get static_pages_help_url 329 | assert_response :success 330 | end 331 | end 332 | ``` 333 | 334 | 现在理解代码清单3.11里面的语法细节还不是很重要,但是我们看到在测试文件中有两个测试,对应着我们在代码清单3.4的命令行上的每个控制器动作。每个测试只是简简单单得到一个动作,然后确认(通过断言)结果成功。这里的**get**的用法表示我们的测试想要主页和帮助也面是普通的网页,使用GET请求进入(旁注3.2)。这个响应**:success**是一个抽象的HTTP[状态码](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)(在这里,是[200 OK](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success)。换句话说,像这个测试: 335 | ```ruby 336 | test "should get home" do 337 | get static_pages_home_url 338 | assert_response :success 339 | end 340 | ``` 341 | 是说“让我们通过发送GET请求到**home**动作来测试主页,然后确认我们收到了“成功”状态码。” 342 | 343 | 为了开始我们的TDD测试驱动开发循环,我们需要运行我们的测试套件来确认目前的测试通过。我们用如下命令,使用**rails**(旁注2.1)来测试。 344 | ``` 345 | 代码清单 3.12:GREEN 346 | $ bundle exec rails test 347 | 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips 348 | ``` 349 | 正如我们想要的,我们的测试套件开始时候是通过的(绿的)。(除非你根据3.7.1节添加了mini-test-reporter,你不会真的看见绿色)。顺便提一下,刚开始测试时需要花费一点时间,这是因为两个因素:(1)启动Spring服务时需要预加载部分Rails环境,这个只会在第一次启动Spring时发生;(2)和Ruby启动时间相关的一些因素。(第二个因素在使用3.7.3节建议的Guard时会改善) 350 | 351 | ###3.3.2 红色 352 | 如同在旁注3.3里说明的一样,测试驱动开发需要先写测试,再写能让测试通过的应用程序代码,然后如果必要的话重构代码。因为许多测试工具用红色表示测试失败,绿色表示测试通过,所以就有了“红色,绿色,重构”更替。在本节中,我们先完成这个循环的第一步,先写变红的(失败的)测试。然后我们在3.3.3节中让测试变绿(通过),然后在3.4.3节中重构。 353 | 354 | 我们的第一步是写一个关于页面的失败测试。遵循代码清单3.11里的模型,你可能大概猜到怎么写测试了,具体如代码清单3.13. 355 | ``` 356 | 代码清单 3.13: 为“关于”页面写的测试。红色 357 | # test/controllers/static_pages_controller_test.rb 358 | 359 | require 'test_helper' 360 | 361 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest 362 | test "should get home" do 363 | get static_pages_home_url 364 | assert_response :success 365 | end 366 | 367 | test "should get help" do 368 | get static_pages_help_url 369 | assert_response :success 370 | end 371 | 372 | test "shold get about" do 373 | get statice_pages_about_url 374 | assert_response :success 375 | end 376 | end 377 | ``` 378 | 379 | 我们从代码清单3.13中看到的高亮的代码就是关于页面的的测试。除了用“about”代替了“home”和“help”之外,基本上是一模一样的。 380 | 381 | 正如我们想要的,测试开始时失败了: 382 | 383 | ```ruby 384 | 代码清单3.14:红色 385 | $ bundle exec rails test 386 | 3 tests, 2 assertions, 0 failures, 1 errors, 0 skips 387 | ``` 388 | ### 3.3.3 绿色 389 | 既然我们有了一个失败的测试(红色),我们将使用这个失败测试的错误信息指导我们通过测试(绿色),因此实现一个工作的关于页面。 390 | 391 | 我们从检查失败测试输出的错误信息开始: 392 | ```terminal 393 | 代码清单 3.15:红色 394 | $ bundle exec rails test 395 | Error: 396 | StaticPagesControllerTest#test_shold_get_about: 397 | NameError: undefined local variable or method `statice_pages_about_url' for 398 | # 399 | test/controllers/static_pages_controller_test.rb:15:in `block in 400 | ' 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 | <!DOCTYPE html> 525 | <html> 526 | <head> 527 | <title>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 | <!DOCTYPE html> 584 | <html> 585 | <head> 586 | <title>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 | ![图3.6: 带标题的主页](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/home_view_full_html.png) 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 | ![图3.7: 根路由主页](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/home_root_route.png) 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 | ![图3.8:云IDE显示红色和绿色](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd0_edition/images/figures/红色_to_绿色.png) 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 | ![图3.9: 在文件导航面板的齿轮图标](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/file_navigator_gear_icon.png) 1123 | ![图3.10:在文件导航栏显示隐藏文件](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/show_hidden_files.png) 1124 | ![图3.11:平常隐藏的**.gitignore**文件可见了](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/gitignore.png) 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 | ![图4.1:String类的继承等级](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/string_inheritance_ruby_1_9.png) 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 | ![图4.2:对于代码清单4.12里的Word类(不是内建的)继承等级](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/word_inheritance_ruby_1_9.png) 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 | ![图4.3:静态页面的继承等级](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/static_pages_controller_inheritance.png) 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 | ![图5.1:示例程序主页的框架图](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/home_page_mockup_3rd_edition.png) 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 | ![图5.7:添加了底部的主页](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/site_with_footer_bootstrap_3rd_edition.png) 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 |
1088 | 1089 | The Ruby on Rails Tutorial 1090 | by Michael Hartl 1091 | 1092 | 1099 |
1100 | ``` 1101 | 现在,我们的布局文件已经有了第三章创建的所有静态页面的链接,即,例如,/about映射到关于页面(图5.8)。 1102 | 1103 | ![图5.8:路径/about的关于页面](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/about_page_styled_3rd_edition.png) 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" | 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 | ![图5.9:在/signup的新注册页](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/new_signup_page_3rd_edition.png) 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 | ![图5.10:生产环境里的Sample App程序](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/layout_production.png) 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 | ![图6.1:用户注册页面原型](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/signup_mockup_bootstrap.png) 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 | ![图6.2:用户表里的示例数据](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/users_table.png) 42 | 43 | ![图6.3:用户数据模型的草图](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/users_table.png) 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 | ![图6.4:代码清单6.2产生的用户数据模型](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/user_model_initial_3rd_edition.png) 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 | ![从云IDE下载文件](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/sqlite_download.png) 94 | 95 | ![图6.6:SQLite数据库浏览器显示新的用户表](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/sqlite_download.png) 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 | ![图6.7:很棒的Rubular正则表达式编辑器](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/rubular.png) 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 | ![图6.8:带password_digest属性的数据模型](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/user_model_password_digest_3rd_edition.png) 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 | ![图6.9:在SQLite数据库db/developement.sqlite3里的用户行](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/sqlite_user_row_with_password_3rd_edition.png) 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 | ![图8.5:登陆失败的闪现消息](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/failed_login_flash_3rd_edition.png) 33 | 34 | 不幸的是,如在文本和在代码清单8.6里的注释里提到的,这段代码不是十分正确。页面看上去很好,不过,发生什么了?问题是flash的内容是持续一个请求,但是--不像重定向,我们在代码清单7.24里使用的--用render重新渲染模板不算请求。结果是flash信息显示的比我们想要的一个请求长。例如,假如我们提交无效的登陆信息,然后点击主页,flash又显示了(图8.6)。解决这个瑕疵是8.1.5的任务。 35 | 36 | ![图8.6:flash持续的例子](https://softcover.s3.amazonaws.com/636/ruby_on_rails_tutorial_3rd_edition/images/figures/flash_persistence_3rd_edition.png) 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 | --------------------------------------------------------------------------------