11.4 微博中的图片
我们已经实现了微博相关的所有操作,本节要让微博除了能输入文字之外还能插入图片。我们首先会开发一个基础版本,只能在生产环境中使用,然后再做一系列功能增强,允许在生产环境上传图片。
添加图片上传功能明显要完成两件事:编写用于上传图片的表单,准备好所需的图片。上传图片按钮和微博中显示的图片构思如图 11.18 所示。[9]
图 11.18:图片上传界面的构思图(包含一张上传后的图片)
11.4.1 基本的图片上传功能
我们要使用 CarrierWave 处理图片上传,并把图片和微博模型关联起来。为此,我们要在 Gemfile
中添加 carrierwave
gem,如代码清单 11.55 所示。为了一次安装完所有 gem,代码清单 11.55 中还添加了用于调整图片尺寸的 mini_magick
(11.4.3 节)和用于在生产环境中上传图片的 fog
(11.4.4 节)。
代码清单 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 所示。[10]
图 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 节会编辑,现在使用生成的默认内容即可。)把这个上传程序添加到微博模型,如代码清单 11.56 所示。
代码清单 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 所示,为了在首页添加图片上传功能,我们要在发布微博的表单中添加一个 file_field
标签,如代码清单 11.57 所示。
代码清单 11.57:在发布微博的表单中添加图片上传按钮
app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<span class="picture">
<%= f.file_field :picture %>
</span>
<% end %>
注意,form_for
中指定了 html: { multipart: true }
参数。为了支持文件上传功能,必须指定这个参数。
最后,我们要把 picture
添加到可通过 Web 修改的属性列表中。为此,要修改 micropost_params
方法,如代码清单 11.58 所示。
代码清单 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 所示。注意,我们使用了 picture?
布尔值方法,如果没有图片就不显示 img
标签。这个方法由 CarrierWave 自动创建,方法名根据保存图片文件名的属性而定。自己动手上传图片后显示的页面如图 11.20 所示。针对图片上传功能的测试留作练习(11.6 节)。
代码清单 11.59:在微博中显示图片
app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>
<span class="content">
<%= micropost.content %>
<%= image_tag micropost.picture.url if micropost.picture? %>
</span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</span>
</li>
图 11.20:发布包含图片的微博后显示的页面
11.4.2 验证图片
前一节添加的上传程序是个好的开始,但有一定不足:没对上传的文件做任何限制,如果用户上传的文件很大,或者类型不对,会导致问题。这一节我们要修正这个不足,添加验证,限制图片的大小和类型。我们既会在服务器端添加验证,也会在客户端(即浏览器)添加验证。
对图片类型的限制在 CarrierWave 的上传程序中设置。我们要限制能使用的图片扩展名(PNG,GIF 和 JPEG 的两个变种),如代码清单 11.60 所示。(在生成的上传程序中有一段注释说明了该怎么做。)
代码清单 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 所示。注意,调用自定义的验证时使用的是 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 中介绍的句法),就向 errors
集合(6.2.2 节简介过)添加一个自定义的错误消息。
除了这两个验证之外,我们还要在客户端检查上传的图片。首先,我们在 file_field
方法中使用 accept
参数限制图片的格式:
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
有效的格式使用 MIME 类型指定,这些类型对应于代码清单 11.60 中限制的类型。
然后,我们要编写一些 JavaScript(更确切地说是 jQuery 代码),如果用户试图上传太大的图片就弹出一个提示框(节省了上传的时间,也减少了服务器的负载):
$('#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),当这个元素的内容变化时,会执行这段代码,如果文件太大,就调用 alert
方法。[11]
把这两个检查措施添加到微博表单中,如代码清单 11.62 所示。
代码清单 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 %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<span class="picture">
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
</span>
<% end %>
<script type="text/javascript">
$('#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.');
}
}); </script>
有一点很重要,你要知道,像代码清单 11.62 这样的代码并不能阻止用户上传大文件。我们添加的代码虽然能阻止用户通过 Web 界面上传,但用户可以使用 Web 审查工具修改 JavaScript,或者直接发送 POST
请求(例如,使用 curl
)。为了阻止用户上传大文件,必须在服务器端添加验证,如代码清单 11.61 所示。
11.4.3 调整图片的尺寸
前一节对图片大小的限制是个好的开始,不过用户还是可以上传尺寸很大的图片,撑破网站的布局,有时会把网站搞得一团糟,如图 11.21 所示。因此,如果允许用户从本地硬盘中上传尺寸很大的图片,最好在显示图片之前调整图片的尺寸。[12]
图 11.21:上传了一张超级大的图片
我们要使用 ImageMagick 调整图片的尺寸,所以要在开发环境中安装这个程序。(如 11.4.4 节所示,Heroku 已经预先安装好了。)在云端 IDE 中可以使用下面的命令安装:[13]
$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing
然后,我们要在 CarrierWave 中引入 MiniMagick 为 ImageMagick 提供的接口,还要调用一个调整尺寸的方法。MiniMagick 的文档中列出了多个调整尺寸的方法,我们要使用的是 resize_to_limit: [400, 400]
,如果图片很大,就把它调整为宽和高都不超过 400 像素,而小于这个尺寸的图片则不调整。(CarrierWave 文档中列出的方法会把小图片放大,这不是我们需要的效果。)添加代码清单 11.63 中的代码后,就能完美调整大尺寸图片了,如图 11.22 所示。
代码清单 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
图 11.22:调整尺寸后的图片
11.4.4 在生产环境中上传图片
前面使用的图片上传程序在开发环境中用起来不错,但图片都存储在本地文件系统中(如代码清单 11.63 中 storage :file
那行所示),在生产环境这么做可不好。[14]所以,我们要使用云存储服务存储图片,和应用所在的文件系统分开。[15]
我们要使用 fog
gem 配置应用,在生产环境使用云存储,如代码清单 11.64 所示。
代码清单 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 中,使用旁注 7.1 中介绍的 production?
布尔值方法根据所在的环境选择存储方式:
if Rails.env.production?
storage :fog
else
storage :file
end
云存储服务有很多,我们要使用其中一个最受欢迎并且支持比较好的——Amazon 的 Simple Storage Service(简称 S3)。[16]基本步骤如下:
注册一个 Amazon Web Services 账户;
通过 AWS Identity and Access Management(简称 IAM) 创建一个用户,记下访问公钥和密钥;
使用 AWS Console 创建一个 S3 bucket(名字自己定),然后赋予上一步创建的用户读写权限。
关于这些步骤的详细说明,参见 S3 的文档。(如果需要还可以搜索。)
创建并配置好 S3 账户后,创建 CarrierWave 配置文件,写入代码清单 11.65 中的内容。注意:如果做了这些设置之后连不上 S3,可能是区域位置的问题。有些用户要在 fog 的配置中添加 :region => ENV['S3_REGION']
,然后在命令行中执行 heroku config:set S3_REGION=<bucket_region>
,其中 bucket_region
是你所在的区域,例如 'eu-central-1'
。如果想找到你所在的区域,请查看 Amazon AWS 的文档。
代码清单 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),代码清单 11.65 也使用 Heroku 中的 ENV
变量,没直接在代码中写入敏感信息。在 10.3 节,电子邮件所需的变量由 SendGrid 扩展自动定义,但现在我们要自己定义,方法是使用 heroku config:set
命令,如下所示:
$ heroku config:set S3_ACCESS_KEY=<access key>
$ heroku config:set S3_SECRET_KEY=<secret key>
$ heroku config:set S3_BUCKET=<bucket name>
配置好之后,我们可以提交并部署了。我们先提交主题分支中的变动,然后再合并到 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 所示。
图 11.23:在生产环境中上传图片