公式チュートリアルから始めるVue.js vol.2「Github コミット」

公式チュートリアルから始めるVue.js vol.2「Github コミット」

第二回は「Vue.js」公式チュートリアル「Githubコミット」をやっていきます。

第一回と同様に、コードを追いつつ、わからないところがあったら調べる、という形式でやっていきます。

概要

今回の「Githubコミット」ではAjaxでGitHubのリポジトリ情報を取得し、コミット履歴をHTML上に表示するというサンプルです。
ブランチの切り替えなどにも対応しています。

html

<div id="demo">
    <h1>Latest Vue.js Commits</h1>
    <template v-for="branch in branches">
    <input
        type="radio"
        :id="branch"
        :value="branch"
        name="branch"
        v-model="currentBranch"
    />
    <label :for="branch">{{ branch }}</label>
    </template>
    <p>vuejs/vue@{{ currentBranch }}</p>
    <ul>
        <li v-for="record in commits">
            <a :href="record.html_url" target="_blank" class="commit">
              {{ record.sha.slice(0, 7) }}
            </a>
            - 
            <span class="message">{{ record.commit.message | truncate }}</span>
            <br />
            by
            <span class="author">
              <a :href="record.author.html_url" target="_blank">{{ record.commit.author.name }}</a>
            </span>
            at
            <span class="date">{{ record.commit.author.date | formatDate }}</span>
        </li>
    </ul>
</div>

template要素は、HTML5で策定された要素です。
template要素はDOMに反映されません。

参考:<template> – HTML | MDN

ここでは、v-forディレクティブを指定するため だけに存在しています。

v-for=”branch in branches”

これは、JavaScriptでいうfor文です。
Vueインスタンスのdata内にあるbranchesの値の数だけ、v-forディレクティブを記述した要素(ここではtemplateが当たります)を生成します。

template要素を複数個生成することになりますが、template要素自体はDOMに描画されないため、DOM上では

<input type="radio" id="master" name="branch" value="master">
<label for="master">master</label>
<input type="radio" id="dev" name="branch" value="dev">
<label for="dev">dev</label> 

のように描画されます。

branchとはbranchesのプロパティ一つ一つを表す引数で、任意につけることができます。
branchesのプロパティが一つ一つ、ループごとにbranchと記述した部分に代入されます。
今回は、branches

branches: ['master', 'dev'],

のようになっています。
この場合、masterdevのそれぞれについて要素が描画されます。

:id、:value、:for

v-bind:idv-bind:valuev-bind:forの略記です。
第一回で説明したので割愛します。

name=”branch”

これはただのvalue属性です。:id="branch"のbranchは引数ですが、name="branch"は定数で、v-forとは何の関係もありませんので違いにご注意ください。

v-model=”currentBranch”

<input v-model="currentBranch">とは
<input v-bind:value="currentBranch" v-on:input="currentBranch = $event.target.value">の糖衣構文です。

第一回で、「入力があるたびに、$event.target.valueで内容を取得してはdataの値を書き換え、その値とbindされているvalueが自動で書き換わる」 という一連の動きがありましたが、その流れをv-modelのみで行うことができます。

inputに入力動作が行われるたびにcurrentBranchの値が書き換えられ、再びinputvalueプロパティに返ってきます。

今回はtype="radio"なので、$event.target.valueで取得した結果はtrueまたはfalse。この値がリアルタイムに切り替わります。

{{ branch }}

mustache構文です。
mustacheとは「口髭」という意味。

v-bindと同様に、dataの値を束縛します。
ここにdata.branchの値が入って来ます。

{{ currentBranch }}

<p>vuejs/vue@{{ currentBranch }}</p>

上記と同様にmustache構文です。
先ほどと違うのは、v-forの中にいない点です。

record in commits

<ul>
    <li v-for="record in commits">
        <a :href="record.html_url" target="_blank" class="commit"
        >{{ record.sha.slice(0, 7) }}</a
        >
            - <span class="message">{{ record.commit.message | truncate }}</span
        ><br />
        by
        <span class="author"
        ><a :href="record.author.html_url" target="_blank"
            >{{ record.commit.author.name }}</a
        ></span
        >
        at
        <span class="date">{{ record.commit.author.date | formatDate }}</span>
    </li>
</ul>

recordプロパティがたくさんbindされています。
commitsオブジェクトは多次元配列になっています。
詳しくはJavaScriptのところで解説します。

JavaScript

var apiURL = 'https://api.github.com/repos/vuejs/vue/commits?per_page=3&sha='

/**
 * Actual demo
 */

var demo = new Vue({

  el: '#demo',

  data: {
    branches: ['master', 'dev'],
    currentBranch: 'master',
    commits: null
  },

  created: function () {
    this.fetchData()
  },

  watch: {
    currentBranch: 'fetchData'
  },

  filters: {
    truncate: function (v) {
      var newline = v.indexOf('\n')
      return newline > 0 ? v.slice(0, newline) : v
    },
    formatDate: function (v) {
      return v.replace(/T|Z/g, ' ')
    }
  },

  methods: {
    fetchData: function () {
      var xhr = new XMLHttpRequest()
      var self = this
      xhr.open('GET', apiURL + self.currentBranch)
      xhr.onload = function () {
        self.commits = JSON.parse(xhr.responseText)
        console.log(self.commits[0].html_url)
      }
      xhr.send()
    }
  }
})

※第一回と重複する部分は割愛します。

API

Github APIを使っています。
GitHub API v3 | GitHub Developer Guide

詳しいことは割愛しますが、Githubではアカウントの持つリポジトリや、そこで行われているコミットなどのデータをオブジェクト化したものが固有のURLを持っています。

このチュートリアルでは、そのURLのデータをdataオブジェクトに入れ、色々と操作をしてDOMに表示させています。

今回のURL

var apiURL = 'https://api.github.com/repos/vuejs/vue/commits?per_page=3&sha='

これは、「アカウント名vuejsvueリポジトリ」の「3つぶんのコミットデータ」を表すURLです。

dataオプション

data: {
  branches: ['master', 'dev'],
  currentBranch: 'master',
  commits: null
},

特筆すべきはcommits:nullです。
これは、後述のmethods部分でGithubのAPIを介して取得します。
よって、ここでは初期化のためにcommits:nullとしています。

nullは「何もない」という状態を定義するのに使います。

createdオプション

created: function () {
  this.fetchData()
},

ここには関数を定義しておきます。
その関数は Vueインスタンスが作成されると同時に 呼び出されます。
呼び出されるタイミング的には elオプションで指定した要素にインスタンスをマウントする直前」 です。

インスタンスは存在するけれどまだDOM要素に紐付けされてない、というタイミングでの呼び出しになります。

Vueインスタンスが作成されたら、fetchData()メソッドを呼び出します。
内容は後ほど解説します。

watchオプション

watch: {
  currentBranch: 'fetchData'
},

watchオプションには、オプションを持つオブジェクトを定義します。
そして、そのオプションを監視し続け、変化があるたびに定義した関数を呼び出します。

第一回に出てきたcomputedのより汎用的なオプションです。

今回は、dataオプションにあるcurrentBranchを監視し、変化があるたびにmethodsオプションに定義されたfetchDataを呼び出します。

ここまでで、fetchData

  • インスタンス作成直後
  • currentBranchに変化があった時

に呼び出されることになります。

filtersオプション

filters: {
  truncate: function (v) {
    var newline = v.indexOf('\n')
    return newline > 0 ? v.slice(0, newline) : v
  },
  formatDate: function (v) {
    return v.replace(/T|Z/g, ' ')
  }
},

フィルタリング処理を行う関数を定義します。
ここでは二つの関数が登録されています。

truncateメソッド

truncate: function (v) {
  var newline = v.indexOf('\n')
  return newline > 0 ? v.slice(0, newline) : v
},

truncateとは「先端を切り取る」という単語。
結論からいうと「一行目を返す」メソッドです。

この関数は、HTML上で以下のようにbindされています。

 - <span class="message">{{ record.commit.message | truncate }}</span>

{{ record.commit.message | truncate }}

record.commit.message | truncateのようにすることで、record.commit.messageプロパティをtruncateメソッドに引数として渡すことができます。
そして、truncateメソッドの返す値がここに入ります。

truncateメソッドに引数として与えられるのは「revert ssr readme edit」のような文字列です。

indexOfメソッド

indexOfメソッドは引数で指定した文字列を探します。
一致すれば、「頭から何文字目で見つかったか」を数値で返します。見つからなければ-1を返します。
\nは「改行コード」と言われるものです。

例えば「revert ssr readme edit」に対してindexOf('\n')メソッドを適用した場合、改行が含まれていないので-1を数値で返します。

また、newline > 0 ? v.slice(0, newline) : vnewline > 0が真ならばv.slice(0, newline)、偽ならばv という値をとります。
slice(0, newline)は、文字列に対して「0からnewlineまで」を返します。

まとめると

改行v.indexOf(‘\n’)truncateが返す値
あり-1頭から改行コードまでの文字列
なし1以上の数全文字列

となり、truncateメソッドは「一行目を返す」ものだとわかります。先端を切り取っています。
よって、{{ record.commit.message | truncate }}にはrecord.commit.messageプロパティの1行目が入ります。

formatDateメソッド

formatDate: function (v) {
  return v.replace(/T|Z/g, ' ')
}

この関数もtruncateと同様に、HTML上で以下のようにbindされています。

{{ record.commit.author.date | formatDate }}

record.commit.author.dateの文字列を引数としformatDateメソッドを呼び出します。
formatDateメソッド内では「/T|Z/gを半角スペースに置き換える」という操作を行い、その結果を返します。

/T|Z/gは正規表現で 「TまたはZと連続マッチ」 を表します。

record.commit.author.dateプロパティは2017-02-13T18:39:39Zのようになっているので、TまたはZとマッチするたびに半角スペースと置換され、2017-02-13 18:39:39のようになります。

{{ record.commit.author.date | formatDate }}にはrecord.commit.author.dateプロパティないの文字列TZが半角スペースに置換されたものが入ります。

methods

methods: {
  fetchData: function () {
    var xhr = new XMLHttpRequest()
    var self = this
    xhr.open('GET', apiURL + self.currentBranch)
    xhr.onload = function () {
      self.commits = JSON.parse(xhr.responseText)
      console.log(self.commits[0].html_url)
    }
    xhr.send()
  }
}

var xhr = new XMLHttpRequest()

サーバーへリクエストを出して、XML形式ドキュメントを受け取るためのAPIです。
このメソッド内では.open,.onload,.send,.responseTextの4つが使われています。

xhr.open() / XMLHttpRequest.open()

任意のURLに対してリクエストを出すための設定を行うメソッドです。
ここで設定を行なったのち、後述のsend()メソッドで実際にリクエストを飛ばします。

xhr.open('GET', apiURL + self.currentBranch)

第一引数にHTTPメソッドを指定します。
第二引数にはアクセスするURLを指定します。
GETは、指定したURLに対して「ファイルの中身」を要求します。

apiURL + self.currentBranchで生成したURLの中身を取得します。

xhr.onload() / XMLHttpRequest().onload

xhr.open()のロードが完了したら、「onload」イベントが発生します。
そのイベントをハンドラとして関数を呼び出す場合の設定をここで行います。

onreadystatechange

xhr.onload = function () {
  self.commits = JSON.parse(xhr.responseText)
  console.log(self.commits[0].html_url)
}

xhr.responseText / XMLHttpRequest().responseText

self.commits = JSON.parse(xhr.responseText)

XMLHttpRequest.open()で取得したデータの結果はXMLHttpRequest.responseTextのプロパティとして格納されます。

JSON.parse()

self.commits = JSON.parse(xhr.responseText)

JSONオブジェクトのメソッドで、文字列をJSONとして解析するものです。
解析結果をJSONオブジェクトとして返します。

ここでは、XMLHttpRequest.open()で取得したデータ文字列をJSONデータとして解析し、JSONオブジェクトとしてself.commitsに格納しています。
selfthisなのでthis.commitsと同意、すなわちdataに格納されることになります。

xhr.send() / XMLHttpRequest().send

サーバーへリクエストを送信します。
諸々のパラメータは先述のonloadopenで設定したものを使います。

流れまとめ

1.Vueインスタンスを定義するとともにfetchData()メソッドを呼び出す

XMLHttpRequest()オブジェクト上で諸々の設定を行い、リクエストを投げ値を取得。
取得されたデータはJSONデータとしてdata.commits内に格納されます。

2.諸々のデータがHTMLに反映

データ取得後、Vueインスタンスが#demoにマウントされます。
その時点で、1で取得したデータ諸々がバインド先に反映されます。

なおその際、一部の値はfilters内のメソッドが適用されます。

ラジオボタンのチェックを変えるとfetchData()メソッドが呼ばれる

watchオプションでcurrentBranchが監視されています。
そして、変化があるたびfetchDataメソッドを呼び出すようになっています。

ラジオボタンにはv-modelディレクティブが記述されているので、ラジオボタンのチェックを変えるたびにcurrentBranchプロパティが変化し、fetchDataメソッドにより新しくデータが取得されます。

表示が更新される

新しいデータを取得した際も、データバインディングされていることにより自動でDOMに反映されます。

おわりに

長かったですが以上です。
流れを見ると、基本的にはfetchDataでデータ取得=>dataを書き換える=>DOMに反映という操作を行なっていて、filtersでは取得したデータに対する表示上の処理を行なう、というだけのものです。

また、今回はXMLHttpRequestを用いていました。
諸々のパラメータを設定したあとにsend()でリクエストを飛ばすと言うふうに考えましょう。

次回もチュートリアル解説を行います。