非同期処理を逐次実行する in Node.js

2 min read読了の目安(約1800字

非同期処理は基本的に前の処理によって止まることなく次の処理をできることが強み。

でも前の処理を待つ逐次実行がしたいんです

なぜって、1000以上のWebページをpuppeteer を使ってPDF化したいから。一気に処理したらPCがパンクしちゃうんです。

というかパンクしました。

環境は次の通り

$ node -v
v15.3.0

長過;未読

Array のメソッドである reduce を利用する場合、「以前の結果を await する必要がある」

const heavyTask = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

(async () => {
const list = [...Array(3).keys()].map((v) => (v + 1) * 1000);
  list.reduce(async (prev, ms) => {
    await prev // ←ココ重要!
    console.log('sleep for', ms);
    return heavyTask(ms);
  }, Promise.resolve());
})();

上のコードをファイルに貼り付けて実行すれば再現できます。
実行可能にするためにゴニョゴニョとしているけど、重要なのは list.reduce の部分。特に await prev
reduce は前回の結果と今回の配列の値を引数に取るけど、その前回の結果を await しないと期待通りに停止してくれない。

ハマったポイント

次のように heavyTaskawait すれば行けるだろうと踏んでいたけど見事に失敗してパソコンが静かに再起動しました。

list.reduce(async (prev, ms) => {
  console.log('sleep for', ms);
  await heavyTask(ms);
}, Promise.resolve());

なんでこの書き方だと await しないのかは正直謎。
node のバージョンで変わったりするのかな?

有識者で答えられる人がいたらコメント頂けると嬉しみです。

ちなみに

for..of の方が素直に書ける

for..of か for..in、どちらかが非推奨だったなぁと思い reduce を使う道を選んだけど、改めて調べたら for..of は使っても問題ないらしい。そしてこっちの方が素直にかけた。

const heavyTask = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

(async () => {
  const list = [...Array(3).keys()].map((v) => (v + 1) * 1000);
  for(const ms of list) {
    console.log('sleep for', ms);
    await heavyTask(ms); // ← ここで素直に await すればいい
  }
})();

puppeteer を使って Web ページを出力するの、すげー簡単で感動

たったこれだけで「指定したURLをPDF化して、指定した path に保存」が完結する。
すげー!

const puppeteer = require('puppeteer');

const pdfnize = async (url, path) => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);

  console.log(`Printing ${path}`);
  await page.pdf({ path });
  await browser.close();
};