DjangoとVueでカンバンアプリケーションを作る(3)

※はじめからは[こちら](https://qiita.com/denzow/items/046f3c8b9bd8d3378eb4#_reference-7b3e4a58510793f8a9c7)

今回はログインに応じたヘッダーメニューの変更を実装していきます。

<img width="1379" alt="KANBAN.png" src="https://qiita-image-store.s3.amazonaws.com/0/44722/aedaf0cf-f684-c8c9-1f0d-08431d84e32e.png">


### ログイン状態を戻すAPIの実装

現在ログインしているアカウント情報を戻すAPIを実装していきます。DjangoでAPIを作る場合は以下の2つを行います。

* 必要な結果を戻すViewロジックを実装
* urls.pyに作成したViewとURLのマッピングを登録

順番に作っていきます。

#### 必要な結果を戻すViewロジックを実装

`application/views/api`をまずは作成します。あわせて`application/views/api/__init__.py`も空ファイルとして作成し、Pythonのモジュールとして認識させます。

その上で、以下のファイルを作成します。

```python:application/views/api/accounts.py
from django.http import JsonResponse
from django.views.generic import View


class AccountsApi(View):

    def get(self, request):
        """
        認証済であればログイン情報を戻す
        """
        account = request.user
        # ログイン済みかのチェック
        if account.is_authenticated:
            return JsonResponse({
                'account_info': {
                    'account_id': account.id,
                    'name': account.username,
                }
            })

        return JsonResponse({
            'account_info': None,
        })

```

ログインしていれば以下のようなJSONが戻されるAPIです。

```json
{"account_info": {"account_id": 2, "name": "denzow"}}
```

#### urls.pyに作成したViewとURLのマッピングを登録

続いて、作成した`AccountsApi`を`/api/accounts/`でアクセスできるようにします。

```diff:application/views/urls.py
 from django.views.generic import TemplateView

 from .accounts import signup as signup_view
+from .api.accounts import AccountsApi


 urlpatterns = [
     path('accounts/signup/', signup_view.SignUpView.as_view(), name='signup'),
     path('accounts/', include('django.contrib.auth.urls')),
     path('admin/', admin.site.urls),
+
+    path('api/accounts/', AccountsApi.as_view())
 ]
 urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
```

ブラウザでアクセスして、JSONが戻ってくればOKです。


### ログイン状態をUIに反映

先程作成したAPIをフロント側に組み込んでいきます。このチュートリアルではVueを使っています。ヘッダー部分は`application/vuejs/src/components/layouts/MyHeader.vue`というファイルになっています。ここで、ログイン名がとれていばそれを表示するといったロジックを組み込んでいきます。

#### API Clientの作成
まずはAPIにアクセスするユーティリティを作ります。

```javascript:application/vuejs/utils/kanbanClient.js
import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar';
import camelcaseKeys from 'camelcase-keys';


const _handleSuccess = response => {
  console.log(response.data);
  // すべてのJSONのキーをスネークケースからキャメルケースに変換する
  response.data = camelcaseKeys(response.data, { deep: true });
  return response;
};

class Client {
  constructor() {
    this.service = axios.create();
    this.service.interceptors.response.use(_handleSuccess);
    loadProgressBar({ showSpinner: false }, this.service);
  }

  _get(path, payload) {
    return this.service.get(path, payload);
  }

  _patch(path, payload) {
    return this.service.patch(path, payload);
  }

  _post(path, payload, config = {}) {
    return this.service.post(path, payload, config);
  }

  _put(path, payload) {
    return this.service.put(path, payload);
  }

  _delete(path) {
    return this.service.delete(path);
  }
}
```

`axios`というjavascriptのHTTPClientライブラリがありますが、その薄いラッパークラスを作成します。これはPythonはスネークケース、JSはキャメルケースが基本なのでその変換を噛ませたりするためです。

続いて、追加したコードのさらに下に以下のように作成したAPIのClientを作ります。

```javascript:application/vuejs/utils/kanbanClient.js
class KanbanClient extends Client {
  constructor() {
    super();
    this.baseUrl = '/api';
  }

  async getAccountInfo() {
    const response = await this._get(`${this.baseUrl}/accounts/`);
    return response.data.accountInfo;
  }
}


export default new KanbanClient();
```

これで、`getAccountInfo`を呼び出すと`/api/accounts/`にアクセスし、その結果をキャメルケースに変換したものが戻されるようになります。

#### Store(vuex)の準備

作成したAPI Clientを使うStoreを作っていきます。Vueはあるコンポーネント内に閉じた状態であれば、そのコンポーネント内の変数として保持するだけでいいのですが、コンポーネントをまたがったりする場合にはStoreというものを用意して、それを参照するようにします。コンポーネント間で共有できる大きな変数のイメージです。

また、Storeはモジュールに分けることができるのでHeader用のStoreはheaderという名前で作成します。

```javascript:vuejs/src/store/header.js
import KanbanClient from '../../utils/kanbanClient';


const state = {
};

const actions = {
};

const mutations = {
};


export default {
  namespaced: true,
  state,
  actions,
  mutations,
};

```

まずはimportだけするStoreの雛形を用意しています。Storeはだいたい3つの要素を持ちます。

|name|role|
|-----|-----|
|state |現在のStoreの状態を示すもの |
|actions |何らかの非同期を含む処理、最終的にmutationを呼び出すことが多い(stateを変更することは許されていない) |
|mutations |stateへの変更処理。非同期処理を含んではならない |

コンポーネントからはActionを呼び出し、その結果をMutationに伝えてStateを更新する流れになることが多いです。このあたりは実装しながら更に見ていくことにします。

なお、このままではこのStoreは使えないのでグローバルなStore(store/index.js)に登録します。

```diff:application/vuejs/src/store/index.js
 import Vuex from 'vuex';
 import createLogger from 'vuex/dist/logger';

+import header from './header';

 Vue.use(Vuex);

@@ -11,6 +12,7 @@ export default new Vuex.Store({
     ? [createLogger()]
     : [],
   modules: {
+    header,
   },
   state: {
   },
```

あとはheaderのStoreを実装していきます。

#### StoreからのAPIClientの利用

headerのStoreを考えると、取得したアカウント情報を保持する必要があると思われるので、Stateに枠を用意します。

```javascript:vuejs/src/store/header.js
const state = {
  accountInfo: null,
};
```

この`accountInfo`に値をセットするには、今回作成したKanbanClient(APIClient)の`getAccountInfo`を実行すれば良いことがわかります。この処理は非同期処理なのでActionに組み込みます。


```javascript:vuejs/src/store/header.js
const actions = {
  async fetchAccountInfo({ commit }) {
    const accountInfo = await KanbanClient.getAccountInfo();
    commit('setAccountInfo', accountInfo);
  },
};
```

`commit('setAccountInfo', accountInfo);`の部分は取得した`accountInfo`をもって、mutationの`setAccountInfo`を呼び出すという意味です。ということでmutationも実装します。


```javascript:vuejs/src/store/header.js
const mutations = {
  setAccountInfo(state, accountInfo) {
    state.accountInfo = accountInfo;
  },
};
```

受け取った`accountInfo`を`state.accountInfo`にセットするだけです。これで`fetchAccountInfo`を呼び出すと、APIからログイン情報を取得しStoreに反映させるコードができました。


#### ComponentからActionの利用

先程作成したActionを`MyHeader`の初期化時に呼び出すようにしていきます。まずは、Storeの値をコンポーネント自身の属性のように割り当てるための`mapState,mapActions`の準備をします。

```diff:application/vuejs/src/components/layouts/MyHeader.vue
 <script>
+import { createNamespacedHelpers } from 'vuex';
+
+const { mapState, mapActions } = createNamespacedHelpers('header');

 export default {
   name: 'MyHeader',
```

`createNamespacedHelpers('header')`で`header`というStoreの値をうまく扱うためのヘルパー関数である`mapState, mapActions`を手に入れます。`mapState`はStoreのstateを、`mapActions`はStoreのActionsとのマッピングを行うためのものです。続いてこれらのヘルパーを使います。


```diff:application/vuejs/src/components/layouts/MyHeader.vue
 export default {
   name: 'MyHeader',
+  computed: {
+    isLoggedIn() {
+      return this.accountInfo !== null;
+    },
+    ...mapState(['accountInfo']),
+  },
+  methods: {
+    ...mapActions(['fetchAccountInfo']),
+  },
 };
 </script>
```

`mapState`は`computed`、`mapActions`は`methods`の中で使用します。それぞれ割り当てたいStoreの属性名を文字列とした配列を受け取ります。ここでは`state. accountInfo`と`state. fetchAccountInfo`を指定しています。また、ログインしているかどうかを示すため`accountInfo`が空かを戻す`isLoggedIn`というメソッドも定義しておきます。

なお`...`はスプレッド演算子と呼ばれるもので、指定した変数を展開して代入するような動きをします。

```javascript:例
> a = {a:1}
{ a: 1 }
> b = {b:2, a}
{ b: 2, a: { a: 1 } }
> b = {b:2, ...a}
{ b: 2, a: 1 }  // aが展開されている
```

さて、割当はできましたので使っていきます。以下のように`created`で`fetchAccountInfo`を呼び出すようにすることで、このMyHeaderコンポーネントが初期化される際に実行されるようになります。

```javascript:application/vuejs/src/components/layouts/MyHeader.vue
export default {
  name: 'MyHeader',
  computed: {
    isLoggedIn() {
      return this.accountInfo !== null;
    },
    ...mapState(['accountInfo']),
  },
  methods: {
    ...mapActions(['fetchAccountInfo']),
  },
  created() {
    this.fetchAccountInfo();
  },
};
```

最後に、取得している値等を表示に利用するようにします。


```diff:application/vuejs/src/components/layouts/MyHeader.vue
@@ -1,9 +1,13 @@
 <template>
   <nav class="navbar navbar-expand-lg navbar-light bg-light">
-    <router-link class="navbar-brand" to="/">Django</router-link>
+    <router-link class="navbar-brand" to="/">KANBAN</router-link>

     <div class="collapse navbar-collapse" id="navbarNavDropdown">
-      <ul class="navbar-nav ml-auto">
+      <ul class="navbar-nav ml-auto" v-if="isLoggedIn">
+        <span class="navbar-text mr-1">Welcome to {{ accountInfo.name }}</span>
+        <a class="btn btn-outline-primary" href="/accounts/logout/">Logout</a>
+      </ul>
+      <ul class="navbar-nav ml-auto" v-if="!isLoggedIn">
         <li class="nav-item">
           <a class="nav-link" href="#">Register</a>
         </li>
```

`v-if="isLoggedIn"`の箇所により、ログイン済であれば、`Welcome to test`といったメッセージとログアウトボタンをだし、そうでなければRegsiter/Loginのリンクを出すコードになりました。

再度`http://localhost:3000/`にアクセスし以下のようにログインアカウント名が出れば成功です。

<img width="338" alt="KANBAN.png" src="https://qiita-image-store.s3.amazonaws.com/0/44722/184f7f84-edfc-6b53-681a-3e6359719e53.png">


### 次回

https://qiita.com/denzow/items/32b87ab6fe0c78a04cbd