Django REST framework with Vue.js

# <i class="fa fa-forward" style="font-size:1em;"></i> はじめに
**Django REST framework**と**Vue.js**が気になったので、組み合わせて動かしてみた。

# <i class="fa fa-graduation-cap" style="font-size:1em;"></i> 構成・動作概要
Djangoのhtmlのテンプレート機能は使わずに、静的ファイルとしてHTMLデータを返却し、ブラウザからVue.jsでRestAPI経由でデータを取得した情報の一覧を表示してみる。
（DBはSQLite3、サンプルデータは自分の記事のストック数を使った。）

![vuesとrest.png](https://qiita-image-store.s3.amazonaws.com/0/82760/85115f0f-1589-8981-d5db-ec5fb5fa0460.png)

# <i class="fa fa-cogs" style="font-size:1em;"></i> 動作環境

Pythonは**Python 3.5.1**
パッケージは以下

    # pip3 freeze
    Django==1.10.5
    django-filter==1.0.1
    djangorestframework==3.5.3

Vue.jsとaxiosはCDNで取ってきていて、投稿時点（2017/1/30）のバージョンは以下。

- Vue.js: 2.1.10
- axios: 0.15.3

# <i class="fa fa-play" style="font-size:1em;"></i> 試す

## とりあえずDjangoプロジェクト作成
いつもの。

    # django-admin startproject qiitalist
    # cd qiitalist/
    # python manage.py startapp stocks

## 完成形
こんな感じの構成になった。①～⑥個別に実装を見ていきます。（▲=今回使わないファイル）

    qiitalist
    │  fixture.json # ②モデル追加
    │  manage.py
    │
    ├─qiitalist
    │  │  settings.py # ①設定追加
    │  │  urls.py # ⑤URL設定追加
    │  │  wsgi.py # ▲
    │  │  __init__.py
    │  │
    │  └─static # ⑥静的ファイル作成
    │          vue_grid.css
    │          vue_grid.html
    │          vue_grid.js
    │
    └─stocks
        │  admin.py # ▲
        │  apps.py # ▲
        │  models.py # ②モデル追加
        │  tests.py # ▲
        │  serializer.py ③シリアライザ追加
        │  urls.py  # ⑤URL設定追加
        │  views.py # ④View追加
        │  __init__.py
        │
        └─migrations

### ①設定追加
実装するアプリと、REST Frameworkの設定を追加。

```settings.py
# 追加分だけ抜粋
# アプリの追加
INSTALLED_APPS += [
    'stocks',
    'rest_framework',
]
# REST API設定（フィルタ、ページング）
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 5
}
# views.pyで使うパスを定義
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
STATICFILES_DIRS = (os.path.join(PACKAGE_ROOT, 'static'),)
```

### ②モデル追加
REST APIで公開するデータモデル、及び初期データ（フィクスチャ）を準備。

```models.py
from django.db import models


class Stock(models.Model):

    class Meta:
        db_table = "stock"
    id = models.AutoField(primary_key=True)
    title = models.TextField()
    stock_count = models.IntegerField()
```
```json:fixture.json
[
  {
    "model": "stocks.stock",
    "pk": 1,
    "fields": {
      "title": "Pythonで機械学習はじめました(Qiitaへの投稿もはじめました) データ準備編",
      "stock_count": 29
    }
  },
  {
        "model": "stocks.stock",
        "pk": 2,
        "fields": {
          "title": "ScrapyとDjangoでラーメンマップ作成",
          "stock_count": 23
     }
  },
  {
      "model": "stocks.stock",
      "pk": 3,
      "fields": {
        "title": "Pythonで機械学習はじめました データ前処理編",
        "stock_count": 22
     }
  },
  {
       "model": "stocks.stock",
       "pk": 4,
       "fields": {
         "title": "PythonでRabbitMQメッセージの通知アプリ with Growl ～ラズパイとJuliusを添えて～",
         "stock_count": 3
     }
   }
]
```

### ③シリアライザ追加
モデルをシリアライズするSerialzierの定義を追加。

```serializers.py
from rest_framework import serializers
from stocks.models import Stock


class StockSerializer(serializers.ModelSerializer):

    class Meta:
        model = Stock
        fields = ("id", "title", 'stock_count')
```

### ④View追加
Viewの定義を追加。

```views.py
import os

from django.conf import settings
from django.http.response import HttpResponse
from rest_framework import viewsets

from stocks.models import Stock
from stocks.serializer import StockSerializer

# 静的ファイルを返すView
def index(_):
    # render等でDjangoのtemplateとして処理すると「{{}}」がVue.jsに渡る前消えてしまう。
    # 良い解決方法が浮かばなかったので、static配下に置いたファイルをopenして投げることで回避。
    html = open(
        os.path.join(settings.STATICFILES_DIRS[0], "vue_grid.html")).read()
    return HttpResponse(html)

# RestAPIのviewsets
class StockViewSet(viewsets.ModelViewSet):
    queryset = Stock.objects.all()
    serializer_class = StockSerializer
    # APIのフィルタで使えるフィールドを指定
    filter_fields = ("id", "title", 'stock_count')
```

### ⑤URLの設定追加
親（qiitalist）と子（stocks）で一応分割。

```py:urls.py(qiitalist)
from django.conf.urls import include, url
from stocks.urls import urlpatterns as qiitalist_url

urlpatterns = [
    url(r'^qiita/', include(qiitalist_url)),
]

```

```py:urls.py(stocks)
from django.conf.urls import include, url
from rest_framework import routers

from stocks.views import StockViewSet, index

router = routers.DefaultRouter()
router.register(r'stock', StockViewSet)

urlpatterns = [
    # qiita/api/stock/
    url(r'api/', include(router.urls)),
    # qiita/
    url(r'', index, name='index'),
]
```

### ⑥静的ファイル作成
<font color="LightSeaGreen">https://jp.vuejs.org/v2/examples/grid-component.html</font>のグリッドコンポーネントの例を真似します。

```vue_grid.css
/* コピペなので割愛 */
```

```vue_grid.js
//上部のコンポーネント実装部分はコピペなので割愛
//new Vue部分だけRestAPIから指定のデータを取得するように改造
var demo = new Vue({
      el: '#demo',
      data: {
        searchQuery: '',
        gridColumns: ["id", "title", 'stock_count'], // 変更
        gridData: [] // データはAPIで取ってくるので削除
      },
      created: function(){ //RestAPIから取ってきてgridDataに追加する。
          var self = this //スコープ的に必要っぽい（this.gridData.pushではエラーになる。）
          axios.get('/qiita/api/stock/')
            .then(function(response){
              for(var i = 0; i < response.data.results.length; i++){
                      self.gridData.push(response.data.results[i]);
                  }
            });
          }
    })
```
```jinja:vue_grid.html
<!-- vue_grid.htmlは表示項目に合わせてちょっと改造。 -->
<!DOCTYPE html>
<html>
<head>
  <title>Welcome to Vue</title>
  <!-- CSS読み込み -->
  <link rel="stylesheet" type="text/css" href="/static/vue_grid.css">

  <!-- JS(CDN)読み込み -->
  <!-- Vue.js -->
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
  <!-- Axios（vue-resourceの代わり） -->
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<script type="text/x-template" id="grid-template">
  <table>
    <thead>
      <tr>
        <th v-for="key in columns"
          @click="sortBy(key)"
          :class="{ active: sortKey == key }">
          {{ key | capitalize }}
          <span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'">
          </span>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="entry in filteredData">
        <td v-for="key in columns">
          {{entry[key]}}
        </td>
      </tr>
    </tbody>
  </table>
</script>

<div id="demo">
  <form id="search">
    Search <input name="query" v-model="searchQuery">
  </form>
  <demo-grid
    :data="gridData"
    :columns="gridColumns"
    :filter-key="searchQuery">
  </demo-grid>
</div>
</body>

<!-- JS読み込み -->
<script src="/static/vue_grid.js"></script>
</html>
```

# <i class="fa fa-hand-rock-o" style="font-size:1em;"></i> 動かしてみる。
DBを作成。

    # python manage.py makemigrations
    # python manage.py migrate

初期データを投入。

    # python manage.py loaddata fixture.json

サーバを起動。

    # python manage.py runserver

ブラウザからAPIコンソール`http://localhost:8000/qiita/api/`にアクセス。

![apitop.png](https://qiita-image-store.s3.amazonaws.com/0/82760/8ff87598-25ee-6e2d-4d98-4976d4c64d11.png)

`http://localhost:8000/qiita/api/stock/`でstockの一覧取得。
（①のページング設定で最大5件まで取得して表示　※注 登録したStockは4件なので特に意味は無し）

![apistock.png](https://qiita-image-store.s3.amazonaws.com/0/82760/03dddb1f-486b-55c7-1f00-c651635f75ab.png)

`http://localhost:8000/qiita/`にアクセスして表示してみる。

![qiitatop.png](https://qiita-image-store.s3.amazonaws.com/0/82760/5de6c531-c762-6cb6-a8a4-87bbaff692e4.png)
 
# <i class="fa fa-line-chart" style="font-size:1em;"></i> 所感
とりあえず動く所までは作れたので、勉強する意欲が沸き始めた。
Vue.jsはほぼ写経だけど、AngularJS、React.jsよりも使いやすい（直感的）気がした。
扱うデータが件数少ないのが致命的。面白くないのでどんどん記事を増やそう…。

# <i class="fa fa-line-chart" style="font-size:1em;"></i> 参考
- [Django REST Frameworkを使って爆速でAPIを実装する](http://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
- [グリッドコンポーネント の例](https://jp.vuejs.org/v2/examples/grid-component.html)
- [Django の Fixture を使ってデータ生成する](http://tech-shiroibanana.hatenablog.com/entry/2017/01/28/182815)
- [VueでAjaxやるときはaxiosがおすすめなんだって。](http://ginpen.com/2016/12/14/vue-ajax-axios/)
- [Vue.jsから手軽に始めるJavaScriptフレームワーク
](http://qiita.com/icoxfog417/items/49f7301be502bc2ad897)