設定なしのバンドラー「Parcel」がWeb開発には最強の予感!だがしかし(期待を込めて)・・・
昨年末あたりから、よく聞くようになった「Parcel」を使いはじめました。
敷居が低くて簡単・便利。でも不安定な動作や実行時エラーに引っかかったりもしましたので、以下に紹介がてら対処法なども書いておきます。
現時点で、自分の知識不足や経験不足に起因する点があるかもしれません。 以下、おかしなことを書いていたら、コメント等で御教示ください。 よろしくお願いいたします m(_ _)m。
「Parcel」は、最近のWebアプリの開発でほぼ必須ツールと言ってよいバンドラー(およびタスクランナー)ですね。 なにが凄いって、常識破りの売り文句「設定不要」というところでしょう。
ワタシはこれまで Grunt+Browserify+Uglify でやっていまして、特にこだわりはないのですが、既に設定が固まっていて、「Grunt遅いよ」「Gulpが速いよ」「WebPackのほうが便利でしょ」などと聞いても、腰が重くて動じない。 新たなツールの新たな設定を覚えるのって邪魔くさいでしょ。そういうのが好きな人がいるのも知っているけどワタシ自身はそうではない。
そんなときに「Parcel は設定不要ですよー」と知って「それならば」と、新規のnpmで使ってみたら「めっちゃ便利ですやんコレ!スゲー!」となったわけです。 前述のように、まだ安定しない点もあるようですが、今後どんどん便利になっていくことを期待しつつてな感じで、以下どうぞー。
※ ちなみに本記事執筆時点のワタシの環境はこちらになります:
- OS: Windows 10 / MSYS2
- node: 8.10.0
- npm: 5.6.0
- devDependencies(parcel関連の一部)
- parcel-bundler: 1.9.7
- babel-plugin-transform-runtime; 6.23.0
- babel-runtime: 6.26.0
Parcelの凄いところ
Parcelの凄いところは、すでにたくさん紹介サイトがありますので、ここでは箇条書きにしておきます。 これ以外にもたくさんあると思います。
- 本当に設定不要(疑ってたわけじゃないけどホントに設定不要で驚いた)
- ソースを更新すると自動でビルド。
- HTTPサーバーが立ち上がっていて、ビルド後モジュールが置き換えられる(HMR - Hot Module Replacement)。
- ビルドはかなり速い気がする。2回目以降はキャッシュを使ってるんですよね。初回も速い気がしますけど。
- 最初からTypeScriptに対応している。
Parcelのいまいちなところ
決してダメだっつってんじゃありませんよ。 今後の改善に期待しつつ、ハマったことなどを書いています。 また、自分の開発環境に依存するところもあるのかも。
自動ビルドがおかしくなることがある。
困り事
長く自動ビルドを繰り返していると、ソースの変更が正しくビルド結果に反映されていないと感じることが何度かありました。 Parcelはキャッシュを利用して高速化を図っているようですが、その辺に何かあるんだと思います。
対処法
Parcelを強制停止して、出力ディレクトリと、キャッシュディレクトリを消去して再度Parcelを立ち上げれば正しい結果が得られました。
$ rm -r dist/ .cache/
ビルドエラーで自動ビルドが止まっちゃう?
困り事
複数ファイルを編集していて、ひとつのファイルを先に保存した時点では整合が取れていなくてエラーになっちゃうことがありますよね。 このとき、当然ビルドは失敗して、HMRでブラウザにエラーが表示されます。 しかし、ソースファイルをキチンと修正しても、ビルドが走ってくれません。
対処法
こちらも、とりあえずParcelを再起動すれば治ります。
それでもおかしいと感じる場合、 rm -r dist .cache
で出力ファイルとビルドキャッシュを消去して再起動ですね。
HMRがうまく動かないことがあります
困り事
どういう状況なのか詳細はまだわかっていないのですが、HMRが動いたタイミングで、ブラウザで延々エラーログが吐かれる状態になったりします。 単純にページをリロードしてくれるとよいのですけど。 それだと再読み込みに時間がかかるのかもしれませんが、動かないよりマシじゃないかと。
対処法
Webページをリロードします。大抵これでなんとかなりますが、それでもだめなら Parcelを再起動しましょう。 必要ならば出力ファイルとキャッシュクリア(ここまですることはないと思います)。
Asyncを使っていると実行時にエラーが出る
困り事
Asyncを使っていると、実行時に regeneratorRuntime is not defined
というエラーが発生します。
対処法
エラーメッセージでググったらGitHub の Issue#871 に解決法が載っていました。
以下のコマンドで、npmにbabelのランタイム?と変換プラグイン?をインストールし、
$ npm install --save-dev babel-plugin-transform-runtime babel-runtime
モジュールのルートに以下のbabelの設定ファイル? .babelrc
を追加すればとりあえずエラーはなくなります。
{ "plugins": [ ["transform-runtime", { "polyfill": false, "regenerator": true }] ] }
しかしワタシ Babel に詳しくないので、今のところ何がどうなってるのか理解していません(Regeneratorってなんでしょう?)。
そもそもAsync/Awaitをそのまま使うって選択肢はないのかな。
Babelのせい?でAsync関数がAsyncFunctionでなくなっちゃう
困り事
上の .babelrc でAsyncが使えるようになりましたけど、実行時にAsync関数の constructor.name が Function
のままなんです。
通常なら async宣言された関数の constructor.name は、 AsyncFunction
のはずなのですが。
何が困るってコールバックを呼び出すときに「 AsyncFunction
なら Async関数でラップして実行をawaitしたい」のですが、全部Functionになっているので同期できないんですよね。
対処法
こちら、今のところ解決法が見つかっていません。 ParcelではなくBabelの問題だと思っていますが。
Parcelの使い方
parcelをnpmにインストール
npmで使う場合、リポジトリ内で、npm install --save-dev parcel-bundler
でインストールして、
package.json の scripts に parcel を登録(以下参照)しておきます。
こうしておけばこのnpmの内部では npm run parcel <パラメータ>
でParcelを実行できます。
グローバルにインストールすれば parcel だけで実行できますが、npmの場合は、依存関係が明記されているほうが親切ですよね。
package.json
... "scripts": { "parcel": "parcel", "test": "mocha" } ...
ビルドする
例えば index.html をビルドするには、コマンドラインから以下のように実行するだけです。 (何がビルド対象かを明示するためにscriptsにindex.htmlまで指定したコマンドも登録しておくほうが良いかもしれません)。
$ npm run parcel index.html > package-name@1.0.0 parcel path/to/repo > parcel "index.html" Server running at http://localhost:1234 √ Built in 4.39s.
ビルド結果は ./dist/
以下に出力されます。
ビルドの前にWebサーバーが起動しており、http://localhost:1234/index.html
をブラウザで開けます。
ビルド後もparcelは動き続けており、ソースファイルが更新されると自動的にビルドされて、HMRでブラウザのアプリも更新されます。
あとがき
結局「設定なし」だけケチが付かず、もろ手を挙げて「最強!」とは言えない感じでもあります。 でもやっぱり設定ファイルを書いていないという点は、それだけでも最強といえるかも。 安定すれば「これしかないっ」て言いきっちゃうぞ。
ホント、現時点でもWebアプリの開発では使わない手はないと思います。 npm scriptsとのシンプルな構成が便利かな。
ワタシ自身、まだ使い始めでよくわかっていない点もあります。 他にもCSSの統合なども便利なようで、継続して追いかけていきたいと思っております。 今後、あっちゅう間に使いやすくなっていくんじゃないかな?
今後JavaScriptでArray.forEachはほとんど使わなくなるのかも
いままでJavaScriptの繰り返しは Array#forEach
を使いまくっていましたけど、2018年現在「インデックスが必要ないなら for..of
構文が使えますよ」と聞きまして、ミーハーなので突然使い始めています。
聞いたことはありました
いや、for..of
については小耳にはさんだことはありましたが「(どうせ)各ブラウザではまだ対応状況がまちまちだったりするんでしょ?」とか思い込んでいて、とりあえず静観してたら忘れちゃっていました(「どうせ」は良くない接頭語w)。
Edge以外はほとんど対応しているみたい
今調べてみると、思った以上に各ブラウザが対応していて、これは無視できないムーヴメントだと認識するに至りました。
MDNによるとEdgeが未対応らしいですね。だから、あまねく世に繰り出すWebサービスでは使えないのかもしれないけれど、個人的にはEdgeはほとんど使わないので、今後for..of
をガンガン使って行こうという所存です。
for ... of の構文は?
MDNでは、以下のように説明されています。
for (variable of iterable) { statement }
- variable
- それぞれの反復処理において、別々のプロパティの値が variable に代入されます。
- iterable
- 列挙可能なプロパティに対して、反復処理を行うオブジェクトです。
iterable
は簡単に言えば配列みたいなオブジェクトで、関数の引数リストである arguments
や、DOMの Node.childNodes
も iterable
。
iterable === Array
ではありませんから、Array#forEach
とかは使えなくて、Array.from
でArray
に変換しなくてはなりません。
そういう意味では、言語の構文として for..of
があるので、便利になったと言えるでしょう。
ちなみにString
もiterable
だそうで、実は本日知りました(内緒)。
一般的な使い方
一般的には以下のように使います。これは配列の場合ですね。他も似たり寄ったりです(多分)。
for(const foo of [0,1,2]) { console.log(foo); } // output: // 0 // 1 // 2
速度に関する考察
入力情報によれば「Array.forEach
よりも関数呼び出しが伴わない分、for..of
のほうが若干速い」とのことでした。
Array.forEach
で各ループを別スレッドに振り分けるような実装がされたら、処理速度は比較にならないのでは?と思っていましたが、for..of
だって別スレッドに割り振れるはずなので、どっこいどっこいなのかな?と。
ほとんどforEach
は使わなくて済みそうですね
見た目に for..of
のほうがスッキリするし若干速そうなので、今後はfor..in
を使いましょうと。
インデックスが必要ない場合だけですけどね。
WebアプリからGoogleドライブにファイルを保存する方法
前回記事の読み込みに引き続いて、WEBアプリからGoogleドライブにファイルを保存するサンプルコードを掲載します。
公式ガイドにWEBブラウザのJavaScript向けサンプルコードは有りませんでした。 しかし前回懲りて学習しました。 XHRで直接APIを叩けばOK。 今回は、あまり停滞せずに0.5人日でなんとかなった。 いくつになってもお勉強です。
ここに書いてる機能を、npmとしてまとめましたので是非どうぞー
目次
シンプルアップロードを使います
Googleドライブへのファイルのアップロードには3つのやり方がありますが、ここで説明しているのは最も単純な「シンプルアップロード」という方法です。 他2つ「マルチパートアップロード」と「リジューマブルアップロード」には言及しません。
シンプルアップロードを使う条件は以下:
- 通信が切断したりしてアップロードが失敗した場合に、再度アップロードするのが苦にならない程度にファイルが小さい。
- メタデータを同時に送信しない。アップロードとは別リクエストでメタデータを送信するか、メタデータを利用しないならOK。
新規作成はAPIを二度叩く
シンプルアップロードでファイルを新規作成するのは以下の手順。
- ファイル名を指定してリソースを作成(API
drive.files.create
)し、ファイルIDを入手(この時点で中身が空のファイルが生成されている) - 上で得たファイルIDで、コンテンツを上書きする(API
drive.files.update
)。
既存ファイルへの上書き保存は簡単です
一方、既にファイルIDが分かっているなら、上記2の「コンテンツの上書き」と同じことを行うだけです。
ということでコードはこちら
以下、少々長くなりますが、関数単位に分割してお送りします。
2つのクラス GdfsUi, Gdfs がありまして、以下の関連になっています。
GdfsUiクラスは、カレントディレクトリとそのファイルリストを管理してます。 GdfsクラスはGoogle Drive APIの低レベルインターフェースクラスです(APIに対して直接的)。
そのほか、OAuth2での認証取得やUIの生成については、以下のGitHubリポジトリを見てください。 API キーを http://localhost:8080/ に対して公開していますので、ローカルのウェブサーバーで動作確認できるはずです。
Googleドライブにファイルを保存する関数
指定された名称のファイルがファイルリスト中にあるなら上書き保存し、無ければ新規作成します。
/** * Create or overwrite a file to current directory. * @param {string} filename The file name. * @param {any} data The file content. * @param {string} contentType The content type. * @returns {Promise<object>} The response of update. */ GdfsUi.prototype.writeFile = async function ( filename, data, contentType) { // Find same file on current directory let fileIds = this._files.filter( file => { return file.name === filename; }).map( file => file.id ); let fileId = (fileIds.length > 0 ? fileIds[0] : null); //Create new file if(fileId == null) { let response = await this._gdfs.createFile( this._folderId, filename, contentType); let file = JSON.parse(response); console.log(JSON.stringify(file, null, " ")); fileId = file.id; } // Write file return await this._gdfs.updateFile( fileId, data, contentType); };
ファイルリソースを新規作成する関数
この関数は、ファイルリソースを作成します。 レスポンスに作成されたファイルのファイルIDが返されますので、 これを使ってファイルコンテンツを上書きできます。
/** * Create a new file's resource. * @param {string} folderId The folder id where the file is created. * @param {string} filename The file name. * @param {string} mimeType The mime type for the new file. * @returns {Promise<object>} The response of the API. */ Gdfs.prototype.createFile = function(folderId, filename, mimeType) { return this.requestWithAuth("POST", "https://www.googleapis.com/drive/v3/files", {}, { "Content-Type": "application/json", }, JSON.stringify({ name: filename, mimeType: mimeType, parents: [folderId], })); };
既存ファイルへコンテンツを上書きする関数
ファイルIDを指定して既存ファイルに上書きします。
APIガイドでは「既存ファイルの更新にはPUTメソッドを使え」と書いてありましたがPUTはエラーになりました。
drive.files.update
APIのリファレンスを参照すると「PATCHメソッドだ」と書いてあった・・・ドナイヤネン。
/** * Upload a file content to update a existing file. * @param {string} fileId The file id to update. * @param {any} data The file content. * @param {string} contentType The content type of the file. * @returns {Promise<object>} The response of the API. */ Gdfs.prototype.updateFile = function(fileId, data, contentType) { return this.requestWithAuth("PATCH", "https://www.googleapis.com/upload/drive/v3/files/"+fileId, { uploadType: "media" }, { "Content-Type": contentType }, data); };
共通関数
以下、上記パブリックメソッドから呼ばれている関数です。
XHRでGoogle Drive APIのリクエスト
XHRで指定されたリクエストを行う関数。 認証情報(アクセストークン)をリクエストヘッダに設定します。
/** * @param {string} method The request method. * @param {string} endpoint The endpoint of API. * @param {object} queryParams The query parameters. * @param {object} headers The request headers. * @param {any} body The request body. * @returns {Promise<object>} The response of the request. */ Gdfs.prototype.requestWithAuth = function( method, endpoint, queryParams, headers, body) { let xhr = new XMLHttpRequest(); xhr.open(method, this.createUrl(endpoint, queryParams), true); headers = headers || {}; Object.keys(headers).forEach( name => { xhr.setRequestHeader(name, headers[name]); }); xhr.setRequestHeader("Authorization", "Bearer " + this.getAccessToken()); xhr.timeout = 30000; return new Promise( (resolve, reject) => { xhr.onload = e => { resolve(xhr.responseText); }; xhr.onerror = e => { reject(new Error(xhr.statusText)); }; xhr.ontimeout = () => { reject(new Error("request timeout")); }; xhr.send(body); }); };
アクセストークンを取得する
現セッションのアクセストークンを取得する。
gapi
はDirve APIをロードしたときから使えるグローバルオブジェクト。
/** * Get access-token on current session. * @returns {string} The access token. */ Gdfs.prototype.getAccessToken = function() { let googleUser = gapi.auth2.getAuthInstance().currentUser.get(); let authResponse = googleUser.getAuthResponse(true); let accessToken = authResponse.access_token; return accessToken; };
APIのURLを作成する
エンドポイントとクエリパラメータからURLを生成。
/** * Create URI including query parameters. * @param {string} endpoint The endpoint of API. * @param {object|null} params The query parameters. * @returns {string} The URI. */ Gdfs.prototype.createUrl = function(endpoint, params) { if(params == null) { return endpoint; } let keys = Object.keys(params).filter( key => (key !== "")); if(keys.length == 0) { return endpoint; } let queryString = keys.map( key => { let value = params[key]; return (value == null ? null : `${key}=${encodeURI(value)}`); }).join("&"); return `${endpoint}?${queryString}`; };
WebアプリでGoogleドライブのファイルを読み込む方法
Flickr: Google Self-Driving Car ( Creator: smoothgroover22, License: CC BY-SA 2.0 )
ここしばらく「できないわけがないじゃないか」と調べていた個人的な大問題、「ブラウザで動作するWebアプリから、ユーザーのGoogleドライブに保存されているファイルを読み込む方法」を解決できたので小躍りしながら書いております。
Googleドライブのファイルを読み込むサンプルコードは、下記APIの説明ページで示されています。 しかしWebページのJavaScript用のがありません。 Node.jsのはありますが、これはWebのJavaScriptでは動きません。fs(ファイルシステム)モジュールを使用しており、パイプでストリームへコンテンツを流しているからです。
先に結論としてまとめてしまうと、Drive REST API Files.get を XHRで直接叩いて読み込みます。
クエリパラメータで alt=media
を指定して、リクエストヘッダにサインインユーザーのアクセストークンを指定する必要がありますが。
どうして Files.get API の通常の(関数としての)呼び出しで、読み込めないのかわからないのですが、エラーになるので仕方がない。
一方、保存するのはこちらから!
takamints.hatenablog.jp
Node.js のfsモジュール的なメソッドでGoogle Driveを扱うモジュールを、なんとかnpmとしてまとめましたので是非どうぞー
目次
売り上げランキング: 1,625
やりたいことは、それじゃない
ダウンロードではありません
ここで示しているのは、WEBアプリが利用するデータファイルをGoogle Driveから読み込む方法であって、ブラウザの機能を利用してユーザーのローカルファイルシステムへダウンロードするものではありません。
Drive REST API Files.get で得られる webContentLink
をhrefにセットしたアンカータグ(<a href="...">...</a>
)を生成すれば、ユーザーがクリックしてファイルがダウンロードできますが、JavaScriptのコードからは利用できません(CORSのエラーが発生します)。
Google Docのファイルは読み込まない。
Google Docのファイル(Googleスプレッド、ドキュメント、スライドなど)は、以下のコードでは読み込めません。
これらは別途 Files.export API で取り出せるらしいのですが、あくまでもエクスポートで、独自のWebアプリで開く方法ではありません。
各ファイルの専用アプリで表示したり編集するには、ファイルリソースの webViewLink
をたどれば可能。
WEBページのJavaScriptでGoogle Driveのファイルを読み込むコード
ということで、前置きが長くなりましたが、以下にコードを示します(ざっくり説明は下のほうに)。 このコードは、現在作成中のモジュール gdrive-fs からの抜粋です。
使用しているGoogle Drive APIはV3。 WebアプリケーションがOAuth2の認証を得ている前提で動作します。 Chrome、Firefox、Edgeで検証済みです。
/** * Get a file content as text from Google Drive. * Even if the file is not a text actually, it could be converted * to ArrayBuffer, Blob or JSON to use by Web App. * @async * @param {string} fileId The file id to download. * @param {boolean|null} acknowledgeAbuse A user acknowledgment * status for the potential to abuse. This parameter is optional. * default value is false. * @returns {Promise<string>} A downloaded content as text. */ GdfsUi.prototype.downloadFile = async function( fileId, acknowledgeAbuse) { return await new Promise( (resolve, reject) => { // Create download URL let apiUrl = "https://www.googleapis.com/drive/v3/files"; // Query parameter let queryParams = { "alt":"media" }; //【1】 if(acknowledgeAbuse) { queryParams.acknowledgeAbuse = acknowledgeAbuse; //【2】 } let urlParam = Object.keys(queryParams).map( key => `${key}=${encodeURI(queryParams[key])}` ).join("&"); let downloadUrl = `${apiUrl}/${fileId}?${urlParam}`; //【3】 // Get access-token on current session【4】 let googleUser = gapi.auth2.getAuthInstance().currentUser.get(); let authResponse = googleUser.getAuthResponse(true); let accessToken = authResponse.access_token; // Send a request with XHR let xhr = new XMLHttpRequest(); xhr.open("GET", downloadUrl, true); xhr.setRequestHeader("Authorization", "Bearer " + accessToken); //【5】 xhr.onload = e => { resolve(xhr.responseText); }; xhr.onerror = e => { reject(new Error(xhr.statusText)); }; xhr.ontimeout = () => { reject(new Error("request timeout")); }; xhr.timeout = 30000; xhr.send(null); }); };
ざっくり説明
※ 以下のリストの項番がコード中の【 】内の数字と合致します。
- ファイルの中身を取り出したいときは、クエリパラメータ
alt
を"media"
としなくてはなりません。でないとリソース情報が返ってきます。 acknowledgeAbuse
はよくわかっていませんが、妙なことをするかもしれないけどユーザーがそれを認識しているかどうかってことでしょうか。少なくとも自分が試した範囲では、指定しなくてもよかったのですけど・・・。Abuseフラグみたいなのがファイルについているみたいで、そのフラグが立っていると、このリクエストパラメータをtrueにしなくてはならないようです。逆に、フラグがfalseの場合に、trueを指定すると怒られます。残念。- ダウンロードURLには、fileId と 上記 alt パラメータが必須です。
- ユーザーがサインインしたときに発行されたアクセストークンを取得しています。
gapi
はAPIを読み込み以降利用できるグローバルオブジェクトです。gapi.auth2.getAuthInstance().currentUser.get()
で、ユーザーがサインインしたときの情報が得られ、その中にアクセストークンがあります。 - このアクセストークンを、リクエストヘッダに設定します。本当ならこの前にアクセストークンの有効期限をチェックして無効になっているならリフレッシュトークンで再発行する必要があるかもしれません。
モジュール全体はコチラ
上記コードを含むモジュール全体は、GitHubのgdrive-fsです。 まだ作成途中ですが、UIのスケルトンやサインインシーケンスなどのコードがあります。 UIをカスタマイズできてWebアプリからGoogle Drive のファイルを扱うモジュールを目指しています。おそらくそのうち npm でPublishすると思います。
今のところhttp://localhost:8080/
をホワイトリストに入れたAPIキーをリポジトリで晒しておりますので、そのままでもご確認いただけます。
セキュリティリスク大丈夫か?って警告を受けていますが、利用者のDriveのファイルが見えるだけなので問題はありません。
利用者側でAPIKeyを取っていただいても大丈夫(というかむしろ推奨)。
所感・あとがき
こんなの、今の時代すぐにでもできると思うじゃないですか。 しかし、情報を探っても辿っても途中でぶつ切れになる感じで、同じところを逡巡して「やりたいのはそれじゃない」って情報がわんさか出てきて自分のググラビリティっていったい・・・。
しかしまあ、自分の Google Drive API に関する知識はチュートリアルをやっと乗り越えられたレベルで、実装経験はほとんどゼロです。 Google Drive APIに関して、根本的な設計思想やドキュメントの行間を読む力が足りていなかったからでしょう。 そういう意味では今回いろんなドキュメントを手繰ってどうにか解決したので少しはレベルアップできたかもしれません。
日々精進。いくつになってもお勉強です。
非同期Lambdaのコンテキストはどこ行った?
Flickr: detective (
Creator: olarte.ollie,
License: CC BY-SA 2.0 )
結局どこへも行ってなかったって結論ですがっ!
この4月からAWS Lambda で Node.js 8.10 が使えるようになっており、AWSのコンソールから新規作成するとひな型がasync関数になっています。
しかし引数リストが event
ひとつだけになっていて、従来第2引数で渡されていた context はどこに行ったの?と調べてみました。
目次
- はじめての Lambda with Node.js 8.10 は非同期関数
- ところで Lambda のコンテキストって何でしょう
- 「this === context?」って仮説は否定されました
- じゃあコンテキストはどこ行った?
- 非同期Lambdaのcallbackの扱いは?
- 結論と所感など
はじめての Lambda with Node.js 8.10 は非同期関数
現在、Node.js 8.10 のLambda を新規作成すると以下のひな型が作られます。async関数になってますね。
そして、引数が event だけ。えらいスッキリしましたね。context は何処へ?
exports.handler = async (event) => { // TODO implement return 'Hello from Lambda!' };
ところで Lambda のコンテキストって何でしょう
AWS Lambdaに渡される context は以下のキーを持つオブジェクト。 Lambda関数自体の情報を保持しています。 結構重要な情報があるはずなので、これが渡されないとはいぶかしい。
- callbackWaitsForEmptyEventLoop
- functionName
- functionVersion
- invokedFunctionArn
- memoryLimitInMB
- awsRequestId
- logGroupName
- logStreamName
- identity
- clientContext
「this === context
?」って仮説は否定されました
ジャバスクリプターが「コンテキスト」と聞いて最初に連想するのは this
ですよね(本当か?)
そこで「Node.js 8.10 のLambda関数内では、this === context
なのではないか?」と仮説を立てて、以下のLambdaを書いてみた。
exports.handler = async (event) => { return JSON.stringify({ callbackWaitsForEmptyEventLoop: this.callbackWaitsForEmptyEventLoop, functionName: this.functionName, functionVersion: this.functionVersion, invokedFunctionArn: this.invokedFunctionArn, memoryLimitInMB: this.memoryLimitInMB, awsRequestId: this.awsRequestId, logGroupName: this.logGroupName, logStreamName: this.logStreamName, identity: this.identity, clientContext: this.clientContext, }); };
で、コンソールから動かしてみたのですが、全てがnullで、仮説は完全否定されました。 まあ、そんなヤヤコシイことしないか。
じゃあコンテキストはどこ行った?
てことでAWSコンソールでしばらく探ってみたのですが、「もしやどこにも行っていないのでは?」と思い、
exports.handler = async (event, context) => { return JSON.stringify({ callbackWaitsForEmptyEventLoop: context.callbackWaitsForEmptyEventLoop, functionName: context.functionName, functionVersion: context.functionVersion, invokedFunctionArn: context.invokedFunctionArn, memoryLimitInMB: context.memoryLimitInMB, awsRequestId: context.awsRequestId, logGroupName: context.logGroupName, logStreamName: context.logStreamName, identity: context.identity, clientContext: context.clientContext, }); };
上のLambdaで確認すると、、、お見事大当たり。なんだちゃんと渡って来てるじゃないかと。
コンソールの実行結果:
非同期Lambdaのcallbackの扱いは?
じゃ、「コールバックはどこ行った?」と確認してみると、しっかり第3引数でもらってました。何も変わっちゃいなかったのね。
そして、Async なLambdaで、従来通り callback を呼び出した場合は、優先的に戻り値として採用されるようになっています。
以下のLambdaを呼び出すと "OK!"
が返されます。
exports.handler = async (event, context, callback) => { callback(null, "OK!"); return JSON.stringify({ callbackWaitsForEmptyEventLoop: context.callbackWaitsForEmptyEventLoop, functionName: context.functionName, functionVersion: context.functionVersion, invokedFunctionArn: context.invokedFunctionArn, memoryLimitInMB: context.memoryLimitInMB, awsRequestId: context.awsRequestId, logGroupName: context.logGroupName, logStreamName: context.logStreamName, identity: context.identity, clientContext: context.clientContext, }); };
結論と所感など
この調査で分かったこと:
- Node.js 8.10 の Async な Lambda にも、contextやcallbackコールバックは渡されている(単にひな型の引数リストからなくなってるだけ)。
- コールバックで解決した値が、return よりも優先的にLambdaの戻り値として採用される。
contextについて、あえて渡さないことにメリットがないので当たり前と言えば当たり前か。 自分の妙な思い込みが邪魔をしました(笑)。
コールバックの件については、古いLambdaを改造するときに、どうしても await を使いたくなったら、Node.js のランタイムバージョンを8.10に変更して、async を付け加えれるだけで済むのですね。レアケースかも知れんけど。
しかし、なんで初期状態の引数リストからcontext
を削除しちゃったのだろうかね?
callback
を、あえて示さないのは理解できるが・・・。