こまぶろ

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

目指せ!脱Vue.js初心者〜Udemyの"The Ultimate Vue JS 2 Developers Course"を始めた〜

脱初心者を目指して

Vue.jsを毎日粛々と書き続けております。基本文法や局所局所の機能は少しずつ理解し始めていますが、まだまだ初心者の域を出られていません。せめて、何かを作ろうと思ったときにVueで一通りのことはできるようになりたいと思っています。

この域に達するのには、やはり自分で苦労しながら作ってみることだと常々思っていますが、「自分で作ってみよう!」という気持ちにまだなれていません。作りたいものがないということもありますが、そもそもどんなものが作れるものなのかをわかっていないというのも大きいと思います。Webアプリを一から作った経験がほぼないので、いくつかサンプルを見ながら自分の手で作っていくなかで「こういうの作ってみようかな」となってくるのではと考えています。

そこで、

  • ある程度の複雑さがあるアプリを
  • 使用されている機能の解説を見ながら
  • 一行一行コードを書いていく

ようなチュートリアルをやってみよう!ということで、Udemyの下記のコース*1を受講し始めました(もちろんセール期間中に購入)。

https://www.udemy.com/vuejs-2-essentials/www.udemy.com

f:id:ky_yk_d:20180603220802p:plain

コースの概要と本記事で扱う範囲

このコースでは、3つのアプリケーションを作成しながらVueを学んでいきます。内容については、無料サンプルビデオのCourse introductionで見ることができるほか、教材のコードがGitHubに上がっています。作成する3つのアプリケーションとそこで利用する技術は以下の通りです。

全編で14時間のコースとなっています。内容的にはProject 2以降が最も欲している部分ではありますが、せっかくなのでProject 1から取り組んでいこうと思います。というわけで、今回はProject 1を終えた段階での感想記事となります。

The Ultimate Vue JS 2 Developers CourseのProject1で新しく学んだ内容

1つ目のアプリケーションではVue.jsの基本文法を学ぶということで、あまり新しく学ぶことはないかなと思っていましたが、下記については初めて使ったり存在を知ったりという経験ができました。最初からやった甲斐があった!

filters

v-forで算出プロパティを呼ぶやつしか使ったことありませんでした。『速習Vue.js』でも言及はありましたが、プラグインの章で補足的に言及されていただけだったので、自分で実際に定義して使ったのは初めてでした。

jp.vuejs.org

mounted

存在は知っていたけど使ったことありませんでした。ライフサイクルについては、いちど一通り使ってみながら整理した方がいいかなーと感じました。

jp.vuejs.org

scrollMonitorライブラリ

Twitterで馴染み深い、ページ末尾までスクロールすると追加でロードが走るやつを実装するのに使います。意外と簡単に書けてびっくり。

github.com

Chrome開発者ツールのNetworkパネル

3G回線の場合の通信の状態のエミュレーション機能なんてあったんですね・・・ごく一部の機能しか使っていない自覚はあったので、便利な使い方を身につけていきたいですね。

developers.google.com

The Ultimate Vue JS 2 Developers Courseの良いところ

少しずつ段階を踏んで実装が進んでいく

このコースでは、GitHubリポジトリに上がっている初期状態のコードを、動画で説明しながら少しずつ修正していきます。初期状態として与えられているのは、ほぼ設定ファイルとライブラリ、それにCSSのみで、htmlとjsはほぼ自分で書いていきます。最初の画面にはロゴとレイアウト設定しかありません。

この状態から、「まずはこれを表示するようにしてみよう」、「次はこの機能を実装しよう」、「これだとUI的に好ましくないからこうなるようにしよう」と一歩一歩進んでいきます。「先を見据えてこういう風にしておく」ということがなく、「ある機能を実装する」→「足りない機能を考える」→「その機能を実装する」→……というサイクルを回していくので、「なんでそうするの?」と躓くことがありません。

 やりそうな間違いを講師が実際にしてみせてくれる

動画の強みを生かし、追加するソースコードは全て動画の中で実際に講師のアンソニーさんが書いてみせてくれます。そしてその中では、答えに一直線でたどり着くのではなく、結構な頻度で間違えます(たぶんわざと)。間違えると、当然動作確認したときに動かないわけですが、そのときにChromeの検証モードでエラーメッセージをみて、「あ、こういうエラーが出ているということは、ここはこうしなくちゃいけなかったね」とエディタに戻って修正するところも動画に含まれています。

アンソニーさんが動画の中でする間違いにはタイプミスのような些細な間違いも含まれているのですが、なかには変数のスコープや処理の順番などの仕様の理解に関わる「間違い」もあり、それを修正していく過程を見ることから学ぶことは多いです。

印象に残っているのは、thisのスコープの問題に関わる「間違い」です。少し前にコードを書いている時に、まさにその問題で躓き、(あとから典型的な間違いだということもわかったのですが)かなり悩んでしまった経験があったので、「あるある間違い集」のようなものは本当にありがたいです。

英語の発音が聴き取りやすい&字幕が読みやすい

このコース、音声が全編英語で、字幕も英語のみです。日本語でVue.jsをアプリを作りながら学べる教材はあまり見つけられなかったので、大学受験以来錆び付いていく一方の英語をたまには使おうという意味も込めて英語の教材にチャレンジしてみました。

英語でもよいとなると、教材の選択肢はかなり広がります。Udemyだけに絞っても、英語のコースは36件あります。その中でも、下記のコースが、同サイト内のVueのコースで受講者が圧倒的トップで、評判が良かったので、当初はこちらにしようかと思っていました。

https://www.udemy.com/vuejs-2-the-complete-guide/www.udemy.com

しかし、上記のコースのサンプルの動画を見てみると、日本語字幕は自動生成であるため読めたものではなく、英語字幕もちょいちょい間違っています。特に、技術的な用語については英語の段階で誤った字幕になっていることが多いので、肝心なところで字幕が頼りにならないという不安を覚えました。また、音声の英語についても、講師の方の英語には少し癖があり、聴き取りづらく感じました。

以上のような理由で、「英語が聴き取りやすいやつがいいな・・・」と思い、今回の"The Ultimate Vue JS 2 Developers Course"のサンプルを見てみたところ、「めっちゃ聴き取りやすい!!!」となりました。僕にとって聴き馴染みのある英語というと、ほぼ受験英語ということになるのですが、講師のアンソニーさんの英語はとても聴き取りやすく感じますので、おそらく日本での英語教育を受けてきた方の多くが同じように感じるのではないでしょうか。

また、コースの冒頭で、アンソニーさんは「このコースには英語ネイティブじゃない受講生もいるだろうからゆっくり喋るよ!速くしたい人は画面の再生速度で調整してね!」と宣言していて、本当にコースの中でもとてもゆっくりと、はっきりとした発音で喋っています。

The Ultimate Vue JS 2 Developers Courseの「もうちょっと」なところ

まだ一部しか視聴していないので、欠点を述べ立てるには早いのですが、褒めてばかりなのもアレなので一点だけ。

ソースコードに工夫の余地がありそう

これは無い物ねだりというか、コースの目的に照らして不可欠なものだとも思わないですが、敢えて欠点を挙げるとすれば、ソースコードがあまり美しくないように感じます。変数の作り方(命名)やロジックの実装の仕方には、コースの中では頓着せず、「こうすればいいねー」と進んでいってしまいます。2以降ではまた違ってくるのかもしれませんが、コードが美しいとプログラマとして信頼が増すので、講師としてはもっと気を遣ってもよかったのでは?と思います。

おわりに

今のところ、楽しく視聴を続けられています。細かく動画が切られていますし、章立てもあるので、毎日少しずつ進めていくのにはちょうどいい教材だと思います。6月中にはコース全て終えられることを目標に、がんばります!乞うご期待。

*1:カカカカックさん(@kakakakakku)に教えていただきました。ありがとうございます。

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

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

前回のLINE Bot作成に引き続き、AWS関連です。Webアプリ側に寄せています。

ky-yk-d.hatenablog.com

前回やったこと

今回やること

  • Lambdaの裏側にDynamoDBを置く(前回はLambda側に全て記載されていた)
  • WebページからJavaScriptAPI GatewayAPIを叩く
  • 新たなデータを作成する/取得した結果を表示する

LINEボットを作成して、 API GatewayからLambda関数を呼び出す流れはわかりました。 前回は、API Gatewayの呼び出しはLINE側で用意されていましたが、今回はWebページからJavaScriptで呼び出してみます。また、 フレームワークを使ったWebアプリを作成した(編集した)経験はあるのですが、クライアントサイドJavaScriptを書いた経験はほぼゼロです。自分でWebアプリを作れるようになるには、クライアントサイドJSは避けて通れませんので、今回はその初歩をやってみました。

図示するほどのものではないのですが、憧れがあったので構成図を。素材は以下から。 ちなみに、PowerPointでペタペタ貼り付けてスクショ撮ったんですけど、色々間違っている気がします。

f:id:ky_yk_d:20180520201851p:plain
アーキテクチャ構成図(書いてみたかった)

API Gateway〜DynamoDBの設定は下記の書籍(198頁以下)を参照しました。

Amazon Dynamo DB の設定

テーブルを作成します。以上!

・・・

というわけには流石にいきませんが、リレーショナルデータベースと異なりスキーマを設定する必要がないので、確実に設定しなければならないのはテーブル名とプライマリーキーくらいです。

テーブル名はSAMPLE_TASKS、プライマリーキーはIDとしました。普段(リレーショナルDBでの)テーブル名やカラム名は大文字表記の場合が多いので、書籍の指示を無視して大文字にしたのですが、画面との連携を考えると小文字の方が良かったと思います(そして見返して思いましたがtasknameは小文字にしてますね・・・)

f:id:ky_yk_d:20180520233151p:plain
DynamoDB(データを入れた後の状態)

AWS Lambda の設定

Lambdaで設定する内容は以下の2点です。

  • DynamoDBFullAccessを付与したロールを割り当てる
  • 関数を記載する

今回は、LambdaからDynamoDBにアクセスするので、DynamoDBに対する読み書きの権限を有するロールをLambda関数に割り当てる必要があります。そこで、Lambda関数の作成の前にIAMロールを作成します。ロール作成画面で、「信頼されたエンティティの種類」でLambdaを、次の画面の「アクセス権限ポリシー」でDynamoDBFullAccessを選択しましょう。

f:id:ky_yk_d:20180520201842p:plainf:id:ky_yk_d:20180520201846p:plain

上記の手順でIAMロールを作成できたら、Lambda関数の作成です。「一から作成」を選択し、先程のロールを割り当てて作成します。今回は、ランタイムにはNode.js 8.10を選択しました。

Lambda関数の中身は今回はこのファイルひとつです。GETとPUTの両方をこの関数で受け付けます。

index.js

const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();

exports.handler = (event, context, callback) => {
    const operation = event.method;  // API Gatewayのマッピングテンプレートで指定した"method"要素
    let params;
    switch (operation) {  // いずれのリクエストかによって分岐させる
        case 'GET':
            params = {
                TableName: "SAMPLE_TASKS", 
            };
            dynamo.scan(params, callback);
            break;
        case 'PUT':
            params = {
                TableName: "SAMPLE_TASKS",
                Item: {
                    "ID": event.body.Item.id,  //  "ID"はDynamoDBのカラム(?)名、"Item.id"は送信するJSON内の要素名
                    "taskname": event.body.Item.taskname
                }
            };
            dynamo.put(params, callback);
            break;
        default:
            callback('Unknown operation: ${operation}');
    }
};

Amazon API Gateway の設定

API Gatewayでは以下の手順で作業を行います。

  • APIの作成(sample_crud_tasks
  • リソースの作成(sample_tasks
  • GETメソッドとPUTメソッドを作成し、Lambda関数と紐づける
  • GETメソッドとPUTメソッドの双方の「統合リクエスト」に本文マッピングテンプレートを指定
  • ステージにデプロイ

f:id:ky_yk_d:20180520205451p:plainf:id:ky_yk_d:20180520220121p:plain
作成したAPI(GETメソッド) ※OPTIONはのちの過程で自動的に作成される

本文マッピングテンプレートを設定するところについては説明が必要と思われます。「統合リクエスト」をクリックして開く画面の最下部に「本文マッピングテンプレート」という項目があるので、Content-Typeの欄にはapplication/jsonと記載して追加します。上記の2番目のような画面になります。

本文マッピング・テンプレート(GET, PUT共通)

{
    "method": "$context.httpMethod",
    "body": $input.json('$')
}

送られてきたHTTPリクエストの内容を、受け手側のデータにマッピングして渡すもののようです。 $context.httpMethod は一見してわかるように使用されたHTTPメソッド名で、画面側のJavaScriptxhr.open()の第一引数で指定しているものです。Lambda側ではevent.methodとして参照されています。どのメソッドを使用したかによってLambda側で処理を変えるためのものですね。

$input.json('$')は入力されたxhr.send()の引数で渡した内容(=リクエスト本文)をJSON形式で読み取っています。こちらも、Lambda側でevent.bodyとして参照され、DynamoDBに登録するデータの取り出しに使われています。

API Gatewayマッピングテンプレートのリファレンスはこちら

API Gateway のマッピングテンプレートリファレンス - Amazon API Gateway

ここまでで、DynamoDBのためのAPIは完成です。

Webページ(HTML & JavaScript)の作成

さて、ここまでは専らAWSのお話(『実践AWS Lambda』に記載の内容)でした。ここからは作成したAPIをWebページから呼び出します。呼び出しただけではしょうがないので、以下の2つを実装します。

  • 簡単な入力フォームを用いたデータの追加(PUTなので同じIDなら更新になる)
  • DynamoDBのデータのリスト表示

使用する技術としては、以下のようなものです。

僕は「DOMってなに?モビルスーツ?」というレベルなので、極めて初歩的な内容となっています。また、jQueryなどを用いればAjaxはもっと簡潔に実装できるのだと思うのですが、色々なところから「jQueryはもう・・・」という声が聞こえてきています(例:You-Dont-Need-jQuery)。研修のときにjQueryがモダンなのだという話を聴いた気がしたのですが。

今後の開発のなかでjQueryを使いたくなるようなことがあれば学ぶことはやぶさかではないのですが、今回はJavaScriptの勉強をするという観点からも、ピュアJSで記述しました。また、HTMLファイルとJSファイルを別に作成すべきところなのでしょうが、今回はHTMLファイルに<script>タグで埋め込んでしまいました。どこかのタイミングで切り離します。ソースコードに重複も多いですし、リファクタをちゃんと考えてみたいです。

f:id:ky_yk_d:20180520233428p:plainf:id:ky_yk_d:20180520233431p:plainf:id:ky_yk_d:20180520233434p:plainf:id:ky_yk_d:20180520233436p:plain
こんな感じになる(大きさバラバラ・・・)

なお、今回のコードを実装するにあたっては、下記を参照しました。

tomosoft.jp

それでは実際のコードです。

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <form>
        <input id="ID" type="text" placeholder="IDを入力">
        <input id="taskname" type="text" placeholder="タスク名を入力">
        <input id="submit" type="button" value="送信">
    </form>
    <hr/>
    <div id="status"></div>
    <form>
        <input id="btn" type="button" value="取得">
    </form>
    <hr/>
    <div id="result"></div>
    <script>
        document.addEventListener('DOMContentLoaded', function(){
            document.getElementById('submit').addEventListener('click',function(){
                var status = document.getElementById('status');
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function(){
                    if (xhr.readyState === 4){ // 通信完了
                        if (xhr.status === 200) { // 通信成功
                            status.textContent = '送信しました';
                        } else {
                            status.textContent = 'サーバーエラーが発生しました';
                        }
                    } else {
                        status.textContent = '送信中';
                    }
                };
                var obj = {
                    Item : {
                        id: document.getElementById('ID').value,
                        taskname: document.getElementById('taskname').value
                    }
                };
                var json = JSON.stringify(obj);
                xhr.open('PUT', 'https://l3uk6hufcf.execute-api.ap-northeast-1.amazonaws.com/prod/sample-tasks', true);
                xhr.setRequestHeader('Content-Type', 'application/json');
                console.log(json);
                xhr.send(json);
            })
        })
    </script>

    <script>
        document.addEventListener('DOMContentLoaded', function(){
            // 取得ボタンクリック時に実行される
            document.getElementById('btn').addEventListener('click', function(){
                var result = document.getElementById('result');
                var xhr = new XMLHttpRequest();
                // 非同期通信時の処理
                xhr.onreadystatechange = function(){
                    if (xhr.readyState === 4){ // 通信完了
                        if (xhr.status === 200) { // 通信成功
                            //var data = JSON.parse(xhr.responseText);
                            var data = xhr.response;
                            // 結果からキーにアクセス
                            if (data === null) {
                                // ない場合はメッセージ
                                result.textContent = 'データが存在しません';
                            } else {
                                // 取得できた場合
                                console.log(typeof(data));
                                console.log(data)
                                var items = data.Items;
                                var ul = document.createElement('ul');
                                for (var i = 0; i < items.length; i++){
                                    var li = document.createElement('li');
                                    var text = document.createTextNode(items[i].ID + ' ' + items[i].taskname);
                                    // 組み立て
                                    li.appendChild(text);
                                    ul.appendChild(li);
                                }
                                // <div id="result">の配下を置き換える
                                result.replaceChild(ul, result.firstChild);
                            }
                        } else {
                            result.textContent = 'サーバエラーが発生';
                        }
                    } else { // 通信中
                        result.textContent = '通信中'
                    }
                };
                // 非同期通信を開始
                xhr.responseType = 'json';
                xhr.open('GET', 'https://l3uk6hufcf.execute-api.ap-northeast-1.amazonaws.com/prod/sample-tasks', true);
                xhr.send(null);
            }, false );
        } ,false);
    </script>
</body>
</html>

HTMLの部分はまぁいいとして、JSの部分を丁寧にみてみようと思います。

送信ボタンの処理

document.addEventListener('DOMContentLoaded', function(){
        / * 中略 */
        })

ページロード時に実行されるイベントリスナーを登録しています。/* 中略 */の部分に記載した内容が初期化処理になります。次は中身をみてみます。

document.getElementById('submit').addEventListener('click',function(){
        /* 中略 */
            })

submitボタンにイベントリスナーを登録しています。さきほどDOMContentLoadedを渡していた第一引数は、今回はclickとなっています。イベントリスナーの種類を指定しているのですね。クリックしたときに、第二引数で渡している関数オブジェクトが実行されるわけです。さらに括弧の中をみていきます。

var status = document.getElementById('status');     // HTMLのid="status"の要素を取得
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){                       // 通信の状態が変化したときに呼び出されるイベントハンドラ
    if (xhr.readyState === 4){ // 通信完了
        if (xhr.status === 200) { // 通信成功
            status.textContent = '送信しました';
        } else {
            status.textContent = 'サーバーエラーが発生しました';
        }
    } else {
        status.textContent = '送信中';
    }
};
var obj = {
    Item : {
        id: document.getElementById('ID').value,
        taskname: document.getElementById('taskname').value
    }
};
var json = JSON.stringify(obj);
xhr.open('PUT', 'https://l3uk6hufcf.execute-api.ap-northeast-1.amazonaws.com/prod/sample-tasks', true);
xhr.setRequestHeader('Content-Type', 'application/json');
console.log(json);
xhr.send(json);

非同期通信を管理するXMLHttpRequestが登場しています。onreadystatechangeプロパティには、通信の状態が変化したときに呼び出されるイベントハンドラを格納します。この例の場合、HTTP通信の状態(readyState)とHTTPステータスコードstatus)によって通信の状態を判断し、status.textContentにメッセージを代入することで通信状態を画面に表示しています。

後半では、PUTリクエストでJSONを送付する処理を記述しています。まず、送信したいJSONデータの元となるJavaScriptのオブジェクトをobjに代入し、それをJSON.stringify()で文字列形式に変換しています。次に、XMLHttpRequestオブジェクトのopen()メソッドで、HTTPリクエストの初期化を行います。第一引数がHTTPメソッド、第二引数がアクセス先URLです。第三引数以降はオプションで、ここでは第三引数にtrueを渡すことで非同期通信であることを明示しています(第三引数はデフォルトがtrueなので書いても書かなくても変わらないはずですが)。

重要なのは、xhr.setRequestHeader('Content-Type', 'application/json');の部分です。リクエストヘッダーを指定しています。API Gatewayで本文マッピングテンプレートを設定した際、「Content-Typeの欄にはapplication/jsonと記載して〜〜」と書いたことに対応しています。この処理の記述を省いてしまうと、リクエストは拒否されます(ステータスコード415)。この点については下記の記事が参考になるかと思います。

dev.classmethod.jp

リクエストヘッダーを指定したら、最後にJSON文字列をリクエストの本文としてsend()メソッドに渡して送信します。ここまでの処理が、すべて`「送信」ボタンをクリックした際に実行されます。

取得ボタンの処理

リクエスト送信時のJSON関係の処理がないこと以外、基本は送信ボタンの処理と同じです。ただ、こちらでは取得したデータをHTML側に埋め込むための処理が加わっています。

var data = xhr.response;
// 結果からキーにアクセス
if (data === null) {
    // ない場合はメッセージ
    result.textContent = 'データが存在しません';
} else {
    // 取得できた場合
    console.log(typeof(data));
    console.log(data)
    var items = data.Items;
    var ul = document.createElement('ul');
    for (var i = 0; i < items.length; i++){
        var li = document.createElement('li');
        var text = document.createTextNode(items[i].ID + ' ' + items[i].taskname);
        // 組み立て
        li.appendChild(text);
        ul.appendChild(li);
    }
// <div id="result">の配下を置き換える
result.replaceChild(ul, result.firstChild);

API Gateway(の背後にいるLambda)からはオブジェクト形式でデータが送られてきます。responseプロパティがレスポンスの本体です。それは下記のような形のデータです(ブラウザでAPIのエンドポイントに直接アクセスすると見られます)。

{
    "Items":[
        {"taskname":"test2","ID":"secondData"},
        {"taskname":"test","ID":"firstData"},
        {"taskname":"変更後!!!","ID":"aaa"}
    ],
    "Count":3,
    "ScannedCount":3
}

(参考)AWS-SDKのscanの仕様(英語)

取得されたデータの個数(Count)やスキャン対象となったデータの個数(ScannedCount※今回はフィルター適用していないのでCountと等しい)も一緒に送られてきていることがわかります。今回欲しいのはそれぞれのデータなので、Itemsを順に扱えば足ります。

ローカル変数itemsItemsの内容を代入したあとは、forループを使いながらid="result"の箇所に挿入する要素を作成しています。古典的なfor文で記載していますが、たぶんもっと近代的な書き方があるのだと思います。

仕上げ(CORSの有効化)

liul要素を順に作成したあと、最後にresult.replaceChild()で置き換えて、この無事表示ができました。めでたしめでたし。

と思いきや、この状態でindex.htmlにアクセスして、送信ボタンや取得ボタンを押すとエラーになります。クロスオリジン通信を可能にする必要があります。ブラウザのコンソールに、Access-Control-Allow-Originがなんちゃらかんちゃらと出るはずなので、これをコピーしてAPI GatewayAPIのアクションで「CORSの有効化」を選択し、Access-Control-Allow-Headersのところに上記のAccess-Control-Allow-Originを加えます。この設定をすることで、ようやくAPIをWebページから叩くことができます。

f:id:ky_yk_d:20180521001112p:plain

API Gateway リソースの CORS を有効にする - Amazon API Gateway

※XDR(Cross-Domain Request)は、元来セキュリティ上の必要性から制限されたもので、対応手段はCORS(Cross-Origin Resource Sharing)以外にも存在するようです。どの手段が適切であるかは、実践レベルでは要検討でしょう。

dev.classmethod.jp

JavaScript のクロス ドメイン (Cross-Domain) 問題の回避と諸注意tsmatz.wordpress.com

終わりに

はい。短くまとめるはずだったのに、今回も長くなってしまいました。個々の技術要素については入門的な記事や書籍がいくらでも存在しているので、あえて記事にするとすれば「実現したことベース」にして繋ぎ方を意識したものがいいかなと思ってるのですが、それゆえにこそ色々な要素を詰め込んでしまい、長くなってしまう傾向がありますね。これだけ長くなるとどこに何書いたかも忘れますし、構成や文章もグダグダになりがちです。なんとかしたいですね。技術的には、次はVue.jsとDynamoDBでも繋げてみようかと思っています。乞うご期待。

ソースコードについてはツッコミどころ満載であることは自覚しており、発展させながら直していこうと思っていますが、幾分初心者であるがために気づかないミスや改善点などあると思いますので、pull request・Issue等なげていただけるととても嬉しいです。

github.com

JS初心者がAWS Lambdaで実装するLINE Bot〜「オウム返し」の一歩先〜

先日まで、Vue.jsの勉強をしてきました。ところが、vue-cliがわけわからなすぎて「Node.jsも勉強しなきゃ」となっています。JavaScript自体にも理解が不足していることを実感してきました。

そこで、今回はNode.jsの勉強を兼ねてLINEのボット開発に挑戦です。以前、ネット上の記事をそのまま写す形で、Lambdaを用いてNode.jsでLINEのボット(オウム返しするだけ)作成していたので、今回はそちらを一歩先へ進めてみようと思います。

この記事が提供するもの

あらかじめお伝えしますが、この記事で紹介するのは、 LINEのMessaging APIを用いて簡単な会話を作成する方法 です。以下のようなことには触れません。

この記事で作成したコードのGitHubリポジトリ

ソースコードの全量および変更履歴はこちらをご参照ください。

github.com

今回の出発点:「オウム返し」

この記事の内容を概ねコピペした形となっています(最初のものを作ったのが少し前なので、そのときにどの記事をみて作成したのかは定かではありません)。

AWS、LINEの設定についてはこちらの記事の方が詳しいです。

今回の到達点

入力された文字列やボタンの選択に応じて、簡単なやりとりが成り立つようにしました。あらかじめ用意したJSON形式のメッセージを渡しているだけなので、チャットボットと言えるようなものではあいりません。あしからず。実際の画面は以下のような感じです。

f:id:ky_yk_d:20180517212449j:plain

修正後のコード(一部省略)

まずは、修正(機能追加+簡単なリファクタリング)後のコードを提示しておきます。大きな変更のなかった箇所については省略していますので、細かい修正履歴はGitHubをご覧ください(改善提案もお待ちしています)。

index.js

let https = require('https');

/**
 * Lambda実行時に呼び出されるハンドラ
 * @param {} event イベント
 * @param {} context コンテキスト
 * @param {} callback コールバック関数(正体は未確認)
 */
exports.handler = (event, context, callback) => {
    let messageObj;
    let replyToken;
    let jsonFile;
    let opts;
    let req;
    let data;
    let replyData;
    data = event.events[0];
    replyToken = data.replyToken;
// LINEでユーザから送られたテキストを取得
// メッセージオブジェクトを記述したJSONファイルを読み込む
// https://developers.line.me/ja/docs/messaging-api/reference/#anchor-e65d8a1fb213489f6475b06ad10f66b7b30b0072
    jsonFile = require("./dialogue.json");
// 入力に応じたメッサージの選択
    console.log(data);
    messageObj = getMessageObj(data, jsonFile);
// 返すデータを作成する
    console.log('データ作成');
    console.log(messageObj);
    replyData = JSON.stringify({
       replyToken: replyToken,
       messages: [
           messageObj
        ] 
    });
    console.log(replyData);
/* 〜共通部分中略〜 */
};

/**
 * 入力されたデータに応じて、返すメッセージを生成する関数
 * @param {} data 入力されたデータ
 * @param {} jsonFile 外部ファイルから読み込んだJSON形式のデータ
 * @return メッセージオブジェクト
 */
let getMessageObj = (data, jsonFile)=> {
    switch (data.type){
        case 'message':
            console.log('メッセージの場合');
            if (data.message.type != 'text'){
                // テキストメッセージ以外の場合
                console.log('テキスト以外のメッセージが入力された');
                return jsonFile.otherType;
            } else {
                // テキストメッセージの場合、入力された文字列に応じて分岐
                if (data.message.text == '住所') {
                    return jsonFile.dialogue2;
                } else {
                    return jsonFile.dialogue1;
                }
            }
        case 'postback':
            console.log('postbackの場合');
            return jsonFile[data.postback.data];
        default :
            console.log('それ以外の場合');
            console.log(data);
            return jsonFile.otherType;
    }
};

dialogue.json

{
    "dialogue1": {
        "type": "template",
        "altText": "テストメッセージ",
        "template": {
            "type": "buttons",
            "text": "メッセージありがとうございます!どんなことに興味がありますか?",
            "defaultAction": {
                "type": "uri",
                "label": "Twitterをみる",
                "uri": "https://twitter.com/ky_yk_d"
            },
            "actions": [
                {
                    "type": "uri",
                    "label": "ブログを読む",
                    "uri": "https://ky-yk-d.hatenablog.com/"
                },
                {
                    "type": "uri",
                    "label": "GitHubをみる",
                    "uri": "https://github.com/ky-yk-d"
                },
                {
                    "type": "postback",
                    "label": "次の質問に行く",
                    "data": "dialogue3",
                    "displayText": "別の質問がいい!"
                }
            ]
        }
   },
   "dialogue2": {
        "type": "location",
        "title": "東京スカイツリー",
        "address": "〒131-0045 東京都墨田区押上1丁目1−2",
        "latitude": 35.710139,
        "longitude": 139.810833
   },
   "dialogue3": {
        "type": "template",
        "altText": "メッセージ",
        "template": {
            "type": "buttons",
            "thumbnailImageUrl": "https://cdn-ak.f.st-hatena.com/images/fotolife/k/ky_yk_d/20180513/20180513094057.png",
            "imageAspectRatio": "rectangle",
            "imageSize": "cover",
            "imageBackgroundColor": "#000000",
            "text": "別の質問へを押しましたね?",
            "actions": [
                {
                    "type": "postback",
                    "label": "おわり",
                    "data": "end",
                    "displayText": "おわりにする"
                },
                {
                    "type": "uri",
                    "label": "やっぱりTwitter",
                    "uri": "https://twitter.com/ky_yk_d"
                }
            ]
        }
   },
   "otherType": {
       "type": "text",
       "text": "ごめんなさい!文章での入力をお願いします!"
   },
   "end": {
       "type": "text",
       "text": "お疲れ様でした!"
   }
}

メッセージオブジェクトを返却する

JSONファイル(dialogue.json)を新たに作成し、以下の部分で読み込んでいます。

jsonFile = require("./dialogue.json");

ここで読み込んだファイルには、複数のメッセージオブジェクトの内容が記載されています。下記の部分が一つのメッセージオブジェクトで、これを返却したものが上記のスクリーンショットの上部で表示されているものです。

"dialogue1": {
    "type": "template",
    "altText": "テストメッセージ",
    "template": {
        "type": "buttons",
        "text": "メッセージありがとうございます!どんなことに興味がありますか?",
        "defaultAction": {
            "type": "uri",
            "label": "Twitterをみる",
            "uri": "https://twitter.com/ky_yk_d"
        },
        "actions": [
            {
                "type": "uri",
                "label": "ブログを読む",
                "uri": "https://ky-yk-d.hatenablog.com/"
            },
            {
                "type": "uri",
                "label": "GitHubをみる",
                 "uri": "https://github.com/ky-yk-d"
            },
            {
                "type": "postback",
                "label": "次の質問に行く",
                "data": "dialogue3",
                "displayText": "別の質問がいい!" 
            }
        ]
    }
},

上記の例では、ボタンを表示するテンプレートメッセージを使用しており、それぞれのボタンには、リンクやpostback(後述)のアクションを持たせています。入力に応じて返すメッセージオブジェクトを切り替えることで、様々な機能を持ったボットを作成することができます。

様々なメッセージオブジェクト

他にも、メッセージオブジェクトには、以下のような種類があります。

  • テキストメッセージ
  • スタンプメッセージ
  • 画像メッセージ
  • 動画メッセージ
  • 音声メッセージ
  • 位置情報メッセージ
  • イメージマップメッセージ

以下は位置情報メッセージの例です。 f:id:ky_yk_d:20180517230336j:plain

"dialogue2": {
     "type": "location",
     "title": "東京スカイツリー",
     "address": "〒131-0045 東京都墨田区押上1丁目1−2",
     "latitude": 35.710139,
     "longitude": 139.810833
},

それぞれの細かい仕様については、LINEのMessaging APIリファレンスをご覧ください。

postbackイベントを使ってみる

さて、ここまででも色々なメッセージを応答として返すことができますが、まだ「決められた入力-決められた出力」しか実現できていません。Lambdaはあくまで関数なので、前回どのような入力があったかを保持することができず、流れのあるやりとりにはなりません。

流れのあるやりとりを実現するための方法として、postbackイベントが用意されています。冒頭の画像で、「別の質問がいい!」というテキストを送っているように見えますが、これは「次の質問へ行く」というボタンを押したときに表示されるテキストです。ソースコードでいうとこの部分。

{
    "type": "postback",
    "label": "次の質問に行く",
    "data": "dialogue3",
    "displayText": "別の質問がいい!"
}

label要素がボタンの文言で、displayTextが送信され(ているように見え)るテキストです。ユーザ側からはテキストメッセージを送っているようにしか見えないのですが、内部的には異なります。postbackイベントの場合、data要素を含めて送信されます。postbackイベントは、templateメッセージの選択肢に対応して送信されるイベントです。

今回の例では、postbackイベントの場合に、data要素で送られた文字列に対応するメッセージオブジェクトをJSONファイルから取得して返すようにしています。画像の例では、data要素でdialogue3という文字列を渡すようにすることで、JSONファイル内の"dialogue3"に対応するメッセージオブジェクトが返されるようになっています。

case 'postback':
    console.log('postbackの場合');
    return jsonFile[data.postback.data];

これを用いることで、流れある会話を実現することができるわけです。JSONファイルさえ用意できれば、何回もやりとりが続く応答も可能になります。ただ、やりとりが増えればその分だけJSONファイルの内容はカオスになっていくので、その辺りをどのように管理するかは今後の課題です。

おわりに

いかがでしたでしょうか。オウム返しだけではつまらなかったLINEボットも、やりとりが成立するようになると随分楽しくなりますね。多くの公式アカウントが存在していますが、一方的に情報を投げつけてくるだけで、こちらからメッセージを送っても「個別のメッセージにはお答えできません」と返ってくるだけのものも多いです。もちろん、今回の方法では、「りんな」のような対話ができるボットは作ることができませんが、一人で作って遊んだり仲間内で遊ぶ程度なら楽しいものが作れるのではないでしょうか。

また、今回条件分岐や関数のくくりだしなどを行ったことで、JavaScript的にもやや考えることがありました(下記におまけとして少し記載しておきます)。まだまだ、オブジェクト指向とは程遠いコードですが、機能追加をしながらJavaScriptのコードとしてもより良くしていけたらなと思っています。

エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方

エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方


おまけ:JavaScriptの変数がつらい話

今回条件分岐を追加したり、変数を新たに作成したりするなかで、若干リファクタリング的なことを行いながら進めました。そのうち大きな変更点としては、当初のソースコードで、var命令を用いていた箇所を、全てlet命令に変更し、また宣言の箇所もブロックの先頭に移動しました。
let命令はES6(ECMAScript2015)から使えるようになったものです。アロー関数も同じタイミングでの採用ということなので、アロー関数が使われていればletも使えるはずということで使ってみることにしました。varletでは、例えば以下のような違いがあるようです。

  • 同じ名称の変数宣言を許さない
  • ブロックスコープに対応している

(参考)

qiita.com

写経した段階のコードでは、ハンドラの冒頭でdata変数が明示的に宣言されずに使われていました。var命令がない場合、変数は(関数の中で初めて使用される場合でも)グローバル変数として宣言される(山田祥寛『JavaScript本格入門』188頁)とのことですが、今回の場合はさらに厄介だったようです。

というのも、関数内の後続する箇所でvar data = ...と宣言されているために、冒頭で利用されているdataもこのローカル変数と解釈されていたみたいです。「変数の巻き上げ」(山田189頁)と呼ばれるこの現象、var命令によって宣言されるローカル変数のスコープが関数全体(宣言箇所から関数の終わりまでではない!)であることによるものらしいのですが、極めて分かりにくいですね。

この点に限らず、ES6以前はブロックスコープ変数が存在しなかったり、Javaにもっとも親しんでいるプログラマとしてはJavaScriptは複雑怪奇です。JavaJavaScriptは全然似ていないというネタをよくみますが、ようやく実感できてきた気がします(とはいえ、JSDocはJavaDocと記法がほぼ一緒なので馴染みやすいです)。学びがいがありますね。

フロントエンド初心者が学ぶ「リンクが展開されるあれ」とVue.js

前回の記事の続きです。
ky-yk-d.hatenablog.com

前回、簡単なwebサイトを作成し、GitHub Pagesで公開するというところまで行いました。今回は、Vue.jsをもう少しちゃんと学んでみようと思います。また、「リンクが展開されるあれ」について学んだことも書いておきます。 今回主に編集したのはこちらのページです。 ky-yk-d.github.io

og:imageとTwitterカードを設定する

前回のブログを公開してTwitterにリンクを貼った際に、カカカカックさん(@kakakakakku)さんから、

ノンデザイナーズ本が og:image になってるのですが,今回の記事の本題ではないので,実際に作った GitHub Page のキャプチャにするか,1番関連する本にするべきかもしれませんねー

というご指摘をいただきました。

「og:image?なにそれ美味しいの?」という第一印象でしたが、調べてみるとあれですね。リンクが展開されるあれ。あれって選択できるんですね。勝手に選ばれるもんだと思ってました

ブログの方は、記事をHTML編集モードで開いて記載しましたが、せっかくなのでGitHub Pagesで公開している方のページにもmetaタグを仕込んでみることにしました。そこで関連情報を調べていると、Twitterカードというものがあるという記事を見つけました。

saruwakakun.com

Twitterカードというのは、Twitterでのリンク付きツイートに画像やその他の情報が添付されるあれですね。URLが貼られているのに画像が展開されたりされなかったりしているのは、Twitterカードの設定の有無に起因していたということを知りました。

実際に作成したサイトの記載は以下の通りです。画像については、ブログでも用いているはてなのアップロード機能を利用しました。

<!DOCTYPE html>
<html>
<head>
〜〜中略〜〜
    <meta name="twitter:card" content="summary_large_image"/>
    <meta name="twitter:site" content="@ky_yk_d"/>
    <meta property="og:url" content="https://ky-yk-d.github.io/github-pages-practice/vue/practice.html"/>
    <meta property="og:title" content="こまどのVue.js練習場"/>
    <meta property="og:description" content="Vue.jsになれるための練習場です。" />
    <meta property="og:image" content="https://cdn-ak.f.st-hatena.com/images/fotolife/k/ky_yk_d/20180504/20180504185225.png"/>
〜〜中略〜〜
</head>

以上の設定をしたページのURLを貼り付けたツイートは、以下のようになります。

ちゃんと画像や説明等が表示されていますね。ベタがきでURLが書かれているだけのツイートとは見栄えが全く違います。せっかく書いた記事や作ったページを多くの人に見てもらうには、こういう工夫は不可欠ですね。勉強になりました!

(補足)og:imageが反映されない場合の対処法

ブログの方で設定を変えたときに、「変わったかなー」と改めてリンクを貼ったツイートをしてみたところ、変更が反映されていませんでした。しばらくしないと反映されないのかなーと思っていましたが、即時反映させる方法がありました。下記のページでURLを入力して「検証」することで、Twitter側から情報を収集しなおされ、即座に反映されるようです。

Twitter Card validator

※今回はTwitterでしたが、facebookにも同等のページがあります。

デバッガー - 開発者向けFacebook

Vue.jsの勉強の続き:基本文法

前回、ネット上の記事を写経してタスク管理画面を作成しました。使い方もわからずただ写経しただけだったので、今回は、体型的に初歩から学んでみます。

公式のチュートリアルなど、教材は複数の選択肢があったのですが、今回は山田祥寛さんの『速習Vue.js』を写経して見た感想を書き留めておきます。

速習Vue.js 速習シリーズ

速習Vue.js 速習シリーズ

Part 1〜7では、Vue.jsの基本文法が丁寧に解説されていますし、写経もしやすかったです。脆弱性対策を考慮した仕様についての説明など、何も言われなければ通り過ぎてしまうところにも説明があったのがよかったと思います(他を知らないのであくまで感想ですが・・・)。

具体的な内容については、内容を用いて作成したページ(あるいはGitHubリポジトリ)を参照いただくとして、印象に残っている箇所をご紹介します。下記の例は、算出プロパティとメソッドの違いについてです。

<div>
    <p>↓クリックを押すとメソッドの方だけ更新されて算出プロパティは更新されない</p>
    <form>
        <input type="button" v-on:click="onclick" value="クリック"/>
    </form>
    <p>算出プロパティ:{{randomc}}</p>
    <p>メソッド:{{randomm()}}</p>
    <p>現在日時:{{current}}</p>
    <p>メソッドは再描画時に常に評価されるが、算出プロパティはそれが依存するプロパティが変更されたときのみ評価される。</p>
</div>
var app = new Vue({
    // このVueを有効にする要素の指定
    el: '#app',
    // 上記で指定した範囲で利用するデータ
    data: {
        current: new Date().toLocaleString()
    },
    computed: {
        // 算出プロパティ経由で乱数を取得
        randomc: function(){
            return Math.random();
        }
    },
    methods: {
        // クリック時に処理を実行する
        onclick: function(){
            this.current = new Date().toLocaleString();
        },
        // メソッド経由で乱数を取得
        randomm: function(){
            return Math.random();
        }
    }
});

この箇所の手前で、同じ機能を算出プロパティとメソッドの両方で実装してみるという箇所があったのですが、そこで当然浮かぶ「じゃあこの二つは何が違うの?」という疑問に答える説明がなされています。違いをただ文章で説明するだけではなく、サンプルコードで実際の動きによって納得させてくれるのも「こまど的にポイント高い(cv:悠木碧)」ですね。

Vue.jsの勉強の続き:コンポーネント、ディレクティブ、フィルター、プラグイン

Part 7 ではコンポーネントを、Part 8 ではディレクティブ以下の部品化技術を説明しています。たくさんの機能を単一のVueインスタンスで作成するのではなく、再利用可能な部品として切り出して利用するという発想ですね。これをうまく使うことが、Vue.js(に限らないのでしょうが)の真骨頂ということになりそうです。フロントエンド初心者にはハードルが高い部分でもあります。

詳しい内容については再度実際のページないしGitHubの参照を乞うことにして、ここでも印象に残った箇所を挙げて一言コメントを。パラメータ付きコンポーネントを利用する方法についての箇所です(サンプルコードそのままではありません。念のため)。

<p>パラメータ付きコンポーネント</p>
<!-- 属性名はケバブケース-->
<my-param-hello your-name="瀧くん"></my-param-hello>
<!-- v-bindを使う場合はシングルクオートでも囲む(数値として渡すなら不要)-->
<my-param-hello v-bind:your-name="'三葉'"></my-param-hello>
Vue.component('my-param-hello', {
    props: ['yourName'], // プロパティはキャメルケース
    template: '<div>{{yourName}}!</div>'
});

他のJSフレームワークを知らないので実感をもっては言えないのですが、先日公開された soussune.fmの40で「VueはReactに比べて書き方に自由度が高い」という話が出ていました。

soussune.com

上記の例でも、略式の書き方とv−bindを使った書き方との両方で書いています。v-bindで文字列を渡す場合はシングルクオートも必要というのは、目が悪いのでソースコードを見ても気づきにくいポイントだと思います。複数の記法が混在するソースコードは避けなければなりませんが、「他の書き方の場合はどうなるのだろう」ということを知っておくことは必要ですね。

Vue.jsの勉強の続き:vue-cli & ルーティング

Part 9では、コマンドラインツールであるvue-cliが紹介されています。インストールと起動にnpmを使うことからも明らかであるように、Node.jsを利用したツールとなります。プロジェクトの雛形の作成(rails newみたいなもの)とビルド&実行も自動化できるとのことで、Vue.jsで本格的にアプリを作成するには欠かせないようです。導入から基本の立ち上げまでは以下のように行います。

npm install -g vue-cli
cd  vue-cli-practice[=プロジェクトを作成するディレクトリ]
vue init webpack[=テンプレート名] my-app[=プロジェクト名]

上記を実行すると、? Project name my-app から始まる割とたくさんの質問が次々と表示されます。書籍はデフォルトで進めていました。すべての質問に答えると、 最初に指定したディレクトリ配下にプロジェクトのフォルダが作成され、その中に必要なフォルダ・ファイルが自動的に作成されます。上述の通り、rails newと同じような感じですね。 自動生成された段階で、動くようになっているので、下記を入力して実行してみます。

cd my-app 
npm start

実行中のメッセージが色々と表示された末尾に、Your application is running here: http://localhost:8080などと表示されれば、起動は成功です。ここで表示されたlocalhostの8080番にブラウザでアクセスするとVueのロゴを含むデフォルトのページが表示されます。

f:id:ky_yk_d:20180513094057p:plain

なるほどー(棒)簡単すぎて初学者にはよろしくないですね。npm startによって何が行われたのかもよくわかりません。vue-cliについては、著者の山田さんも、

初学者が最初から vue-cli を利用するのはお勧めしません。特に Node.js による開発に精通していない場合には、導入のハードルを上げるだけです。

と述べています。また、このあと、Part 10ではルーティングについて記載されており、その部分も写経をしたのですが、紙幅の都合かこの部分は書籍でも説明が薄く、こちらについても正直いってあまりよく理解できていません。あとは自分なりのものを作りながら試行錯誤の中で学ぶべきなのでしょう。基本的なVue.jsの道具立ては獲得したので、公式のレファレンスに当たることもできるでしょうし、ネット上の記事も少しは読めるようになっていると信じます。

HTML & CSS の勉強もやり直し始めています。まだ、こちらも写経以上のことはできていないのですが、これからVue.jsも使いながらまずはある程度整った(目的のある)静的Webサイトを作成し、ゆくゆくはNode.jsなどでサーバーサイドも構築して公開・・・といったところまで行ければと思っています。「フロントエンド初心者」という肩書きが取れるのはいつになるのでしょうか?ではでは。