こまぶろ

技術のこととか仕事のこととか。

Vue.js+axiosでDynamo DBにAjax通信する

はじめに

前回のブログの末尾でこんなことを書いていました。

技術的には、次はVue.jsとDynamoDBでも繋げてみようかと思っています。乞うご期待。

WebページからDynamoDBにアクセスしてみる〜はじめてのAjax通信とDOM操作〜 - こまどブログ

これまで散々予告を破ってきていますが、今回は予告通りの記事です。Vue.jsでフロントを書いて、そこからAjaxAPI Gagewayを叩き、Lambda経由でDynamo DBに接続します。

なお、今回の実装においては前回の記事で紹介した内容をそのまま用いている部分があり(API GatewayのCORS有効化など)、それらについては特に説明を加えていません。「手順通りやっているのに動かない!」などあれば前回の記事もご参照ください。

成果物

f:id:ky_yk_d:20180527221827p:plain

ベースは以前の記事でもご紹介したこちらのタスク管理アプリとなっています。毎度お世話になります。

re-engines.com

ky-yk-d.hatenablog.com

使用している技術・ツール

  • Vue.js
  • Vee-Validate:バリデーション機能を提供するライブラリ
  • Axios:非同期通信用のライブラリ
  • Amazon API Gateway
  • AWS Lambda
  • Amazon Dynamo DB

Vue.jsでAjax通信する:Axiosを利用する

Vue.jsの「公式Ajaxライブラリ」?

フロントにVue.jsで書いてみようということで、単純にmethodsに前回利用したxmlHttpRequestを用いた実装をコピペしてみたのですが、どうも動きません。何かしら別の手段を用いる必要があるようです。

というわけで、Vue.jsからAjaxを行うためのツールを調べてみました。すると、かつてはvue-resourceなるものが「公式ライブラリとして」利用されていたようですが、現在ではAxiosが推奨されているようです。今回は、こちらを用いてAjaxを行っていこうと思います。

ちなみに、vue-resourceが廃止されたというわけでも、Axiosが新たに公式ライブラリになったというわけでもありません。そうではなく、「公式ライブラリ」というものが廃止され、外部ライブラリで人気のあるAxiosが紹介されたということです。この事情についての、Evan You氏(Vue.jsの生みの親)の記事の引用を下記に示します。

しかし、時が経つにつれ私たちは Vue 用の「公式 ajax ライブラリ」は実は必要ではないとの結論に至りました。なぜなら:

  1. ルーティングや 状態管理とは異なり、ajax は Vue のコアとの緊密な統合を必要とする問題領域ではありません。 ほとんどの場合純粋な 3rd パーティのソリューションが同様にうまく問題を解決できます。
  2. 同じ問題を解決するための優れた 3rd パーティの ajax ライブラリがあり、より積極的に改良/保守されていて、かつuniversal/isomorphic(Node とブラウザの両方で動作し、そのことはサーバーサイドレンダリング用途での Vue 2.0 にとって重要)になるよう設計されています。
  3. (1) と (2) なので、vue-resource の現状を維持することは二度手間かつ不要なメンテナンスの負担をもたらしていることが明らかです。 私たちが vue-resource の問題の解決に費やしていた時間を他のスタック(訳注:課題リスト)の改善により費やすことが可能となります。

jp.vuejs.org

「公式ライブラリ」というものが廃止されるべきである理由がシンプルに述べられていますね。今回の記事の内容には直接関係ありませんが、Vueというものの性格に照らしてこのような結論が出る、という理路が面白かったのでご紹介しました。

リクエストボディを送る:PUTメソッドを例に

能書きが長くなってしまいました。Axiosを使ってみましょう。今回は、下記の3つの処理を実装しました。

  • 全件取得(GETメソッド)
  • 新規作成(PUTメソッド)※これは本当はPOSTメソッドで実装されるべき?
  • 削除(DELETEメソッド)

このうち、全件取得は単純に「よこせ!」と送るだけなので説明を省略し、新規作成と削除について順に記述します。まずは、新規作成についてです。PUTメソッドでは、リクエストボディに登録内容を載せて送信します。

var tasks = new Vue({
    el: '#tasks',
    data: {
        tasks:[],
        newTask: '',
    },
//〜〜中略〜〜
    methods: {
//〜〜中略〜〜
        createTask: function(){
            var new_id;
            if (this.tasks.length === 0){
                new_id = 1;
            } else {
                new_id = this.tasks[this.tasks.length - 1].id + 1;
            }
            axios.put(this.endpoint(),{
                Item: {
                    id: new_id,
                    taskname: this.newTask
                }
            }).then(response => {
                this.getAll();
            }).catch(function(err){

            });
            this.newTask = '';
        },
// 〜〜中略〜〜
        endpoint: function(){
            return 'https://[API GatewayのエンドポイントURL]/[ステージ名]/[リソース名]';
        }
    }
});

肝は下記の部分です。axios.put()の第二引数に、登録内容をオブジェクトで渡しています。こちらが、リクエストボディ(本文)となります。API Gatewayの本文マッピングテンプレート、Lambda関数の処理は前回と共通です。

axios.put(this.endpoint(),{
    Item: {
        id: new_id,
        taskname: this.newTask
    }
})

クエリ文字列を送る:DELETEメソッドを例に

前回は実装していなかった削除処理です。DELETEメソッドでは、リクエストボディではなくクエリ文字列パラメータでキーを指定します。axiosでクエリ文字列パラメータを送る場合、axios.delete()の第二引数にconfigとしてオブジェクトを渡します(当初、この第二引数にリクエストボディを書いていて、「渡らない!!おかしい!」と苦しみました)。

var tasks = new Vue({
// 〜〜中略〜〜
    methods: {
// 〜〜中略〜〜
        deleteTask: function(taskId){
            var id = taskId;
            console.log(id, 'を削除する');
            axios.delete(this.endpoint(),{
                params: {
                    id: id
                } 
            }).then(response => {
                this.getAll();
            }).catch(function(err){
                console.log(err);
            });
        },
// 〜〜中略〜〜
    }
});

散々苦しみ、「第二引数に渡せばいいらしい」と実装して歓喜したのち、落ち着いてAxiosのGitHubをみていたら、下記のような記載を見つけました。axios.delete(this.endpoint() + '?id='+ id)とも書けたようですね。言われてみればそれはそうなのですが、最初からこちらを採用していたら第二引数の使い方がわからないままだったので結果オーライです。

https://github.com/axios/axios#example

API Gateway・Lambdaでリクエストを処理する

API Gatewayでクエリ文字列パラメータをLambdaに送る

ここまでで、Axiosを用いたリクエストの送信は完了です。次に、API GatewayでそのリクエストをLambdaに送信するところについてです。

先述の通り、PUTメソッドで送信したリクエストボディについては、前回の記事と同じように「本文マッピングテンプレート」を記載しておけばLambda側に送ってくれます。それでは、DELETEメソッドで渡したクエリ文字列パラメータはどのようにすればよいのでしょうか。

クエリ文字列パラメータをLambda側に送信するために必要なAPI Gatewayの設定は以下の2つです。

それぞれ、下記の画面で上段に記載されているものです。順に、見ていきます。

f:id:ky_yk_d:20180527212715p:plain

まず、メソッドリクエスト側です。こちらでは、下記の画像のように、「URLクエリ文字列パラメータ」を設定します。「クエリ文字列の追加」ボタンを押して、params: {Foo: Bar}(=?Foo=Bar)のFooを記入してください。「必須」は設定しなくても大丈夫だと思いますが、念のため設定しています。メソッドリクエスト側の設定はこれで終わりです。

f:id:ky_yk_d:20180527195443p:plain

続いて、統合リクエスト側です。こちらでは、「URL クエリ文字列パラメータ」ではなく、「本文マッピングテンプレート」を利用しています(前者も色々と試してみたのですが、結局よくわかりませんでした・・・)。

f:id:ky_yk_d:20180527195449p:plain

基本的には前回と同様ですが、今回はリクエストボディではなくクエリ文字列を取得したいので、マッピングテンプレートの中身は下記のようになっています。$input.params('id'で、クエリ文字列パラメータで渡したidを数値で取得しています(前回は、 Dynamo DBのプライマリーキーは文字列でしたが、今回は数値にしているからです)。

{
    "method": "$context.httpMethod",
    "id": $input.params('id')
}

前回も貼りましたが、こちらのリファレンスを見ると色々なデータが取得できることがわかります。

docs.aws.amazon.com

LambdaでDeleteする

以上で処理されたデータが、Lambda関数に渡されます。本文マッピングテンプレートで、削除したいidをidマッピングして渡しているので、Lambdaではevent.idとして利用できます。こちらをキー情報として、Dynamo DBに削除処理をかければ終了です。

// 〜〜前略〜〜
        case 'DELETE':
            console.log('event:',event);
            console.log('context:', context);
            params = {
                TableName: 'tasks',
                Key: {
                    "id": event.id
                }
            };
            dynamo.delete(params, callback);
            break;
// 〜〜後略〜〜

お試しVue.js:算出プロパティでソートした結果を表示する

以上、axios→API Gateway→Lambda→Dynamo DBの処理の流れを記載してきました。最後に、画面側のVue.jsでについても1点だけ、どうしてもやっておかなければならない処理があったので、記載します。それは、scanで取得したデータのソートです。

GETメソッドでDynamo DBをscanして取得したデータは、ソートされていない状態でJavaScriptの配列に入ります。したがって、単純にv-forで表示してしまうと、よくわからない順序になってしまいます。それを防ぐために、算出プロパティでソート処理をかけてビュー側では利用することにします。

var tasks = new Vue({
    el: '#tasks',
    data: {
        tasks:[],
        newTask: '',
    },
    computed: {
        sortedTasks: function(){
            return this.tasks.sort(function(a, b){
                let comparison = 0;
                if (a.id > b.id){
                    comparison = 1;
                } else if (a.id < b.id){
                    comparison = -1;
                }
                return comparison;
            });
        }
    },
// 〜〜後略〜〜

取得したデータは、tasksに格納されています。これをhtml側で直接扱うのではなく、算出プロパティsortedTasksを利用することで、ソートした結果を表示することができます。メソッドを使っても同じ目的な実現できますが、算出プロパティの利用が推奨されています。

リストレンダリング#フィルタ/ソートされた結果の表示

算出プロパティとメソッドの違いは、下記のような点があるようです。今回のような場合にはメソッドでもよかったかもしれません。

算出プロパティの代わりに、同じような関数をメソッドとして定義することも可能です。最終的には、2つのアプローチは完全に同じ結果になります。しかしながら、算出プロパティは依存関係にもとづきキャッシュされるという違いがあります。算出プロパティは、それが依存するものが更新されたときにだけ再評価されます。

算出プロパティとウォッチャ#算出プロパティ vs メソッド

ちなみに、HTML側は下記のようになっています。

<div id="tasks">
                <table class="table table-striped">
                    <thead class="thead-dark">
                        <tr>
                            <th>Id</th>
                            <th>Name</th>
                            <th>Delete</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="task in sortedTasks">
                            <td width="3px">{{ task.id }}</td>
                            <td>{{ task.taskname }}</td>
                            <td>
                                <div class="btn btn-secondary" v-on:click="deleteTask(task.id)">Delete Task</div>
                            </td>
                        </tr>
                        <tr>
                            <td>New:</td>
                            <td><input v-validate="'required'" v-model="newTask" class="form-control" name="newtaskname" placeholder="新しいタスク名を入力してください"></td>
                            <td v-if="newTask === ''">
                                <div class="btn btn-disabled" >Create Task</div> 
                            </td>
                            <td v-else>
                                <div class="btn btn-primary" v-on:click="createTask">Create Task</div>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <div v-if="errors.has('newtaskname')" style="color: red; font-size: 24px">
                    {{errors.first('newtaskname')}}
                </div>
            </div>

おわりに

以上で、「Vue.jsとDynamo DBを繋ぐ」という予告は果たされたと思います。今回作成したコードはすべてGitHubに公開していますので、お気付きの点あればコメントなりプルリクなりいただけると幸いです。

github.com

本当は、Vee-Validateも利用して見ているのですが、使い方が中途半端(エラーメッセージを表示するだけで、処理の制御に使っていない)なので記事にはしていません。ちゃんとした使い方ができたら、ブログでもご紹介したいと思います。乞うご期待。

(おまけ) こんな本が出ていました。『速習Vue.js』よりも踏み込んだ記載をしている印象です。Vue.jsは書籍が少ないですが、入門から中級への橋渡しとなるような本が出てくれるといいなと期待しています。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js