11.4 微博中的图片
最后更新于:2022-04-01 22:30:27
# 11.4 微博中的图片
我们已经实现了微博相关的所有操作,本节要让微博除了能输入文字之外还能插入图片。我们首先会开发一个基础版本,只能在生产环境中使用,然后再做一系列功能增强,允许在生产环境上传图片。
添加图片上传功能明显要完成两件事:编写用于上传图片的表单,准备好所需的图片。上传图片按钮和微博中显示的图片构思如[图 11.18](#fig-micropost-image-mockup) 所示。[[9](#fn-9)]
![micropost image mockup](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_57333075d7360.png)图 11.18:图片上传界面的构思图(包含一张上传后的图片)
## 11.4.1 基本的图片上传功能
我们要使用 [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) 处理图片上传,并把图片和微博模型关联起来。为此,我们要在 `Gemfile` 中添加 `carrierwave` gem,如[代码清单 11.55](#listing-gemfile-carrierwave) 所示。为了一次安装完所有 gem,[代码清单 11.55](#listing-gemfile-carrierwave) 中还添加了用于调整图片尺寸的 `mini_magick`([11.4.3 节](#image-resizing))和用于在生产环境中上传图片的 `fog`([11.4.4 节](#image-upload-in-production))。
##### 代码清单 11.55:在 `Gemfile` 中添加 CarrierWave
```
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'carrierwave', '0.10.0' gem 'mini_magick', '3.8.0' gem 'fog', '1.36.0' gem 'will_paginate', '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
.
.
.
```
然后像之前一样,执行下面的命令安装:
```
$ bundle install
```
CarrierWave 自带了一个 Rails 生成器,用于生成图片上传程序。我们要创建一个名为 `picture` 的上传程序:
```
$ rails generate uploader Picture
```
CarrierWave 上传的图片应该对应于 Active Record 模型中的一个属性,这个属性只需存储图片的文件名字符串即可。添加这个属性后的微博模型如[图 11.19](#fig-micropost-model-picture) 所示。[[10](#fn-10)]
![micropost model picture](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_57333075f31c3.png)图 11.19:添加 `picture` 属性后的微博数据模型
为了把 `picture` 属性添加到微博模型中,我们要生成一个迁移,然后在开发服务器中执行迁移:
```
$ rails generate migration add_picture_to_microposts picture:string
$ bundle exec rake db:migrate
```
告诉 CarrierWave 把图片和模型关联起来的方式是使用 `mount_uploader` 方法。这个方法的第一个参数是属性的符号形式,第二个参数是上传程序的类名:
```
mount_uploader :picture, PictureUploader
```
(`PictureUploader` 类在 `picture_uploader.rb` 文件中,[11.4.2 节](#image-validation)会编辑,现在使用生成的默认内容即可。)把这个上传程序添加到微博模型,如[代码清单 11.56](#listing-micropost-model-picture) 所示。
##### 代码清单 11.56:在微博模型中添加图片上传程序
app/models/micropost.rb
```
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
```
在某些系统中可能要重启 Rails 服务器,测试组件才能通过。
如[图 11.18](#fig-micropost-image-mockup) 所示,为了在首页添加图片上传功能,我们要在发布微博的表单中添加一个 `file_field` 标签,如[代码清单 11.57](#listing-micropost-create-image-upload) 所示。
##### 代码清单 11.57:在发布微博的表单中添加图片上传按钮
app/views/shared/_micropost_form.html.erb
```
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<%= link_to micropost.user.name, micropost.user %>
<%= micropost.content %>
<%= image_tag micropost.picture.url if micropost.picture? %>
```
![microposts with image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_57333076173f8.png)图 11.20:发布包含图片的微博后显示的页面
## 11.4.2 验证图片
前一节添加的上传程序是个好的开始,但有一定不足:没对上传的文件做任何限制,如果用户上传的文件很大,或者类型不对,会导致问题。这一节我们要修正这个不足,添加验证,限制图片的大小和类型。我们既会在服务器端添加验证,也会在客户端(即浏览器)添加验证。
对图片类型的限制在 CarrierWave 的上传程序中设置。我们要限制能使用的图片扩展名(PNG,GIF 和 JPEG 的两个变种),如[代码清单 11.60](#listing-validate-picture-format) 所示。(在生成的上传程序中有一段注释说明了该怎么做。)
##### 代码清单 11.60:限制可上传图片的类型
app/uploaders/picture_uploader.rb
```
class PictureUploader < CarrierWave::Uploader::Base
storage :file
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# 添加一个白名单,指定允许上传的图片类型
def extension_white_list
%w(jpg jpeg gif png) end
end
```
图片大小的限制在微博模型中设定。和前面用过的模型验证不同,Rails 没有为文件大小提供现成的验证方法。所以我们要自己定义一个验证方法,我们把这个方法命名为 `picture_size`,如[代码清单 11.61](#listing-micropost-model-image-validation) 所示。注意,调用自定义的验证时使用的是 `validate` 方法,而不是 `validates`。
##### 代码清单 11.61:添加图片大小验证
app/models/micropost.rb
```
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
validate :picture_size
private
# 验证上传的图片大小 def picture_size if picture.size > 5.megabytes errors.add(:picture, "should be less than 5MB") end end end
```
这个验证会调用指定符号(`:picture_size`)对应的方法。在 `picture_size` 方法中,如果图片大于 5MB(使用[旁注 8.2](chapter8.html#aside-time-helpers) 中介绍的句法),就向 `errors` 集合([6.2.2 节](chapter6.html#validating-presence)简介过)添加一个自定义的错误消息。
除了这两个验证之外,我们还要在客户端检查上传的图片。首先,我们在 `file_field` 方法中使用 `accept` 参数限制图片的格式:
```
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
```
有效的格式使用 [MIME 类型](https://en.wikipedia.org/wiki/Internet_media_type)指定,这些类型对应于[代码清单 11.60](#listing-validate-picture-format) 中限制的类型。
然后,我们要编写一些 JavaScript(更确切地说是 [jQuery](http://jquery.com/) 代码),如果用户试图上传太大的图片就弹出一个提示框(节省了上传的时间,也减少了服务器的负载):
```
$('#micropost_picture').bind('change', function() {
var size_in_megabytes = this.files[0].size/1024/1024;
if (size_in_megabytes > 5) {
alert('Maximum file size is 5MB. Please choose a smaller file.');
}
});
```
本书虽然没有介绍 jQuery,不过你或许能理解这段代码:监视页面中 CSS ID 为 `micropost_picture` 的元素(如 `#` 符号所示,这是微博表单的 ID,参见[代码清单 11.57](#listing-micropost-create-image-upload)),当这个元素的内容变化时,会执行这段代码,如果文件太大,就调用 `alert` 方法。[[11](#fn-11)]
把这两个检查措施添加到微博表单中,如[代码清单 11.62](#listing-format-jquery-file-test) 所示。
##### 代码清单 11.62:使用 jQuery 检查文件的大小
app/views/shared/_micropost_form.html.erb
```
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
$ heroku config:set S3_SECRET_KEY=
$ heroku config:set S3_BUCKET=
```
配置好之后,我们可以提交并部署了。我们先提交主题分支中的变动,然后再合并到 `master` 分支:
```
$ bundle exec rake test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push
```
然后部署,重设数据库,再重新把示例数据载入数据库:
```
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:seed
```
Heroku 已经安装了 ImageMagick,所在生产环境中调整图片尺寸和上传功能都能正常使用,如[图 11.23](#fig-image-upload-production) 所示。
![image upload production](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5733307688656.png)图 11.23:在生产环境中上传图片
';
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
<%= f.submit "Post", class: "btn btn-primary" %>
<%= f.file_field :picture %>
<% end %>
```
注意,`form_for` 中指定了 `html: { multipart: true }` 参数。为了支持文件上传功能,必须指定这个参数。
最后,我们要把 `picture` 添加到可通过 Web 修改的属性列表中。为此,要修改 `micropost_params` 方法,如[代码清单 11.58](#listing-micropost-params-picture) 所示。
##### 代码清单 11.58:把 `picture` 添加到允许修改的属性列表中
app/controllers/microposts_controller.rb
```
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
.
.
.
private
def micropost_params
params.require(:micropost).permit(:content, :picture) end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
```
图片上传后,在微博局部视图中可以使用 `image_tag` 辅助方法渲染,如[代码清单 11.59](#listing-micropost-partial-image-display) 所示。注意,我们使用了 `picture?` 布尔值方法,如果没有图片就不显示 `img` 标签。这个方法由 CarrierWave 自动创建,方法名根据保存图片文件名的属性而定。自己动手上传图片后显示的页面如[图 11.20](#fig-micropost-with-image) 所示。针对图片上传功能的测试留作练习([11.6 节](#user-microposts-exercises))。
##### 代码清单 11.59:在微博中显示图片
app/views/microposts/_micropost.html.erb
```
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
<%= f.submit "Post", class: "btn btn-primary" %>
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
<% end %>
```
有一点很重要,你要知道,像[代码清单 11.62](#listing-format-jquery-file-test) 这样的代码并不能阻止用户上传大文件。我们添加的代码虽然能阻止用户通过 Web 界面上传,但用户可以使用 Web 审查工具修改 JavaScript,或者直接发送 `POST` 请求(例如,使用 `curl`)。为了阻止用户上传大文件,必须在服务器端添加验证,如[代码清单 11.61](#listing-micropost-model-image-validation) 所示。
## 11.4.3 调整图片的尺寸
前一节对图片大小的限制是个好的开始,不过用户还是可以上传尺寸很大的图片,撑破网站的布局,有时会把网站搞得一团糟,如[图 11.21](#fig-large-uploaded-image) 所示。因此,如果允许用户从本地硬盘中上传尺寸很大的图片,最好在显示图片之前调整图片的尺寸。[[12](#fn-12)]
![large uploaded image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_57333076362f4.png)图 11.21:上传了一张超级大的图片
我们要使用 [ImageMagick](http://www.imagemagick.org/) 调整图片的尺寸,所以要在开发环境中安装这个程序。(如 [11.4.4 节](#image-upload-in-production)所示,Heroku 已经预先安装好了。)在云端 IDE 中可以使用下面的命令安装:[[13](#fn-13)]
```
$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing
```
然后,我们要在 CarrierWave 中引入 [MiniMagick](https://github.com/minimagick/minimagick) 为 ImageMagick 提供的接口,还要调用一个调整尺寸的方法。[MiniMagick 的文档](http://www.rdoc.info/github/jnicklas/carrierwave/CarrierWave/MiniMagick)中列出了多个调整尺寸的方法,我们要使用的是 `resize_to_limit: [400, 400]`,如果图片很大,就把它调整为宽和高都不超过 400 像素,而小于这个尺寸的图片则不调整。([CarrierWave 文档](https://github.com/carrierwaveuploader/carrierwave#using-minimagick)中列出的方法会把小图片放大,这不是我们需要的效果。)添加[代码清单 11.63](#listing-image-uploader-resizing) 中的代码后,就能完美调整大尺寸图片了,如[图 11.22](#fig-resized-image) 所示。
##### 代码清单 11.63:配置图片上传程序,调整图片的尺寸
app/uploaders/picture_uploader.rb
```
class PictureUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick process resize_to_limit: [400, 400]
storage :file
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# 添加一个白名单,指定允许上传的图片类型
def extension_white_list
%w(jpg jpeg gif png)
end
end
```
![resized image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5733307663470.png)图 11.22:调整尺寸后的图片
## 11.4.4 在生产环境中上传图片
前面使用的图片上传程序在开发环境中用起来不错,但图片都存储在本地文件系统中(如[代码清单 11.63](#listing-image-uploader-resizing) 中 `storage :file` 那行所示),在生产环境这么做可不好。[[14](#fn-14)]所以,我们要使用云存储服务存储图片,和应用所在的文件系统分开。[[15](#fn-15)]
我们要使用 `fog` gem 配置应用,在生产环境使用云存储,如[代码清单 11.64](#listing-image-uploader-production) 所示。
##### 代码清单 11.64:配置生产环境使用的图片上传程序
app/uploaders/picture_uploader.rb
```
class PictureUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
process resize_to_limit: [400, 400]
if Rails.env.production? storage :fog else storage :file end
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# 添加一个白名单,指定允许上传的图片类型
def extension_white_list
%w(jpg jpeg gif png)
end
end
```
在[代码清单 11.64](#listing-image-uploader-production) 中,使用[旁注 7.1](chapter7.html#aside-rails-environments) 中介绍的 `production?` 布尔值方法根据所在的环境选择存储方式:
```
if Rails.env.production?
storage :fog
else
storage :file
end
```
云存储服务有很多,我们要使用其中一个最受欢迎并且支持比较好的——Amazon 的 [Simple Storage Service](http://aws.amazon.com/s3/)(简称 S3)。[[16](#fn-16)]基本步骤如下:
1. 注册一个 [Amazon Web Services](http://aws.amazon.com/) 账户;
2. 通过 [AWS Identity and Access Management](http://aws.amazon.com/iam/)(简称 IAM) 创建一个用户,记下访问公钥和密钥;
3. 使用 [AWS Console](https://console.aws.amazon.com/s3) 创建一个 S3 bucket(名字自己定),然后赋予上一步创建的用户读写权限。
关于这些步骤的详细说明,参见 [S3 的文档](http://aws.amazon.com/documentation/s3/)。(如果需要还可以搜索。)
创建并配置好 S3 账户后,创建 CarrierWave 配置文件,写入[代码清单 11.65](#listing-carrier-wave-configuration) 中的内容。注意:如果做了这些设置之后连不上 S3,可能是区域位置的问题。有些用户要在 fog 的配置中添加 `:region => ENV['S3_REGION']`,然后在命令行中执行 `heroku config:set S3_REGION=<bucket_region>`,其中 `bucket_region` 是你所在的区域,例如 `'eu-central-1'`。如果想找到你所在的区域,请查看 [Amazon AWS 的文档](http://docs.aws.amazon.com/general/latest/gr/rande.html)。
##### 代码清单 11.65:配置 CarrierWave 使用 S3
config/initializers/carrier_wave.rb
```
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_credentials = {
# Amazon S3 的配置
:provider => 'AWS',
:aws_access_key_id => ENV['S3_ACCESS_KEY'],
:aws_secret_access_key => ENV['S3_SECRET_KEY']
}
config.fog_directory = ENV['S3_BUCKET']
end
end
```
和生产环境的电子邮件配置一样([代码清单 10.56](chapter10.html#listing-sendgrid-config)),[代码清单 11.65](#listing-carrier-wave-configuration) 也使用 Heroku 中的 `ENV` 变量,没直接在代码中写入敏感信息。在 [10.3 节](chapter10.html#email-in-production),电子邮件所需的变量由 SendGrid 扩展自动定义,但现在我们要自己定义,方法是使用 `heroku config:set` 命令,如下所示:
```
$ heroku config:set S3_ACCESS_KEY=