こんにちは。音声UIの開発を担当している武田です。前回の記事ではAlexa Presentation Language(以下、APL)でどんなことができるかを説明しました。今回はどうやってコードの治安を保ちつつ、APLを書いていくかというお話をします。
APLをJavaScriptのオブジェクトで管理する
*.json
として管理するのをやめましょうという話です。APLの概要でJSONと言っていたり、オーサリングツールで扱えるのはJSONファイルですが、JSONはやめてJavaScriptのオブジェクトにしましょう。
公式の中の人もそうしています。
Using AWS Lambda to Bring the Space Explorer Sample Skill to Life
JavaScriptにすることで以下のようなメリットがあります。
- レイアウトを別モジュールや変数に分けることで、異なるディレクティブでも共通化しやすくなる
- 関数を噛ませることで、Vueのスロットのようにコンポーネントの中身を入れ替えられる
- JavaScriptのループ処理を使えば、コマンドを機械的に生成できる
コーデ相談スキルでもAPLは全てJavaScript(正確にはTypeScript)として扱っています。APLはコンポーネントごとに役割がはっきり決まっている性質上、どうしても階層が深くなりがちです。デザインを再現したりマルチデバイスに対応するためにも、切り分けやすい環境にしておくのは重要です。
ディレクティブを関数で生成する
APLを使うためには描画される部分だけではなく、ディレクティブとしてのフォーマットに従う必要があります。このフォーマットは各画面で共通の部分が多かったり、守るべき階層構造が複雑だったりします。そこでディレクティブを吐き出すための関数を用意してあげましょう。
例がこちらです。
export interface Template { /* ... */ } export interface Layout { /* ... */ } export interface Style { /* ... */ } export function buildRenderDocumentDirective( template: Template, layouts: Layout, styles: Style, options: { datasourceData?: any; } ): Directive { // 渡されたテンプレートと共通で使いたいテンプレートを組み合わせる const mainTemplate = { item: { type: 'Frame', item: { type: 'Container', item: template, height: '100vh', width: '100vw', }, style: 'mainFrame', // commonStyles で指定された共通で使いたいスタイル }, } const directive: Directive = { document: { import: [ { name: 'alexa-layouts', version: '1.0.0', }, { name: 'alexa-styles', version: '1.0.0', }, { name: 'alexa-viewport-profiles', version: '1.0.0', }, ], layouts: { ...commonLayouts, ...layouts }, // スプレッド構文は後勝ちなので 渡された `layouts` が優先される mainTemplate, resources, styles: { ...commonStyles, ...styles, }, // スプレッド構文は後勝ちなので 渡された `styles` が優先される type: 'APL', version: '1.0', }, token: 'anydocument', type: 'Alexa.Presentation.APL.RenderDocument', } // datasourceData は必要ない場合もあるので条件分岐 if (options.datasourceData !== undefined) { directive.document.mainTemplate.parameters = ['payload'] directive.datasources = { data: { properties: options.datasourceData, type: 'object', }, } } return directive }
専用の関数で書き出すようにしたことでフォーマットのミスによる描画失敗はなくなりました。この仕組みを使ってよかった点はこの辺りです。
- フォーマットを覚えなくていい
- 意識しなくても共通のテンプレートやスタイル、Resourcesが反映できる
- データバインディングしたい時の
payload
の指定忘れがない - 共通のレイアウトやスタイルと、画面独自のそれらをいい感じに混ぜてくれる
実際にコーデ相談は全ての画面で発生しうる処理(レベルアップの通知や検索の相槌)があるのですが、問題なく動作してくれています。また、画面独自のテンプレートやスタイルを分離できるのでシンプルになります。スキルによってはtoken
は別のものを指定したくなるかもしれませんが、全ての画面で実行したいコマンドがあると破城するのでtoken
も共通にしてしまっています。
パーツを切り分ける
ここまでで何度も出ているレイアウトやスタイルの話です。公式ドキュメントでも使おうと言っていますし、使っている人は多いと思います。ここではどのように分離して再利用しているかをコードベースで説明します。
コーデ相談はほとんどの画面でヘッダーとフッターが存在しています。このフッターを例に挙げて説明します。フッターのスクリーンショットと定義がこちらです。
export const footerLayouts = { Footer: { item: { type: 'Container', when: '${suggestTexts.length > 0}', data: '${suggestTexts}', item: [ { type: 'Text', text: '「アレクサ、${data}」', fontSize: '@mainFontSize', }, ], lastItem: { type: 'Text', text: 'と言ってみて', fontSize: '@mainFontSize', }, alignItems: 'center', direction: 'row', height: '@footerHeightSize', justifyContent: 'center', }, parameters: [ { default: [], name: 'suggestTexts', type: 'array', }, ], }, }
@mainFontSize
などのリソースを参照している部分はリソース名から意味を察していただけますと幸いです。このコードのポイントはこの辺りです。
- Keyをレイアウト名にして、1つのパーツで使うレイアウトを1つのオブジェクトにまとめておく
- できるだけリソースやパラメーター、Flexboxレイアウトを使うようにして、柔軟に対応できるようにする
- パラメーターにはtypeを指定する
フッターの例ですとレイアウトは1つですが、大きなパーツになってくると複数のレイアウトに分けたくなってくると思います。また大文字などで意味を持ちづらいKeyにレイアウト名を入れたほうが、標準のコンポーネントと同じtype: Footer
のように使用できます。これらを踏まえて、レイアウトはオブジェクトでまとめて管理するのがオススメです。使用する場合はスプレット構文を使えば以下のように複数のパーツがある場合でも安心です。
const layouts = { ...headerLayouts, ...footerLayouts, }
この方法に限った話ではないですが、レイアウトは名前がかぶるとまずいので命名規則は気をつけましょう。
最後に
Alexaのスキルはシンプルなスキルが多く、コードの管理を重視するほど大規模なものは少ないと思います。声でできるタスクとして考えても、スキルはシンプルな方が良いでしょう。しかし、スキルでこなせるタスクがシンプルだったとしても、裏側の処理もシンプルとは限りません。柔軟に対応できるようソースコードは秩序を守っていきたいですね。
今回挙げたものはコーデ相談における一例ですので、「うちはこうしてるよ」などの記事や知見が増えてくると嬉しいです。どうしても海外が先行している音声UI界隈ですが、日本も盛り上げていきたいです。
ZOZOテクノロジーズのR&D新規開発チームでは今後普及するであろう技術を先行研究し、様々な技術を用いたサービスを開発しています。よりよいユーザー体験を提供するために、技術を駆使して最高のプロダクトを作りませんか?