Django REST APIをVue3からAxiosでアクセスするようなSPAサイトを開発する

IT系

環境構成(Django+Vue3+Vuetify+Axios)に沿った手順を順に説明します。


🌐 全体構成

vue_django/                  # PJとして環境変数に設定
│
├── project/              # Django プロジェクト
│   ├── settings.py
│   ├── urls.py
│   └── ...
├── app/                  # Django アプリ
│   ├── views.py
│   ├── urls.py
│   └── models.py
│
├── frontenv/             # Vue3 + Vuetify + Axios
│   ├── package.json
│   ├── src/
│   │   ├── main.js
│   │   ├── App.vue
│   │   └── views/
│   ├── public/
│   └── vue.config.js
│
├── static/               # Django 側静的ファイル出力先
└── templates/            # Django 側 HTML テンプレート出力先

環境構築

最初にDjangoの環境構築

プロジェクト名をprojectとし、アプリケーション名をappとして構築します。

mkdir vue_django
cd vue_django
export PJ=`pwd`

python -m venv ./venv
source venv/bin/activate
pip install django django-restframework
django-admin startproject project
chmod +x ./manage.py
./manage.py startapp app

Vueのリリース先を作成しておきます。

cd $PJ
mkdir static
mkdir templates

Vue3 + Vuetify 環境構築

vueの環境を構築:

npm install @vue/cli
cd $PJ

vue create frontend
Vue CLI v5.0.9
? Please pick a preset: (Use arrow keys)
 Default ([Vue 3] babel, eslint) 
  Default ([Vue 2] babel, eslint) 
  Manually select features 

axiosとrouterの導入:

cd $PJ/frontend
vue add axios router

Vuetify 初期設定:

mkdir $PJ/frontend/public
cd $PJ/frontend/
vue add vuetify
? Choose a preset: 
  Vuetify 2 - Configure Vue CLI (advanced) 
  Vuetify 2 - Vue CLI (recommended) 
  Vuetify 2 - Prototype (rapid development) 
  Vuetify 3 - Vite (preview) 
 Vuetify 3 - Vue CLI (preview) 

$PJ/frontend/vue.config.jsを以下のように編集:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  publicPath: "/static",
  outputDir: "../static",
  assetsDir: "",
  indexPath: "../templates/index.html",
  transpileDependencies: true,
  pluginOptions: {
    vuetify: {
		}
  },
})

$PJ/frontend/src/main.js を以下のように編集:

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import vuetify from './plugins/vuetify'
import { loadFonts } from './plugins/webfontloader'
loadFonts()

createApp(App).use(router)
  .use(router)
  .use(vuetify)
  .mount('#app')

$PJ/frontend/src/App.vueを以下のように編集:

<template>
  <router-view/>
</template>

簡易なREST API連携の例

DjangoでSampleViewというOKのみを返すViewを作成し、それをVueで表示させるようにしてみます。

Vueのサンプルを作成

まずはじめに、Vue側のコードを作成します。

$PJ/frontend/src/views/SampleView.vue を作成:

<template>
  <v-container>
    <v-card class="ma-5 pa-5">
      <v-card-title>Sample API Call</v-card-title>
      <v-card-text>
        <v-btn color="primary" @click="callApi">Call Django API</v-btn>
        <div class="mt-4">Response: {{ response }}</div>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

const response = ref('')

onMounted(async () => {
  try {
    const res = await axios.get('//localhost:8000/api/v1/sample/')
    response.value = res.data;
  } catch (err) {
    response.value = 'Error: ' + err.message
  }
});
</script>

$PJ/frontend/src/router/index.js にルートを追加:

import { createRouter, createWebHistory } from 'vue-router'
import SampleView from '../views/SampleView.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: SampleView,
  },
]

export default createRouter({
  history: createWebHistory(),
  routes,
})

アプリをビルドしてDjangoのtemplatesおよびstaticへファイルを配置します。

cd $PJ/frontend/
npm run build

Django 側のアプリ構築

Djangoの設定をしていきます。

$PJ/project/settings.py (変更箇所のみ記載しています):

# 他のホストからアクセスする場合に備えて * にした
ALLOWED_HOSTS = ['*']   

# appと、rest_frameworkをついか
INSTALLED_APPS = [
    'app',
    'rest_framework',

# templatesのDIRSにVueのリリース先であるtemplatesの場所を指定
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "templates"],

# 言語とタイムゾーンを指定
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

# Vueのcssやjsなどのリリース先であるstaticの場保を指定
STATIC_URL = 'static/'
STATICFILES_DIRS = [
        BASE_DIR / "static",
]

$PJ/project/urls.py を編集します:

from django.contrib import admin
from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static

from app.views import IndexView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('app.urls')),
    re_path(r"^.*$", IndexView.as_view()),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

次にappアプリの設定をします。

$PJ/app/views.py を編集します。

from django.shortcuts import render

# Create your views here.
from rest_framework import status, viewsets
from rest_framework.views import APIView
from rest_framework.response import Response

from django.views.generic.base import TemplateView


class IndexView(TemplateView):
	template_name = "index.html"

class SampleAPIView(APIView):
	def get(self, request):
		return Response("OK", status=status.HTTP_200_OK)

$PJ/app/urls.py を作成します:

from django.urls import path
from app.views import SampleAPIView

urlpatterns = [
	path("sample/", SampleAPIView.as_view(), name="sample"),
]

以上で REST Framework と SampleAPIView が動くようになったので、
Djangoの開発用サーバを起動します:

# Django サーバ起動
cd $PJ
./manage.py runserver 0.0.0.0:8000

Django の開発サーバが起動したら、ブラウザで http://localhost:8000 にアクセスします。Vueのビルドで生成したtemplates/index.html が呼び出され static/以下のcssやVueのjsが読み込まれて、axiosが、http://localhost:8000/api/v1/sample/ にアクセスしてAPIでOKを得て、画面にOKが表示されます。

以上で、VueとDjangoのREST APIにアクセスするサンプルアプリが完成しました。

次に、DBと連携した本の一覧を表示するアプリの作成をVueのルーティングで振り分ける形で追加作成していきたいと思います。

動作確認:

Vue 側で axios 経由でDjangoのAPIのsampleの値「OK」を呼び出せます。

GET http://localhost:8000/


Booksアプリの作成

まずはSampleAPIViewでOKを得て表示される簡単なサンプルアプリを作りました。次は書籍を登録して一覧表示させるようなものを作って見ようと思います。

データベースの設定

ここではDjango標準搭載のSQLite3を使うことにします。
まず初めに、データベースへアクセスするための管理者用のアカウントを作成します。パスワードが短すぎたりすると注意がでてvalidationをbypassして良いか聞いてきますがここではyします。

cd $PJ
./manage.py makemigrations
./manage.py migrate
./manage.py createsuperuser
ユーザー名 (leave blank to use 'user'): admin
メールアドレス: <省略>
Password: admin
Password (again): admin
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

Booksアプリの作成

booksアプリのモデルを作成します。

$PJ/app/models.py 例:

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    published = models.DateField()

    def __str__(self):
        return self.title

$PJ/app/serializers.py を追加:

from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

$PJ/app/views.py に追加:

from rest_framework import status, viewsets
from rest_framework.views import APIView
from rest_framework.response import Response

from django.views.generic.base import TemplateView
from .models import Book
from .serializers import BookSerializer

class IndexView(TemplateView):
    template_name = "index.html"

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    
class SampleAPIView(APIView):
    def get(self, request):
		    return Response("OK", status=status.HTTP_200_OK)

$PJ/app/urls.py を更新:

from rest_framework import routers
from app.views import BookViewSet, IndexViewSet

router = routers.DefaultRouter()
router.register(r'books', BookViewSet)

urlpatterns = [
    path('sample/', SampleAPIView.as_view(), name='sample'),
]
urlpatterns += router.urls

$PJ/project/urls.pyを更新:

from django.contrib import admin
from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static

from app.views import IndexView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('app.urls')),
    re_path(r"^.*$", IndexView.as_view()),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

DBマイグレーション:

cd $PJ/
./manage.py makemigrations
./manage.py migrate

データベースの確認

データベースにテーブルやカラムがちゃんとできているか確認しておきます。sqlite3がインストールされていなければ事前にインストールしておきます。

sudo apt install sqlite3

cd $PJ
sqlite3 db.sqlite3 

SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
app_book                    auth_user_user_permissions
auth_group                  django_admin_log          
auth_group_permissions      django_content_type       
auth_permission             django_migrations         
auth_user                   django_session            
auth_user_groups
sqlite> .schema app_book
CREATE TABLE IF NOT EXISTS "app_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(200) NOT NULL, "author" varchar(100) NOT NULL, "published" date NOT NULL);
sqlite> .quit

データの登録

一覧表示させるための書籍データを登録しておきます。

http://localhost:8000/api/v1/books/

に、アクセスして以下の画面でタイトルと著者、発行年月日を何件か登録してください。

Vue3にBookViewを追加

$PJ/frontend/src/views/BooksView.vue を作成:

<template>
  <v-container>
    <v-card class="ma-5 pa-5">
      <v-card-title>Books</v-card-title>
      <v-card-text>
        <v-btn color="primary" @click="callApi">Call Django API</v-btn>
        <p v-if="isLoading">読み込み中...</p>
        <p v-else-if="errorMessage">{{ errorMessage }}</p>
	      <ul v-else>
          <li v-for="item in response" :key="item.id">
            {{ item.id }} - {{ item.title }} - {{ item.author }} - {{ item.published }}
          </li>
        </ul>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';

const response = ref([]);
const isLoading = ref(true);
const errorMessage = ref('');

onMounted(async () => {
  try {
    const res = await axios.get('//localhost:8000/api/v1/books/');
    response.value = res.data;
  } catch (err) {
    errorMessage.value = 'データ読み込みに失敗しました';
    response.value = 'Error: ' + err.message;
  } finally {
    isLoading.value = false;
  }
});
</script>

$PJ/frontend/src/router/index.js を以下の通り修正:

import { createRouter, createWebHistory } from 'vue-router'
import SampleView from '../views/SampleView.vue'
import BooksView from '../views/BooksView.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: SampleView,
  },
  {
    path: '/books',
    name: 'Books',
    component: BooksView,
  },
]

export default createRouter({
  history: createWebHistory(),
  routes,
})

コードができたらビルドします。

cd $PJ/frontend
npm run build

以下の通りアクセスすると登録したデータが出力されます。

http://localhost:8000/books

テーブル表示させてみる

v-data-tableを使ってテーブルを用いてデータを表示させてみます。

iconが正しく表示されるように以下の mdi/fontをインストールする

npm cache clean --force
npm install @mdi/font --save

$PJ/frontend/src/main.js を以下のように修正:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import vuetify from './plugins/vuetify'
import '@mdi/font/css/materialdesignicons.css'
import './plugins/webfontloader'; 

createApp(App).use(router)
  .use(router)
  .use(vuetify)
  .mount('#app')

$PJ/frontend/src/views/BooksView.vue を以下のように編集する:

<template>
  <v-container>
    <div>
        <v-text-field
          v-model="search"
          label="検索"
          outlined
          clearable
        >
        </v-text-field>
        <v-data-table
          :headers="headers"
          v-models="selected"
          :items="filteredItems"
          item-value="name"
          select-strategy="single"
          show-select
          :search="search"
        >
        </v-data-table>
    </div>
  </v-container>
</template>

<script setup>
import { ref, computed } from 'vue';
import axios from 'axios';

const isLoading = ref(true);
const errorMessage = ref('');
const search = ref('');
const items = ref([]);
const headers = [
    { title: 'ID', align: 'start', key: 'id' },
    { title: '書籍名', align: 'start', key: 'title' },
    { title: '著者', align: 'start', key: 'author' },
    { title: '発行日', align: 'start', key: 'published' },
]

const fetchData = async () => {
  try {
    const response = await axios.get('//localhost:8000/api/v1/books/');
    items.value = response.data;
  } catch (err) {
    errorMessage.value = 'データ読み込みに失敗しました';
    console.error(errorMessage.value);
  } finally {
    isLoading.value = false;
  }
};
fetchData();

const filteredItems = computed(() => {
  if (!search.value) {
    return items.value;
  }
  return items.value.filter(item =>
    item.name.tolowerCase().includes(search.value.tolowerCase())
  );
});
</script>

上記の変更が終わったらビルドします。

cd $PJ/frontend/
npm run build

ブラウザでアクセスすると、以下のような感じになります。

http://localhost:8000/books

上記のVue側でフィルタをする例だとデータ数が大きくなった際に毎回DjangoからREST APIで全件取得してくることになり遅くて使えないという状況になると思います。

データ量が多くなる場合は、DjangoにQueryでフィルタ結果だけを取得するように、そしてページネーションでその結果に対してOffsetで必要数だけ取得するような作りに変える必要があると思います。

レイアウトを適用する

よく見かけるページは上段にナビゲーションバーがあり、左ペインにリスト、右ペインがのようなレイアウトにして見ます。

✅ まとめ

要素技術状況
フロントVue3 + VuetifySPA構築OK
通信AxiosDjango REST Frameworkと通信
バックエンドDjango + DRF + SQLite3REST API提供
統合vue.config.js の proxy 設定 + Django IndexViewSPA統合OK

今後、Vue 側で CRUD 画面(一覧・追加・削除)を実装や、Django 側で認証(JWT / Session)対応などへ進めていってアプリケーションとして機能するものを作ってみようと思います。

タイトルとURLをコピーしました