Edited at
aptpodDay 2

JSで描画したViewをHeadless ChromeでPDF化するまで

aptpodが贈る、aptpod Advent Calendar 2018の2日目担当蔵下です。普段はJSでごにょごにょ開発しています。


本記事の経緯

普段の開発ではIoTの高粒度なデータを扱うことが多く、Viewを実装する上でもかなり工夫しないと動作が激重で使い物にならない...ことになりかねません:fearful: 今回、複数の高粒度のデータ(数十件分)をJSの描画ロジックで描画しブラウザで印刷するという要件があったのですが、ある程度のスペックがないと印刷まで数分、ネットワーク上のプリンターで出力するまでまた数分(場合によってはプリンターが不安定に...)という状況に出くわしました。

この状況を解決するためにクライアント側で実装している描画ロジックを切り出し、Node.js(サーバー)側でHeadless Chromeに描画 → PDF化 → クライアント側でPDFをダウンロードして印刷するまでを試してみることにしました。


必要なもの


  • Node.js


  • Puppeteer


    • Node.jsからHeadless Chromeを簡単に扱えるフレームワーク

    • 何気に初めて触ったけど意外と簡単だった



  • 印刷用HTML(切り出した描画ロジックを実行)


インストール

任意の場所に作業用のフォルダーを用意して、Puppeteerをインストールします。

npm i puppeteer


描画からPDF化するまで

ソースコードを書く前に、今回の Node.js(サーバー)側でHeadless Chromeに描画 → PDF化 を実現するためには次のような処理が必要になります。


  1. Headless Chromeから描画ロジックを実行するHTMLを読み込む

    → HTML側でデータの読み込み処理は実行させたくなかったためNode.js側からデータを渡す

  2. HTML側で描画ロジック(JS)を実行

  3. 描画ロジックが完了したことをHeadless Chromeへ伝える

  4. Headless Chrome側でHTMLをキャプチャしPDF化

このフローでは 3. 描画ロジックが完了したことをHeadless Chromeへ伝える が肝です。それがないと描画ロジックが実行される前にキャプチャの処理が走るため、真っ白なPDFが出来上がるという残念な感じになります:fearful:


ソースコード


Node.js側のタスク


build-pdf.js

const fs = require('fs')

const puppeteer = require('puppeteer')

(async () => {
// PDFの出力先
const OUTPUT_PATH = './pdf/'
// PDFのファイル名
const FILE_NAME = 'capture-pdf'
// PDFの用紙フォーマット
const FORMAT = 'A4';
const URL = '描画ロジックを実行するHTMLのURL'

// Headless Chromeを起動
await puppeteer.launch()
.then(async browser => {
// ページ
const page = await browser.newPage()

// 描画ロジックに必要なデータ
const data = {
text: 'お酒大好き!'
}
// HTMLの描画ロジック側からwindow.puppeteerGetDataでdataを取得できるように
await page.exposeFunction('puppeteerGetData', () => data)

// HTMLの描画ロジック側からwindow.puppeteerPdfでPDF生成
await page.exposeFunction('puppeteerPdf',
async () => {
// 出力先のディレクトリが無ければ生成
if (!fs.existsSync(OUTPUT_PATH)) {
fs.mkdirSync(OUTPUT_PATH);
}

// PDF作成処理
await page.pdf({
path: `${OUTPUT_PATH}${FILE_NAME}.pdf`,
format: FORMAT
});

// Headless Chromeを閉じる
browser.close();
}
})
})();



Puppeteerと連携するために描画用ロジックに追加するプログラム

描画ロジックで使うデータをPuppeteer側から受け取る処理。

if (window.puppeteerGetData) {

// Puppeteer側からデータを受け取る
const data = window.puppeteerGetData()
console.log(data.text) // 'お酒大好き!'
}

描画ロジック完了後、Puppeteer側でのHTMLキャプチャ処理を実行する。

if (window.puppeteerPdf) {

// Puppeteer側のキャプチャ処理を実行
window.puppeteerPdf()
}


タスクの実行

上のソースコードが書けたらタスク実行!!!!

node build-pdf

出力先にPDFが作成さえれば無事成功:tada:


いろいろ試行錯誤したけど思い返せば簡単にできた

Headless Chromeを使ったHTMLのPDF化は、GitHubなどでライブラリとして公開されていたものもありましたが、ページのload完了は見てくれるけどJSの実行完了はみてくれなかったので、最終的には自分でごにょごにょしました(Puppeteer使ったけど:sweat_smile:)。

体感では、描画ロジック実行 → 印刷できる状態(ブラウザでは印刷ダイアログ表示)に比べると数倍速くなったかなと思います(ちゃんとベンチ取れよ〜 お仕事の兼ね合いであれがあれなもんで)。

aptpod Advent Calendar 2018の別日にまた他のネタで書く予定なので、引き続きお付き合いいただけると嬉しいです!