ことのいきさつ
AWS Lambda が Node.js v14 をサポートしたらしいです。
そのニュースを見て、「そういえば Azure Functions はどのバージョンが動いているんだっけ?」と確認してみると、既に Node.js v14.15.4 が最新のようでした。
でも Node.js v14 って何ができるんだっけ?と思い、また調べてみると、Top-Level await のサポートがあるんですね。
で、ここで表題の疑問、
「Azure Functions の関数コードで Node.js の Top-Level await は使えるのか?」
が湧いた訳です。
結論
使える。
ちゃんと書く
Top-Level await については、下記 Qiita の記事がわかりやすいと思うのでお任せして。
確認したいことは、関数コードで module.exports
の外で await
が使えるかどうか、です。
# 単なる技術的興味なので、何に使えるかはこの際忘れましょう。
こんな感じ。await する関数 sleep
は、指定のミリ秒数待機する単純な例です。
[index.js]
const sleep = (time) => { return new Promise(resolve => { setTimeout(() => { resolve(); }, time); }); } const now = Date.now(); let responseMessage = `before await at ${Date.now() - now} `; await sleep(60000); // <-- このコードは動くのか? responseMessage +=`after await at ${Date.now() - now}`; module.exports = async function (context, req) { context.res = { // status: 200, /* Defaults to 200 */ body: responseMessage }; }
実行してみます。
やっぱり「await は async 関数の中だけ。」と怒られます。
Top-Level await は、ES modules の仕様なので、関数コードが ES modules として扱われる必要があります。
Azure Functions の Node.js Worker では、デフォルト (ファイル拡張子が .mjs
以外) だと関数コードを ( import
ではなく) require
しているので、CommonJS として使用することが想定されています。
なので、この関数コードを何とかして ES modules として読ませてやればよさそうです。
具体的には下記 2 つの変更をします。
- ファイル拡張子を
.mjs
に変更
index.js
->index.mjs
- エクスポートの書式を ES module 化
module.exports =
->export default
[index.mjs]
const sleep = (time) => { return new Promise(resolve => { setTimeout(() => { resolve(); }, time); }); } const now = Date.now(); let responseMessage = `before await at ${Date.now() - now} `; await sleep(60000); // <-- こいつ・・・動くぞ! responseMessage +=`after await at ${Date.now() - now}`; export default async function (context, req) { context.res = { // status: 200, /* Defaults to 200 */ body: responseMessage }; }
実行してみると、きちんとエクスポートした関数内から、関数の外で await した (60000 ミリ秒経過) 結果を参照することができています。
await 中、例えば、20000 ミリ秒経過後に上記の HTTP 関数に対してアクセスすると、残りの 40000 ミリ秒待機した後にレスポンスが返却されます。
ちなみに、ES modules としてモジュールをロードする方法として、 package.json
内に "type": "module"
を追加する方法もあります。
しかし、上記の通り Azure Functions の Node.js Worker は、ファイル拡張子が .mjs
の時のみ ES modules として import
する実装なので、上記のような拡張子の変更が必須です。
TypeScript は?
Azure Functions Core Tools では TypeScript で書かれた関数コードのテンプレートも作ってくれます。
この TypeScript のコードは、デプロイ時に tsc
でトランスパイルされるので、実質 JavaScript で実行されます。
なので、TypeScript でも Top-Level await が使えそうですが、結果的には下記 2 つの追加作業が必要です。
- トランスパイルの設定の変更
tsc
で Top-Level await を通すようtsconfig.json
を以下のように修正します。
tsc
の出力結果の修正
tsc
の出力結果は、デフォルトでは./dist/<関数名>/index.js
に出力されますが、上記のとおり.js
のままだと ES modules としてロードされないので、.mjs
に拡張子を変更して、function.json
の中のscriptFile
の値もmjs
ファイルを変更します。
ただ 2. は tsc
を実行するたびにやらないといけないです。ちょっと面倒ですね。。。
結局何に使えるのか。
パッと思いつく使い方としては、エクスポートした関数の外で (つまり当該関数コード ファイルのロード時 1 回だけ) node-fetch
や axios
などで外部リソースを await
で取得し、関数コード内で取得した外部リソースを再利用する、てことはできそうです。
# 副作用までは追いかけていませんが。
Azure Functions の関数コード用テンプレートはここで公開されているので、需要があるなら issue や PR を挙げてもよいかもしれませんね。