Export file PDF bằng gem PDFKit
Bài đăng này đã không được cập nhật trong 3 năm
1. Giới thiệu
Hôm này mình sẽ trình bày chi tiết cách export file pdf
bằng gem PDFKit
(Ngoài PDFKit
chúng ta có thể dùng gem Wicked PDF
hay Prawn
, mình sẽ gửi đến các bạn trong các bài viết tiếp theo).
Bài viết dành cho NEWBIE nên khá dài, mọi người hãy cân nhắc thời gian đọc nhé ~~ .
Bài viết mình chia làm 2 phần chính, đó là phần Tạo HTML
và phần Xuất file PDF
.
Bạn có thể tìm hiểu thêm PDFKIT.
2. Tạo HTML
- Bắt đầu bằng tạo project mới
$ rails new html_to_pdf
$ cd html_to_pdf
$ rails generate model user name
$ rails generate model product name price:float user:references
$ rake db:migrate
- Tạo liên kết giữa các bảng
app/models/user.rb
class Product < ApplicationRecord
belongs_to :user
end
app/models/product.rb
class User < ApplicationRecord
has_many :products, dependent: :destroy
end
dependent: :destroy
: khi một user
bị xóa, nó sẽ xóa các product
liên quan
- Tạo dữ liệu ảo trong file
db/seeds.rb
user = User.create!([{name: "iicanfly"},{name: "alex"}])
products = Product.create!([
{ name: "pen", price: 5.0, user_id: rand(1..2) },
{ name: "snack", price: 4.5, user_id: rand(1..2) },
{ name: "notebook", price: 10, user_id: rand(1..2) },
{ name: "biscuit", price: 5, user_id: rand(1..2) },
{ name: "book", price: 20, user_id: rand(1..2) },
{ name: "beverage", price: 11, user_id: rand(1..2) },
{ name: "nuts", price: 7, user_id: rand(1..2) },
{ name: "ice-cream", price: 15, user_id: rand(1..2) },
{ name: "beer", price: 25, user_id: rand(1..2) },
{ name: "toys", price: 22, user_id: rand(1..2) }
])
Hàm rand(*range)
giúp tạo dữ liệu ngẫu nhiên
Đừng quên chạy lệnh rake db:seed
trên terminal để tạo dữ liệu
- Tạo
User Controller
bằng câu lệnhrails generate controller Users index show
app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def show
@user = User.find_by id: params[:id]
end
end
- Cài đặt routes trong file
config/routes
app/config/routes.rb
Rails.application.routes.draw do
root "users#index"
resources :users, only: [:index, :show]
end
- Tiếp theo là xây dựng những view cơ bản
app/views/users/index.html.erb
<ul>
<% @users.each do |user| %>
<li>
<%= link_to "Invoice - #{user.id} - #{user.name} >>", user_path(user) %>
</li>
<% end %>
</ul>
app/views/users/show.html.erb
<div class="invoice">
<h1>7 ELEVEN</h1>
<h3>To: <%= @user.name %></h3>
<h3>Date: <%= DateTime.now.to_time %></h3>
<table>
<thead>
<tr>
<th>Description</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<% total = 0 %>
<% @user.products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= number_to_currency product.price %></td>
<% total += product.price %>
</tr>
<% end %>
<tr class="total">
<td style="text-align: right">Total: </td>
<td><%= number_to_currency total %></span></td>
</tr>
</tbody>
</table>
- Chúng ta không quên định nghĩa stylesheet nữa chứ
app/assets/stylesheets/custom.scss
.invoice {
width: 700px;
max-width: 700px;
border: 1px solid black;
margin: 50px;
padding: 50px;
h1 {
text-align: center;
margin-bottom: 100px;
}
.notes {
margin-top: 100px;
}
table {
width: 90%;
text-align: left;
margin: 0 auto;
}
th {
padding-bottom: 15px;
}
.total td {
font-size: 20px;
font-weight: bold;
padding-top: 25px;
}
}
- Bây giờ bạn thử chạy
rails s
và xem thử thành quả từ nãy giờ nào !! - Giao diện
users/index
- Giao diện của
users/show
3. Xuất file PDF
- Cài đặt 2 gem hỗ trợ xuất file pdf
gem 'pdfkit'
gem 'wkhtmltopdf-binary'
Sau đó chạy lệnh bundle
để cài đặt gem
- Cài đặt môi trường ảo trong
app/config/application.rb
, phần này khá quan trọng đấy, không có là không chạy được đâu ~~
module HtmlToPdf
class Application < Rails::Application
config.middleware.use PDFKit::Middleware
....
end
end
- Phần xử lý pdf , mình để trong thư mục
../services
app/services/pdf_service.rb
class PdfService
def initialize user
@user = user
end
def to_pdf
kit = PDFKit.new(as_html, page_size: 'A4')
kit.to_file("#{Rails.root}/public/invoice.pdf")
end
def filename
"User #{user.id}.pdf"
end
private
attr_reader :user
def as_html
render template: "downloads/show", layout: "invoice_pdf",
locals: { user: user }
end
end
Phương thức as_html
dùng để tạo ra file HTML
Phương thức to_pdf
sử dụng PDFKit để lưu file PDF trong thư mục public
của rails
- Xây dựng phần layout của file PDF
app/views/layouts/invoice_pdf.erb
<!DOCTYPE html>
<html>
<head>
<title>Download</title>
<style>
<%= Rails.application.assets.find_asset('application.scss').to_s %>
</style>
</head>
<body>
<%= yield %>
</body>
</html>
- Tiếp theo, mình xây dựng
downloads_controller.rb
để có thể xuất ra file PDF
app/controllers/downloads_controller.rb
class DownloadsController < ApplicationController
before_action :load_user, only: [:index, :show]
def show
respond_to do |format|
format.pdf { send_user_pdf }
format.html { render_sample_html } if Rails.env.development?
end
end
private
def load_user
@user = User.find_by id: params[:user_id]
end
def create_user_pdf
PdfService.new user
end
def send_user_pdf
send_file create_user_pdf.to_pdf,
filename: user_pdf.filename,
type: "application/pdf",
disposition: "inline"
end
def render_sample_html
render template: "downloads/show", layout: "invoice_pdf",
locals: { uesr: @user }
end
end
Khi có format kiểu .pdf
,sẽ gọi đến phương thức send_user_pdf
. Phương thức send_user_pdf
gọi đến phương thức to_pdf
để tạo file pdf trên trình duyệt
Ở phần này tùy vào trình duyệt, bạn có thể đổi đuôi .pdf
sang .html
để có thể chỉnh sửa giao diện dễ dàng hơn (Ví dụ: http://localhost:3000/users/1/downloads.pdf thành http://localhost:3000/users/1/downloads.html). Có trình duyệt sẽ download luôn file nên bạn sẽ không có cơ hội đổi đâu ~~ .
- vì phần show của user chính là phần show của file pdf nên
downloads/show.html.erb
vàusers/show.html.erb
có chung giao diện, ta tạo ra partial_template.html.erb
trong folderapp/view/users
app/views/users/_template.html.erb
<div class="invoice">
<h1>7 ELEVEN</h1>
<h3>To: <%= @user.name %></h3>
<h3>Date: <%= DateTime.now.to_time %></h3>
<table>
<thead>
<tr>
<th>Description</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<% total = 0 %>
<% @user.products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= number_to_currency product.price %></td>
<% total += product.price %>
</tr>
<% end %>
<tr class="total">
<td style="text-align: right">Total: </td>
<td><%= number_to_currency total %></span></td>
</tr>
</tbody>
</table>
Hàm number_to_currency(*number)
để tạo ký hiệu $
- Ta chỉnh sửa lại một số giao diện
app/views/users/show.html.erb
<%= render "template", user: @user %>
<%= link_to "Download", user_downloads_path(@user, format: "pdf"),
target: :_blank %>
app/view/downloads/show.html.erb
<%= render "users/template", user: @user %>
- Ta cần phải chỉnh lại
routes
để rails nhậndownloads_controller
app/config/routes.rb
Rails.application.routes.draw do
root "users#index"
resources :users, only: [:index, :show] do
resource :downloads, only: :show
end
end
resource
làm đường link trở nên ngắn gọn hơn (../downloads/
thay cho ../downloads/:id/
)
- Chạy
rails s
để kiểm tra thành quả nào !! - Giao diện
users/show
- Giao diện
downloads.pdf
- Giao diện
downloads.html
4. Kết luận
Bài viết khá dài nhưng cũng khá chi tiết đấy chứ ~~ , hi vọng bài viết của mình giúp ích cho các bạn. Rất mong nhận được sự đóng góp của mọi người để mình có thể làm các bài viết chất lượng hơn nữa (bow). Link github: https://github.com/vinhnguyen2210/html_to_pdf
5. Tham khảo
https://code.tutsplus.com/tutorials/generating-pdfs-from-html-with-rails--cms-22918 https://www.sitepoint.com/pdf-generation-rails/
All rights reserved