# 11.4 微博中的圖片
我們已經實現了微博相關的所有操作,本節要讓微博除了能輸入文字之外還能插入圖片。我們首先會開發一個基礎版本,只能在生產環境中使用,然后再做一系列功能增強,允許在生產環境上傳圖片。
添加圖片上傳功能明顯要完成兩件事:編寫用于上傳圖片的表單,準備好所需的圖片。上傳圖片按鈕和微博中顯示的圖片構思如[圖 11.18](#fig-micropost-image-mockup) 所示。[[9](#fn-9)]
圖 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)]
圖 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>
```
圖 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)]
圖 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
```
圖 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=<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) 所示。
圖 11.23:在生產環境中上傳圖片
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習