at backyard

Color my life with the chaos of trouble.

Top level awaitについて整理した

トップレベルawait(Top level await)について改めて自分の中で整理することにした。

これはその際の備忘録的なものとなる。

目次

Top level awaitとは?

そもそもTop level awaitとは?というところだが、これはawaitキーワードを非同期関数(async function)の外で使用できるようにするものである。

いままではasync 関数の外で await を使おうとすると SyntaxError が発生した。

await Promise.resolve(console.log('Hello World'))
// これは構文エラー(Syntax error)となる

そのため開発者側は上記のようにawait関数を利用するための手段として、すぐに呼び出される非同期関数(async function)を定義していた。

(async function () {
    await Promise.resolve(console.log('Hello World'));
}());

だが、Top level awaitが利用できる環境であれば、最初に書いた下記の書き方でも問題なく動作する。

await Promise.resolve(console.log('Hello World'))
// Hello World

上は単純な例だが、例えば下記のようにCDN経由でjQueryを読み込むなども行える。

const jQuery = await import('https://code.jquery.com/jquery-3.6.0.slim.min.js')
console.log(jQuery)
// Module {Symbol(Symbol.toStringTag): 'Module'}

try~catchも可能なため、下記のようなコードも書ける

let aModule;
try {
  aModule = await import('http://example.com');
} catch {
 aModule = await import('http://example.net');
}

Top level awaitをブラウザで試すシンプルな方法

Chromeがtop level awaitに対応した際に、開発者ツールのコンソール上で await が使えるようになったので便利になった、というツイートを見かけた気がする。

例えば下記のようなコードをChromeの開発者ツールのconsole上で書くことができる。

const response = await fetch(url);
response.status
// => 200

私自身、開発者ツール上であれこれデバッグしたいときにawait周りで無駄に思考を張り巡らせていた経験があるので、シンプルに上のように書けるのはとても便利で感動した記憶がある。

さて、では実際にファイル上にtop level awaitを書きたい場合、どのように書けばよいか?

実際に読み込まれたJSファイル上でTop level awaitを行う場合は type="module"をつけて呼び出す

一応、今回の動作環境となるChromeのversionを先にメモしておく。

Google Chrome: 94.0.4606.71 (Official Build) (arm64)

まずはTop level awaitを定義したJSファイルを用意する( src/index.js)

const a = await Promise.resolve("<h1>hello</h1>");
document.getElementById("app").innerHTML = a;

そして次にHTMLファイルを用意する。

<!DOCTYPE html>
<html>
  <head>
    <title>Top level await sample (Vanilla JS)</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="src/index.js"></script>
  </body>
</html>

type="module"と書かないと、src/index.jsのTop level awaitは有効にならない。

これでローカルサーバを経由してアクセスすることで想定した結果が得られる。
なお、ファイルを直接ブラウザで開くやり方の場合、下記のようなエラーが表示される。

Access to script at {file path} from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.

type="module"をつけない場合はsyntax errorとなる

なお、下記のように type="module"をつけない場合は、従来の通り、構文エラーとなる。

<!DOCTYPE html>
<html>
  <head>
    <title>Top level await sample (Vanilla JS)</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>
    <script src="src/index.js"></script>
  </body>
</html>

この場合、下記のようなエラーがコンソール上に出力されるはず。

Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

Top level awaitをVanilla JSで試す際の最もシンプルなサンプル

勿論ファイル経由で呼び出さず、下記のように書くこともできる。

たぶん、これが一番シンプルなTop level awaitの呼び出しサンプルかと思います。

<!DOCTYPE html>
<html>
  <head>
    <title>Top level await sample (Vanilla JS)</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>
    <script type="module">
      const a = await Promise.resolve("hello");
      document.getElementById("app").innerHTML = a;
    </script>
  </body>
</html>

type="module"のあるファイルと、ないファイルが混在している場合

次にtype="module"があるファイルと、ないファイルが混在している場合だが、type="module"がついているファイルは最後に実行されることになる。 よって、awaitしている間にHTMLのパース自体がブロックされるということはない。

サンプルを下に上げる。

<!DOCTYPE html>
<html>
  <head>
    <title>Top level await sample (Vanilla JS)</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="src/index.js"></script>
    <script src="src/index2.js"></script>
  </body>
</html>
console.log('call index.js');
console.log('call index2.js');

このようなファイル構成の場合、Chromeの開発者ツールからコンソールを確認するとログは下記のようになる。

call index2.js
call index.js

CodeSandbox上だとtop level awaitは有効になっていない?

ちなみに上に書いたようなコードをCodeSandbox上で試そうとしても、エラーになるようだった。

これは単にCodeSandbox環境だとTop level awaitに対応していないだけだろうか?

f:id:shinshin86:20211010065322p:plain
Vanilla JSのテンプレートで作成して実行してみたところ

ReactでTop level awaitを試す(webpack使用)

これについてはセットアップなどを記載していくと長くなりそうだったので別ポストに書いた。

shinshin86.hateblo.jp

Top level awaitを試すための方法をサクッと書いてしまうと、webpackのv5を用いて、webpack configに下記の記述を追加することで対応できる。

experiments: {
    topLevelAwait: true,
  },

Rollupの場合は experimentalTopLevelAwait というオプションを利用するようだが、こちらについては試していません。

Node.jsでTop level awaitを試す

Node.js環境では 14.8.0以降、標準にTop level awaitが組み込まれている。

node/CHANGELOG_V14.md at master · nodejs/node · GitHub

例えば下記のようなコードを書く。
(ちなみに今の環境内のNode.jsのversionを見ると、 v14.17.1 だったので併せて明記しておく)

const text = await Promise.resolve('Hello World');
console.log(text);

これを通常通りに実行すると、下記のようなエラーが表示される。

const text = await Promise.resolve('Hello World');
             ^^^^^

SyntaxError: await is only valid in async function

Node.jsから実行する場合はpackage.jsontype: moduleを追加する。

{
  ・
  "type": "module",
  ・
  ・
}

もしくは、ファイルの拡張子を mjs にすることで実行可能となる。

DenoでTop level awaitを試す

確か記憶だとDenoは最初からTop level awaitが使えるようになっていた気がする。

自分の環境のDenoは最新から比べるとやや古くなっていた。せっかくなのでupgradeする。

denoでは下記のコマンドで最新にupgradeできるのでかんたん。

deno upgrade

というわけで、動作させるバージョンは 1.14.3

下記のようなコードを書く。
(といっても先程と同じコード)

// main.ts
const text = await Promise.resolve("Hello World");
console.log(text);

そして下記のコマンドで実行する。

 deno run main.ts

これで問題なく実行できる。