Export file PDF bằng gem PDFKit
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 ...
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
class Product < ApplicationRecord belongs_to :user endapp/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ệnh rails generate controller Users index show
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
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
<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ứ
.invoice { awidth: 700px; max-awidth: 700px; border: 1px solid black; margin: 50px; padding: 50px; h1 { text-align: center; margin-bottom: 100px; } .notes { margin-top: 100px; } table { awidth: 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
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
<!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
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 folder app/view/users
<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
<%= 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ận downloads_controller
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/