ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 7.1 显示用户的信息 ![profile mockup profile name bootstrap](https://box.kancloud.cn/2016-05-11_5732bd102e129.png)图 7.1:本节实现的用户资料页面构思图 本节要实现的用户资料页面是完整页面的一小部分,只显示用户的名字和头像,构思图如[图 7.1](#fig-profile-mockup-profile-name) 所示。[[1](#fn-1)] 最终完成的用户资料页面会显示用户的头像、基本信息和微博列表,构思图如[图 7.2](#fig-profile-mockup) 所示。[[2](#fn-2)](在图 7.2 中,我们第一次用到了“lorem ipsum”占位文字,[这些文字背后的故事](http://www.straightdope.com/columns/read/2290/what-does-the-filler-text-lorem-ipsum-mean)很有意思,有空的话你可以了解一下。)这个资料页面会和整个演示应用一起在[第 12 章](chapter12.html#following-users)完成。 如果你一直坚持使用版本控制,现在像之前一样,新建一个主题分支: ``` $ git checkout master $ git checkout -b sign-up ``` ![profile mockup bootstrap](https://box.kancloud.cn/2016-05-11_5732bd1049c33.png)图 7.2:最终实现的用户资料页面构思图 ## 7.1.1 调试信息和 Rails 环境 本节要实现的用户资料页面是第一个真正意义上的动态页面。虽然视图的代码不会动态改变,不过每个用户资料页面显示的内容却是从数据库中读取的。添加动态页面之前,最好做些准备工作,现在我们能做的就是在网站布局中加入一些调试信息,如[代码清单 7.1](#listing-rails-debug) 所示。这段代码使用 Rails 内置的 `debug` 方法和 `params` 变量([7.1.2 节](#a-users-resource)会详细介绍),在各个页面显示一些对开发有帮助的信息。 ##### 代码清单 7.1:在网站布局中添加一些调试信息 app/views/layouts/application.html.erb ``` <!DOCTYPE html> <html> . . . <body> <%= render 'layouts/header' %> <div class="container"> <%= yield %> <%= render 'layouts/footer' %> <%= debug(params) if Rails.env.development? %> </div> </body> </html> ``` 因为我们不想在线上网站中向用户显示调试信息,所以上述代码使用 `if Rails.env.development?` 限制只在开发环境中显示调试信息。开发环境是 Rails 默认支持的三个环境之一([旁注 7.1](#aside-rails-environments))。[[3](#fn-3)]`Rails.env.development?` 的返回值只在开发环境中才是 `true`,所以下面这行嵌入式 Ruby 代码 ``` <%= debug(params) if Rails.env.development? %> ``` 不会在生产环境和测试环境中执行。(在测试环境中显示调试信息虽然没有坏处,但也没什么好处,所以最好只在开发环境中显示。) ##### 旁注 7.1:Rails 环境 Rails 定义了三个环境,分别是测试环境、开发环境和生产环境。Rails 控制台默认使用的是开发环境: ``` $ rails console Loading development environment >> Rails.env => "development" >> Rails.env.development? => true >> Rails.env.test? => false ``` 如前所示,`Rails` 对象有一个 `env` 属性,属性上还可以调用各环境对应的布尔值方法,例如,`Rails.env.test?`,在测试环境中的返回值是 `true`,在其他两个环境中的返回值则是 `false`。 如果需要在其他环境中使用控制台(例如,在测试环境中调试),只需把环境名传给 `console` 命令即可: ``` $ rails console test Loading test environment >> Rails.env => "test" >> Rails.env.test? => true ``` Rails 本地服务器和控制台一样,默认使用开发环境,不过也可以在其他环境中运行: ``` $ rails server --environment production ``` 如果要在生产环境中运行应用,先要有一个生产数据库。在生产环境中执行 `rake db:migrate` 命令可以生成这个数据库: ``` $ bundle exec rake db:migrate RAILS_ENV=production ``` (我发现在控制台、服务器和迁移命令中指定环境的方法不一样,可能会混淆,所以特意演示了这三个命令的用法。) 顺便说一下,把应用部署到 Heroku 后,可以使用 `heroku run console` 命令进入控制台查看使用的环境: ``` $ heroku run console >> Rails.env => "production" >> Rails.env.production? => true ``` Heroku 是用来部署网站的平台,自然会在生产环境中运行应用。 为了让调试信息看起来漂亮一些,我们在[第 5 章](chapter5.html#filling-in-the-layout)创建的自定义样式表文件中加入一些样式规则,如[代码清单 7.2](#listing-mixin-and-debug) 所示。 ##### 代码清单 7.2:添加美化调试信息的样式,使用了一个 Sass 混入 app/assets/stylesheets/custom.css.scss ``` @import "bootstrap-sprockets"; @import "bootstrap"; /* mixins, variables, etc. */ $gray-medium-light: #eaeaea; @mixin box_sizing { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } . . . /* miscellaneous */ .debug_dump { clear: both; float: left; width: 100%; margin-top: 45px; @include box_sizing; } ``` 这段代码用到了 Sass 的“混入”(mixin)功能,创建的这个混入名为 `box-sizing`。混入用来打包一系列样式规则,可多次使用。预处理器会把 ``` .debug_dump { . . . @include box_sizing; } ``` 转换成 ``` .debug_dump { . . . -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } ``` [7.2.1 节](#using-form-for)会再次用到这个混入。美化后的调试信息如[图 7.3](#fig-home-page-with-debug) 所示。 [图 7.3](#fig-home-page-with-debug) 中的调试信息显示了当前页面的一些有用信息: ``` --- controller: static_pages action: home ``` 这是 `params` 变量的 YAML [[4](#fn-4)]形式,和哈希类似,显示当前页面的控制器名和动作名。[7.1.2 节](#a-users-resource)会介绍其他调试信息的意思。 ![home page with debug 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd106151c.png)图 7.3:显示有调试信息的演示应用首页 ## 7.1.2 用户资源 为了实现用户资料页面,数据库中要有用户记录,这引出了“先有鸡还是先有蛋”的问题:网站还没有注册页面,怎么可能有用户呢?其实这个问题在 [6.3.4 节](chapter6.html#creating-and-authenticating-a-user)已经解决了,那时我们自己动手在 Rails 控制台中创建了一个用户,所以数据库中应该有一个用户记录: ``` $ rails console >> User.count => 1 >> User.first => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2014-08-29 02:58:28", updated_at: "2014-08-29 02:58:28", password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW..."> ``` (如果你的数据库中现在没有用户记录,回到 [6.3.4 节](chapter6.html#creating-and-authenticating-a-user),在继续阅读之前完成那里的操作。)从控制台的输出可以看出,这个用户的 ID 是 `1`,我们现在的目标就是创建一个页面,显示这个用户的信息。我们会遵从 Rails 使用的 REST 架构([旁注 2.2](chapter2.html#aside-rest)),把数据视为资源,可以创建、显示、更新和删除。这四个操作分别对应 [HTTP 标准](http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol)中的 `POST`、`GET`、`PATCH` 和 `DELETE` 请求方法([旁注 3.2](chapter3.html#aside-get-etc))。 按照 REST 架构的规则,资源一般由资源名加唯一标识符表示。我们把用户看做一个资源,若要查看 ID 为 1 的用户,就要向 /users/1 发送 `GET` 请求。这里没必要指明用哪个动作,Rails 的 REST 功能解析时,会自动把这个 `GET` 请求交给 `show` 动作处理。 [2.2.1 节](chapter2.html#a-user-tour)介绍过,ID 为 1 的用户对应的 URL 是 /users/1,不过现在访问这个 URL 的话,会显示错误信息,如[图 7.4](#fig-profile-routing-error) 中的服务器日志所示。 ![profile routing error 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd107e1a1.png)图 7.4:访问 /users/1 时服务器日志中显示的错误 我们只需在路由文件 `config/routes.rb` 中添加如下的一行代码就可以正常访问 /users/1 了: ``` resources :users ``` 修改后的路由文件如[代码清单 7.3](#listing-users-resource) 所示。 ##### 代码清单 7.3:在路由文件中添加用户资源的规则 config/routes.rb ``` Rails.application.routes.draw do root 'static_pages#home' get 'help' => 'static_pages#help' get 'about' => 'static_pages#about' get 'contact' => 'static_pages#contact' get 'signup' => 'users#new' resources :users end ``` 我们的目的只是为了显示用户资料页面,可是 `resources :users` 不仅让 /users/1 可以访问,而且还为演示应用中的用户资源提供了符合 REST 架构的所有动作,[[5](#fn-5)]以及用来获取相应 URL 的具名路由([5.3.3 节](chapter5.html#using-named-routes))。最终得到的 URL、动作和具名路由的对应关系如[表 7.1](#table-restful-users) 所示(和[表 2.2](chapter2.html#table-demo-restful-users) 对比一下)。接下来的三章会介绍[表 7.1](#table-restful-users) 中的所有动作,并不断完善,把用户打造成完全符合 REST 架构的资源。 表 7.1:[代码清单 7.3](#listing-users-resource) 中添加的用户资源规则实现的 REST 架构路由 | HTTP 请求 | URL | 动作 | 具名路由 | 作用 | | --- | --- | --- | --- | --- | | `GET` | /users | `index` | `users_path` | 显示所有用户的页面 | | `GET` | /users/1 | `show` | `user_path(user)` | 显示单个用户的页面 | | `GET` | /users/new | `new` | `new_user_path` | 创建(注册)新用户的页面 | | `POST` | /users | `create` | `users_path` | 创建新用户 | | `GET` | /users/1/edit | `edit` | `edit_user_path(user)` | 编辑 ID 为 1 的用户页面 | | `PATCH` | /users/1 | `update` | `user_path(user)` | 更新用户信息 | | `DELETE` | /users/1 | `destroy` | `user_path(user)` | 删除用户 | 添加[代码清单 7.3](#listing-users-resource) 中的代码之后,路由就生效了,但是页面还不存在([图 7.5](#fig-user-show-unknown-action))。下面我们在页面中添加一些简单的内容,[7.1.4 节](#a-gravatar-image-and-a-sidebar)还会添加更多内容。 ![user show unknown action 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd10a1b48.png)图 7.5:/users/1 的路由生效了,但页面不存在 用户资料页面的视图保存在标准的位置,即 `app/views/users/show.html.erb`。这个视图和自动生成的 `new.html.erb`([代码清单 5.28](chapter5.html#listing-generate-users-controller))不同,现在不存在,要手动创建,然后写入[代码清单 7.4](#listing-stub-user-view) 中的代码。 ##### 代码清单 7.4:用户资料页面的临时视图 app/views/users/show.html.erb ``` <%= @user.name %>, <%= @user.email %> ``` 在这段代码中,我们假设存在一个 `@user` 变量,使用 ERb 代码显示这个用户的名字和电子邮件地址。这和最终实现的视图有点不一样,届时不会公开显示用户的电子邮件地址。 我们要在用户控制器的 `show` 动作中定义 `@user` 变量,用户资料页面才能正常渲染。你可能猜到了,我们要在用户模型上调用 `find` 方法([6.1.4 节](chapter6.html#finding-user-objects)),从数据库中取出用户记录,如[代码清单 7.5](#listing-user-show-action) 所示。 ##### 代码清单 7.5:含有 `show` 动作的用户控制器 app/controllers/users_controller.rb ``` class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new end end ``` 在这段代码中,我们使用 `params` 获取用户的 ID。当我们向用户控制器发送请求时,`params[:id]` 会返回用户的 ID,即 1,所以这就和 [6.1.4 节](chapter6.html#finding-user-objects)中直接调用 `User.find(1)` 的效果一样。(严格来说,`params[:id]` 返回的是字符串 `"1"`,`find` 方法会自动将其转换成整数。) 定义视图和动作之后,/users/1 就可以正常访问了,如[图 7.6](#fig-user-show-rails) 所示。(如果添加 bcrypt 之后没重启过 Rails 服务器,现在或许要重启。)留意一下调试信息,证实了 `params[:id]` 的值和前面分析的一样: ``` --- action: show controller: users id: '1' ``` 所以,[代码清单 7.5](#listing-user-show-action) 中的 `User.find(params[:id])` 才会取回 ID 为 1 的用户。 ![user show 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd10be549.png)图 7.6:添加 `show` 动作后的用户资料页面 ## 7.1.3 调试器 在 [7.1.2 节](#a-users-resource)看到,调试信息能帮助我们理解应用的运作方式。从 Rails 4.2 开始,可以使用 `byebug` gem([代码清单 3.2](chapter3.html#listing-gemfile-sample-app))更直接地获取调试信息。我们把 `debugger` 加到应用中,看一下这个 gem 的作用,如[代码清单 7.6](#listing-debugger) 所示。 ##### 代码清单 7.6:在用户控制器中使用调试器 app/controllers/users_controller.rb ``` class UsersController < ApplicationController def show @user = User.find(params[:id]) debugger end def new end end ``` 现在访问 /users/1 时,会在 Rails 服务器的输出中显示 `byebug` 提示符: ``` (byebug) ``` 我们可以把它当成 Rails 控制台,在其中执行代码,看一下应用的状态: ``` (byebug) @user.name "Example User" (byebug) @user.email "example@railstutorial.org" (byebug) params[:id] "1" ``` 若想退出 `byebug`,继续执行应用,可以按 Ctrl-D 键。然后把 `show` 动作中的 `debugger` 删除,如[代码清单 7.7](#listing-debugger-removed) 所示。 ##### 代码清单 7.7:删除调试器后的用户控制器 app/controllers/users_controller.rb ``` class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new end end ``` 只要你觉得 Rails 应用中哪部分有问题,就可以在可能导致问题的代码附近加上 `debugger`。`byebug` 很强大,可以查看系统的状态,查找应用错误,以及交互式调试应用。 ## 7.1.4 Gravatar 头像和侧边栏 前面创建了一个略显简陋的用户资料页面,这一节要再添加一些内容:用户头像和侧边栏。首先,我们要在用户资料页面中添加一个“全球通用识别”的头像,或者叫 [Gravatar](http://gravatar.com/)。[[6](#fn-6)]这是一个免费服务,让用户上传图片,将其关联到自己的电子邮件地址上。使用 Gravatar 可以简化在网站中添加用户头像的过程,开发者不必分心去处理图片上传、剪裁和存储,只要使用用户的电子邮件地址构成头像的 URL 地址,用户的头像就会显示出来。([11.4 节](chapter11.html#micropost-images)会介绍如何处理图片上传。) 我们的计划是,定义一个名为 `gravatar_for` 的辅助方法,返回指定用户的 Gravatar 头像,如[代码清单 7.8](#listing-user-show-view-with-gravatar) 所示。 ##### 代码清单 7.8:显示用户名字和 Gravatar 头像的用户资料页面视图 app/views/users/show.html.erb ``` <% provide(:title, @user.name) %> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> ``` 默认情况下,所有辅助方法文件中定义的方法都自动在任意视图中可用,不过为了便于管理,我们会把 `gravatar_for` 方法放在用户控制器对应的辅助方法文件中。根据 [Gravatar 的文档](http://en.gravatar.com/site/implement/hash/),头像的 URL 地址中要使用用户电子邮件地址的 [MD5 哈希值](http://en.wikipedia.org/wiki/MD5)。在 Ruby 中,MD5 哈希算法由 `Digest` 库中的 `hexdigest` 方法实现: ``` >> email = "MHARTL@example.COM". >> Digest::MD5::hexdigest(email.downcase) => "1fda4469bcbec3badf5418269ffc5968" ``` 电子邮件地址不区分大小写,但是 MD5 哈希算法区分,所以我们要先调用 `downcase` 方法把电子邮件地址转换成小写形式,然后再传给 `hexdigest` 方法。(在[代码清单 6.31](chapter6.html#listing-email-downcase) 中的回调里我们已经把电子邮件地址转换成小写形式了,但这里最好也转换,以防电子邮件地址来自其他地方。)我们定义的 `gravatar_for` 辅助方法如[代码清单 7.9](#listing-gravatar-for-helper) 所示。 ##### 代码清单 7.9:定义 `gravatar_for` 辅助方法 app/helpers/users_helper.rb ``` module UsersHelper # 返回指定用户的 Gravatar def gravatar_for(user) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end end ``` `gravatar_for` 方法的返回值是一个 `img` 元素,用于显示 Gravatar 头像。`img` 标签的 CSS 类为 `gravatar`,`alt` 属性的值是用户的名字(对视觉障碍人士使用的屏幕阅读器特别有用)。 用户资料页面如[图 7.7](#fig-profile-with-gravatar) 所示,页面中显示的头像是 Gravatar 的默认图片,因为 `user@example.com` 不是真的电子邮件地址(example.com 这个域名是专门用来举例的)。 ![profile with gravatar 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd10d4433.png)图 7.7:显示 Gravatar 默认头像的用户资料页面![profile custom gravatar 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd10ecb5d.png)图 7.8:显示真实头像的用户资料页面 我们调用 `update_attributes` 方法([6.1.5 节](chapter6.html#updating-user-objects))更新一下数据库中的用户记录,然后就可以显示用户真正的头像了: ``` $ rails console >> user = User.first >> user.update_attributes(name: "Example User", ?> email: "example@railstutorial.org", ?> password: "foobar", ?> password_confirmation: "foobar") => true ``` 我们把用户的电子邮件地址改成 `example@railstutorial.org`。我已经把这个地址的头像设为了本书网站的 LOGO,修改后的结果如[图 7.8](#fig-profile-custom-gravatar) 所示。 我们还要添加一个侧边栏,才能完成[图 7.1](#fig-profile-mockup-profile-name) 中的构思图。我们要使用 `aside` 标签定义侧边栏。`aside` 中的内容一般是对主体内容的补充(例如侧边栏),不过也可以自成一体。我们要把 `aside` 标签的类设为 `row col-md-4`,这两个类都是 Bootstrap 提供的。在用户资料页面中添加侧边栏所需的代码如[代码清单 7.10](#listing-user-show-with-sidebar) 所示。 ##### 代码清单 7.10:在 `show` 动作的视图中添加侧边栏 app/views/users/show.html.erb ``` <% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> </aside> </div> ``` 添加 HTML 结构和 CSS 类之后,我们再用 SCSS 为资料页面定义一些样式,如[代码清单 7.11](#listing-sidebar-css) 所示。[[7](#fn-7)](注意:因为 Asset Pipeline 使用 Sass 预处理器,所以样式中才可以使用嵌套。)实现的效果如[图 7.9](#fig-user-show-sidebar-css) 所示。 ##### 代码清单 7.11:用户资料页面的样式,包括侧边栏的样式 app/assets/stylesheets/custom.css.scss ``` . . . /* sidebar */ aside { section.user_info { margin-top: 20px; } section { padding: 10px 0; margin-top: 20px; &:first-child { border: 0; padding-top: 0; } span { display: block; margin-bottom: 3px; line-height: 1; } h1 { font-size: 1.4em; text-align: left; letter-spacing: -1px; margin-bottom: 3px; margin-top: 0px; } } } .gravatar { float: left; margin-right: 10px; } .gravatar_edit { margin-top: 15px; } ```