先日13.2.0が出たばかりなのですが、puppeteer 13.3.0がリリースされました。
さっそくリリースノートを見ていきましょう。
Features
・puppeteer: export esm modules in package.json
https://github.com/puppeteer/puppeteer/releases/tag/v13.3.0
今回の修正はesm modulesの対応でした。ところで Common JS と ES module の違いはご存知でしょうか?
Common JS
Common JS は従来より NodeJS のデフォルトとなっていたモジュールの管理方式です。 require
文を使ってモジュールを呼び出す書き方でよく見ると思います。実際、 puppeteer のサンプルも Common JS で書かれています。
Common JS では async/await を使うときに注意があります。グローバルスコープでは async/await を書くことができないのです。そのため、(async () => { ... })()
のように async/await の処理を即時関数の中に閉じ込める必要があります。下にサンプルコードを示しておきます。13.2.0で追加されたiPhone 13のデバイス定義をさっそく使っています。
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true
});
const page = await browser.newPage();
await page.emulate(puppeteer.devices['iPhone 13']);
await page.goto('https://www.magisystem.net/blog/', {
waitUntil: 'networkidle0'
})
await page.screenshot({ path: 'example.png' });
await browser.close();
})();
Common JSでは従来の .js 拡張子に加えて .cjs を使うことができます。これは .js だと Common JS か ES module かの区別が付き難いからです。明示的に Common JS と言いたいときは .cjs とするとよいでしょう。
ES module
一方で、 ES module はES2015 (ECMAScript 2015/ES6)で標準化されたJavaScriptの仕様です。 Common JS との最大の違いは、ES2015に準拠したブラウザでも使えるということです。つまり、ブラウザでもモジュールを使えるようにしたのがこの仕様です。 import
文と export
文を使ってモジュールを使用します。最近の npm モジュールもこの書き方が増えてきましたので、目にする機会もあると思います。
ES moduleの import
の特徴は2つあります。
1つは require
と異なり、 export
した名前(クラス、定数、関数など)を後から上書きすることを防止しています。つまり、悪意のあるモジュールが別のモジュールの関数やクラスを上書きできないようにして、安全性を高めているわけです。
2つ目は import
の処理が非同期であることです。 Common JS の require
は同期処理です。そのため、 require
しているモジュールが更にいくつものモジュールを読み込んでいたり、何か重たい初期化処理があると readFileSync()
のように require
の部分で待ちが発生します。 ES module の import
は読み込み自体は非同期に行い、モジュールのクラスや関数を使うときにリゾルブして処理を始めるように動作します。よって、多くのモジュールを読み込む場合には速度メリットが得られるようになります。その反面、実行時まで解決されないので、トランスパイラや静的パーサーと相性が悪いようです。
さて、 ES module は import
が非同期であるため、グローバルスコープで async/await をそのまま書くことができます。即時関数に閉じ込める必要がなくなるのは便利です。下にサンプルコードを示しておきます。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true
});
const page = await browser.newPage();
await page.emulate(puppeteer.devices['iPhone 13']);
await page.goto('https://www.magisystem.net/blog/', {
waitUntil: 'networkidle0'
})
await page.screenshot({ path: 'example.png' });
await browser.close();
ES moduleでは従来の .js 拡張子に加えて .mjs を使うことができます。明示的に Common JS と区別したい場合は .mjs とするとよいでしょう。 ES Lint などのパーサーも .mjs にしないと ES module と認識しないのか、グローバルスコープの async/await をエラーにする場合があります。
Common JS or ES module?
Common JS で書くか ES module で書くか…これは宗教戦争になってしまうので明言は避けておきますが、指針は出しておきたいと思います。
まず、 NodeJS でしか実行しない場合は Common JS でいいと思います。普及している npm モジュールも Common JS ですし、 ES module に対応していないものもまだあります。ただ、 async/await を標準として使って書く場合は、上記の理由から ES module にした方が可読性が上がります。また、 async/await 系の処理を使う npm モジュールは、大抵の場合は import
にも対応しています。
次にブラウザと NodeJS で共通する処理を書く場合です。この場合は ES module で書くことを始めた方がよいでしょう。従来のように全てをグローバルスコープに読み込む方法から、モジュールを使う方法に変えていくのがよいです。ブラウザに関して言えば、Chromeはもちろん、Firefox、Opera、Safariもサポートしているので、現在のブラウザ環境では問題なく使えると言えます。なお、ブラウザで使う場合は拡張子を .js のままにしておくのがよいです。というのはウェブサーバは .js しかJavaScriptと扱わないことが多いからです。
おまけ: sandboxについて
puppeteerのノウハウには --no-sandbox
を指定するように書いている記事が多く見られます。しかし、これはセキュリティ上からはお薦めしません。セキュリティに気を使うGoogleがデフォルトで用意しているsandboxの仕組みをあえて無効にする必要はありません。
[kusanagi@kusanagi8 pp]$ node index.cjs
/home/kusanagi/work/pp/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:229
reject(new Error([
^
Error: Failed to launch the browser process!
[0210/100340.343786:FATAL:zygote_host_impl_linux.cc(117)] No usable sandbox! Update your kernel or see https://chromium.googlesource.com/chromium/src/+/main/docs/linux/suid_sandbox_development.md for more information on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox.
#0 0x55c3695e7a59 base::debug::CollectStackTrace()
#1 0x55c36954ee93 base::debug::StackTrace::StackTrace()
#2 0x55c369561e60 logging::LogMessage::~LogMessage()
#3 0x55c3676715ab content::ZygoteHostImpl::Init()
#4 0x55c3690ffc8a content::ContentMainRunnerImpl::Initialize()
#5 0x55c3690fd6d6 content::RunContentProcess()
#6 0x55c3690fe0c7 content::ContentMain()
#7 0x55c369158f1a headless::(anonymous namespace)::RunContentMain()
#8 0x55c369158c25 headless::HeadlessShellMain()
#9 0x55c365e6633b ChromeMain
#10 0x7fa6c7054555 __libc_start_main
#11 0x55c365e6616a _start
(以下省略)
もしもCentOSの環境で上記のようなエラーが出た場合は、以下のコマンドを実行してみてください。
[kusanagi@kusanagi8 pp]$ sudo sysctl -w user.max_user_namespaces=28633
usernsを有効にすることでsandboxが使えるようになります。