Export file PDF bằng gem PDFKit
Bài đăng này đã không được cập nhật trong 4 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 Controllerbằ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 svà 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 publiccủ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.erbvàusers/show.html.erbcó chung giao diện, ta tạo ra partial_template.html.erbtrong 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