はじめに
この開発をするきっかけは、同居人がイカリングを見ずにスプラトゥーンのステージ情報を知りたいとお願いしてきたからです。
自分としても、開発経験を積みたかったので承諾しました。
Alexaスキルの設定
諸々選んだあとの設定画面。
今回はJavascriptで行きます。理由はなんとなく。
呼び出し名は一旦「ステージ通知」とします。ここはあとで使いやすいように変えよう。
スキルを起動したとき最初に動作する関数"LaunchRequestHandler"内の変数"speakOutput"にデフォルトで入力されている回答文Welcome, you can say Hello or Help. Which would you like to try?
を、スプラトゥーン3の現在のステージをお知らせします。
と書き換えます。
テストタブに初めて移動したときは、スキルが非公開になっていてテストが出来ないので開発中に変更。
テキストエリアに「ステージ通知を起動」と入力しEnter。(マイクで喋ってもいいらしい)
先ほど入力した文言で応答してくれましたね。
基本的な動きがわかったので、あとはステージ情報のAPIを叩いてJSONファイルを取得し、必要な情報を抜いてくれば実装出来そうです!
今回利用させていただくAPIのサイトはここ。非公式です。
許諾云々については、あくまで個人利用の範囲なので問題ないと判断。
コーディング開始
というわけで、まずは完成形のコードがこちら↓
const Alexa = require('ask-sdk-core');
const axios = require('axios');
// Xマッチのルルステを返す関数
async function getXMatchInfo(){
return await axios.get('https://spla3.yuu26.com/api/x/now').then(res => {
const result = res.data.results[0];
const ruleName = result.rule.name;
const stageName1 = result.stages[0].name;
const stageName2 = result.stages[1].name;
return '現在のXマッチのルールは' + ruleName + 'で、ステージは' + stageName1 + 'と、' + stageName2 + 'です。';
})
.catch(err =>{
console.log('Xマッチのデータの取得に失敗しました。\n' + err);
return 'Xマッチのデータの取得に失敗しました。';
});
}
// バンカラマッチチャレンジのルルステを返す関数
async function getBankaraMatchChallengeInfo(){
return await axios.get('https://spla3.yuu26.com/api/bankara-challenge/now').then(res => {
const result = res.data.results[0];
const ruleName = result.rule.name;
const stageName1 = result.stages[0].name;
const stageName2 = result.stages[1].name;
return '現在のバンカラマッチチャレンジのルールは' + ruleName + 'で、ステージは' + stageName1 + 'と、' + stageName2 + 'です。';
})
.catch(err =>{
console.log('バンカラマッチチャレンジのデータの取得に失敗しました。\n' + err);
return 'バンカラマッチチャレンジのデータの取得に失敗しました。';
});
}
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
async handle(handlerInput) {
const xMatchInfo = await getXMatchInfo();
const bankaraMatchChallengeInfo = await getBankaraMatchChallengeInfo();
const speakOutput = xMatchInfo + bankaraMatchChallengeInfo;
return await handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
------以下略------
シンプルな記述だけど2年ぶりにJSを触ったということもあり、初めて使うライブラリということもあり、5時間くらいかかりました......
特に、というか唯一引っかかってたのが、async/awaitを利用した非同期処理ですね。
axiosライブラリを使ってURL先から引っ張ってきた戻り値の型がPromiseといって、非同期通信における状態を保存するものらしい。
return await axios.get('https://spla3.yuu26.com/api/x/now')
// ↑こんな風にthenをつながずに返しちゃうと、戻り値がpending状態のPromiseのままになっちゃう。
// pending : 未解決 (処理が終わるのを待っている状態)
// resolved: 解決済み (処理が終わり、無事成功した状態)
// rejected: 拒否 (処理が失敗に終わってしまった状態)
// 戻り値
Promise { <pending> }
参考ページ(https://octomblog.com/posts/promise/ )
だからその戻り値に対してあらかじめthenをつないであげることで、通信の完了したデータとして扱うことが出来るようになるんだけど、結論から言ってこれじゃあうまくいきませんでした。
最初は"LaunchRequestHandler"内の処理をこんな風↓に書いていたんだけど
//
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle(handlerInput) {
const xMatchInfo = getXMatchInfo();
const bankaraMatchChallengeInfo = getBankaraMatchChallengeInfo();
const speakOutput = xMatchInfo + bankaraMatchChallengeInfo;
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
これで実行すると、"speakOutput"の中身が無くてエラー吐いちゃうんだよね。
もっとわかりやすく当時の状況を再現すると、
async function getXMatchInfo(){
return await axios.get('https://spla3.yuu26.com/api/x/now').then(res => {
console.log('getXMatchInfoを実行中')
const result = res.data.results[0];
return result;
})
.catch(err =>{
console.log('Xマッチのデータの取得に失敗しました。\n' + err);
return 'Xマッチのデータの取得に失敗しました。';
});
}
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle(handlerInput) {
console.log('getXMatchInfoの起動前');
const xMatchRule = getXMatchInfo().rule.name;
const speakOutput = '現在のXマッチのルールは' + xMatchRule + 'です。';
console.log('getXMatchInfoの起動後')
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
こんな感じ。(細かいところは違うかも)
これで実行したらコンソールの表示がどうなるかというと、時系列順にこうなる↓。
INFO getXMatchInfoの起動前
↓
INFO getXMatchInfoの起動後
↓
INFO getXMatchInfoを実行中
そんでエラーを吐く。まあそれはそう。
"getXMatchInfo()"の実行が終わっていないのに、"speakOutput"の中身が無い状態で"handle"の"return"に入ってるからね。
しかしこの原因がず~~~~~っとわからなくて、困った。
5000msの遅延を入れたりとかいろいろ試したけど、結局この表示の順番は変わらなくて、エラーも解消できなかった。
それで似たような事例がないかすごい調べてたら、こんな記事(https://qiita.com/HorikawaTokiya/items/9822ba5af62b2ba92987 )を発見。
async/awaitは呼び出し元の親関数も指定しないと非同期処理よりも先に親関数が終了してしまう
これじゃね!?!?!?!?
確かに呼び出し元のLaunchRequestHandlerでは非同期に関する処理はひとつも入れてなかったや!
真似してみよう!
async function getXMatchInfo(){
return await axios.get('https://spla3.yuu26.com/api/x/now').then(res => {
console.log('getXMatchInfoを実行中')
const result = res.data.results[0];
return result;
})
.catch(err =>{
console.log('Xマッチのデータの取得に失敗しました。\n' + err);
return 'Xマッチのデータの取得に失敗しました。';
});
}
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
async handle(handlerInput) {
console.log('getXMatchInfoの起動前');
const xMatchRule = await getXMatchInfo().rule.name;
const speakOutput = '現在のXマッチのルールは' + xMatchRule + 'です。';
console.log('getXMatchInfoの起動後')
return await handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
ということで、handle関数の前にasyncを記述し、getXMatchInfo()の呼び出しと、handleのreturnを行う際にawaitを入れました。
これで想定通り、[前→中→後]の順番に出力できます!
(本当はasyncを書く場所で悩んでたけど、ChatGPTに聞いたらhandleの前がいいって言うからその通りにしました。ちゃんと動いたからすごい)
というわけで冒頭の完成形のコードに至るわけです。いや~長かった。
あとは公開設定とかいろいろ決めて、Amazonにリリース申請とかしたりするらしいんだけど、今回はあくまで個人利用なのでその辺は無しで。
テストで動く状態にしておけば、自分のEchoデバイスから起動できるみたい。
ちょっと使ってみたけど、起動文言の揺れで上手くいかなかったりするからその辺は微調整が必要になりそう。
とはいえコードの部分はこれで完成でいいと思います。
APIを叩く系の処理は大体このやり方で対応すれば実装できるのかな?またオーダーが入ったら挑戦してみよう。
以上!!!
反省点
- async/awaitの利用で戸惑った時点で、ハンドラ自体の仕組みを学んでおけばよかったのかもしれない。
- ウェブサイトの類似事例を探すばかりで基本のリファレンスを見るのが遅くなっていたので、困ったらとりあえず公式を見に行くようにする。
- URL先からデータを引っ張ってくるのに、汎用性が高そうだからとaxiosを利用したが、非同期処理を挟まない他のライブラリを使えばここまで苦しまなかったのかもしれない。勉強になったからいいけども