跳至主要內容

在 Laravel 使用 Headless Chrome 輸出 PDF

Pamis Wang大约 5 分鐘後端LaravelChromePDF

在 Laravel 使用 Headless Chrome 輸出 PDF

前言

使用 Laravel 進行開發,免不了會有一些文件報表套印的功能,
例如電商網站會需要列印訂單、公司內部系統需要銷售統計報表等等......

本篇教學會使用 Headless Chromeopen in new windowChrome PHPopen in new window 來產生 PDF 檔案,
也會附上一些踩坑心得與解決方法。

常見解決方案

當聽到 PDF 檔案產製的需求一般都是直接 Googleopen in new window
比較常看到的解決方案大概如下:

既然有這麼多套件可以幫助,為什麼還要寫個文章呢?
為了混分 XD

業務場景問題

mpdf 的官方文件 About CSS support and development stateopen in new window 給出了答案。

一言以蔽之:套件舊了,對於瀏覽器的新樣式支援跟不上了。

dompdf 官方文件寫了這句:

Handles most CSS 2.1 and a few CSS3 properties, including @import, @media & @page rules

重點就是這個 few CSS3

(所以我說那個樣式呢?)

laravel-dompdf 是基於 dompdf 的再封裝,也會有樣式支援不足的問題。

laravel-snappy 是透過 QT Webkit 渲染引擎來輸出 PDF,
但是 QT Webkit 與目前主流的瀏覽器引擎不同。

QT Webkit 是基於開源的 WebKit 引擎開發,
QtWebEngine 則是基於 Chromium 的 Blink 引擎,
所以在渲染結果上會出現誤差的可能。

Introducing the Qt WebEngineopen in new window

如果文件的樣式不複雜,或是使用者對於誤差的容忍度高,那麼使用上述的方案固然沒問題;
反之,上述的套件受限於 CSS 版本,在文件的設計與測試上會花費許多時間做調整。

Headless Chrome 介紹

看看官方文件怎麼說:

Headless Chrome is shipping in Chrome 59. It's a way to run the Chrome browser in a headless environment

其實就是用命令列執行 Chrome 瀏覽器的功能,
可用於執行網頁測試、列印、截圖等功能。

版本資訊

從 Chrome 59 就包含了 Headless Chrome ,支援 Mac 和 Linux 作業系統。
從 Chrome 60 則是跟進支援 Windows 作業系統。

安裝套件

本次要使用的套件為 Chrome PHPopen in new window
主要是把執行 Headless Chrome 的指令進行封裝,省下自己寫指令的麻煩。

先決條件

  • PHP 7.4-8.2。
  • 安裝 Chrome。

安裝 Chrome 瀏覽器

這裡列出三種開發環境的安裝方式。

這個應該最簡單了吧 XD

Google Chrome 網路瀏覽器open in new window 點進去下載安裝

就算死了還是要拿出來鞭
就算死了還是要拿出來鞭

安裝 Chrome PHP 套件

這個就比較簡單了,用 Composer 安裝就可以了
如果是用 Docker 記得一樣要先進入容器與專案目錄再安裝。

composer require chrome-php/chrome

使用範例

這裡展示 Laravel 基本 MVC 的作法,
使用 Bootstrap 5 來作為簡單的使用範例。

新增 View

直接用 Bootstrap 5 的一些表單元件做測試,
如果要用自己的 CSS 樣式,
可參考 Laravel 官方教學的靜態資源引用。

resources/views/report/test.blade.php

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Bootstrap demo</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
      crossorigin="anonymous"
    />
  </head>

  <body>
    <div class="container-md">
      <h1>Hello, world!</h1>

      <table class="table">
        <thead>
          <tr>
            <th scope="col">Class</th>
            <th scope="col">Heading</th>
            <th scope="col">Heading</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <th scope="row">Default</th>
            <td>Cell</td>
            <td>Cell</td>
          </tr>

          <tr class="table-primary">
            <th scope="row">Primary</th>
            <td>Cell</td>
            <td>Cell</td>
          </tr>

          <tr class="table-success">
            <th scope="row">Success</th>
            <td>Cell</td>
            <td>Cell</td>
          </tr>
          <tr class="table-danger">
            <th scope="row">Danger</th>
            <td>Cell</td>
            <td>Cell</td>
          </tr>
        </tbody>
      </table>
      <a href="{{ route('download') }}">
        <button type="button" class="btn btn-primary">印出來</button>
      </a>
    </div>
  </body>
</html>

新增 Controller

新增兩個方法,一個用於展示畫面,一個用於輸出檔案。

app/Http/Controllers/ReportController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;
use HeadlessChromium\BrowserFactory;

class ReportController extends Controller
{
    /**
     * 顯示文件畫面
     */
    public function show(): View
    {
        return view('report.test');
    }

    /**
     * 透過 Headless Chrome  產出 PDF
     */
    public function download()
    {
        // 產生 Blade 引擎渲染後的 HTML 字串
        $render = view('report.test')->render();
        $browserFactory = new BrowserFactory();
        // 如果在容器中使用要加入這段
        $browserOptions = ['noSandbox' => true];
        $browser = $browserFactory->createBrowser($browserOptions);
        $page = $browser->createPage();
        $page->setHtml($render);
        // 列印頁面設定
        $pageOptions = [
            'marginTop' => 0.0,
            'marginBottom' => 0.0,
            'marginLeft' => 0.0,
            'marginRight' => 0.0,
            'paperWidth' => 8.3,
            'paperHeight' => 11.7,
            'printBackground' => true,
        ];
        // 產生隨機檔名並存放於暫存資料夾
        $random = Str::random(40);
        $savePath = storage_path('temp') . '/' . $random . '.pdf';
        $pdf = $page->pdf($pageOptions);
        $pdf->saveToFile($savePath);
        $downloadName = "{$random}.pdf";
        // 下載檔案
        $response = response()->download($savePath);
        $response->headers->set('content-disposition', "attachment; filename={$downloadName}");
        return $response->deleteFileAfterSend();
    }
}

新增 Route

routes/web.php

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ReportController;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/report/show', [ReportController::class, 'show']);
Route::get('/report/download', [ReportController::class, 'download'])->name('download');

畫面展示

輸入顯示畫面的網址,就可以看到測試的畫面了。
畫面出現之後可以點選右上角的列印功能,
上面的 pageOptions 陣列就是對應列印功能選項。

原則上客戶端的列印選項一樣,那麼伺服器輸出的檔案就會一樣。

詳細可用的設定可以參考官方文件 Print as PDFopen in new window

參考資料

Chrome PHPopen in new window
Getting Started with Headless Chromeopen in new window

上次編輯於:
貢獻者: EXMAIL\pamis,pamis