【Django】DjangoによるWeb開発プロジェクトPart6 - 例外処理とVuexでフロントエンド状態管理パターンを整理する -

## はじめに

リリースに向けての最低限の機能は出来上がってきそうなので、ここからはちゃんと例外処理を進めるための実装を行なっていきます。

## HTTPレスポンスの拡張

Djangoによる例外はPythonの例外の他にDjango独自の例外もあります。以前まで利用していた`Http404`は[HttpResponseNotFound](https://docs.djangoproject.com/en/2.1/topics/http/views/#the-http404-exception)を返す例外です。流石に404だけでは辛くなってきたので、HTTPResponseメソッドを各々の例外に合わせて修正するようにします。

```python
class HttpResponseBase:
    """
    An HTTP response base class with dictionary-accessed headers.

    This class doesn't handle content. It should not be used directly.
    Use the HttpResponse and StreamingHttpResponse subclasses instead.
    """

    status_code = 200

    def __init__(self, content_type=None, status=None, reason=None, charset=None):

[docs]class HttpResponse(HttpResponseBase):
    """
    An HTTP response class with a string as content.

    This content that can be read, appended to, or replaced.
    """

    streaming = False
```

HttpResponseBaseに対応するステータスコードとメッセージを渡しておきます。

```django:views.py
    except (***.errors.***):
        return HttpResponse('%s is already running' % ****, status=409)
```

returnで返したResponseをVue側でcatchできるようにします。今回はとりあえず、ステータスコードを見て紐づくエラーメッセージを返せるようにします。

```vue:vm_start.vue
<template>
  <button type="button" class="btn btn-success" @click="vm_start">Start</button>
</template>

<script>
    import axios from 'axios'

    axios.defaults.xsrfCookieName = 'csrftoken'
    axios.defaults.xsrfHeaderName = 'X-CSRFToken'
    export default {
        name: 'VM-Start',
        props: {
            vm_name: String
        },
        methods: {
            vm_start: function () {
                axios.put('http://0.0.0.0:8080/****/start', {
                    vm_name: this.vm_name
                })
                    .then(response => {
                        console.log(response)
                    })
                    .catch(error => {
                        switch (error.response.status) {
                            case 409:
                                alert(error.response.data)
                        }
                    })
            }
        }
    }
</script>

<style scoped>

</style>
```

以上でひとまず、404以外のステータスコードに対応する事ができました。続いて、Vueの改善を行います。

## Vuex入門

これまで、Vueによる単一ファイルコンポーネントでフロントエンドを開発していましたが、そろそろVuexを使った開発を行おうと思います。
そもそも、単一ファイルコンポーネントでは3つの要素を含んでいます。
下記は[Vuex とは何か？](https://vuex.vuejs.org/ja/)からの引用です。
```
* 状態、これは私達のアプリを動かす信頼できる情報源(the source of truth)です。
* ビュー、これは状態のただの宣言的なマッピングです。
* アクション、これはビューからのユーザー入力に反応して、状態の変更を可能にする方法です。
```

![Vueアーキテクチャ](https://vuex.vuejs.org/flow.png)

このような三つの要素が回るサイクルモデルなため、比較的単純に実装ができる訳ですが、`共通の状態を共有する複数のコンポーネントを持ったときに、すぐに破綻します。`
そこで、Vuexを利用して`各コンポーネントから共有している状態を抽出し、管理するようにします。`

### Vuexインストール

Vuexをyarnパッケージマネージャからインストールします。また、`Promise`も必須になってくるため、一緒にインストールします。

```bash
% yarn add vuex
% yarn add es6-promise
```

Vuexのアーキテクチャは下記のようになります。
まずは、storeから作成していきます。

![Vuexのアーキテクチャ](https://vuex.vuejs.org/vuex.png)

### Storeの作成

下記は

Vuex アプリケーションの中心にあるものはストアです。"ストア" は、基本的にアプリケーションの 状態（state） を保持するコンテナです。
グローバルオブジェクトは以下のような違いがあります([Vuex 入門](https://vuex.vuejs.org/ja/guide/)から引用)

```
* Vuex ストアはリアクティブです。Vue コンポーネントがストアから状態を取り出すとき、もしストアの状態が変化したら、ストアはリアクティブかつ効率的に更新を行います。

* ストアの状態を直接変更することはできません。明示的にミューテーションをコミットすることによってのみ、ストアの状態を変更します。これによって、全ての状態の変更について追跡可能な記録を残すことが保証され、ツールでのアプリケーションの動作の理解を助けます。
```

VuexをインストールしたらシンプルなStoreとMutationを作成します。MutationはStoreを更新できる唯一のコンテナです。今回のシステムではaxiosでjsonレスポンスを返しているため、このstate.vmsにレスポンスデータを入れるようにします。またAPIサーバー名を指定できるように、もう一つstateを指定しておきます(本当は不変な定数として扱いたいので、ここに入れるのは違うような気もするのですが、、、)

```js:store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export const store = new Vuex.Store({
    state: {
        serverName: 'http://localhost',
        vms: {}
    }
})
```

### ミューテーションの作成

Vuexのストアを更新できる唯一の方法はミューテーションをコミットします。以下は、[ミューテーション](https://vuex.vuejs.org/ja/guide/mutations.html)からの引用です。

```
各ミューテーションはタイプとハンドラを持ちます。ハンドラ関数は Vuex の状態（state）を第1引数として取得し、実際に状態の変更を行います:
ここではindexページに存在するボタンを押すとvmsリストを取得するGETリクエストをもう一度送信するメソッドを定義します。
```

また、ミューテーションのハンドラは定数として定義する事もできます。ハンドラを定数とする事で、Vueアプリケーションを全体的に見た時にどのミューテーションが利用できるのかが理解しやすくなります。

```js:store/mutation-type.js
export const RELOAD_VMS = 'RELOAD_VMS'
```

また、ハンドラは、追加の引数を渡すこともできます。今回は、indexページを再更新するために、state.vmsをaxiosのGETリクエストで取得した新たなvmsで置き換えるようなミューテーションを作成しました。

```js:store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { RELOAD_VMS } from'mutation-type.js'
Vue.use(Vuex)

export const store = new Vuex.Store({
    state: {
        serverName: 'http://localhost',
        vms: {}
    },
    mutations: {
        [RELOAD_VMS] (state, vms) {
            state.vms = vms
        }
    }
})
```

最後にこのミューテーションを呼び出すためのアクションを最後に作成します。

### アクションの作成

アクションは、ミューテーションと似ているところがありますが、ミューテーションを`commit`をトリガーとして実行します。また、ミューテーションが同期的でなければならないのに対し、アクションは任意の非同期処理を含むことができます。　そのため、ミューテーションをコミットする前に、アクションの中で非同期な操作を行うことができます。


先ほどのstore/index.jsにactionハンドラを追加します。

```js:store/index.js
    actions: {
        async reload_vms(context) {
            let vms = {};
            await axios.get(this.state.serverName + '/vms/fetch_vms', {})
                .then(response => {
                   vms = response.data
                });
            context.commit(types.RELOAD_VMS, vms);
        }
    }
```

このアクションハンドラをコンポーネントでdispatchをトリガーとして呼ぶ事でアクションハンドラを実行することができます。

```js:components/****.vue
    export default {
        name: '****',
        components: {
            'vm-delete': VmDelete,
            'vm-start': VmStart,
            'vm-stop': VmStop,
        },
        created: function () {
            this.$store.dispatch('reload_vms')
        }
    }
```

各vueファイルでdispatchを実行する事でactionハンドラを実行できます。

```js:vm_start.vue
    import axios from 'axios'

    axios.defaults.xsrfCookieName = 'csrftoken'
    axios.defaults.xsrfHeaderName = 'X-CSRFToken'
    export default {
        methods: {
            vm_start: function () {
                axios.put(this.$store.state.serverName + '/****/start', {
                })
                    .then(response => {
                        console.log(response)
                        this.$store.dispatch('reload_vms')
                    })
                    .catch(error => {
                        alert(error.response.data)
                    })
            }
        }
    }
```

### 動作確認

![jails.gif](https://qiita-image-store.s3.amazonaws.com/0/144991/5fcf2c48-fee9-ff16-5b4f-4c3526af113b.gif)

## まとめ

α版リリースの記事をそろそろ執筆します。

## シリーズ

[Part5](https://qiita.com/himrock922/items/001d1a58d7ed61270bf7) ← [Part6](https://qiita.com/himrock922/items/5ea559e8d006449f6145) → [Part6.5](https://qiita.com/himrock922/items/5b8f39a61de9b640d840)

## 参考文献

* [Source code for django.http.response](https://docs.djangoproject.com/en/2.1/_modules/django/http/response/#HttpResponse)
* [Vuex とは何か？](https://vuex.vuejs.org/ja/)
* [Vuex 入門](https://vuex.vuejs.org/ja/guide/)
* [ミューテーション](https://vuex.vuejs.org/ja/guide/mutations.html)
* [アクション](https://vuex.vuejs.org/ja/guide/actions.html)