Django
vue.js
webpack

Django + Vue.js な開発環境を作ろう

なにをしたいか

フロントエンドにはVue.jsを用いたシングルページアプリケーションを、バックエンドにはDjangoDjango REST frameworkを使ったWeb APIを用意して、組み合わせてみます。
先駆者様が既にいらっしゃいますが、ちょっと異なるアプローチを試したので記事にしました。

どうやって?

Djangoプロジェクトでは通常htmlファイルはテンプレートとして解釈されてレンダリングされますが、そのテンプレート構文がVueと丸かぶり。そのためにVue的なテンプレート構文を持ったhtmlはDjangoテンプレートと共存できません。
そこで、webpackを利用してフロントエンドをVueの単一ファイルコンポーネントに分割しながら書いていくことでこの問題を回避します。DjangoテンプレートとVueとではレンダリングのタイミングがサーバサイドとクライアントサイドで別れていますから、コンポーネントとしてVue部分をhtmlファイルから分離することでどちらも共存できるというわけです。もっとスマートな方法あるかもですけどね!

環境をインストール

フロントエンドはVuetifyというVue.js用のマテリアルコンポーネントのフレームワークを使って、楽してマテリアルデザインっぽく仕上げていきましょう。vue-cliを使えばあっという間にwebpack利用のフロントエンドプロジェクトがセットアップできます。楽チンやったー!
npmは事前にインストールしておきましょう。プロジェクト名はお好みで。ここではminiblogとします。

% npm i -g vue-cli
% vue init vuetifyjs/webpack miniblog

いくつかの設定入力がありますが、特にこだわりがなければそのままEnter押しましょう。完了したら早速npm run devでwebpack開発サーバーを立てて、http://localhost:8080/を開くとサンプルが表示されるはず。

次にDjangoのプロジェクトを作成します。まずはvirtualenvでPython3の仮想環境を作りDjangoをインストールします。Django2.0からPython2は対応していないのでPython2の場合は古いバージョンを使いましょう。virtualenvは事前にインストール。仮想環境名はなんでもいいですが、ここではvenvとします。

% cd miniblog/
% virtualenv -p /path/to/python3 venv
% . venv/bin/activate
(venv) % pip install django djangorestframework

そしてDjangoプロジェクトを作成。miniblogディレクトリをそのままプロジェクトルートとします。ついでにentryというDjangoアプリも今で追加しておきます。

(venv) % django-admin startproject miniblog .
(venv) % django-admin startapp entry

完了したらpython manage.py runserverでDjango開発サーバーを立てて、http://localhost:8000/を開くとスターティングページが表示されるはず。
これで必要なものは揃いました。

環境に手を加える

webpackの設定をいじって、生成したstatic assetsをDjango所定のディレクトリに納めるようにします。config/index.jsを編集します。

config/index.js
...

module.exports = {
  dev: {

    ...
    proxyTable: {
      '/api' : {
        target: 'http://localhost:8000',
        changeOrigin: true,
        pathRewrite: {  // 開発サーバーでもDjangoのAPIにアクセスできるように設定
          '^/api': 'api'
        }
      }
    },

    ...
    // デフォルトのSourceMapは相対パスを参照しているらしく、この環境ではバグになる。
    cssSourceMap: false,
  },

  build: { // コンパイルされたファイルをDjangoの所定位置へ
    // Template for index.html
    index: path.resolve(__dirname, '../templates/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../static'),
    assetsSubDirectory: 'build',
    assetsPublicPath: '/static/',

    ...
  }
}

環境自体はこれで完成です。あとはDjango側の設定がいくつか必要なので、動作の確認がてらやっていきます。

APIを作る

簡単なブログAPIを作ります。そのついでにwebpackが生成したシングルアプリケーションへのルーティングもやってしまいます。まずは設定にいくつか追記。

miniblog/settings.py
INSTALLED_APPS = [
    ~

    # 追記
    'rest_framework',
    'entry',
]

~
TEMPLATES = [
    {
        'DIRS': [ # webpackはindex.htmlをtemplatesディレクトリに生成する
            os.path.join(BASE_DIR, 'templates'),
        ],
    },
]

~

# webpackは先の設定でstaticディレクトリにcssとjsファイルを生成する
STATIC_URL = '/static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

次にモデルを書きます。

entry/models.py
class Entry(models.Model):
    title = models.CharField(max_length=200)
    contnt = models.TextField()
    date = models.DateField()

続いてserializers.pyentry/に追加して下記のように編集します。

entry/serializers.py
from rest_framework import serializers
from .models import Entry

class EntrySerializer(serializers.ModelSerializer):
    class Meta:
        model = Entry
        fields = (
            'id',
            'title',
            'content',
            'date'
        )

さらにviews.pyにも以下のように追記。

entry/views.py
from rest_framework import generics
from rest_framework.permissions import AllowAny
from .models import Entry
from .serializers import EntrySerializer


class EntryListAPIView(generics.ListAPIView):
    permission_classes = (AllowAny,)
    serializer_class = EntrySerializer
    queryset = Entry.objects.all()

下記のようなurls.pyentry/に追加します。

entry/urls.py
from django.urls import path
from .views import EntryListAPIView


urlpatterns = [
    path('entries/', EntryListAPIView.as_view()),
]

下記内容をentry/admin.pyに追記。

entry/admin.py
from django.contrib import admin
from .models import Entry


admin.site.register(Entry)

もう少し!miniblog/urls.pyに追記します。

miniblog/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from .views import index

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('entry.urls')), # APIへのルーティング
    path('', index, name='index'), # vueシングルページアプリへのルーティング
] + static(settings.STATIC_URL) # staticディレクトリにルーティング

トドメにminiblog/views.pyを追加します。

miniblog/views.py
from django.shortcuts import render


# webpackが生成したhtmlをそのままテンプレートとして読み込ませる
def index(request):
    return render(request, 'index.html', {})

やっとできた!確認の前に、Entryモデルのマイグレーションを忘れずに。

(venv) % python manage.py makemigrations
(venv) % python manage.py migrate

これで最低限APIのようなものができました。管理者をpython manage.py createsuperuserで追加してpython manage.py runserverで開発サーバーを立ち上げ、管理者ページにアクセスしてEntryをいくつか追加してください。その後にhttp://127.0.0.1:8000/api/entries/にアクセスすると、jsonに成形されたEntryモデルのリストがあるはずです。いじるところ多いですね。
ほんとはCRUDとかTestとかちゃんと書いたほうがいいんですが、本題ではないのでこの辺で…。
これでDjango側の設定は終わり。

Vue.jsでAPIを読んでみる

やっとVueです。APIのrequest処理にはaxiosを使いましょう。サクッとインストール。

% npm i --save axios

済みましたら、Vueのコードを書いていきましょう。すでにVuetifyのデモコードがあるはずなので、それを編集していきます。

src/app.vue
<template>
  <v-container fluid grid-list-md>
    <v-slide-y-transition mode="out-in">
      <v-layout row wrap align-center>
        <v-flex v-for="entry in entryList">
          <v-card>
            <v-card-title>
              <span class="headline">{{ entry.title }}</span>
            </v-card-title>
            <v-card-text>
              <blockquote>
                {{ entry.content }}
                <footer>
                  <small>
                    <em>&mdash;{{ entry.date }}</em>
                  </small>
                </footer>
              </blockquote>
            </v-card-text>
          </v-card>
        </v-flex>
      </v-layout>
    </v-slide-y-transition>
  </v-container>
</template>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<script>
import axios from 'axios'

export default {
  data () {
    return {
      entryList: []
    }
  },
  mounted: function () {
    console.log('mounted')
    // APIを叩く。
    // 開発サーバで動作中はちゃんとDjangoの8000番ポートを叩いてくれます。
    axios.get('/api/entries/')
      .then((response) => {
        this.entryList = response.data
      })
      .catch((error) => {
        console.log(error)
      })
  }
}
</script>

できました!npm run buildしてからdjango manage.py runserverで開発サーバーを立ち上げると、http://127.0.0.1:8000/にそれっぽい感じでEntryモデルが並んでいるはずです。ちゃんとAPI取れてる!

この環境のいいところ

個人的にこの開発環境の最も素晴らしいと思う点は、Django開発サーバとwebpack開発サーバを二つ同時に起動することでフロントエンドとバックエンド両方の開発でホットリロードの恩恵に与れるということです。どっちも書いたそばからすぐ反映!ブラウザリロード必要なし!
それと一応Djangoテンプレートも共存可能ですが、まあVueでシングルページアプリにするなら正直Djangoテンプレートの出番はないのでこれは大した利点じゃないかも。

まとめ

Django + Vue.jsの開発環境が整いました。webpackのconfigにちょっと手を入れただけであまり無茶な設定もしていないので、拡張もしやすいんじゃないでしょうか。ただプロジェクトが散らかるのは少し問題かもしれません。もうちょっと頑張れば整理もできそうですが。
ともかく、これでDjangoとVue.jsで遊び放題!

実際に動作するプロジェクトをgithubに用意しました。こちら