Dockerを用いてNuxt.js & Djangoで実装【AxiosでCRUD】

#はじめに
本記事へのアクセスありがとうございます。
投稿主はプログラミング初心者であり、この方法が**「最適解」**かは分かりません。

今度お仕事させて頂く実務でフロントにNuxt、バックエンドにDjangoを用いるので、全体の流れを少しでも把握するために本記事を作成しました。
同様な記事は環境構築までは確認できたのですが、せっかくならCRUD処理まで実装しようと思います。

＊今回のNuxtの設定はSPAで行っていきます。時間があったらSSRバージョンも執筆します。
＊DBはMySQLを使用します。

#この記事から得られるものは？
・Dockerを用いたNuxtとDjangoの環境構築
・上記の環境でAxiosでCRUD実装（get,post,put,delete　全て扱っている記事は少ない）

#さっそくスタート
Dockerを用いた環境構築は[こちら](https://qiita.com/ryomatube/items/1b36fe6d73b9a6c3468c)の記事を参考にさせて頂きました。
環境構築完了イメージです。
![スクリーンショット 2021-01-06 16.07.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691815/b0af88ed-2092-5496-e3b2-7bcce964ec18.png)

##ディレクトリ構造

```
.
├─django
│  ├─manage.py
│  ├─qiitaexample
│  │   ├─settings.py
│  │   ├─urls.py
│  │   ├─wsgi.py
│  │   └─__init__.py
│  └─myapp
│      ├─migrations
│      ├─admin.py
│      ├─apps.py
│      ├─models.py
│      ├─renderers.py
│      ├─serializers.py
│      ├─tests.py
│      ├─urls.py
│      ├─views.py
│      └─__init__.py
│
├─docker-compose.yml
│
├─dockerfiles
│  ├─django_docker
│  │   ├─dockerfile
│  │   └─requirements.txt
│  └─nuxt_docker
│      └─dockerfile
│
├─mysql
│  └─conf.d
│
└─nuxt
    └─front
       └─以下略
```

##初期段階で必要なファイル内容
###docker-compose.yml

```docker-compose.yml
version: '3'

services:
  db:
    image: mysql:latest
    restart: always
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_USER: test
      MYSQL_DATABASE: test
      MYSQL_PASSWORD: test
    ports:
      - 3306:3306
    expose:
      - 3306
    volumes:
      - mysqldata:/var/lib/mysql
      - ./mysql/conf.d:/etc/mysql/conf.d
    command: --default-authentication-plugin=mysql_native_password

  web:
    container_name: django
    build: ./dockerfiles/django_docker
    command:
      python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./django:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

  front:
    container_name: nuxt
    build: ./dockerfiles/nuxt_docker
    tty: true
    volumes:
      - ./nuxt:/code
    ports:
      - "3000:3000"

volumes:
  mysqldata:

```
###dockerfiles/nuxt_doceker/dockerfile


```dockerfile
FROM node:latest

RUN mkdir -p /code

ENV NODE_ENV=development

RUN yarn install
RUN yarn add @nuxtjs/axios

WORKDIR /code

EXPOSE 3000
```
###dockerfiles/django_doceker/dockerfile

```dockerfile
FROM python:3.7
ENV PYTHONUNBUFFERED 1

RUN mkdir /code
WORKDIR /code

COPY requirements.txt /code/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

COPY . /code/
```
###dockerfiles/django_doceker/requriements.txt

```requriements.txt
Django
djangorestframework
django-webpack-loader
django-cors-headers
mysqlclient
```

##Djangoの設定
###プロジェクトの作成
docker-composeでdbコンテナを立ち上げSqliteからMySQLの設定へと変更していきます。

```
> docker-compose up -d db
```
次にdocker-composeでコンテナを立ち上げます。初回なのでbuildも行っていきます。

```
> docker-compose up -d --build
```
Djangoのプロジェクトを作ります。(プロジェクト名は任意です)

```
> docker-compose run web django-admin.py startproject qiitaexample .
```
続いて、setting.pyを編集していきます。

```
#<DATABASESの'default'内を書き換える>

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'test',
        'USER': 'test',
        'PASSWORD': 'test',
        'HOST': 'db',   
        'PORT': '3306',
    }
}
```
DATABASES変更後にDBのマイグレーションを行います。

```
> docker-compose run web ./manage.py migrate
```
続いてsuperuserを作成します。

```
> docker-compose run web ./manage.py createsuperuser --username admin --email admin@localhost
Password: #パスワードを聞かれるので入力
Password (again): #再度入力
The password is too similar to the username.
This password is too short. It must contain at least 8 characters.
This password is too common. #パスワードが短いと警告が出る
Bypass password validation and create user anyway? [y/N]: y #yを入力
Superuser created successfully.
```
docker-compose up -d コマンドコンテナを全て立ち上げ、正常に動作しているか挙動を確かめます。

```
> docker-compose up -d

> docker ps 

#３つのコンテナが動作しているはずです
・コンテナその１
・コンテナその２
・コンテナその３
```
django開発用サーバはポート8000で待ち受けているので、
http://localhost:8000/admin にアクセスしてみます。
表示された画面でUsername（ここではadmin）とpasswordを入力します。
すると、GroupsとUsersという２つの項目が表示されます。
###アプリの作成
さて、次はアプリを作成していきます。(ここではmyappという名前で作成します)

```
> docker-compose run web ./manage.py startapp myapp
```
setting.pyにmyappをを追記して使用できるようにします。

```./django/qiitaexample/setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp', #追加
]
```
models.pyにモデルを追記していきます。
ここではStudentというモデルを作成します。
models.pyを追記後、マイグレーションをすることでMySQL上にテーブルが作成されます。

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

class Student(models.Model):
    name = models.CharField(max_length=100)
    course = models.CharField(max_length=100)
    rating = models.IntegerField()

    def __str__(self):
      return self.name

    class Meta:
        ordering = ['name']

```
下記でマイグレーションを行います。
アプリを追加した場合はmakemigrationsが必要です。

```
> docker-compose run web ./manage.py makemigrations
> docker-compose run web ./manage.py migrate
```
admin.pyに下記を追記することで、管理画面から作成したモデルを確認できます。

```./django/myapp/admin.py
from django.contrib import admin
from .models import Student

admin.site.register(Student)
```
再び http://localhost:8000/admin/ に接続するとMYAPPとStudentが増えていることが確認できます。
GUI操作で適当にStudentを登録し、MySQLのコンテナに入り確認してみます。

```
> docker exec -it mysql /bin/bash #MySQLコンテナに入る

> mysql -u test -p #MySQL起動
Enter password: #本記事の例だとtestでログイン可能

> use test;
> select * from myapp_student;

#確認できたら < shift + d > でコンテナから出る
```
##Django REST frameworkの構築
Djangoの準備ができたところで、次はDjango側にAPIを作っていきます。
まずはsetting.pyに追記していきます。(Nuxtとの通信の際に必要な処理も予め記載しておきます）

```./django/qiitaexample/setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp', 
    'rest_framework', #追加
    'corsheaders', #追加
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', #追加
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000',
]

CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
```
'rest_framework'を追記することで、Django REST frameworkの機能を使用できるようになります。
続いて、serializer.pyとurls.py(myapp配下)を手動で新規作成します。
作成後にserializer.py、urls.py、views.pyに下記内容を追記します

```./django/myapp/serializers.py
from rest_framework.serializers import ModelSerializer
from .models import Student

class StudentSerializer(ModelSerializer):
  class Meta:
    model = Student
    fields = ['id' , 'name' ,'course' ,'rating']
```
```./django/myapp/views.py
from django.shortcuts import render
from rest_framework.generics import ListAPIView, CreateAPIView,UpdateAPIView,RetrieveAPIView,DestroyAPIView
from rest_framework.permissions import AllowAny
from .models import Student
from .serializers import StudentSerializer

class StudentsViewSet(ListAPIView):
    model = Student
    queryset = Student.objects.all()
    serializer_class = StudentSerializer
    permission_classes = (AllowAny, )

class StudentsDetailSet(RetrieveAPIView):
    model = Student
    queryset = Student.objects.all()
    serializer_class = StudentSerializer
    permission_classes = (AllowAny, )

class StudentsCreateSet(CreateAPIView):
    model = Student
    queryset = Student.objects.all()
    serializer_class = StudentSerializer
    permission_classes = (AllowAny, )

class StudentsUpdateSet(UpdateAPIView):
    model = Student
    queryset = Student.objects.all()
    serializer_class = StudentSerializer
    permission_classes = (AllowAny, )

class StudentsDeleteSet(DestroyAPIView):
    model = Student
    queryset = Student.objects.all()
    serializer_class = StudentSerializer
    permission_classes = (AllowAny, )

```
```./django/myapp/urls.py
from django.urls import path
from .views import StudentsViewSet, StudentsCreateSet, StudentsUpdateSet,StudentsDetailSet,StudentsDeleteSet

urlpatterns = [
    path('get_myapp/', StudentsViewSet.as_view()),
    path('get_myapp/<int:pk>/', StudentsDetailSet.as_view()),
    path('post_myapp/', StudentsCreateSet.as_view()),
    path('put_myapp/<int:pk>/', StudentsUpdateSet.as_view()),
    path('delete_myapp/<int:pk>/', StudentsDeleteSet.as_view()),
]
```
qiitaexample配下のurls.pyを編集し、myapp配下のurls.pyを有効にします。

```
from django.contrib import admin
from django.urls import path,include
from myapp import urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/',include(urls))
]
```

##Nuxt.jsの設定
始めにnuxt.jsのコンテナに入り、プロジェクトを作成します(プロジェクト名は任意です)。

yarn create nuxt-app frontを入力後の選択肢は基本全てデフォルト値を入れていますが、
今回はSPAを選択しています。

```
> docker exec -it nuxt /bin/bash

> yarn create nuxt-app front
```
確認のために開発用サーバを動かす前に、nuxt.config.jsに追記します。
docker上でnuxt.jsを動かす際は下記が必要です。
また、ここから編集していく際にホットリロードを有効にします。
docker上で開発するためには明示的に指定が必要です。

さらにaxiosの設定を追記していきます。
axiosはnode.jsで動くHTTPクライアントで、これを利用してnuxt.jsから先ほどのAPIのURLを叩きます。
前述のnuxtのdockerfileに記載してあるので、すでにコンテナにはインストール済です。
modulesに下記を追記、及びaxiosを追加します。

```./nuxt/front/nuxt.config.js
  server: {
    port: 3000,
    host: '0.0.0.0',  
  },

  watchers: {
    webpack: {
      poll: true
    }
  }


//~略~
  modules: [
    '@nuxtjs/axios',
  ],
  axios: {
    baseURL: "http://localhost:8000"
  },
//~略~
```
これでdocker上でNuxt.jsを表示させることができます

```
> cd front

> yarn run dev
```
無事Nuxt.jsの初期画面が現れればひとまず成功です。

###AxiosでCRUD実装　Nuxt ⇆ Django
ここからは実際にNuxtとDjango間のデータの受け渡しについて記載していきます。
componentsの中にTestFormを新規作成します。

```./nuxt/front/components/TestForm.vue
<template>
  <div>
    <div>
      <form @submit.prevent="submitForm(student)">
        <div class="from-group row">
          <input
            type="text"
            class="form-control col-3 mx-2"
            placeholder="Name"
            v-model="student.name"
          />
          <input
            type="text"
            class="form-control col-3 mx-2"
            placeholder="Course"
            v-model="student.course"
          />
          <input
            type="text"
            class="form-control col-3 mx-2"
            placeholder="Rating"
            v-model="student.rating"
          />
          <button class="btn btn-success">Submit</button>
        </div>
      </form>
    </div>
    <div>
      <table class="tabel">
        <thead>
          <th>Name</th>
          <th>Course</th>
          <th>Rating</th>
        </thead>
        <tbody>
          <tr
            v-for="student in students"
            :key="student.id"
            @dblclick="$data.student = student"
          >
            <td>{{ student.name }}</td>
            <td>{{ student.course }}</td>
            <td>{{ student.rating }}</td>
            <td><button @click="deleteStudent(student)">x</button></td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      student: {
        name: "",
        course: "",
        rating: "",
      },
      students: [],
    };
  },
  async created() {
    await this.getStudents();
  },
  methods: {
    submitForm(student) {
 //新規にPOSTする場合はcrete , 既存にデータがある場合はputメソッドを利用するように条件分岐
      if (this.student.id === undefined) {
        this.createStudent();
      } else {
        this.editStudent(student);
      }
    },
    //データを全権取得
    async getStudents() {
      const url = "/api/get_myapp/";
      const response = await this.$axios.get(url);
      this.students = response.data;
    },
    //データを新規登録
    async createStudent() {
      await this.getStudents();
      const url = "/api/post_myapp/";
      this.$axios.defaults.xsrfCookieName = "csrftoken";
      this.$axios.defaults.xsrfHeaderName = "X-CSRFTOKEN";
      await this.$axios.post(url, {
        name: this.student.name,
        course: this.student.course,
        rating: this.student.rating,
      });
      await this.getStudents();
    },
   //データを編集
    async editStudent(student) {
      await this.getStudents();
      const url = "/api/put_myapp/" + student.id + "/";
      const modify = {
        name: this.student.name,
        course: this.student.course,
        rating: this.student.rating,
      };
      await this.$axios.put(url, modify);
      await this.getStudents();
      this.student = {};
    },
   //データを削除
    async deleteStudent(student) {
      await this.getStudents();
      const url = "/api/delete_myapp/" + student.id + "/";
      const modify = {
        name: this.student.name,
        course: this.student.course,
        rating: this.student.rating,
      };
      await this.$axios.delete(url)
      await this.getStudents();
    },
  },
};
</script>
```
作成したTestFormを読みます。
不必要なら、template内の初期表示部分を削除してしまって構いません。

```./nuxt/front/pages/index.vue
//追記
　 <div>
      <TestForm />
   </div>

--------------------
<script>
import TestForm from '~/components/TestForm.vue'
export default {
  components:{
    TestForm
  }
}
</script>
```
これでAixosでCRUD処理のコードが完成です。
最後に全てのコンテナを立ち上げて、Nuxtのコンテナでyarn run devして確認してみましょう。

```
> docker-compose up -d

> docker exec -it nuxt  /bin/bash

> cd front

> yarn run dev
```

#おわりに
お疲れ様でした。
以上でNuxt.js　⇆　Djangoのデータの連携が完成です。
個人的にはAxiosでputする時に以下のコードをurls.pyに書き忘れていて、ずっと404not foundで言われて少し苦戦しました。
解決してみれば確かに、詳細ページのurl設定を行っていないのでページが表示（一つ単位の情報を取得）できるはずもありませんでした。

```
path('get_myapp/<int:pk>/', StudentsDetailSet.as_view()),
```
**エラーコードを読みどこがいけないのか判断、予測して実装しなければ。**と改めて思いました。

余談ではありますが、DRFの理解の際に役に立ったリンクを紹介します。
[Django REST framework カスタマイズ方法 - チュートリアルの補足](https://qiita.com/okoppe8/items/c58bb3faaf26c9e2f27f)
[DRFのGeneric viewの使い方](https://qiita.com/murapon/items/8a6a1715712ddd1a4ac1)

少しでもみなさんのお役に立てたら嬉しいです。