現在私は、非同期処理をするときは callback
を使っています。
ですが、ネストが深くなるにつれて混乱してしまいます。( ゚Д゚)<メダパニ‼
Node.js には promise
という callback 地獄を解消するためのオブジェクトが用意されています。
promise
での処理を覚えれば、見やすいコーディングが可能です!
…と、言うことは理解できているのですが、何故か未だにあやふやな promise 処理。
こりゃもう、身体で覚えるしかねぇ!( ゚Д゚)
憧れの PROMISE マスターに、なりたいな。ならなくちゃ。絶対になってやるー!(*‘∀‘)
幸い(?)なことに前回作った LINEWORKS の BOT 登録プログラムが callback 地獄になっているので、こちらを promise に書き換えてみたいと思います。
(前回の記事)API を使って LINEWORKS BOT を登録する
const api_id = "api id";
const consumer_key = "consumer key";
const token = "server list token";
const account_id = "ryo_saeba@hunter.city";
const domain_id = 00000000;
const callback_url = "https;//www.xyz.jp/callback";
// トーク Bot のテナント登録
const request = require('request');
const uri_text = "https://apis.worksmobile.com/" + api_id;
let options = {
uri: uri_text + "/message/registerBot/v4",
headers: {
"Content-type": "application/json",
"consumerKey": consumer_key,
"Authorization": "Bearer " + token
},
json: {
"name": "test bot",
"photoUrl": "https://developers.worksmobile.com/favicon.png",
"description": "defeat the promise process bot",
"managerList": [account_id]
}
};
request.post(options, (error, response, body) => {
if(body.errorMessage) return;
let botNo = body.botNo;
// トーク Bot のドメイン登録
options.uri = uri_text + "/message/registerBotDomain/v3"
options.json = {
"botNo": botNo,
"domainId": domain_id,
"usePublic": true,
"usePermission": false
}
request.post(options, (error, response, body) => {
if(body.errorMessage) return;
// メッセージ受信サーバー追加
options.uri = uri_text + "/message/setCallback/v2";
options.json = {
"botNo": botNo,
"callbackUrl": callback_url,
"callbackEventList": ["text", "sticker", "image"]
};
request.post(options, (error, response, body) => {
if(body.errorMessage) return;
// トーク Bot からメッセージ送信
options.uri = uri_text + "/message/sendMessage/v2",
options.json = {
"botNo": botNo,
"accountId": account_id,
"content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
};
request.post(options, (error, response, body) => {
console.log(body);
if(body.errorMessage) return;
console.log("success");
});
});
});
});
このコードを promise 処理したコードに置き換えていくのですが、このあとしばらく私の恥ずかしい失敗談が続きますので**「お前の失敗なんざどうでもいいから成功したコードを見せてくれ!」**という一般的なご意見をお持ちの方は、是非!読み飛ばして一番最後の完成コードをご確認くださいませ。
promise に置き換えるには(失敗談)
さぁ、それではこの callback たちを promise に置き換えてみましょう!
「簡単だよ! .then でつなげばいいんだよ!」という先輩エンジニアのアドバイスの元、よく理解していないまま私が書いたコードがこちらです!
// パラメータ等は前回と同じなので割愛
const request = require('request');
const uri_text = "https://apis.worksmobile.com/" + api_id;
// トーク Bot のテナント登録
new Promise((resolve,reject) => {
request.post(options, (error, response, body) => {
(body.errorMessage) ? reject() : resolve(body.botNo);
})
}).then((botNo) => { // トーク Bot のドメイン登録
options.uri = uri_text + "/message/registerBotDomain/v3"
options.json = {
"botNo": botNo,
"domainId": domain_id,
"usePublic": true,
"usePermission": false
}
request.post(options, (error, response, body) => {
(body.errorMessage) ? reject() : resolve(botNo);
})
}).then((botNo) => { // メッセージ受信サーバー追加
options.uri = uri_text + "/message/setCallback/v2";
options.json = {
"botNo": botNo,
"callbackUrl": callback_url,
"callbackEventList": ["text", "sticker", "image"]
};
request.post(options, (error, response, body) => {
(body.errorMessage) ? reject() : resolve(botNo);
})
}).then((botNo) => { // トーク Bot からメッセージ送信
options.uri = uri_text + "/message/sendMessage/v2",
options.json = {
"botNo": botNo,
"accountId": account_id,
"content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
};
request.post(options, (error, response, body) => {
if(body.errorMessage) reject();
console.log("success");
resolve();
});
});
今見ると、ひっどいですねぇ~。恥さらしですな。
のちに、このコードを見た先輩が「なんでこうなるのか、本気でわけわからん」とマジ顔でおっしゃられました。
きっと、今この記事を読んでいる皆様も同じ顔をしているのでしょう。
恥ずかしさのあまり、つい山籠もりをしてしたくなりますが、後回しにして先に進みます。
上記コードですが、もちろんエラーになりました!(/・ω・)/
ReferenceError: reject is not defined
ようは「なんで何度も reject してんの?」ってことです。
コンピュータさん、頭の悪い子でごめんなさい。
promise にとって、reject
は一回こっきり。
最初に new
するときだけなのです。
元のコードでやっていたエラー処理を return
から単に reject
に置き換えたのがいけない。
理解が浅いからこういうことになるのです。
まったく恥ずかしいやつですね!(/ω\)
.catch でエラー処理をする
ってなわけで、たくさんの reject
をひとつにすべく .catch
でエラー処理をしていきます!
promise オブジェクトでは .catch
でエラー処理いっぺんに指定できてしまうんです!
なんて素晴らしい仕様!
処理が複雑になり、.then
の数が増えていっても、エラー処理は1つだけ。
わかりやすいし、見やすいですね~。
ってなわけで .catch
を追加してエラー処理を1つにまとめたものがコチラ。
// パラメータ等は前回と同じなので割愛
const request = require('request');
const uri_text = "https://apis.worksmobile.com/" + api_id;
// トーク Bot のテナント登録
new Promise((resolve,reject) => {
request.post(options, (error, response, body) => {
(body.errorMessage) ? reject() : resolve(body.botNo);
})
}).then((botNo) => { // トーク Bot のドメイン登録
options.uri = uri_text + "/message/registerBotDomain/v3"
options.json = {
"botNo": botNo,
"domainId": domain_id,
"usePublic": true,
"usePermission": false
}
request.post(options, () => {});
}).then((botNo) => { // メッセージ受信サーバー追加
options.uri = uri_text + "/message/setCallback/v2";
options.json = {
"botNo": botNo,
"callbackUrl": callback_url,
"callbackEventList": ["text", "sticker", "image"]
};
request.post(options, () => {});
}).then((botNo) => { // トーク Bot からメッセージ送信
options.uri = uri_text + "/message/sendMessage/v2",
options.json = {
"botNo": botNo,
"accountId": account_id,
"content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
};
request.post(options, () => {});
}).catch((err) => { // エラー処理
console.log(err);
});
おー!見やすくなったぞー(*´▽`)
などと最初は喜んだおバカな私。もちろんエラーになりましたとさ。ぎゃふん。
TypeError: Cannot read property 'botNo' of undefined
まぁ、ちゃんとエラーが catch
できていたので良しとしましょう。
return で次の .then に値を渡す
さて、肝心のエラー内容ですが「botNo が空っぽだよ」と言われています。
そう、return
していないので、次の .then
に何にも渡していないんですよね!
超絶おバカですね。
return botNo;
return
文をそれぞれの .then
に追加して、これで大丈夫だろう!と実行した私。
エラーメッセージ出なかったぞ!やったぁ!成功だ!
と、喜んだのも束の間。待てど暮らせど登録完了メッセージが送信されてこない。
「トーク Bot からメッセージ送信 API」の実行ログを見てみると、以下のエラーが。
{ errorMessage: 'Service fail, HTTP/1.1 400 Bad Request, {"code":400,"message":"Bad Request Parameters: Cannot access bot"}', errorCode: '090', code: 'SERVICE_UNAVAILABLE' }
ふむ。Bot にアクセスできないとな?・・・なんで?
これ、2つ目の「トーク Bot のドメイン登録」をしていないメッセージ送信すると起きる現象なんですよね。
試しにもう一度トーク Bot からメッセージ送信 API を叩いてみる。…成功。
つまり、だ。今はメッセージ送信ができて、登録時には、できない。
.then
で非同期処理をしているはずなのに、ドメイン登録時の request
の結果を待たずに次の .then
に進んでいって、ドメイン登録完了前にメッセージを送信している?
つまーり!非同期処理になってないっ!!どうして!?
.then 内の request は結局 promise 化していない
どうしても何も、私がよく理解していないまま使ったのが悪いのです。反省。orz
色々と調べてみたところ、.then
の中の request.post()
は promise 化されていない模様。
promise 化されていないってことは非同期処理されていないってことで、もちろん順番はめちゃめちゃに。
よし、トーク Bot のテナント登録の際に promise 化した request を使おう!
と、そこでふと思った。いちいち request を promise 化するの、面倒くさくね?
面倒くさいということは!誰かが便利なものを作っているはず!
そうして私は request-promise
と出会いました。先輩の皆様、ありがとうございます。
では、さっそく npm install request-promise
してコードを書き替えていきたいと思います。
// パラメータ等は前回と同じなので割愛
const request = require('request-promise');
let botNo;
// トーク Bot のテナント登録
request(options).then((body) => {
botNo = body.botNo;
// トーク Bot のドメイン登録
options.uri = uri_text + "/message/registerBotDomain/v3"
options.json = {
"botNo": botNo,
"domainId": domain_id,
"usePublic": true,
"usePermission": false
}
return request(options);
}).then((body) => {
// メッセージ受信サーバー追加
options.uri = uri_text + "/message/setCallback/v2";
options.json = {
"botNo": botNo,
"callbackUrl": callback_url,
"callbackEventList": ["text", "sticker", "image"]
};
return request(options);
}).then((body) => {
// トーク Bot からメッセージ送信
options.uri = uri_text + "/message/sendMessage/v2",
options.json = {
"botNo": botNo,
"accountId": account_id,
"content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
};
return request(options);
}).catch((err) => {
console.log(err);
});
return request(options);
に書き換えているので、botNo
が次の .then
に渡っていかないことに注意。
一番最初の body
で botNo
を変数に入れておきましょう!
さぁ、実行してみましょう!ドン!
だーいせーこーう♪ (*‘∀‘)<promise bot ゲットだぜ!
思った通りの動作をしてくれたので、良かった良かったホッと一息。
これで終わった!と、思いきや…。
実はある落とし穴があったことに、このときの私は気づいていなかったのだ。。。
エラー処理ができていない!?
さっき、.catch
でエラー処理の設定をしました。
API ID や TOKEN などのパラメータの入力が間違っていた場合には、.catch
に飛んでいってエラーを表示してくれるはず。
しかし、.catch
には飛ばず、次の .then
に進んでいってしまい、エラーを連発しているのです!
register-tenant
{ errorMessage: 'API ID not exists',
errorCode: '052',
code: 'NOT_FOUND' }
register-domain
{ errorMessage: 'API ID not exists',
errorCode: '052',
code: 'NOT_FOUND' }
register-callback
{ errorMessage: 'API ID not exists',
errorCode: '052',
code: 'NOT_FOUND' }
send-message
{ errorMessage: 'API ID not exists',
errorCode: '052',
code: 'NOT_FOUND' }
ナントイウコトデショウ
LINEWORKS API のエラーは .catch しない
これ、今回大変勉強になったところです。
例えばなんですが、request
する際に uri
が NULL
であるとか、パラメータの型指定が間違っているとかだと、ちゃんと .catch
してくれるのですよ。
でも、LINEWORKS API からの返答がエラーだった場合には .catch
してくれないのです。
request 自体は成功しているから、API がエラーだって言おうが request は成功してるもん!
ってことなんですね。
API に届いた時点で、成功。
その先でエラーだった場合には返ってきた値を見て .catch
に投げる必要があるんですね。
try ~ catch
における throw
をする必要があるんですね。
throw しないで、reject しよう
JavaScript Promise の本に書かれていますが、promise
オブジェクトを使うときは Promise.reject
を使います。
body
の中を見てエラーだったら Promise.reject(body);
とすれば .catch
に飛んでエラー表示してくれます。
LINEWORKS API からの Response がエラーかどうかを判別するには、errorCode
または errorMessage
が含まれているかを判定すれば良いでしょう。
なので、下記一文を諸所に追加します。
if(body.errorMessage) return Promise.reject(body);
これで、ついに完成となりました!
完成したコードです!
const api_id = "api id";
const consumer_key = "consumer key";
const token = "server list token";
const account_id = "ryo_saeba@hunter.city";
const domain_id = 00000000;
const callback_url = "https;//www.xyz.jp/callback";
const request = require('request-promise');
const uri_text = "https://apis.worksmobile.com/" + api_id;
// トーク Bot のテナント登録
let options = {
method: "POST",
uri: uri_text + "/message/registerBot/v4",
headers: {
"Content-type": "application/json",
"consumerKey": consumer_key,
"Authorization": "Bearer " + token
},
json: {
"name": "test bot",
"photoUrl": "https://developers.worksmobile.com/favicon.png",
"description": "defeat the promise process bot",
"managerList": [account_id]
}
};
let botNo;
console.log("register-tenant");
request(options).then((body) => {
if(body.errorMessage) return Promise.reject(body);
console.log(body);
botNo = body.botNo;
// トーク Bot のドメイン登録
options.uri = uri_text + "/message/registerBotDomain/v3"
options.json = {
"botNo": botNo,
"domainId": domain_id,
"usePublic": true,
"usePermission": false
}
console.log("register-domain");
return request(options);
}).then((body) => {
if(body.errorMessage) return Promise.reject(body);
console.log(body);
// メッセージ受信サーバー追加
options.uri = uri_text + "/message/setCallback/v2";
options.json = {
"botNo": botNo,
"callbackUrl": callback_url,
"callbackEventList": ["text", "sticker", "image"]
};
console.log("register-callback");
return request(options);
}).then((body) => {
if(body.errorMessage) return Promise.reject(body);
console.log(body);
// トーク Bot からメッセージ送信
options.uri = uri_text + "/message/sendMessage/v2",
options.json = {
"botNo": botNo,
"accountId": account_id,
"content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
};
console.log("send-message");
return request(options);
}).then((body) => {
if(body.errorMessage) return Promise.reject(body);
console.log(body);
}).catch((err) => {
console.log("error:")
console.log(err);
});
うん、長い!( ゚Д゚)
おわりに
ここまでお付き合いいただきありがとうございました。いや、本当に。
ここまでの道のりも長かったし、比例して記事も長くなってしまいました。
結局、コードの長さ的には Callback
と変わらなかったなぁ。
でも!ネストしていないから見やすいし、promise
オブジェクトの構造がわかってると、こっちの方が拡張や修正はしやすいかも!
promise.finally
とか Promise.all
とか、まだ仲良くなれていない子たちがいるから、憧れの PROMISE マスターへの道のりはまだまだ遠いけど、諦めずに頑張りたいと思います!(^ω^)
ではまた!(^^)/
参考にさせていただきましたm(_ _)m
JavaScript Promiseの本
LINEWORKS Developers
JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる
今更だけどPromise入門
CoffeeScriptでPromiseを使ったときにハマった
request-promiseを使ったHTTPクライアントを作る