Tải lên Với Rails và Carrierwave
() translation by (you can also view the original English article)
Đây là một bài viết khác trong loạt bài "Tải lên với Rails". Hôm nay chúng ta sẽ gặp gỡ Carrierwave—một trong những giải pháp tải lên tập tin phổ biến nhất cho Rails. Tôi thích Carrierwave bởi vì nó rất dễ làm quen, nó có rất nhiều tính năng và nó cung cấp hàng chục bài hướng dẫn "cách làm thế nào để" được viết bởi các thành viên của cộng đồng, vì vậy bạn sẽ không bị lạc lõng.
Trong bài này, bạn sẽ học cách:
- Tích hợp Carrierwave vào ứng dụng Rails của bạn
- Thêm xác thực
- Duy trì các tập tin xuyên suốt các yêu cầu
- Xoá các tập tin
- Tạo các thumbnail (hình thu nhỏ)
- Tải lên các tập tin từ xa
- Giới thiệu về tải lên nhiều tập tin
- Thêm hỗ trợ cho lưu trữ đám mây
Mã nguồn cho bài viết này có trên GitHub. Hãy đọc vui nhé!
Đặ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:
1 |
rails new UploadingWithCarrierwave -T
|
Đối với bản demo này tôi sẽ sử dụng Rails 5.0.2. Xin lưu ý rằng Carrierwave 1 chỉ hỗ trợ Rails 4+ và Ruby 2. Nếu bạn vẫn đang sử dụng Rails 3, hãy sử dụng phiên bản Carrierwave 0.11.
Để thử nghiệm Carrierwave, chúng ta sẽ tạo ra một ứng dụng blog rất đơn giản với một model 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 một hình ảnh (chính xác là một tên của tập tin) gắn với bài viết
Tạo ra và áp dụng một migration (di trú) mới:
1 |
rails g model Post title:string body:text image:string |
2 |
rails db:migrate |
Thiết lập một số route (tuyến):
config/routes.rb
1 |
resources :posts |
2 |
root to: 'posts#index'
|
Ngoài ra, tạo một controller rất cơ bản:
posts_controller.rb
1 |
class PostsController < ApplicationController |
2 |
before_action :set_post, only: [:show, :edit, :update] |
3 |
|
4 |
def index |
5 |
@posts = Post.order('created_at DESC') |
6 |
end
|
7 |
|
8 |
def show |
9 |
end
|
10 |
|
11 |
def new |
12 |
@post = Post.new |
13 |
end
|
14 |
|
15 |
def create |
16 |
@post = Post.new(post_params) |
17 |
if @post.save |
18 |
redirect_to posts_path |
19 |
else
|
20 |
render :new |
21 |
end
|
22 |
end
|
23 |
|
24 |
def edit |
25 |
end
|
26 |
|
27 |
def update |
28 |
if @post.update_attributes(post_params) |
29 |
redirect_to post_path(@post) |
30 |
else
|
31 |
render :edit |
32 |
end
|
33 |
end
|
34 |
|
35 |
private
|
36 |
|
37 |
def post_params |
38 |
params.require(:post).permit(:title, :body, :image) |
39 |
end
|
40 |
|
41 |
def set_post |
42 |
@post = Post.find(params[:id]) |
43 |
end
|
44 |
end
|
Bây giờ chúng ta hãy tạo ra view index:
views/posts/index.html.erb
1 |
<h1>Posts</h1> |
2 |
|
3 |
<%= link_to 'Add post', new_post_path %> |
4 |
|
5 |
<%= render @posts %> |
Và một phần tương ứng:
views/posts/_post.html.erb
1 |
<h2><%= link_to post.title, post_path(post) %></h2> |
2 |
|
3 |
<p><%= truncate(post.body, length: 150) %></p> |
4 |
|
5 |
<p><%= link_to 'Edit', edit_post_path(post) %></p> |
6 |
<hr>
|
Ở đây tôi đang sử dụng phương thức truncate
của Rails để chỉ hiển thị 150 ký hiệu đầu tiên từ bài viết. Trước khi chúng ta tạo ra các view khác và một form, trước tiên hãy tích hợp Carrierwave vào ứng dụng.
Tích hợp Carrierwave
Thêm một gem mới vào trong Gemfile:
Gemfile
1 |
gem 'carrierwave', '~> 1.0' |
Chạy:
1 |
bundle install |
Carrierwave lưu trữ cấu hình của nó bên trong các uploader được bao gồm trong các model của bạn. Để tạo một uploader, hãy sử dụng lệnh sau:
1 |
rails generate uploader Image |
Bây giờ, bên trong app/uploaders, bạn sẽ thấy một tập tin mới có tên là image_uploader.rb. Lưu ý rằng nó có một số comment và ví dụ hữu ích, vì vậy bạn có thể sử dụng nó để bắt đầu. Trong demo này chúng ta sẽ sử dụng ActiveRecord, nhưng Carrierwave cũng hỗ trợ Mongoid, Sequel và DataMapper.
Tiếp theo, chúng ta cần bao gồm hoặc gắn uploader này vào model:
models/post.rb
1 |
mount_uploader :image, ImageUploader |
Uploader đã có các cài đặt mặc định đúng chuẩn, nhưng ít nhất chúng ta cần phải chọn nơi tập tin tải lên sẽ được lưu trữ. Lúc này, hãy sử dụng bộ nhớ tập tin (file storage):
uploaders/image_uploader.rb
1 |
storage :file |
Mặc định, các tập tin sẽ được đặt bên trong thư mục public/uploads, vì vậy cách tốt nhất là loại trừ nó khỏi hệ thống quản lý mã nguồn:
.gitignore
1 |
public/uploads |
Bạn cũng có thể sửa đổi phương thức store_dir
bên trong uploader để chọn một số vị trí khác.
Tại thời điểm này, chúng ta có thể tạo một view và một form mới để bắt đầu tải lên các tập tin:
views/posts/new.html.erb
1 |
<h1>Add post</h1> |
2 |
|
3 |
<%= render 'form', post: @post %> |
views/posts/_form.html.erb
1 |
<%= form_for post do |f| %> |
2 |
<div>
|
3 |
<%= f.label :title %> |
4 |
<%= f.text_field :title %> |
5 |
</div>
|
6 |
|
7 |
<div>
|
8 |
<%= f.label :body %> |
9 |
<%= f.text_area :body %> |
10 |
</div>
|
11 |
|
12 |
<div>
|
13 |
<%= f.label :image %> |
14 |
<%= f.file_field :image %> |
15 |
</div>
|
16 |
|
17 |
<%= f.submit %> |
18 |
<% end %> |
Lưu ý rằng PostsController
không cần phải được sửa đổi vì chúng ta đã cho phép thuộc tính image
.
Cuối cùng, hãy tạo view edit (chỉnh sửa):
views/posts/edit.html.erb
1 |
<h1>Edit post</h1> |
2 |
|
3 |
<%= render 'form', post: @post %> |
Xong rồi! Bạn có thể khởi động máy chủ và thử tạo ra một bài viết với một hình ảnh. Vấn đề là hình ảnh này không hiển thị ở bất kỳ đâu, vì vậy chúng ta hãy sang phần tiếp theo và thêm một trang hiển thị!
Hiện thị Hình ảnh
Như vậy, view duy nhất mà chúng ta chưa tạo ra là show (hiển thị). Hãy thêm nó ngay bây giờ:
views/posts/show.html.erb
1 |
<%= link_to 'All posts', posts_path %> |
2 |
<h1><%= @post.title %></h1> |
3 |
|
4 |
<%= image_tag(@post.image.url, alt: 'Image') if @post.image? %> |
5 |
|
6 |
<p><%= @post.body %></p> |
7 |
|
8 |
<p><%= link_to 'Edit', edit_post_path(@post) %></p> |
Như bạn có thể thấy, việc hiển thị một tập tin đính kèm thật sự dễ dàng: tất cả những gì bạn cần làm là nói @post.image.url
để lấy URL của một hình ảnh. Để có được một đường dẫn đến tập tin, sử dụng phương thức current_path
. Lưu ý rằng Carrierwave còn cung cấp một phương thức image?
để chúng ta kiểm tra xem một tập tin đính kèm có hay không (bản thân phương thức image
sẽ không bao giờ trả về nil
, ngay cả khi tập tin không tồn tại).
Bây giờ, sau khi điều hướng đến bài viết, bạn sẽ thấy một hình ảnh, nhưng nó có thể xuất hiện quá lớn: sau tất cả, chúng ta vẫn chưa giới hạn kích thước. Tất nhiên, chúng ta có thể thu nhỏ hình ảnh xuống bằng một số thuộc tính CSS, nhưng tốt hơn là tạo một hình thumbnail sau khi tập tin đã được tải lên. Tuy nhiên, điều này đòi hỏi một số bước bổ sung.
Tạo Thumbnail
Để cắt xén và co giãn kích thước hình ảnh, chúng ta cần một công cụ riêng biệt. Mặc định Carrierwave có hỗ trợ các gem RMagick và MiniMagick, lần lượt được sử dụng để thao tác lên hình ảnh với sự giúp đỡ của ImageMagick. ImageMagick là một giải pháp mã nguồn mở cho phép bạn chỉnh sửa các hình ảnh hiện có và tạo ra những hình ảnh mới, do đó, trước khi tiến hành bạn cần phải tải về và cài đặt nó. Tiếp theo, bạn được tự do chọn một trong hai gem. Tôi sẽ chọn MiniMagick, bởi vì nó dễ cài đặt hơn và nó có hỗ trợ tốt hơn:
Gemfile
1 |
gem 'mini_magick' |
Chạy:
1 |
bundle install
|
Sau đó bao gồm MiniMagick vào uploader của bạn:
uploaders/image_uploader.rb
1 |
include CarrierWave::MiniMagick |
Bây giờ chúng ta chỉ cần giới thiệu một phiên bản mới cho uploader của chúng ta. Khái niệm các phiên bản (hoặc kiểu) được sử dụng trong nhiều thư viện tải lên tập tin; nó đơn giản có nghĩa là các tập tin bổ sung dựa trên tập tin đính kèm ban đầu sẽ được tạo với, ví dụ như các kích thước hoặc định dạng khác nhau. Giới thiệu một phiên bản mới gọi là thumb
:
uploaders/image_uploader.rb
1 |
version :thumb do |
2 |
process resize_to_fill: [350, 350] |
3 |
end |
Bạn có thể có nhiều phiên bản tùy ý và hơn thế nữa, các phiên bản thậm chí có thể được xây dựng trên các phiên bản khác:
uploaders/image_uploader.rb
1 |
version :small_thumb, from_version: :thumb do |
2 |
process resize_to_fill: [20, 20] |
3 |
end |
Nếu bạn đã tải lên một số hình ảnh, thì chúng sẽ không có thumbnail. Tuy nhiên, điều đó không thành vấn đề, vì bạn có thể tạo lại chúng từ dòng lệnh của Rails:
1 |
rails c |
2 |
Post.find_each {|post| post.image.recreate_versions!(:thumb) if post.image?} |
Sau cùng, hiển thị hình thumbnail của bạn với một liên kết đến hình ảnh gốc:
views/posts/show.html.erb
1 |
<%= link_to(image_tag(@post.image.thumb.url, alt: 'Image'), @post.image.url, target: '_blank') if @post.image? %> |
Khởi động máy chủ và quan sát kết quả!
Thêm Kiểm tra
Hiện tại, việc tải lên của chúng ta đã hoạt động, tuy nhiên chúng ta chưa kiểm tra đầu vào từ người dùng, đó tất nhiên là không tốt. Miễn là chúng ta chỉ làm việc với hình ảnh, thì chúng ta có thể chọn danh sách trắng các phần mở rộng tập tin .png, .jpg và .gif:
uploaders/image_uploader.rb
1 |
def extension_whitelist |
2 |
%w(jpg jpeg gif png) |
3 |
end |
Bạn cũng có thể thêm các kiểm tra loại nội dung bằng cách định nghĩa phương thức content_type_whitelist
:
uploaders/image_uploader.rb
1 |
def content_type_whitelist |
2 |
/image\// |
3 |
end |
Ngoài ra, có thể có danh sách đen một số loại tập tin, ví dụ tập tin có thể thực thi, bằng cách định nghĩa phương thức content_type_blacklist
.
Ngoài việc kiểm tra kiểu và đuôi mở rộng của tập tin, chúng ta hãy bắt buộc nó phải nhỏ hơn 1 megabyte. Để thực hiện việc này, chúng ta sẽ require (yêu cầu) một gem bổ sung hỗ trợ việc kiểm tra tập tin cho ActiveModel:
Gemfile
1 |
gem 'file_validators' |
Cài đặt nó:
1 |
bundle install
|
Bây giờ đưa ra các kiểm tra mong muốn (lưu ý rằng tôi cũng thêm kiểm tra cho các thuộc tính title
và body
):
models/post.rb
1 |
validates :title, presence: true, length: {minimum: 2} |
2 |
validates :body, presence: true |
3 |
validates :image, file_size: { less_than: 1.megabytes } |
Việc tiếp theo cần làm là thêm hỗ trợ dịch I18n cho các thông báo lỗi của Carrierwave:
config/locales/en.yml
1 |
en: |
2 |
errors: |
3 |
messages: |
4 |
carrierwave_processing_error: "Cannot resize image." |
5 |
carrierwave_integrity_error: "Not an image." |
6 |
carrierwave_download_error: "Couldn't download image." |
7 |
extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}" |
8 |
extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}" |
Hiện tại, chúng ta không hiển thị lỗi kiểm tra ở bất kỳ đâu, vì vậy hãy tạo một phần chia sẻ:
views/shared/_errors.html.erb
1 |
<% if object.errors.any? %> |
2 |
<h3>Some errors were found:</h3> |
3 |
<ul>
|
4 |
<% object.errors.full_messages.each do |message| %> |
5 |
<li><%= message %></li> |
6 |
<% end %> |
7 |
</ul>
|
8 |
<% end %> |
Sử dụng phần này bên trong form:
views/posts/_form.html.erb
1 |
<%= render 'shared/errors', object: post %> |
Bây giờ hãy thử tải lên một số tập tin không hợp lệ và quan sát kết quả. Nó sẽ hoạt động, nhưng nếu bạn chọn một tập tin hợp lệ và không điền vào các tiêu đề hoặc thân, thì một lỗi sẽ được hiển thị. Tuy nhiên, trường dữ liệu sẽ bị xóa và người dùng sẽ cần phải chọn lại hình ảnh một lần nữa, đây không phải là điều thuận tiện cho lắm. Để khắc phục điều đó, chúng ta cần thêm một trường khác vào form.
Duy trì các Tập tin Xuyên suốt các Yêu cầu
Duy trì các tập tin trên các form hiện thị lại thật sự khá dễ dàng. Tất cả những gì bạn cần làm là thêm một trường ẩn mới và cho phép nó bên trong controller:
views/shared/_form.html.erb
1 |
<%= f.label :image %> |
2 |
<%= f.file_field :image %><br> |
3 |
<%= f.hidden_field :image_cache %> |
posts_controller.rb
1 |
params.require(:post).permit(:title, :body, :image, :image_cache) |
Bây giờ image_cache
sẽ được tự động nhập và hình ảnh sẽ không bị mất. Điều đó có thể giúp ích trong việc hiển thị hình thumbnail để người dùng hiểu được hình ảnh đã được xử lý thành công:
views/shared/_form.html.erb
1 |
<% if post.image? %> |
2 |
<%= image_tag post.image.thumb.url %> |
3 |
<% end %> |
Xoá Hình ảnh
Một tính năng rất phổ biến khác là khả năng xoá các tập tin đính kèm khi chỉnh sửa một bản ghi. Với Carrierwave, việc cài đặt tính năng này không phải là vấn đề. Thêm một hộp checkbox mới vào form:
views/shared/_form.html.erb
1 |
<% if post.image? %> |
2 |
<%= image_tag post.image.thumb.url %> |
3 |
<div>
|
4 |
<%= label_tag :remove_image do %> |
5 |
Remove image |
6 |
<%= f.check_box :remove_image %> |
7 |
<% end %> |
8 |
</div>
|
9 |
<% end %> |
Và cho phép thuộc tính remove_image
:
posts_controller.rb
1 |
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache) |
Xong rồi! Để xóa hình ảnh một cách thủ công, hãy sử dụng phương thức remove_image!
:
1 |
@post.remove_image! |
Tải lên Từ xa
Carrierwave còn cung cấp một tính năng rất tuyệt vời khác: khả năng tải lên các tập tin từ xa bằng URL của chúng. Hãy tìm hiểu khả năng này ngay bây giờ bằng cách thêm một trường mới và cho phép thuộc tính tương ứng:
views/shared/_form.html.erb
1 |
<%= f.text_field :remote_image_url %> |
2 |
<small>Enter URL to an image</small> |
posts_controller.rb
1 |
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url) |
Điều đó tuyệt làm sao? Bạn không cần phải thực hiện bất kỳ thay đổi nào cả, và bạn có thể kiểm tra tính năng này ngay lập tức!
Làm việc Với Tải lên Nhiều Tập tin
Giả sử chúng ta muốn bài viết của chúng ta có nhiều tập tin đính kèm. Với thiết lập hiện tại thì không thể, nhưng may mắn thay, Carrierwave cũng hỗ trợ một trường hợp như vậy. Để cài đặt tính năng này, bạn cần phải thêm trường serialized (cho SQLite) hoặc trường JSON (cho Postgres hoặc MySQL). Tôi thích tùy chọn thứ hai hơn, vì vậy chúng ta hãy chuyển sang một adapter cơ sở dữ liệu mới ngay bây giờ. Loại bỏ gem sqlite3 khỏi Gemfile và thay vào đó thêm pg:
Gemfile
1 |
gem 'pg' |
Cài đặt nó:
1 |
bundle install
|
Chỉnh sửa cấu hình cơ sở dữ liệu như sau:
config/database.yml
1 |
default: &default
|
2 |
adapter: postgresql |
3 |
pool: 5 |
4 |
timeout: 5000 |
5 |
|
6 |
development: |
7 |
<<: *default |
8 |
database: upload_carrier_dev |
9 |
username: 'YOUR_USER' |
10 |
password: 'YOUR_PASSWORD' |
11 |
host: localhost |
Tạo cơ sở dữ liệu Postgres tương ứng, sau đó tạo và áp dụng di trú:
1 |
rails g migration add_attachments_to_posts attachments:json |
2 |
rails db:migrate |
Nếu bạn thích gắn bó với SQLite hơn, hãy làm theo các hướng dẫn được liệt kê trong tài liệu của Carrloodave.
Bây giờ gắn kết các uploader (lưu ý dạng số nhiều!):
model/post.rb
1 |
mount_uploaders :attachments, ImageUploader |
Tôi đang sử dụng cùng một uploader cho tập tin đính kèm, nhưng tất nhiên bạn có thể tạo một tập tin mới với một cấu hình khác.
Thêm trường chọn nhiều tập tin vào form của bạn:
views/shared/_form.html.erb
1 |
<div>
|
2 |
<%= f.label :attachments %> |
3 |
<%= f.file_field :attachments, multiple: true %> |
4 |
</div>
|
Miễn là trường attachments
còn chứa một mảng, thì nó phải được cho phép theo cách sau:
posts_controller.rb
1 |
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url, attachments: []) |
Cuối cùng, bạn có thể lặp qua các tập tin đính kèm của bài viết và hiện thị chúng như bình thường:
views/shared/show.html.erb
1 |
<% if @post.attachments? %> |
2 |
<ul>
|
3 |
<% @post.attachments.each do |attachment| %> |
4 |
<li><%= link_to(image_tag(attachment.thumb.url, alt: 'Image'), attachment.url, target: '_blank') %></li> |
5 |
<% end %> |
6 |
</ul>
|
7 |
<% end %> |
Lưu ý rằng mỗi tập tin đính kèm sẽ có một hình thumbnail như cấu hình trong ImageUploader
của chúng ta. Tuyệt!
Sử dụng Lưu trữ trên Đám mây
Việc gắn bó với lưu trữ tập tin không phải lúc nào cũng thuận tiện và/hoặc có thể, ví dụ như trên Heroku, bạn không thể lưu các tập tin tuỳ biến. Do đó bạn có thể hỏi làm thế nào để cưới Carrierwave cho dịch vụ lưu trữ đám mây Amazon S3? Vâng, đó cũng là một nhiệm vụ khá dễ dàng. Carrierwave phụ thuộc vào gem fog-aws để cài đặt tính năng này:
Gemfile
1 |
gem "fog-aws" |
Cài đặt nó:
1 |
bundle install
|
Hãy tạo một trình khởi chạy cho Carrierwave và cấu hình bộ nhớ đám mây trên toàn cục:
config/initializers/carrierwave.rb
1 |
CarrierWave.configure do |config| |
2 |
config.fog_provider = 'fog/aws' |
3 |
config.fog_credentials = { |
4 |
provider: 'AWS', |
5 |
aws_access_key_id: ENV['S3_KEY'], |
6 |
aws_secret_access_key: ENV['S3_SECRET'], |
7 |
region: ENV['S3_REGION'], |
8 |
} |
9 |
config.fog_directory = ENV['S3_BUCKET'] |
10 |
end |
Có một số tùy chọn khác sẵn có, có thể được tìm thấy trong tài liệu hướng dẫn.
Tôi đang sử dụng gem dotenv-rails để đặt các biến môi trường một cách an toàn, nhưng bạn có thể chọn bất kỳ tùy chọn nào khác. Tuy nhiên, hãy đảm bảo rằng cặp khóa S3 của bạn không được công khai, bởi vì nếu không, ai đó sẽ có thể tải lên bất cứ thứ gì vào bộ lưu trữ của bạn!
Tiếp theo, thay thế dòng storage:file
bằng:
uploaders/image_uploader.rb
1 |
storage :fog |
Ngoài S3, Carrierwave còn hỗ trợ tải lên Google Storage và Rackspace. Các dịch vụ này cũng dễ dàng thiết lập.
Tóm tắt
Đó là những gì cho bài viết ngày hôm nay! Chúng tôi đã giới thiệu tất cả các tính năng chính của Carrierwave, và bây giờ bạn có thể bắt đầu sử dụng nó trong các dự án của bạn. Nó có một số tùy chọn bổ sung có sẵn, do đó, hãy tham khảo tài liệu hướng dẫn.
Nếu bạn gặp vướng mắt, đừng ngần ngại gửi câu hỏi của bạn. Ngoài ra, có thể sẽ rất hữu ích khi bạn lướt qua trang wiki của Carrierwave, nơi lưu trữ các bài viết "làm thế nào để" hữu ích để trả lời nhiều câu hỏi phổ biến.
Lời cuối cùng, tôi cảm ơn bạn đã theo dõi, và viết code thật vui nhé!