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 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ệnh rails 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 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.erbusers/show.html.erb có chung giao diện, ta tạo ra partial _template.html.erb trong folder app/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ận downloads_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/