Vue.js×Django REST Frameworkで画像登録及び表示処理を実装

# 概要

Vue.jsとDjango REST Frameworkを用いて、画像の登録及び表示する。

# GitHubリンク

https://github.com/ryo-keima/drf-image-demo

# 環境

- macOS Monterey
- Chip Apple M1 Max

- Python 3.10.3
- Django 4.1.4
- Django REST Framework 3.14.0
- vue-cli 5.0.1
- Vue.js 3.2.13

# 手順

## 仮想環境構築

仮想環境を構築します。

```
% python -m venv venv
```

アクティベートします。

```
% source venv/bin/activate
```

## Django環境構築

Django, djangorestframework, Pillowをインストールします。

```
(venv)% pip install Django==4.1.4 djangorestframework==3.14.0 Pillow==9.3.0
```

djangoプロジェクトを生成します。

```
(venv)% django-admin start project project .
```

開発サーバーを起動し、ブラウザで`http://127.0.0.1:8000`を表示し、Djangoのデフォルト画面が表示されることを確認しましょう。

```
(venv)% python manage.py runserver
```

## バックエンドを実装

djangoアプリを生成します。

```
(venv)% python manage.py startapp app
```

project/settings.pyを一部編集します。

```python
...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # 追加
    'app' # 追加
]

...

# 以下を追記
MEDIA_URL = '/media/' # 画像ファイルを参照するためのURL
MEDIA_ROOT = BASE_DIR / "media" # 画像保存先ディレクトリ

# 信頼できる発信元リストに、Vueのオリジンを追加
CSRF_TRUSTED_ORIGINS = ['http://localhost:8080']
```

app/models.pyに、Imageモデルを定義します。

```python
from django.db import models

class Image(models.Model):

    image = models.ImageField(
        verbose_name="画像",
        blank=True,
        null=True
    )

    class Meta:
        db_table = "image"
```

app/views.pyにViewを定義します。

Vue.js側は後程実装します。

```python
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Image
from .serializers import ImageSerializer

class ImageViewSet(viewsets.ModelViewSet):
    queryset = Image.objects.all()
    serializer_class = ImageSerializer
```

app/serializers.pyを作成し、ImageSerializerを定義します。

```python
from rest_framework import serializers
from .models import Image

class ImageSerializer(serializers.ModelSerializer):

    class Meta:
        model = Image
        fields = "__all__"
```

project/urls.pyを編集

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('app.urls')) # appのurls.pyを指定
]

if settings.DEBUG:
    # 画像表示用
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)
```

app/urls.pyを作成します。

```python
from django.urls import path, include

from rest_framework.routers import DefaultRouter
from app.views import ImageViewSet

# ImageViewSetを設定
router = DefaultRouter()
router.register(r'image', ImageViewSet)

urlpatterns = [
    path('', include(router.urls)),
]
```

マイグレーションします。

```
% python manage.py makemigrations
% python manage.py migrate
```

開発サーバーを起動し、http://127.0.0.1:8000/api/v1/imageを開きます。

![Screen Shot 2022-12-18 at 13.53.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/fe04649d-b767-b357-48ec-7d720364b751.png)
試しにChoose Fileに画像を設定し、POSTボタンで画像登録ができるか確認します。

![Screen Shot 2022-12-18 at 14.05.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/0a8d8279-4a7e-a9e6-581f-fc36c1db32ee.png)
URLにアクセスしてみます。

![Screen Shot 2022-12-18 at 14.06.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/aba0f111-b645-7c39-6761-d4d1ec7bb7a1.png)
登録できました！

## Vue.jsプロジェクトを生成

本プロジェクトをVueプロジェクトを生成します。

```
% vue create frontend
```

Vue3を選択します。

```python
Vue CLI v5.0.1
┌─────────────────────────────────────────┐
│                                         │
│   New version available 5.0.1 → 5.0.8   │
│                                         │
└─────────────────────────────────────────┘

? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 3] babel, eslint) 
  Default ([Vue 2] babel, eslint) 
  Manually select features
```

メッセージが表示されればOK。

```python
Vue CLI v5.0.1
✨  Creating project in /Users/ryosukemaeda/Programming/Issues/django-vue-env/frontend.
⚙️  Installing CLI plugins. This might take a while...

added 848 packages in 12s
🚀  Invoking generators...
📦  Installing additional dependencies...

added 96 packages in 3s
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project frontend.
👉  Get started with the following commands:

 $ cd frontend
 $ npm run serve
```

開発サーバーを起動します。

```python
% cd frontend
frontend% npm run serve
```

Vue.jsのデフォルト画面が表示されればOKです。

![aaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/f973f921-39f9-a20e-d400-5c0fe1c97a88.png)

## フロントエンドを実装

API実行のため、axiosをインストールします。

```
frontend% npm install axios
```

frontend/src/common/api.service.jsを作成し、以下を設定します。

```jsx
import axios from "axios";

axios.defaults.xsrfCookieName = "csrftoken";
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN";
axios.defaults.withCredentials = true;

export { axios };
```

frontend/src/components/RegisterImage.vueを作成します。

```jsx
<template>
  <div>
    <form @submit.prevent="onSubmit">
      <div>画像</div>
      <div>
        <input
          type="file"
          name=""
          id="img-upload"
          ref="file"
          multiple="multiple"
          @change="uploadFile"
          @click="
            (e) => {
              e.target.value = '';
            }
          "
        />
      </div>
      <button>登録</button>
    </form>
    <!-- プレビュー表示 -->
    <img :src="imgUrl" />
  </div>
</template>

<script>
import { axios } from "@/common/api.service.js";

export default {
  data() {
    return {
      imgData: null,
      imgUrl: null,
    };
  },
  methods: {
    uploadFile() {
      this.imgData = this.$refs.file.files[0];
      if (!this.imgData) {
        return;
      }
    },
    async onSubmit() {
      const formData = new FormData();
      formData.append("imageData", this.imgData);
      console.log(this.imgData);
      try {
        const endpoint = "/api/v1/image/";
        const response = await axios.post(endpoint, formData, {
          headers: {
            "Content-Type": "multipart/form-data",
          },
        });
        this.imgUrl = `http://localhost:8000${response.data}`;
        alert("Success!");
      } catch (error) {
        console.log(error);
        alert(error.response.data);
      }
      this.imgData = null;
    },
  },
};
</script>
```

※本来であれば、画像ファイルのみ受付できるよう実装すべきですが、今回は省略します。

frontend/vue.config.jsを編集します。

```jsx
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  publicPath: "/",
  devServer: {
    host: "localhost",
    hot: "only",
    proxy: {
      "^/api": {
        target: "http://localhost:8000",
        changeOrigin: true,
      },
    },
  },
})
```

frontend/src/App.vueを編集し、RegisterImageコンポーネントを表示するようにします。

```jsx
<template>
  <div>
    <!-- <img alt="Vue logo" src="./assets/logo.png" /> -->
    <!-- <HelloWorld msg="test" /> -->
    <RegisterImage />
  </div>
</template>

<script>
// import HelloWorld from "./components/HelloWorld.vue";
import RegisterImage from './components/RegisterImage.vue'

export default {
  name: "App",
  components: {
    // HelloWorld,
    RegisterImage
  },
};
</script>

...
```

ここで、バックエンドのソースを一部編集します。

app/views.pyを編集し、フロントエンドからの登録リクエストにより、画像データを登録するよう修正します。

```python
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Image
from .serializers import ImageSerializer

class ImageViewSet(viewsets.ModelViewSet):
    queryset = Image.objects.all()
    serializer_class = ImageSerializer

		# 以下を追加します
    def create(self, request, *args, **kwargs):
	      # 画像登録処理
        img_data = request.data['imageData']
        if img_data is not None:
            img = Image.objects.create(image=img_data)
            return Response(img.image.url,
                            status=status.HTTP_200_OK)

        return Response("Failed to register image.",
                        status=status.HTTP_400_BAD_REQUEST)
```

## 画像登録を実施

ここまでできたら、実際に登録処理を試してみましょう。

DjangoとVue.jsの開発用サーバーを両方起動します。

```
(venv)% python manage.py runserver
```

```
% cd frontend
% npm run server
```

ブラウザで`http://localhost:8080`を開き、以下の画面が開くことを確認します。

![Screen Shot 2022-12-18 at 15.20.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/f1cfe0ea-5213-fc1e-ffb5-685f4499320d.png)
ファイルをセットし、登録ボタンを押下します。

![Screen Shot 2022-12-18 at 15.24.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/929a7115-9093-b28b-71cc-1b1357c89002.png)
成功しました。

アラートのOKを押下すると、アップロードしたプレビューが表示されることを確認します。
![Screen Shot 2022-12-18 at 15.24.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/873149ee-8be6-7f23-acad-5b361171ab94.png)

以上になります。
ありがとうございました。