🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 11.4 微博中的图片 我们已经实现了微博相关的所有操作,本节要让微博除了能输入文字之外还能插入图片。我们首先会开发一个基础版本,只能在生产环境中使用,然后再做一系列功能增强,允许在生产环境上传图片。 添加图片上传功能明显要完成两件事:编写用于上传图片的表单,准备好所需的图片。上传图片按钮和微博中显示的图片构思如[图 11.18](#fig-micropost-image-mockup) 所示。[[9](#fn-9)] ![micropost image mockup](https://box.kancloud.cn/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://box.kancloud.cn/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 %> <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](#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 ``` <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](https://box.kancloud.cn/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 %> <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](#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://box.kancloud.cn/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://box.kancloud.cn/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 =&gt; ENV['S3_REGION']`,然后在命令行中执行 `heroku config:set S3_REGION=&lt;bucket_region&gt;`,其中 `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=<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](#fig-image-upload-production) 所示。 ![image upload production](https://box.kancloud.cn/2016-05-11_5733307688656.png)图 11.23:在生产环境中上传图片