Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

Puppeteer の await 書きすぎ問題を多少マシにする

immer っぽい API だとうれしいなと思って書いてみました。

元となるコード

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.emulate(iPhone);
  await page.goto('https://example.com');
  await page.screenshot({ path: 'screenshot.png' });

  await browser.close();
})();

await が多くないですか?

関数でラップするタイプ

こんな感じになります。

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await removeAwait(page, (page) => {
    page.emulate(iPhone);
    page.goto('https://example.com');
    page.screenshot({ path: 'screenshot.png' });
  });

  await browser.close();
})();

removeAwait の実装がこちら

const removeAwait = async <T extends object>(
  obj: T,
  callback: (obj: T) => void
) => {
  const commands: [string | symbol, unknown[]][] = [];
  const proxied = new Proxy(obj, {
    get: (...[, methodName]) =>
      new Proxy(() => {}, {
        apply: (...[, , argumentsList]) => {
          commands.push([methodName, argumentsList]);
        },
      }),
  });
  callback(proxied);
  for (const [methodName, args] of commands) {
    await (obj[methodName as keyof typeof obj] as any)(...args);
  }
};

ネストが増えない版

こんな感じになります。

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  const draft = createDraft(page);

  draft.emulate(iPhone);
  draft.goto('https://example.com');
  draft.screenshot({ path: 'screenshot.png' });

  await finish(draft);
  await browser.close();
})();

finish を呼び出すまでメソッド呼び出しが保留されます。createDraft の実装はこちら

const FinishKey = Symbol('finish');
const createDraft = <T extends object>(obj: T) => {
  const commands: [string | symbol, unknown[]][] = [];
  const processCommands = async () => {
    for (const [methodName, args] of commands) {
      await (obj[methodName as keyof typeof obj] as any)(...args);
    }
  };

  return new Proxy(obj, {
    get: (...[, methodName]) => {
      if (methodName !== FinishKey) {
        return new Proxy(() => {}, {
          apply: (...[, , argumentsList]) => {
            commands.push([methodName, argumentsList]);
          },
        });
      }
      return processCommands;
    },
  }) as T & { [FinishKey]: () => void };
};
const finish = (draft: { [FinishKey]: () => void }) => draft[FinishKey]();

書いておいてなんですが、割と微妙だな…というのが正直な感想です。

他に注意点として、Puppeteer で使うことを前提にいくらか実装をサボっている(関数であることを検証するのが面倒で as any 使ってたり、page.mouse.up みたいなのを想定してなかったり)ので、ほかの用途にこの関数は使わないでください