Đây là bài viết tiếp theo trong loạt bài "Tải lên với Rails". Hôm nay chúng ta sẽ làm quen với CarrierWave—một trong những giải pháp tải lên tệp phổ biến nhất cho Rails. Tôi thích CarrierWave vì nó dễ dàng bắt đầu, có sẵn nhiều tính năng ngay từ đầu, và cung cấp hàng chục bài viết "hướng dẫn" do cộng đồng viết, nên bạn sẽ không bị lạc lối.
Trong bài viết này, bạn sẽ học cách:
- Tích hợp CarrierWave vào ứng dụng Rails
- Thêm các xác thực (validations)
- Lưu giữ tệp qua các request
- Xóa tệp
- Tạo ảnh thu nhỏ (thumbnails)
- Tải lên tệp từ vị trí từ xa
- Triển khai tải lên nhiều tệp
- Thêm hỗ trợ lưu trữ đám mây
Mã nguồn cho bài viết này có sẵn trên GitHub. Chúc bạn đọc vui vẻ!
Đặt Nền Móng
Như thường lệ, bắt đầu bằng cách tạo một ứng dụng Rails mới:
rails new UploadingWithCarrierwave -T
Đối với demo này, tôi sẽ sử dụng Rails 5.0.2. Lưu ý rằng CarrierWave 1 chỉ hỗ trợ Rails 4+ và Ruby 2. Nếu bạn vẫn đang dùng Rails 3, hãy sử dụng CarrierWave phiên bản 0.11.
Để thấy CarrierWave hoạt động, chúng ta sẽ tạo một ứng dụng blog đơn giản với mô hình Post duy nhất. Nó sẽ có các thuộc tính chính sau:
title(string)body(text)image(string)—trường này sẽ chứa hình ảnh (tên tệp, chính xác hơn) đính kèm với bài viết
Tạo và áp dụng migration mới:
rails g model Post title:string body:text image:string rails db:migrate
Thiết lập một số routes:
config/routes.rb
resources :posts root to: 'posts#index'
Cũng tạo một controller cơ bản:
posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update]
def index
@posts = Post.order('created_at DESC')
end
def show
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to posts_path
else
render :new
end
end
def edit
end
def update
if @post.update_attributes(post_params)
redirect_to post_path(@post)
else
render :edit
end
end
private
def post_params
params.require(:post).permit(:title, :body, :image)
end
def set_post
@post = Post.find(params[:id])
end
end
Bây giờ tạo view index:
views/posts/index.html.erb
<h1>Posts</h1> <%= link_to 'Add post', new_post_path %> <%= render @posts %>
Và partial tương ứng:
views/posts/_post.html.erb
<h2><%= link_to post.title, post_path(post) %></h2> <p><%= truncate(post.body, length: 150) %></p> <p><%= link_to 'Edit', edit_post_path(post) %></p> <hr>
Ở đây tôi dùng phương thức truncate của Rails để chỉ hiển thị 150 ký tự đầu tiên của bài viết. Trước khi tạo các view khác và partial form, hãy tích hợp CarrierWave vào ứng dụng trước.
Tích hợp CarrierWave
Thêm gem mới vào Gemfile:
Gemfile
gem 'carrierwave', '~> 1.0'
Chạy:
bundle install
CarrierWave lưu cấu hình bên trong các uploaders được include vào model. Để tạo uploader, dùng lệnh:
rails generate uploader Image
Bây giờ, trong thư mục app/uploaders, bạn sẽ thấy file image_uploader.rb. File này có các comment và ví dụ hữu ích, bạn có thể tham khảo để bắt đầu. Trong demo này chúng ta dùng ActiveRecord, nhưng CarrierWave cũng hỗ trợ Mongoid, Sequel, và DataMapper.
Tiếp theo, cần mount uploader này vào model:
models/post.rb
mount_uploader :image, ImageUploader
Uploader đã có cài đặt mặc định hợp lý, nhưng ít nhất cần chọn nơi lưu trữ tệp tải lên. Tạm thời dùng lưu trữ file:
uploaders/image_uploader.rb
storage :file
Mặc định tệp sẽ nằm trong thư mục public/uploads, nên tốt nhất loại trừ nó khỏi version control:
.gitignore
public/uploads
Bạn cũng có thể sửa phương thức store_dir trong uploader để chọn vị trí khác.
Lúc này có thể tạo view mới và form partial để bắt đầu tải lên tệp:
views/posts/new.html.erb
<h1>Add post</h1> <%= render 'form', post: @post %>
views/posts/_form.html.erb
<%= form_for post do |f| %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<div>
<%= f.label :image %>
<%= f.file_field :image %>
</div>
<%= f.submit %>
<% end %>
Lưu ý PostsController không cần sửa đổi vì đã permit thuộc tính image.
Cuối cùng, tạo view edit:
views/posts/edit.html.erb
<h1>Edit post</h1> <%= render 'form', post: @post %>
Xong! Bạn có thể khởi động server và thử tạo bài viết với hình ảnh. Vấn đề là hình ảnh không hiển thị đâu, nên hãy sang phần tiếp theo để thêm trang show!
Hiển Thị Hình Ảnh
View chưa tạo duy nhất là show. Thêm ngay:
views/posts/show.html.erb
<%= link_to 'All posts', posts_path %> <h1><%= @post.title %></h1> <%= image_tag(@post.image.url, alt: 'Image') if @post.image? %> <p><%= @post.body %></p> <p><%= link_to 'Edit', edit_post_path(@post) %></p>
Như bạn thấy, hiển thị đính kèm rất dễ: chỉ cần @post.image.url để lấy URL hình ảnh. Để lấy đường dẫn file, dùng phương thức current_path. CarrierWave cũng cung cấp phương thức image? để kiểm tra xem có đính kèm hay không (phương thức image không bao giờ trả về nil ngay cả khi file không tồn tại).
Sau khi vào trang bài viết, bạn sẽ thấy hình ảnh, nhưng nó có thể quá lớn vì chưa giới hạn kích thước. Có thể thu nhỏ bằng CSS, nhưng tốt hơn là tạo thumbnail sau khi upload. Tuy nhiên điều này cần thêm một vài bước.
Tạo Thumbnail
Để cắt và thay đổi kích thước ảnh, cần công cụ riêng. CarrierWave hỗ trợ sẵn các gem RMagick và MiniMagick, dùng ImageMagick để thao tác ảnh. ImageMagick là giải pháp mã nguồn mở cho phép chỉnh sửa và tạo ảnh mới, nên trước tiên cần tải và cài đặt nó. Tôi chọn MiniMagick vì dễ cài đặt và hỗ trợ tốt hơn:
Gemfile
gem 'mini_magick'
Chạy:
bundle install
Include MiniMagick vào uploader:
uploaders/image_uploader.rb
include CarrierWave::MiniMagick
Bây giờ chỉ cần thêm version mới cho uploader. Khái niệm versions (hoặc styles) dùng trong nhiều thư viện tải tệp; đơn giản nghĩa là các file thêm được tạo dựa trên file gốc với kích thước hoặc định dạng khác nhau. Thêm version tên thumb:
uploaders/image_uploader.rb
version :thumb do process resize_to_fill: [350, 350] end
Bạn có thể tạo bao nhiêu version tùy thích, hơn nữa version có thể xây dựng dựa trên version khác:
uploaders/image_uploader.rb
version :small_thumb, from_version: :thumb do process resize_to_fill: [20, 20] end
Nếu đã upload một số hình ảnh, chúng sẽ không có thumbnail. Không sao, bạn có thể tạo lại từ Rails console:
rails c
Post.find_each {|post| post.image.recreate_versions!(:thumb) if post.image?}
Cuối cùng, hiển thị thumbnail kèm link đến ảnh gốc:
views/posts/show.html.erb
<%= link_to(image_tag(@post.image.thumb.url, alt: 'Image'), @post.image.url, target: '_blank') if @post.image? %>
Khởi động server và quan sát kết quả!
Thêm Xác Thực (Validations)
Hiện tại upload hoạt động nhưng không validate đầu vào người dùng, điều này không tốt. Vì chỉ làm việc với ảnh, hãy whitelist các đuôi .png, .jpg, .gif:
uploaders/image_uploader.rb
def extension_whitelist %w(jpg jpeg gif png) end
Bạn cũng có thể kiểm tra content type bằng phương thức content_type_whitelist:
uploaders/image_uploader.rb
def content_type_whitelist /image\// end
Hoặc blacklist một số loại file, ví dụ file thực thi, bằng phương thức content_type_blacklist.
Ngoài kiểm tra loại và đuôi file, hãy giới hạn dung lượng dưới 1 megabyte. Cần gem hỗ trợ validation file cho ActiveModel:
Gemfile
gem 'file_validators'
Cài đặt:
bundle install
Thêm các validation mong muốn (cũng thêm kiểm tra cho title và body):
models/post.rb
validates :title, presence: true, length: {minimum: 2}
validates :body, presence: true
validates :image, file_size: { less_than: 1.megabytes }
Tiếp theo, thêm bản dịch I18n cho thông báo lỗi của CarrierWave:
config/locales/en.yml
en:
errors:
messages:
carrierwave_processing_error: "Cannot resize image."
carrierwave_integrity_error: "Not an image."
carrierwave_download_error: "Couldn't download image."
extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
Hiện tại chưa hiển thị lỗi validation, hãy tạo partial shared:
views/shared/_errors.html.erb
<% if object.errors.any? %>
<h3>Some errors were found:</h3>
<ul>
<% object.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
Sử dụng partial này trong form:
views/posts/_form.html.erb
<%= render 'shared/errors', object: post %>
Thử upload file không hợp lệ và quan sát. Sẽ hoạt động, nhưng nếu chọn file hợp lệ mà không điền title hoặc body, validation vẫn fail và hiển thị lỗi. Tuy nhiên ô file sẽ bị xóa trắng, người dùng phải chọn lại ảnh, khá bất tiện. Để khắc phục, cần thêm trường khác vào form.
Lưu Giữ File Qua Các Request
Lưu giữ file khi form hiển thị lại khá dễ. Chỉ cần thêm hidden field và permit nó trong controller:
views/shared/_form.html.erb
<%= f.label :image %> <%= f.file_field :image %><br> <%= f.hidden_field :image_cache %>
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :image_cache)
Bây giờ image_cache sẽ tự động điền và ảnh không bị mất. Có thể hiển thị thumbnail để người dùng biết ảnh đã xử lý thành công:
views/shared/_form.html.erb
<% if post.image? %> <%= image_tag post.image.thumb.url %> <% end %>
Xóa Hình Ảnh
Tính năng phổ biến khác là cho phép xóa file đính kèm khi chỉnh sửa record. Với CarrierWave, việc này không khó. Thêm checkbox vào form:
views/shared/_form.html.erb
<% if post.image? %>
<%= image_tag post.image.thumb.url %>
<div>
<%= label_tag :remove_image do %>
Remove image
<%= f.check_box :remove_image %>
<% end %>
</div>
<% end %>
Và permit thuộc tính remove_image:
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache)
Xong! Để xóa ảnh thủ công, dùng phương thức remove_image!:
@post.remove_image!
Tải Lên Từ Vị Trí Từ Xa
CarrierWave còn cung cấp tính năng thú vị: tải file từ URL. Hãy thêm trường mới và permit thuộc tính tương ứng:
views/shared/_form.html.erb
<%= f.text_field :remote_image_url %> <small>Enter URL to an image</small>
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url)
Tuyệt vời quá! Không cần thay đổi gì thêm, có thể test ngay tính năng này!
Làm Việc Với Nhiều Upload
Giả sử muốn bài viết có nhiều đính kèm. Với cài đặt hiện tại không thể, nhưng CarrierWave hỗ trợ trường hợp này. Cần thêm trường serialized (cho SQLite) hoặc JSON (cho Postgres/MySQL). Tôi thích JSON, nên chuyển sang database adapter mới. Xóa gem sqlite3 khỏi Gemfile và thêm pg:
Gemfile
gem 'pg'
Cài đặt:
bundle install
Sửa cấu hình database:
config/database.yml
default: &default adapter: postgresql pool: 5 timeout: 5000 development: <<: *default database: upload_carrier_dev username: 'YOUR_USER' password: 'YOUR_PASSWORD' host: localhost
Tạo database Postgres tương ứng, rồi generate và apply migration:
rails g migration add_attachments_to_posts attachments:json rails db:migrate
Nếu muốn giữ SQLite, làm theo hướng dẫn trong tài liệu CarrierWave.
Mount uploaders (lưu ý dạng số nhiều!):
model/post.rb
mount_uploaders :attachments, ImageUploader
Tôi dùng cùng uploader cho attachments, nhưng bạn có thể tạo uploader mới với cấu hình khác.
Thêm trường file multiple vào form:
views/shared/_form.html.erb
<div> <%= f.label :attachments %> <%= f.file_field :attachments, multiple: true %> </div>
Vì attachments chứa mảng, permit như sau:
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url, attachments: [])
Cuối cùng, lặp qua attachments của bài viết và hiển thị như bình thường:
views/shared/show.html.erb
<% if @post.attachments? %>
<ul>
<% @post.attachments.each do |attachment| %>
<li><%= link_to(image_tag(attachment.thumb.url, alt: 'Image'), attachment.url, target: '_blank') %></li>
<% end %>
</ul>
<% end %>
Lưu ý mỗi attachment sẽ có thumbnail như cấu hình trong ImageUploader. Tuyệt!
Sử Dụng Lưu Trữ Đám Mây
Lưu trữ file cục bộ không luôn tiện lợi/khả thi, ví dụ trên Heroku không thể lưu file tùy chỉnh. Làm sao kết hợp CarrierWave với Amazon S3? Rất dễ dàng. CarrierWave phụ thuộc gem fog-aws:
Gemfile
gem "fog-aws"
Cài đặt:
bundle install
Tạo initializer cho CarrierWave và cấu hình cloud storage toàn cục:
config/initializers/carrierwave.rb
CarrierWave.configure do |config|
config.fog_provider = 'fog/aws'
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: ENV['S3_KEY'],
aws_secret_access_key: ENV['S3_SECRET'],
region: ENV['S3_REGION'],
}
config.fog_directory = ENV['S3_BUCKET']
end
Có thêm các tùy chọn khác, xem trong tài liệu.
Tôi dùng gem dotenv-rails để set biến môi trường an toàn, bạn có thể chọn cách khác. Nhưng hãy chắc chắn cặp key S3 không công khai, vì nếu không ai cũng có thể upload vào bucket của bạn!
Thay dòng storage :file bằng:
uploaders/image_uploader.rb
storage :fog
Ngoài S3, CarrierWave còn hỗ trợ Google Storage và Rackspace. Các dịch vụ này cũng dễ thiết lập.
Kết Luận
Đến đây là hết bài viết hôm nay! Chúng ta đã bao quát các tính năng chính của CarrierWave, bạn có thể bắt đầu dùng cho dự án. Nó còn nhiều tùy chọn khác, hãy đọc tài liệu.
Nếu bị mắc chỗ nào, đừng ngần ngại đặt câu hỏi. Cũng có thể tham khảo wiki của CarrierWave, nơi chứa nhiều bài "how to" trả lời câu hỏi thường gặp.
Cảm ơn bạn đã theo dõi, và happy coding!