11.4 微博中的图片

我们已经实现了微博相关的所有操作,本节要让微博除了能输入文字之外还能插入图片。我们首先会开发一个基础版本,只能在生产环境中使用,然后再做一系列功能增强,允许在生产环境上传图片。

添加图片上传功能明显要完成两件事:编写用于上传图片的表单,准备好所需的图片。上传图片按钮和微博中显示的图片构思如图 11.18 所示。[9]

micropost image mockup图 11.18:图片上传界面的构思图(包含一张上传后的图片)

11.4.1 基本的图片上传功能

我们要使用 CarrierWave 处理图片上传,并把图片和微博模型关联起来。为此,我们要在 Gemfile 中添加 carrierwave gem,如代码清单 11.55 所示。为了一次安装完所有 gem,代码清单 11.55 中还添加了用于调整图片尺寸的 mini_magick11.4.3 节)和用于在生产环境中上传图片的 fog11.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]

micropost model picture图 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>

microposts with image图 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]

large uploaded image图 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

resized image图 11.22:调整尺寸后的图片

11.4.4 在生产环境中上传图片

前面使用的图片上传程序在开发环境中用起来不错,但图片都存储在本地文件系统中(如代码清单 11.63storage :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]基本步骤如下:

  1. 注册一个 Amazon Web Services 账户;

  2. 通过 AWS Identity and Access Management(简称 IAM) 创建一个用户,记下访问公钥和密钥;

  3. 使用 AWS Console 创建一个 S3 bucket(名字自己定),然后赋予上一步创建的用户读写权限。

关于这些步骤的详细说明,参见 S3 的文档。(如果需要还可以搜索。)

创建并配置好 S3 账户后,创建 CarrierWave 配置文件,写入代码清单 11.65 中的内容。注意:如果做了这些设置之后连不上 S3,可能是区域位置的问题。有些用户要在 fog 的配置中添加 :region =&gt; ENV['S3_REGION'],然后在命令行中执行 heroku config:set S3_REGION=&lt;bucket_region&gt;,其中 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 所示。

image upload production图 11.23:在生产环境中上传图片