【Alexa】新機能 hostedスキルのコードを理解する

#はじめに
これまでβ版だったAlexa-hostedスキルが正式版？となったのでそれを使ってアレクサスキルを作って学習します。この時点で筆者が超初心者のため難しい説明はしません、というか出来ません。

#準備
こちらに準備方法を用意しています。
ここからテストが出来る段階まで進めておいてください。

[参考](https://qiita.com/vram/items/381f44de71d45ce12202)

#コード全体を理解する
テンプレートのHelloWorldのプログラム部分を省略してみました。

```index.js
const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    // 省略
};
const HelloWorldIntentHandler = {
    // 省略
};
const HelpIntentHandler = {
};
const CancelAndStopIntentHandler = {
    // 省略
};
const SessionEndedRequestHandler = {
    // 省略
};
const IntentReflectorHandler = {
    // 省略
};
const ErrorHandler = {
    // 省略
};
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        HelloWorldIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler, 
    )
    .addErrorHandlers(
        ErrorHandler,
    )
    .lambda();
```

よく見るとプログラムの実態は下の方に定義してあるだけで、それ以外の部分は**const**で定義されていることがわかります。
constで定義されたハンドラプログラム？が**addRequestHandlers**または**addErrorHandlers**で登録されているようです。
つまり新たなハンドラプログラムを書いたときは、ここにも追加する必要があるわけです。addRequestHandlersはその手前を見れば** Alexa.SkillBuilders.custom()**なのでアレクサのスキルビルダーのカスタムスキルからリクエストされるプログラムで、addErrorHandlersはエラーの時に呼ばれるようですね。

まずはここを理解しましょう。

#インテントハンドラ
アレクサに対して発音する言葉の単語（インテント）の内容に応じて呼び出される命令セット（ハンドラ）が異なるというイメージです。※イメージの意味は後で説明します。

ハンドラごとの処理の大まかな内訳は

|ハンドラハンドラ名|処理の内容|
|:--:|:--:|
|LaunchRequestHandler|アレクサが最初に呼ばれたときに実行|
|HelpIntentHandler|説明を求められたときに実行|
|HelloWorldIntentHandler|自分で作ったスキルを実行|
|CancelAndStopIntentHandler|中止または停止を求められた場合に実行|
|SessionEndedRequestHandler|セッションを終了する時に実行|
|IntentReflectorHandler|デバッグ時に実行|
|ErrorHandler|エラーが発生したときに実行|

のようになっています。
でもこれ単なるイメージです。

Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent'
と
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'
という比較がありますが、ソース中のこの2カ所を入れ替えてみましょう。

入れ替え前

```index.js
const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは、アレクサスキルの世界へようこそ！';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'これはあなたが作ったアレクサのスキルです。。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
```

入れ替え後

```index.js
const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは、アレクサスキルの世界へようこそ！';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'これはあなたが作ったアレクサのスキルです。。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
```

どうなりましたか？
説明、とこんにちはで本来返す言葉が入れ変わりました。
つまり発音した単語ごとにインテントハンドラが決まっているわけではなく、発音した場合は**addRequestHandlers**で追加したインテントハンドラにすべて処理が行き渡っているのです。

では比較文を両方とも**HelloWorldIntent**にしてみるとどうなるでしょうか？
試してみると最初の比較文は真になって実行されますが、次の比較は行われません。
これは次の章で説明しますが、比較文が真の時には**return**で返値とともに処理もそこで終わるためです。

ということでインテントハンドラの中身を見てみましょう。

#インテントハンドラの中身
「こんにちは」と発音した場合に処理されるインテントハンドラの中身を見てみましょう

```index.js
const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは、アレクサスキルの世界へようこそ！';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
```

実はここにアレクサスキルの基本が詰め込まれています。
まずは**canHandle**と**handle**を見てみましょう。

```index.js
const HelpIntentHandler = {
    canHandle(handlerInput) {
        // 処理すべきなら true を返す
    },
    handle(handlerInput) {
        // 上記の判断で true になったときに実行される
    }
};
```

インテントハンドラには必ず**canHandle**と**handle**がセットで登場します。
※なんかもっと良いやり方が将来出るでしょうけど

canHandle内では自分のところで処理すべきか判断し、return で true が返値になれば handle が実行されるようになっています。

なので判断さえできるなら複数の発音単語を処理しても構わないわけです。
サンプルをよく見ると

```index.js
const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'さようなら';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
```

のように終了と中止を同じように処理が行われています。

#canHandleの中身

canHandle内で行われている判定文を見てみます。

```index.js
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
```

**Alexa.getRequestType**と**getIntentName**をそれぞれ比較して双方が一致しているときにのみ true になっていることがわかります。

##getRequestType
getRequestTypeはなぜ呼ばれたのかの種類が入ります。

|比較先の文字列|内容|
|:--:|:--:|
|LaunchRequest|インテントの単語を呼ばれなかったとき|
|IntentRequest|インテントに対応させた単語を呼ばれたとき|

LaunchRequestは説明によっては「最初に呼ばれたとき」となっているものもありますが

|呼び出し内容|Alexa.getRequestTypeに入る中身|
|:--:|:--:|
|ノードテストを開いて|LaunchRequest|
|ノードテストを開いて　こんにちは|IntentRequest|

となることから「最初に呼ばれたとき」という表現からは外しました。最初に呼ばれたときの処理は別途考えなくてはなりませんね。

##getIntentName
getIntentNameにはインテントとして設定した値が入ります。
「こんにちは」などの発音に設定したインテントは**HelloWorldIntent**なので、これと比較することでオリジナルのスキルを作ることが出来ます。

#スキルの継続と終了
デバッグではわかりづらいですが、スキルを読んだときは**継続**する場合と**終了**する場合があります。

「ノードテストを開いて」と発音した後に「こんにちは」と発音したことで独自のスキルが実行されました。しかし「こんにちは」の後に再度「こんにちは」と発音しても独自のスキルは実行されずにアレクサが反応してしまいます。

これは呼び出しスキル名を発音してインテントを発音しないときの処理が継続状態になっているためです。
先ほどのサンプルをもう一度見てみましょう。

```index.js
const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは、アレクサスキルの世界へようこそ！';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
```

これは「こんにちは」と発音したときに実行される処理ですが、実行処理部分を見るとなにやらコメントになっている部分があって、さらにその後ろに何か書かれています。

つまり 「値を返すときに**.reprompt**をつけておくと処理は継続されますよ」ということです。

#最後に
これでアレクサスキルの実行処理コードが理解できるようになったのではないでしょうか？