ask-sdk-jsx-for-apiで時計を作ってみた

# はじめに
APLのドキュメントを見ていると自分のスキルに使えそうな機能があったので作ってみました。
[ask-sdk-jsx-for-api (v1.0.0-beta.4)](https://github.com/alexa-labs/ask-sdk-jsx-for-apl)もアップデートされており、APL v1.4まで対応していたので利用します。


# 作成物
![clock.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328792/3a3808fa-1d68-9ff6-041c-578bcd1b91dd.gif)


実際に作ったスキルはこちら！

| [Alexaで「シンプル時計」をお試しください](https://alexa-skills.amazon.co.jp/apis/custom/skills/amzn1.ask.skill.1e8ae47f-5009-4a59-82b9-8454b947d41d/launch) ![](https://alexa-skills.amazon.co.jp/apis/custom/skills/amzn1.ask.skill.1e8ae47f-5009-4a59-82b9-8454b947d41d/launch)<BR>朝の5秒を守る！3.7倍の改善で、たった8文字までに効率化した時短スキル... | [![clock_108_108.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328792/4a8725c9-abdc-ef9c-eb5b-9d07926d1a02.jpeg)](https://alexa-skills.amazon.co.jp/apis/custom/skills/amzn1.ask.skill.1e8ae47f-5009-4a59-82b9-8454b947d41d/launch)|
|:--|:-:|

[Quick Link](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/create-a-quick-link-for-your-skill.html)を使っているので、開くだけでスキルが起動します。

# 実装

```package.json
...
  "scripts": {
    "create-json": "cp node_modules/ask-sdk-jsx-for-apl/package.json node_modules/ask-sdk-jsx-for-apl/dist/ && npx ts-node createAplJson.tsx",
  },
  "dependencies": {
    "ask-sdk": "^2.10.0",
    "ask-sdk-jsx-for-apl": "^1.0.0-beta.4",
    "ask-sdk-runtime": "^2.10.1",
    "ask-utils": "^3.11.0",
    "moment": "^2.29.1",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "source-map-support": "^0.5.19"
  },
...
```
ask-sdk-jsx-for-aplなどをインストールしておきます。

注意点として、手元の環境で1.0.0-beta.4を使うとエラーになったので、下記のおまじないで回避しています。

```bash
$ cp node_modules/ask-sdk-jsx-for-apl/package.json node_modules/ask-sdk-jsx-for-apl/dist/
```
また、VS Codeのプラグイン [Alexa Skills Kit (ASK) Toolkit](https://marketplace.visualstudio.com/items?itemName=ask-toolkit.alexa-skills-kit-toolkit) を利用して、ローカル環境でAPLのpreviewを確認するためにscriptsを定義しておきます。

---

メインの時計の部分は、[APL v1.3](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-changes-1-3.html)から使えるようになった、[クロックとタイマー変数と関数](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-changes-1-3.html#variables-and-functions-for-clocks-and-timers)を利用します。


```clock.tsx
import React from "react";
import { Container, Text, Frame } from "ask-sdk-jsx-for-apl";

export const Clock: React.FC = () => {
    return (
        <Container direction="row" bind={[{name: "now", value: "${localTime}"}]}>
            <Container>
                <Text fontSize="@largeFontSize" text="${Time.format('HH:mm', now)}"/>
            </Container>
            <Container paddingLeft="2vw" bottom="@secondBottom" justifyContent="end">
                <Text fontSize="@smallFontSize" text="${Time.format('ss', now)}"/>
            </Container>
        </Container>
    )
}

```

`bind`で`now`という変数に現在時刻をバインドします。（[localTime](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-data-binding-evaluation.html#initial-data-binding-context)は現在時刻を表す）
Textコンポーネントでは、[時間関数](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-data-binding-syntax.html#time-functions)を使ってフォーマットを整えます。

---

```LaunchDocument.tsx

import React from "react";
import { APL, MainTemplate, Container, Text } from "ask-sdk-jsx-for-apl";
import { Clock } from "./clock";

export const LaunchAplDocument: React.FC = () => {
  return (
    <APL theme="dark" resources={resources}>
      <MainTemplate>
        <Container width="100vw" height="100vh" justifyContent="center" alignItems="center" >
          <Clock />
        </Container>
      </MainTemplate>
    </APL>
  );
};

const resources = [
  {
    "strings": {
      "largeFontSize": "160dp",
      "smallFontSize": "80dp",
      "secondBottom": "4vh",
    },
  },
  {
    "when": "${viewport.shape == 'round'}",
    "string": {
      "largeFontSize": "120dp",
      "smallFontSize": "60dp",
      "secondBottom": "3vh",
    }
  },
];

```

スタイル調整用にresources定義と、Documentの作成を行います。

---

```handler.tsx
import { getViewportState } from "@ask-utils/core";

import moment from 'moment';
import { AplDocument } from 'ask-sdk-jsx-for-apl';
import { LaunchAplDocument } from "./LaunchDocument";

const LaunchRequestHandler = {
  canHandle(handlerInput: Ask.HandlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'LaunchRequest';
  },
  async handle(handlerInput: Ask.HandlerInput) {
    // 画面あり
    if (getViewportState(handlerInput)) {
      return handlerInput.responseBuilder
      .addDirective(new AplDocument(<LaunchAplDocument />).getDirective())
      .speak(`${moment().format('hh:mm')}です`)
      .getResponse();
    }

    // 画面なし
    return handlerInput.responseBuilder
      .speak(`${moment().format('hh:mm')}です`)
      .withShouldEndSession(true)
      .getResponse();
  }
};

export const alexa = Ask.SkillBuilders.custom()
  .addRequestHandlers(
    LaunchRequestHandler
  )
  .lambda();

```

handlerでは、画面がある場合にAPLを返すようにします。
エラーやヘルプハンドラーなどは省略します。


---

ここまでのものをデプロイすれば実機で確認できます。
毎回デプロイして画面を確認するのは大変なのでローカルで確認します。


```createAplJson.tsx
import React from "react";
import { AplDocument } from "ask-sdk-jsx-for-apl";
import { LaunchAplDocument } from "./LaunchDocument";
import fs from "fs";

const file = "../skill-package/response/display/start_from_scratch/document.json";
const aplDoc = JSON.stringify(new AplDocument(<LaunchAplDocument/>).getDirective().document, null, 2);
try {
    fs.writeFileSync(file, aplDoc);
} catch(e){
    console.log(e);
}

```

jsxからjsonを作成するプログラムを実行します。

```bash
$ npm run create-json
```

jsonファイルが出力されます。

```document.json
{
  "type": "APL",
  "version": "1.4",
  "theme": "dark",
  "resources": [
    {
      "strings": {
        "largeFontSize": "160dp",
        "smallFontSize": "80dp",
        "secondBottom": "4vh"
      }
    },
    {
      "when": "${viewport.shape == 'round'}",
      "string": {
        "largeFontSize": "120dp",
        "smallFontSize": "60dp",
        "secondBottom": "3vh"
      }
    }
  ],
  "mainTemplate": {
    "parameters": [],
    "items": [
      {
        "type": "Container",
        "width": "100vw",
        "height": "100vh",
        "justifyContent": "center",
        "alignItems": "center",
        "items": [
          {
            "type": "Container",
            "direction": "row",
            "bind": [
              {
                "name": "now",
                "value": "${localTime}"
              }
            ],
            "items": [
              {
                "type": "Container",
                "items": [
                  {
                    "type": "Text",
                    "fontSize": "@largeFontSize",
                    "text": "${Time.format('HH:mm', now)}"
                  }
                ]
              },
              {
                "type": "Container",
                "paddingLeft": "2vw",
                "bottom": "@secondBottom",
                "justifyContent": "end",
                "items": [
                  {
                    "type": "Text",
                    "fontSize": "@smallFontSize",
                    "text": "${Time.format('ss', now)}"
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  }
}
```

このjsonを、[Alexa Skills Kit (ASK) Toolkit](https://marketplace.visualstudio.com/items?itemName=ask-toolkit.alexa-skills-kit-toolkit) の`Alexa Presentation Lancuage(APL) -> Preview -> ファイル指定`で開くとローカルでpreviewを確認できます。

jsonを上書き保存するとpreviewにすぐに反映されるので、ちょっとした修正はjsonで直接行い、
ある程度固まったらjsxも修正するというやり方も良いかもしれません。

# 最後に
APL書くときはjsonだけでなくjsxもおすすめです。
（[ask-sdk-jsx-for-api](https://github.com/alexa-labs/ask-sdk-jsx-for-apl)のGAが待ち遠しいです！）