◆ puppeteer が便利

前作ったもの

以前ネタ半分で作ったこれ
簡単に言うと確認のしづらい CSS で全ブラウザで問題ないように印刷画面を頑張って作るなら サーバサイドでつくればいいよね というものです
IE なんてとくに印刷時の表示のデバッグなんて辛いですからね
サーバサイドでは Chrome を使って画像化してそれをレスポンスとして返します
作る側は Chrome だけを考えて作ればよくて 新しめの機能も使えていい事づくしです

画像化するのは画面ごとにそういう機能を作るのじゃなく 印刷ボタン押したら現状の HTML を POST して 受け取った HTML を画像化するサービスを用意しておけば使い回せて楽そうです
ということで作ってみたものでした
実際のところは セキュリティ的に何らかのデータが入ってそうなデータを POST するのが嫌という人がいたり HTML を POST するので相対パスのリソースが解決ができないとか ユーザが入力した textarea の情報がないとか canvas が表示されないとか問題も多いです
実用に耐えません

自サイトのみを対象とするなら 表示する HTML 自体がこちらからユーザへ提供してるものなので ユーザがサイド送信することに抵抗は無いと思います
textarea 等は属性に反映させて base タグを追加して相対パスも解決できれば良いところまで行きそうな気もします
ただ 自身のサイトなら入力項目と必要ならユーザ情報付きで URL を POST すれば HTML を全部送るなんてことしなくても十分対応できそうですけど
意味があるとすれば details 的な開閉機能や ダイアログ・ポップアップ等の UI の状態まで再現できるくらいでしょうか
なんにせよ実用性は(略

puppeteer

実用性なんてどうでもいいのです
ツールやサービスなんて思いつきと勢いで作るものです

ところで 前回作ったものはサーバサイドで POST された HTML をレンダリングしてスクリーンショットする部分がすごく大変でした
単純に nightmare.js でやろうとしたらヘッドレス起動できずうまく動かず 別の方法にしました
Chrome をランチャーで起動させて chrome-remote-interface というものを使って起動させた Chrome を devtools から操作することで devtools にあるスクリーンショット機能でスクリーンショットを撮りました


今回 ヘッドレス Chrome を簡単かつ便利に操作できる puppeteer というのが話題になっていたので置き換えてみます
puppeteer では PDF 形式で出力できるので 印刷用データに向いていますしこっちにします
ユーザは PDF を印刷できますし そのまま PDF ファイルを保存しておくこともできますから

作ってみたものがこれです
const puppeteer = require("puppeteer");
const http = require("http")
const qs = require("querystring")

http.createServer(async function (req, res) {
if(req.method === "POST"){
const post = await getPostData(req)
const url = "data:text/html;base64," + new Buffer(post.source).toString("base64")
res.writeHead(200, {"Content-Type": "application/pdf"})
res.end(await createPDF(url))
}else{
res.writeHead(200, {"Content-Type": "text/html"})
res.end(`
<!doctype html>
<div contenteditable>Please Edit Here</div>
<form method="post" onsubmit="onSubmit()">
<input type="submit" value="submit" />
<input type="hidden" name="source" id="source" />
</form>
<script>
function onSubmit(){
document.querySelector("#source").value = "<!doctype html>\\n" + document.documentElement.outerHTML
}
document.write(1000)
</script>
`)
}
}).listen(3333)

async function createPDF(url){
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page._client.send("Emulation.setScriptExecutionDisabled", { value: true })
await page.goto(url, {waitUntil: "load"})
const buffer = await page.pdf({format: "A4"})
browser.close()
return buffer
}

function getPostData(req){
return new Promise((success, fail) => {
let body = ""
req.on("data", data => body += data || "")
req.on("end", () => success(qs.parse(body)))
})
}

比べてみるとわかりますが すごく短いです

createServer の POST 以外の場合の処理は テスト用の HTML を返す部分です
document.write をしているのは JavaScript が 2 回実行されないことを確認するためです
ユーザの画面にレンダリングされたときに 「1000」 というテキストはすでに HTML 中にあるので サーバに送信したときに再度 script タグが実行されると 1000 が 2 回表示されます
前の方法では POST する前に script タグを全部除去するという面倒なことをしていましたが puppeteer では簡単に script タグの実行を無効化できたのでそっちで対応しています

また viewport のサイズを指定しなくても済みます
前の方法だとページ全体の場合は全体の高さを取得して viewport に設定する必要がありましたが puppeteer では何もしなくても全体が対象です


そんな感じで puppeteer はすごく便利なんです
さすが Chrome のプロジェクトのひとつと言うだけはあります
一応中では Devtools Protocol を使っているらしいので前の方法でやったことを隠ぺいしてくれてるだけかもしれませんけど

ところでインストールは npm で puppeteer をインストールするだけです
Chromium が内蔵されてます
前の方法みたいに Chrome 入れて ランチャ―とデバッガを操作するパッケージを入れて……としなくて済みます