Python Django チュートリアル SPA編(3)

勉強会用の資料です．
今回の記事ではフロント側のデザインを整えていきます．
また，最初のチュートリアルで作成したAPIを修正し，質問一覧を取得，表示させます．

>このチュートリアルから始めるかたは以下のリンクからソースコードをダウンロードするか， 
`tutorial3-start` のタグが付いているリビジョン(`96ab6f4`)をチェックアウトしてください．
>https://github.com/usa-mimi/tutorial-spa/tree/tutorial3-start

## コンポーネントの話

コードを書いていく前に，Vueの **コンポーネント** について説明していきます．

## コンポーネントについて

ページを作っていく前に，コンポーネントについて公式のドキュメントを読んでおくことをおすすめします．
[コンポーネントについて公式の説明](https://jp.vuejs.org/v2/guide/#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AB%E3%82%88%E3%82%8B%E6%A7%8B%E6%88%90)

![Kobito.QGenep.png](https://qiita-image-store.s3.amazonaws.com/0/58048/697a0a24-bc5b-c072-3c05-145cd6824ae7.png "Kobito.QGenep.png")

Vueではページの部分部分をコンポーネントという単位で分割しており，これを組み合わせることでページを作成していきます．
コンポーネントはDOM構造(html)，動作(js)，デザイン(css)で構成されています．
そのため，コンポーネントを定義するvueファイルを　`<template>`, `<script>`, `<style>` の3つ組で書いていくことになります．
以下のvueファイルの例を示します．

```vue:sample.vue
<template>
  <div class="content">
    contents
  </div>
</template>

<script>
export default {
  name: 'SampleComponent',
}
</script>

<style>
.content {
  color: red;
}
</style>
```

公式の図にあるように，定義されたコンポーネントは別のコンポーネント内で使用することができます．
使用する場合は`<script>`内で使用したいコンポーネントをimportし，`components`に登録します．
登録したコンポーネントは`<template>`内でhtmlのタグのように使用することができます．
以下の例を示します．

```vue:other.vue
<template>
  <div>
    <sample-component></sample-component>
  </div>
</template>

<script>
import SampleComopnent from './sample'

export default {
  name: 'OtherComponent',
  components: {
    SampleComponent,
  },
}
</script>
```

script内のコンポーネント名は **CamelCase**，template内では **kebab-case** になっていますが，
区別されないのでどちらをどう書いても大丈夫です．
ただ，慣例的にjsコードでは変数はCamelCaseで書き，htmlやcssではkabab-caseで書かれているので，合わせたほうが無難です．

コンポーネントのスタイルガイドは以下を参考にしてください．
https://github.com/pablohpsilva/vuejs-component-style-guide/blob/master/README-JP.md

コンポーネント単位で分割することで，異なるページでパーツを使いまわしたり，
本当に関心のある処理だけに着目してコードを記述していくことが可能です．
`v-btn`などのようにマテリアルデザインのボタンとクリックした際の波打つような動作を記述しただけの小さなコンポーネントもあれば，
ボタンや入力をまとめて登録Formをコンポーネントとして作るのもいいでしょう．

## コンポーネントのグローバルな登録

>公式ドキュメント：https://jp.vuejs.org/v2/guide/components.html

公式ドキュメントを見て分かる通り，自作のコンポーネントをグローバルに登録することも可能です．
例えばmain.jsに下記のように書いておくと，先ほどのother.vue内でscriptでの登録なしに`<sample-component>`を記述することができるようになります．

```javascript:main.js
import SampleComponent from '@/components/sample'

Vue.component(SampleComponent.name, SampleComponent)
```

> `@` は `project_root/src/` を指すためのエイリアスです．

前回の記事では App.vueのtemplate部に`<v-btn>マテリアルボタン</v-btn>` と書くことでマテリアルデザインのボタンが配置されましたが，
これは `main.js` に下記のような2行を追加したからです．

```javascript:main.jsに追加した2行
import Vuetify from 'vuetify'
Vue.use(Vuetify)
```

この2行があるお陰で，Vuetifyがpluginとして登録され，`v-btn`などのvuetifyコンポーネントがどこからでも使用できるようになります．
`Vue.use(Plugin)` は内部的にはVue自身を引数としてinstallメソッドを呼び出すそうです．
実際にコードを辿っていくと， Vuetify内ではさらにvuetify自身が持っているボタンを始めとしたコンポーネントを
`Vue.use(SomeComponent)` の形で呼び出しています．

VBtnコンポーネントのinstallメソッドを確認すると以下のようになっています．

```javascript:node_modules/vuetify/src/components/VBtn/index.js
import VBtn from './VBtn'

/* istanbul ignore next */
VBtn.install = function install (Vue) {
  Vue.component(VBtn.name, VBtn)
}

export default VBtn
```

上記の通り，やっていることは `Vue.component` メソッドを利用して `v-btn` というコンポーネントをグローバルに登録しているだけです．
結果として， `App.vue` 内で `<v-btn>` が使えるようになったのでした．
プラグインの全部を読み込まず，特定のコンポーネントだけを利用することももちろん可能です．

## vue-router

>公式ドキュメント: https://router.vuejs.org/ja/

普段利用するウェブページはリンクを押すとURLが切り替わり，内容が入れ替わります．
この動作を模倣するために，`vue-router`というプラグインが用意されています．
`vue-router`はURLとコンポーネントを紐付け，特定の位置に紐付けられたコンポーネントを配置します．

> `<router-view>` も `<v-btn>` のようなコンポーネントと根本的には同じものです．
ただ，表示される内容がボタンではなく，別のコンポーネントを出力するproxy的な役割をしているだけなのです．

`App.vue`をよくみると `<router-view/>` というタグがあるのがわかると思います．
ここに紐付けられた別コンポーネントが配置されます．

```html:App.vue
<template>
  <v-app id="app">
    <div>
      <v-btn>マテリアルボタン</v-btn>
      <img src="./assets/logo.png">
      <router-view/>  # ←←←← router-viewコンポーネント
    </div>
  </v-app>
</template>
```

componentの紐付けは `router/index.js` で定義します．
デフォルトでは `HelloWorld` コンポーネントがルートに紐付けされているのが見て取れます．

```js:router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld,
    },
  ],
})
```

## TOPデザインの修正

>ソース: `96ab6f4` → `5f59a32`

コンポーネントやrouterの話が大丈夫そうならTOPのデザインを修正してみましょう．
vuetifyでは決まりきったデザインを用意してくれています．

>https://vuetifyjs.com/ja/layout/pre-defined

今回は`App.vue`の `<template>` をリンク先の `Default application markup` に置き換えてみます．
なお，デフォルトでは中寄せのstyleが記述されていますが，特に必要ないのでこれも消しておきます．

```html:App.vue
<template>
  <v-app id="app">
    <v-navigation-drawer app></v-navigation-drawer>
    <v-toolbar app></v-toolbar>
    <v-content>
      <v-container fluid>
        <router-view></router-view>
      </v-container>
    </v-content>
    <v-footer app></v-footer>
  </v-app>
</template>

<script>
export default {
  name: 'App',
}
</script>

<style>
</style>
```

ゴテゴテと書いてますが，名前を見るとなんとなく分かる通り，サイドバー, Header, Footerと中身(v-content）です．

### 開発サーバーで確認

現在の状態を確認すると,Headerなどの中身は何も書いてないので当然空っぽです．
なお，vuetifyは当然ながらレスポンシブデザインなので，ブラウザのサイズを小さくするとサイドバーが隠れます．

- 確認方法は

1. frontendディレクトリに移動します.
2. 開発サーバーを起動します.

```sh:開発サーバーの起動
$ npm run dev
```

>![Kobito.MlSIbZ.png](https://qiita-image-store.s3.amazonaws.com/0/58048/cae41e80-06c1-950d-433e-e1e381c9c0f2.png "Kobito.MlSIbZ.png")
>画面幅が大きい場合


>![Kobito.OMBYel.png](https://qiita-image-store.s3.amazonaws.com/0/58048/dc83c4b8-b960-7436-b839-7194efa5be63.png "Kobito.OMBYel.png")
> 画面幅が小さい場合

## 質問一覧画面の作成

ページの外側はできましたので（といっても真っ白ですが），次は内容を変えていきましょう．
Djangoチュートリアルの内容は `投票アプリ` ですので，ひとまずTOPページで質問一覧を見えるようにしてみます．

## 質問一覧コンポーネントの用意と表示

>ソース: `5f59a32` → `7b6f2f9`

作り方は人それぞれとは思いますが，私はまず見える状態のものを用意し，そこに色々追加していく方法をオススメします．
まず `frontend/src/components` の下に `Poll` ディレクトリを用意します．
ディレクトリを作成するのは，投票アプリ用のコンポーネントとして質問一覧表示，投票画面，新規質問追加など複数のコンポーネントが必要になると見込まれるからです．
この中に一覧表示用のコンポーネントとして `Index.vue` を作ります．
> django側のアプリ名が `polls` なので複数形にしたほうがいいかもしれないですが，個人的にアプリ名は単数形派なので...

- Poll/Index.vueの編集

```vue:frontend/src/components/Poll/Index.vue
<template>
  <div>
    polls
  </div>
</template>

<script>
export default {
  name: 'PollIndex',
}
</script>
```

ブラウザ上から確認できることが最優先なので内容はこれくらいで大丈夫です．

- router.js(router/index.js)を編集

今 `/` にマッピングされている `HelloWorld` を `PollIndex` に差し替えます．

```javascript:frontend/src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import PollIndex from '@/components/Poll/Index'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'PollIndex',
      component: PollIndex,
    },
  ],
})
```

- 開発サーバーで確認

ここまで書くと，殺風景ではありますがブラウザ上に `polls` の文字が表示されていると思います．

```sh:開発サーバーの起動
$ npm run dev
```

![Kobito.e4wvv8.png](https://qiita-image-store.s3.amazonaws.com/0/58048/ba92622e-8cc8-06a1-8c2e-27ddc4163021.png "Kobito.e4wvv8.png")

- ちなみにrouter/index.js内の `name: 'PollIndex'` の部分は逆引き用の名前です．

- componentが増えてきてURLで切り替えするようになったら説明します．

## APIからデータ取得

### 必要ライブラリのインストールとPollIndexの修正

>ソース: `7b6f2f9` → `ad2db08`

そのままvuetifyのきれいなデザインを追加するのもいいですが，先にサーバからデータ取得をしましょう．
ajax通信用のライブラリはいくつかありますが，今回は [axios](https://github.com/axios/axios)を使用します．

- npmで`axios`ライブラリのインストールです．

```sh
$ npm install axios --save
```

次に，先ほどのPollIndexコンポーネントにAPIの接続処理を書いていきます．
まず，データ取得用に `fetchData` メソッドを用意します．
このメソッドはページを開いた瞬間に実行したいので， `mounted` メソッドを書き，そこでfetchDataメソッドを呼び出すようにします．
fetchDataメソッドでは，第1回で作成したAPI(`http://localhost:8000/api/1.0/questions/`)からデータを取得し，
それをコンソールログに出力してみます．
ソースコードとしては以下のような感じになります．

- poll/Index.vueの編集

```vue:frontend/src/components/Poll/Index.vue
<script>
import axios from 'axios'

export default {
  name: 'PollIndex',
  methods: {
    fetchData () {
      axios.get('http://localhost:8000/api/1.0/questions/').then(res => {
        console.log(res)
      })
    },
  },
  mounted () {
    this.fetchData()
  },
}
</script>
```

> Vueには[ライフサイクルフック](https://jp.vuejs.org/v2/guide/instance.html#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF)が用意されており，特定のタイミングで処理を挟むことができます．
今回はページを開いた時にAPIでデータを取得したいので `mounted` に処理を挟みました．
詳しい処理のタイミングや，フックポイントを知りたい方は[ライフサイクルダイアグラム](https://jp.vuejs.org/v2/guide/instance.html#%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%A9%E3%83%A0)を確認してください．

- 開発サーバーを起動してブラウザから確認

1. `frontend`ディレクトリに移動
2. `$ npm run dev` で開発サーバーの起動

chromeなどでConsoleログを開くと下記のようなエラーが出ているはずです．

```
Failed to load http://localhost:8000/api/1.0/questions/: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://localhost:8080' is therefore not allowed access.
```

> `GET http://localhost:8000/api/1.0/questions/ net::ERR_CONNECTION_REFUSED` とエラーが出ている方はDjangoのサーバが起動していません．

- バックエンド(Django)側のサーバーの起動

1. `workon` で仮想環境に入ります。
2. `(tutorial-spa) $python manage.py runserver` でサーバを起動しましょう．

---

- `venv`で仮想環境を起動するには

作業ディレクトリ(tutorial-spa) → Django環境(tutorial-spa)の順に`cd`で移動します.その下に`bin`ディレクトリがあるはずです.確認し、

 ```sh:仮想環境の起動
 $ source bin/activate
 ```

---

JavaScriptにはセキュリティのため **CORS** (Cross-Origin Resource Sharing) という仕様が定められており，ブラウザ上から異なるサーバへアクセスする際に，特別なヘッダーが必要になります．

今回のチュートリアルでは，Djangoは `localhost:8000`，Vueは `localhost:8080` で起動しているため，同じlocalhostですが，ポートが違うと別サーバと見なされ,でエラーが発生しています．

### Django側の修正

>ソース: `ad2db08` → `fdb7c34`

CORS用のヘッダーはサーバ側，つまりDjango側から送信する必要があります．
というわけで，今度はDjango側の修正です．

ライブラリのインストールがあるので，忘れずに仮想環境の起動しておきましょう．

CORS対応もWebサーバとしてはほぼ当たり前の起動ですので，もちろんDjango用のライブラリが用意されています．

```sh
(tutorial-spa) $ pip install django-cors-headers
```

- /tutorial/settings.pyの編集

修正箇所は3箇所で，
1. INSTALLED_APPSに`corsheaders`を追加，
2. MIDDLEWAREに`corsheaders.middleware.CorsMiddleware`追加，
3. `CORS_ORIGIN_ALLOW_ALL = True` を追加，
です．

```python:tutorial/settings.py
INSTALLED_APPS = [
...
    'corsheaders', 
...
]

MIDDLEWARE = [
...
    'corsheaders.middleware.CorsMiddleware',
...
]

...

CORS_ORIGIN_ALLOW_ALL = True
```

ライブラリをインストールしたので，忘れずに 作業ディレクトリにある`requirements.txt` と `constraints.txt` を更新しておきましょう．

```sh
(tutorial-spa) $ echo 'django-cors-headers' >> requirements.txt
```
```sh
(tutorial-spa) $ pip freeze > constraints.txt
```

[解説]
> `>>` はファイルの末尾に追加する，というシェルのリダイレクト表記です．

> エディタで追加してももちろん大丈夫です．

### 再度開発サーバーで確認

修正が完了したらブラウザを更新してみましょう．
ちゃんと修正できていればブラウザのコンソールログに下記のように表示されるはずです．

- vue側のサーバーの起動

1.frontendディレクトリに移動
```sh:サーバーの起動
npm run dev
```

- Django側のサーバの起動

1. 仮想環境の起動
2. manage.pyがある階層に移動
3. サーバーの起動

```sh:サーバーの起動
$ python manage.py runserver
```

![Kobito.ciVDM9.png](https://qiita-image-store.s3.amazonaws.com/0/58048/0bac61b5-cf88-ac44-5510-1ee31edbb421.png "Kobito.ciVDM9.png")

### Responseの修正(ページネーションの追加)

>ソース: `fdb7c34` → `3ee9bcf`

ここまでで取り敢えずデータは取得出来ていますが，内容が配列で直接返ってます．
APIの戻り値としてはトータルの件数や，次のリンクなどが欲しいところです．

ページングの設定，ページネーターはDRFで標準で用意されているので，デフォルト設定を変更してあげれば完了です．
有効にするには，ページサイズ(一回あたりの最大取得件数)と，ページネーションクラス(どういう形式で返すかを定義したクラス)を設定する必要があります．
今回は`LimitOffsetPagination`にします．

```python:tutorial/settings.py
# =========================
# django-restframework 設定
# =========================
REST_FRAMEWORK = {
    'PAGE_SIZE': 100,
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
}
```

### データの確認

停止していたら、vue側、Django側のサーバーを起動します.

- 以下をクリックしてレコードを確認します.  

 [http://localhost:8000/api/1.0/questions/](http://localhost:8000/api/1.0/questions/)

>![Kobito.ITDkIC.png](https://qiita-image-store.s3.amazonaws.com/0/58048/bf043fc0-4e45-f3df-8201-89d996505d02.png "Kobito.ITDkIC.png")
設定前

>![Kobito.z8Cw5i.png](https://qiita-image-store.s3.amazonaws.com/0/58048/59e39f0b-b719-72a4-5a85-a26207cb0794.png "Kobito.z8Cw5i.png")
設定後

[解説]
設定前は結果が配列でしたが，設定後は辞書になり，内容が `results` の中に入っていることがわかります．
設定した件数（今回の設定では100件）以上の件数がある場合， `next` の中に次の100件を取得するためのURLが返ってきます．
また，101〜200件目を表示しているときには，`prev` の中に前の100件を取得するためのURLが返ってきます．
`count` は総数です．

### Responseの修正(キャメルケースへの変換)

>ソース: `3ee9bcf` → `3124f4b`

`pub_date` や `question_text` など，変数名がスネークケースですが，JavaScript的には変数名はキャメルケースが望ましいです．
APIのクライアントによってはスネークケースで欲しい場合もあるため，js側で変数名をキャメルケースに戻してもいいです．
ただ，django側で処理をするなら，これもライブラリで用意されていて手間がかからないので，今回はdjango側で変換することにします．

変換用のライブラリは `djangorestframework_camel_case` です．
例のごとくpipでインストールします．

```sh
(tutorial-spa) $ pip install djangorestframework_camel_case
```

このライブラリを使って，DRFの出力時（＝レンダリング）の処理を変更します．
また，JavaScriptからの入力も恐らくキャメルケースでしょうから，入力時（＝パース）の処理も変更します．
先ほどページネーションの処理を加えたように，RENDERERとPARSERを追加してあげるだけです．

```python:tutorial/settings.py
# =========================
# django-restframework 設定
# =========================
REST_FRAMEWORK = {
    'PAGE_SIZE': 100,
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',

# === 以下を追加

    'DEFAULT_RENDERER_CLASSES': (
        'djangorestframework_camel_case.render.CamelCaseJSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ),
    'DEFAULT_PARSER_CLASSES': (
        'djangorestframework_camel_case.parser.CamelCaseJSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
    ),

# === ここまで

}
```

再度、vueおよびDjangoのサーバーを起動します。

- 以下をクリックしてレコードを確認します.

 [http://localhost:8000/api/1.0/questions/](http://localhost:8000/api/1.0/questions/)

これで戻り値の変数名がキャメルケースになります．
![Kobito.3vDNuH.png](https://qiita-image-store.s3.amazonaws.com/0/58048/76e6ac1e-a666-51c6-369a-f16a41e9c585.png "Kobito.3vDNuH.png")


ライブラリをインストールしたので，再びrequirements.txtとconstraints.txtを更新しておきましょう．

```sh
(tutorial-spa) $ echo djangorestframework_camel_case >> requirements.txt
```
```sh
(tutorial-spa) $ pip freeze > constraints.txt
```

### データ取得, 表示

>ソース: `3124f4b` → `5592d25`

APIからのデータがきれいになったので，再度フロントエンドに戻って表示させていきます．
取得した質問一覧を入れるために， `data` に `questions` を用意し，
apiの通信完了時に`res.data.results`を代入させます．

```frontend/src/components/Poll/Index.vue
<script>
import axios from 'axios'

export default {
  name: 'PollIndex',

# === 以下を追加　=== 

  data () {
    return {
      questions: [],
    }
  },

# === ここまで

  methods: {
    fetchData () {
      axios.get('http://localhost:8000/api/1.0/questions/').then(res => {

# === 以下を編集

        this.questions = res.data.results

# === ここまで

      })
    },
  },
  mounted () {
    this.fetchData()
  },
}
</script>
```

続いてtemplate部を編集し，内容を表示させてみます．
出し方は色々ありますが，今回はカードスタイルで出してみます．

>https://vuetifyjs.com/ja/components/cards

```html:src/components/Poll/Index.vue
<template>
  <div>
    <v-card>
      <v-container grid-list-lg>
        <v-layout row wrap>
          <v-flex xs6 v-for="data in questions" :key="data.id">
            <v-card color="primary" class="white--text">
              <v-card-title primary-title>
                <div>{{ data.questionText }}</div>
              </v-card-title>
              <v-card-text>
                <div>{{ data.pubDate }}</div>
              </v-card-text>
            </v-card>
          </v-flex>
        </v-layout>
      </v-container>
    </v-card>
  </div>
</template>
```

### サーバーで確認

これで質問内容がカードのように並びます．

![Kobito.ur6FR6.png](https://qiita-image-store.s3.amazonaws.com/0/58048/ab5a8921-fad8-d819-0aa9-001d1ac188fd.png "Kobito.ur6FR6.png")

- `<template>` 内のデザインについては[vuetify](https://vuetifyjs.com/en/components/api-explorer)のサイトからコンポーネントの使い方を色々試してみてください．

- デザインがどうでもいいなら `<div>` や `<li>` で並べるだけでも大丈夫です．

### 日付フォーマットの修正

>ソース: `5592d25` → `26e0b3a`

apiからの公開日（日時型）の戻り値は `2018-04-03T17:59:57+09:00` のような表示になっていると思います．
これはisoformatと呼ばれ，ISO8601で定められた国際規格の形式です．
年月日時間に加え，タイムゾーン情報も付いており，世界中どこでも使用できますが，少し見辛いです．

JavaScriptではこのような文字列から日時型のオブジェクトを生成し，表示や計算を行うための[momentjs](https://momentjs.com/)というライブラリがあります．
今回はmonent.jsを使用して公開日のフォーマットを整えてみます．

ライブラリはnpmでインストールします．

```sh
$ npm install moment --save
```

次にscript内で，日付型のフォーマットを整える処理を書きます．
methodsに定義する方法とfiltersに定義する方法がありますが，今回はfiltersに書きます．
methodsに定義したmethodは，template内から `{{ methodName(val) }}` のように呼び出し可能です．
また，filtersで定義したmethodは `{{ val|filterMethod }}` のように呼び出し可能です．
methodは引数の数がいくつでも大丈夫ですが，filterのほうは1つだけしか値を受け取ることができません．
その代わり，filterのほうは適用したい値の後ろにパイプ(`|`)で繋ぐ形式なので，わかりやすく書きやすいです．

filtersへの定義は以下のような感じになります．

```src/components/Poll/Index.vue
<script>
import axios from 'axios'

# === 以下を追加

import moment from 'moment'

# === ここまで

export default {
...
# === 以下を追加

  filters: {
    printDate (val) {
      return moment(val).locale('ja').format('YYYY年MM月DD日(ddd) HH時mm分ss秒')
    },
  },

# === ここまで
...
</script>
```

`moment` に文字列を渡すことで内部的に日時型のオブジェクトに変換してくれます．
今回はisoformatなので間違いないですが，もう少し曖昧な形式でもよしなにパースしてくれます．
また，引数を与えずに `moment()` とした場合は現在日時を示すオブジェクトを生成します．

その後ろで `.locale('ja')` は出力時の言語を日本語に設定しています．
formatでmomentオブジェクトを適切な文字列に変換します．
`YYYY` なら4桁の西暦， `ddd` は曜日の短縮形(日，月，火...)など，いくつかルールがあります．

>もっと詳しい説明が知りたいかたは
[公式サイト](https://momentjs.com/docs/#/displaying/)を参照してください．


filterが書けたらtemplate側を修正しましょう．
上記で書いた通り，filterの呼び出しは `{{ val|filterMethod }}` の形式で書きます．

```html:src/components/Poll/Index.vue
...
<div>{{ data.pubDate|printDate }}</div>
...
```

ブラウザ上で日時フォーマットが適切に設定できていることを確認しましょう．

![Kobito.bXljlT.png](https://qiita-image-store.s3.amazonaws.com/0/58048/94053dc5-bb6e-bb5c-97dd-1dcf4820e22c.png "Kobito.bXljlT.png")

--- 

次はサーバ側にAPIを追加し，投稿処理を行えるようにしてみます．
→ [チュートリアル4へ](https://qiita.com/maisuto/items/9c44a914dae57bf187f9)

→ [チュートリアルまとめ](http://qiita.com/maisuto/items/17653f344d1f64afa019)