GAミント至上主義

Web Monomaniacal Developer.

ヘッドレスChromeを使ったHTML→PDF変換サーバーを作る 改善編

以前とりあえず作ってみたサーバーが不安定なので、書き直すことにした。

Dockerで動いてGETパラメータでURLを渡したらPDFが返ってくるというシンプルなもの。

PDFのサイズ(現在はA4固定)等のオプションは起動時に固定してしまうので、変えられない。

ローカルネットワーク内でAPIとして使うので、外に直接公開することは考えていない。

そのほか詳細は以前の記事参照
uyamazak.hatenablog.com

Dockerfile

FROM node:9

RUN mkdir /varuna/
WORKDIR /varuna/

# Install google-chrome from .deb file using gdebi for dependencies.
RUN apt-get update && \
    apt-get install -y gdebi
COPY google-chrome-stable_current_amd64.deb ./
RUN gdebi --non-interactive google-chrome-stable_current_amd64.deb

# Install chrome-headless-render-pdf
RUN npm install \
    chrome-headless-render-pdf@1.5 \
    express \
    connect-timeout
RUN chmod 700 ./node_modules/chrome-headless-render-pdf/cli/chrome-headless-render-pdf.js

# add option --no-sandbox
COPY node_modules/chrome-headless-render-pdf/index.js node_modules/chrome-headless-render-pdf/index.js

# Install fonts
COPY fonts /usr/share/fonts

# COPY my app dir
COPY app app

EXPOSE 8000

CMD ["node", "app/pdf-server-proto.js"]

chromeのインストールファイルgoogle-chrome-stable_current_amd64.debは下記ページの「別のプラットフォーム向けの Chrome をダウンロード」から取得して同じディレクトリに配置しておく。

www.google.com

ついでにnodeのバージョンも9にした。

chrome-headless-render-pdfも1.5になり、paper-width、paper-heightを渡せるようになったが、まだ Docker上で動かすのに必要な--no-sandboxを渡せないので、これだけindex.jsの192行目に追加してとりあえず使う。
プルリしたいけどまだ自信ない。

www.npmjs.com



サーバー本体
pdf-server-proto.js

const execSync = require('child_process').execSync;
const chromeBinary = '/usr/bin/google-chrome-stable'
const chromeVersion =  execSync(chromeBinary + " --no-sandbox --version");
console.log("chrome version:", chromeVersion.toString());

const options = {
  printLogs: true,
  printErrors: true,
  chromeBinary: chromeBinary,
  noMargins: true,
  landscape: true,
  paperWidth: 11.70,
  paperHeight: 8.26772,
  includeBackground: true
};

console.log("RenderPDF options\n", options);

const RenderPDF = require('chrome-headless-render-pdf/index');
const renderer = new RenderPDF(options);
renderer.spawnChrome();
renderer.waitForDebugPort();

const express = require('express');
const app = express();
const timeout = require('connect-timeout');
const timeout_msec = 5000;

app.use(timeout(timeout_msec));
app.listen(8000, function(){
  console.log('Listening on 8000');
});

app.get('/', async (req, res) => {
  console.log("async start");
  var url = req.query.url;
  if (! url) {
    res.status(404);
    res.end('parameter "url" is not set');
    return;
  }
  try {
    const buff = await renderer.renderPdf(url, renderer.generatePdfOptions());
    res.contentType("application/pdf");
    res.send(buff);
    res.status(200);
  } catch (e) {
    renderer.error('error:', e);
    console.log(e);
    res.status(503);
  }
  res.end();
});

// Health Check
app.get('/hc', function (req, res) {
  console.log('health check ok');
  res.status(200);
  res.end('ok');
});

process.on('SIGINT', function() {
  console.log('process exit with SIGINT');
  renderer.killChrome();
  process.exit();
});

ざっと下記の部分を直した

  • 2重にexecSync(chrome-headless-render-pdfコマンドと、その内部)していたのを、RenderPDFを直接使うようにして省いた。
  • 一度ファイルに書き出して、それを読み込んでいたのを直接バッファを返すようにした
  • chromeをアクセスのたび起動したり、停止したりしていたのを、起動しっぱなしにした。
  • 最初にChromeのバージョンとかoptionを出してみたり。

まだ本番利用はしていないが、レスポンスはかなり早くなり(A4一枚の書類で200ms程度)、たまに起きる謎の待ち時間、フリーズも見られなくなった。

動きは普通のChromeなので、外部のjs、cssなどのライブラリはなるべくインライン化し、サイズの大きなWEBフォント読み込みはやめてサーバーにインストールしておくと早くなる。キャッシュはしないっぽい?

変数宣言はできるだけconstにしておいた。
まだまだ己のnode.js力の足りなさを実感する。

async、awaitは初めて使ったけどこれなら理解できそう。