Lambda(Node.js)をES2015のPromiseを使って今っぽく書いてみる
モバイルアプリサービス部の五十嵐です。
私のNode.jsの知識は0.x系で止まっているので、今っぽい(と言ってもAWS Lambdaで使いたいのでv4.3.2の)書き方を調べてみました。その中でも一番良く使われるであろうPromiseの使い方をまとめてみます。
サンプルコードは、以前作成したLambda Function(bisque33/slack-slash-commands-with-serverless)をPromiseを使って書き直しました。実際の差分比較はこちらです。
環境
- node.js 4.3.2
- npm 2.14.12
Promiseとは
Promiseは、リンク先の言葉を借りると
Promiseオブジェクトは処理の延期(deferred)と非同期処理のために使われます。Promiseはまだ完了していないが、いずれ完了する処理を表します。
とあります。ScalaでいうFutureと似たような機能ですね。(ScalaのFutureについては【Scala】Future と未来のセカイが詳しいです。)
この機能を使うことで、Node.jsでありがちなcallback地獄を解消し、同期処理のように記述することができるようです。
AWS-SDKでPromiseを使う
JavascriptのAWS-SDKは、v2.3.0からPromiseに対応しています。
- Release: AWS SDK for JavaScript v2.3.0 : Release Notes : Amazon Web Services
- Class: AWS.Config — AWS SDK for JavaScript
詳しい使い方は、以下の記事で紹介されています。
環境設定
実際に使ってみましょう。まずはじめに、実行環境にPromiseが定義されているかを確認します。
// Check if environment supports native promises if (typeof Promise === 'undefined') { AWS.config.setPromisesDependency(require('bluebird')); } else { console.log("Promiseは定義されています。"); }
以下は、このコードをAWS Lambda(v4.3.2)の実行環境で実行したログです。既にPromiseが定義されていることが分かります。
2016-09-11 12:07:29.294 (+09:00) undefined Promiseはは定義されています。 START RequestId: df8b2323-77cc-11e6-a30f-6db6cb958ee9 Version: $LATEST 2016-09-11 12:07:29.692 (+09:00) df8b2323-77cc-11e6-a30f-6db6cb958ee9 {"errorMessage":"Command accepted."} END RequestId: df8b2323-77cc-11e6-a30f-6db6cb958ee9 REPORT RequestId: df8b2323-77cc-11e6-a30f-6db6cb958ee9 Duration: 396.99 ms Billed Duration: 400 ms Memory Size: 1024 MB Max Memory Used: 28 MB
Promiseが定義されていれば特別な準備をする必要はありません。環境にPromiseが定義されていない場合は AWS.config.setPromisesDependency
で bluebird
などのPromiseを実装するライブラリを設定することで使えるようになります。
書き方
あるLambdaから別のLambdaを非同期で実行する部分をPromiseを使って書きなおしてみます。
before
const lambda = new AWS.Lambda(); lambda.invoke(options, (error, data) => { if (error) { console.error(error, error.stack); callback("Command failed.", null); } else { callback(null, "Command accepted."); } });
after
const lambda = new AWS.Lambda(); const invokePromise = lambda.invoke(options).promise(); invokePromise.then((data) => { callback(null, "Command accepted."); }) .catch((error) => { console.error(error, error.stack); callback("Command failed.", null); });
ネストが1段浅くなりました。 lambda.invoke
の後に .promise()
を付け加えることでPromiseを返すオブジェクトを作成しています。後はこのオブジェクトに対して .then
.catch()
などのメソッドチェーンを繋いでいきます。これらはPromiseの処理が完了した後に同期的に処理されます。
自分で定義した関数でPromiseを使う
自分で定義した関数でもPromiseを使えるようにしてみます。やり方は簡単で、既存の関数からコールバック引数をなくし、処理部分を new Promise(function(resolve, reject) { ... });
で包んでやればよいです。
before
const asyncInvokeLambda = (params, callback) => { // 途中省略 const lambda = new AWS.Lambda(); const invokePromise = lambda.invoke(options).promise(); invokePromise.then((data) => { callback(null, "Command accepted."); }).catch((error) => { console.error(error, error.stack); callback("Command failed.", null); }); };
after
const asyncInvokeLambda = (params) => { return new Promise((resolve, reject) => { // 途中省略 const lambda = new AWS.Lambda(); const invokePromise = lambda.invoke(options).promise(); invokePromise.then((data) => { resolve("Command accepted."); }).catch((error) => { console.error(error, error.stack); reject("Command failed."); }); }); };
こうすることで asyncInvokeLambda
関数は .then
.catch()
などのメソッドを使うことができるようになります。 Promise.resolve()
が処理に成功した結果、 Promise. reject()
が処理に失敗した結果を返します。
連続したPromise
何重にもなったcallbackから解放されるには .then
でPromiseオブジェクトを返すことで、次の .then
に結果を渡すことができます。実際に例を見てみましょう。
const kms = new AWS.KMS(); const decryptPromise = kms.decrypt(cipherText).promise(); decryptPromise // 1つめのPromise .then((data) => { // 1つめのPromiseの結果 token = data.Plaintext.toString('ascii'); return asyncInvokeLambda(params); // 2つめのPromise }) .then((data) => { // 2つめのPromiseの結果 resolve(data); }) .catch((error) => { reject(error); });
このようにPromiseを連続して使うことで、ネスト一定の深さでおさまることが分かりました。
まとめ
ここまでPromiseの基本的な使い方を試してみました。Promiseを使うことで非同期処理を同期的に記述でき、callback地獄からも解放されることがわかりました。もっとスマートに書くならGenerator関数とかを使うと良いらしいので、試してみたいと思います。