DRF & Vue.js & Dockerでの環境構築

# 今回やる事
DRF、Vue.js、dockerでの環境構築をする上で、各ファイルの意味だったりコマンドだったりをよく忘れるので、とりあえずDRFとVueが連携出来るまでを、ほぼ自分用に残しておきます。

例として、ユーザー一覧を取得するだけの処理を実装します。

# 環境
__docker__

```
$ docker version

Version: 20.10.6
```

__docker_compose__

```
$ docker-compose -v

docker-compose version 1.29.1, build c34c88b2
```

__python__

```
$ python --version

Python 3.8.10
```




# 構築編

DBは開発段階ではsqliteを使い、DRF用にコンテナ1つ、Vue用にコンテナ1つを用意します。
サーバーはそれぞれの開発サーバーを使用するとします。


まずは、以下の様なディレクトリ構成でファイルを作成します。
アプリ名は「k2」です。


```
k2/
 ├ api
 │  ├ Dockerfile
 │  └ requirements.txt
 ├ web
 │  └ Dockerfile
 └docker-compose.yml

※一部省略。
```


## docker-composeを定義

まずは、docker-composeでDRF用のコンテナとVue用のコンテナを定義します。

```docker-compose.yml
version: '3'
services:
    api:
        # Dockerfileの場所
        build: ./api

        # どのimageを使うか。Dockerfileでimageは作成する。
        image: k2-drf-image

        # コンテナ名を指定出来る。
        container_name: k2-api

        # 公開用のポート。ホスト側:コンテナ側を指定
        # コンテナ側のみ指定も可能だが、その時ホスト側はランダムになる。
        ports:
          -  '8000:8000'

        # マウントするパス
        volumes:
          -  ./api/:/usr/src/k2/api/
        
        # dockerコンテナを落とさず起動し続けるようにする
        tty: true
    web:
        build: ./web
        image: k2-vue-image
        container_name: k2-web
        ports:
          -  '8080:8080'
        volumes:
          -  ./web/:/usr/src/k2/web/
        tty: true
```


次に、apiとwebのDockerfileを定義します。

## apiのDockerfile

```Dockerfile
# イメージを選択
FROM python:3.9.5-alpine

# バイナリレイヤ下での標準出力とエラー出力を抑制
ENV PYTHONUNBUFFERED 1

# 開始ディレクトリ位置
WORKDIR /usr/src/k2/api

# 必要なものをインストール
# 今回はalpineなのでapkを使用する。
RUN apk update
RUN apk add --no-cache --vertual=wow-ini-module  \
  net-tools \
  sudo \
  bzip2 \
  curl \
  gcc \
  git \
  python3-dev \
  vim \
  bash

# ADDかCOPYを使うなら.dockerignoreを書いておく。
ADD . .

RUN pip install --upgrade pip
RUN pip install -r requirements.txt
```

とりあえず最小限をrequirements.txtに記述する。

```requirements.txt
Django==3.2.4
django-cors-headers==3.4.0
django-environ==0.4.5
django-filter==2.3.0
djangorestframework==3.12.4
djangorestframework-jwt==1.11.0
gunicorn==20.0.4
requests==2.24.0
```

## webのDockerfile

```Dockerfile
FROM node:10.17.0

WORKDIR /usr/src/k2/web/

RUN apt-get update
RUN apt-get install -y --no-install-recommends \
	net-tools \
	sudo \
	bzip2 \
	curl \
	gcc \
	git \
	vim
RUN apt-get clean

RUN npm install -g yarn \
	&& yarn global add @vue/cli \
```


## イメージ構築、コンテナ作成～立ち上げ

```
$ docker-compose up -d --build
```


## コンテナに接続し、環境を整える

ターミナルを2つ立ち上げてそれぞれで接続し、それぞれの最低限の環境を整える。

### DRF側

```
$ docker-compose exec api bash
$ django-admin startproject api
$ django-admin startapp k2
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser
```

上記で作ったファイル群を以下のフォルダ構成に直します。

```
k2/
 └ api
    ├ api/
    ├ k2/
    ├ db.sqlite3
    ├ Dockerfile
    ├ manage.py
    └ requirements.txt
```

とりあえずこの段階で、```python manage.py runserver 0.0.0.0:8000```で開発サーバーは立ち上がる。


### Vue側

```
$ docker-compose exec web bash
```

#### vueプロジェクトの作成


##### ①現在のディレクトリにフォルダ群を展開する。

```
$ vue create .
? Generate project in current directory? (Y/n) y
```

##### ②マニュアルで導入するものを選択する。

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

##### ③導入するものにスペースで☑を入れる。



```
? Check the features needed for your project:
 ◉ Choose Vue version
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◉ Unit Testing
❯◉ E2E Testing
```
・Babel
　JSのコードを新しい書き方から古い書き方へと変換するツール。
　ブラウザによって古いバージョンしか対応していないため、このようなツールが必要になる。

・PWA
　ネイティブアプリのような使い勝手を実現したWEBアプリの事。
　インストール不要で、ホーム画面やアイコン追加やプッシュ通知などが可能。
　そのほかに読み込み速度や表示の高速化、オフラインで閲覧化などのメリットもある。

・CSS Pre-processor
　Sass, Less, Stylusなどのプリプロセッサ独自の構文でCSSを作成するプログラム。
　純粋なCSSにはない、ミックスイン、セレクターの入れ子、セレクター継承などが可能。

・Linter
　ESLint等のコードフォーマッターなどを使用するか選択する。

・Unit Testing（単体テスト）
　Mocha+Chainや、Jestなどを使用するか選択する。

・E2E Testing（End to Endテスト）
　システム全体を通してテストを行う事。
　それらのライブラリなどを使うか選択する。



##### ④vueのバージョンを選択する。

3.x系だとvuetify Alphaしか対応してないみたいなので、今回は普通の2.xを使います。

```
? Choose a version of Vue.js that you want to start the project with
❯ 2.x
  3.x
```

##### ⑤routerのhistoryモードを使うか選択。

```
? Use history mode for router? (Requires proper server setup for index fallback in producti
on) (Y/n) y
```

・historyモード
　ページのリロード無しにURL遷移を実現するhistory.pushState APIを利用したルーターのモード。シングルページのクライアントアプリなので、適切なサーバーの設定をしないと、404エラーが起きる。そのため、どのアセットにもURLがマッチしなかった場合に、index.htmlを返す設定が必要。

・hashモード（vue-routerのデフォルト）
　完全なURLをhashを使ってシミュレートし、URLが変更された時にページのリロードが起きない。


##### ⑥ どのCSS pre-processorを使うか選択。

```
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default)
: (Use arrow keys)
❯ Sass/SCSS (with dart-sass)
  Sass/SCSS (with node-sass)
  Less
  Stylus
```


##### ⑦ linterを選択する。

```
? Pick a linter / formatter config: (Use arrow keys)
❯ ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
  ESLint + Prettier
```

##### ⑧ lintの追加の機能を設定

```
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert
 selection)
❯◉ Lint on save
 ◯ Lint and fix on commit
```

##### ⑨ unitテストに何を使用するか。

```
? Pick a unit testing solution: (Use arrow keys)
❯ Mocha + Chai
  Jest
```

・Mochaは、BDD/TDDをするための枠組みを提供、chaiはテストを実施するための便利なメソッドを提供してくれる。TDD（テスト駆動開発）/BDD（振る舞い駆動開発）

・JestはFacebookが開発したテストランナ。

##### ⑩ E2Eテストに何を使用するか。

```
? Pick an E2E testing solution:
❯ Cypress (Chrome only)
  Nightwatch (WebDriver-based)
  WebdriverIO (WebDriver/DevTools based)
```

・Cypress
　E2Eテストフレームワーク

・Nightwatch
　Seleniumを基盤として作られており、Node.jsでブラウザを操作してE2Eテストを行うツール

・WebdriverIO 
　ウェブブラウザによるテストを自動的に実行するライブラリ


##### ⑪ Babel, ESLintなどをどのファイルで管理するか。

```
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
  In package.json
```

##### ⑫ プリセットをセーブするか

```
? Save this as a preset for future projects? (y/N) n
```

##### ⑬ どちらのパッケージマネージャーを使うか。

```
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM
```

これでvueのプロジェクトが出来上がり、```yarn serve```で開発サーバーが立ち上がる。

あと、必要に応じて下記のライブラリ等も導入しておく。

下の連携テストでは、
・vuetify
・sass
・axios
・boxicons
・vue-loading-template
を使っている。

```
# update
yarn install

# vuetify
yarn add vuetify (vue add vuetify)

# sass
yarn add --dev sass

# vuesax
npm install vuesax

# material-icons（アイコン）
npm install material-icons --save

# axios
npm install --save axios vue-axios

# boxicons（アイコン）
npm i boxicons

# lodash
npm i --save lodash

# vue-persistedstate
npm i vuex-persistedstate

# vue-session
npm i vue-session

# vue-loading-template
npm install vue-loading-template --save
```


# 連携テスト

とりあえず、ユーザー一覧をDRFから受け取ってそれを表示させるところまでをやります。

## DRF側

__やる事__

1. 初期設定
1. ルーティング
1. View記述


### DRF: 初期設定

```k2/api/api/settings.py

# 追加
import logging
logging.basicConfig(
    level=logging.DEBUG,
    format='''%(levelname)s %(asctime)s %(pathname)s:%(funcName)s 行数:%(lineno)s:%(lineno)s
    %(message)s'''

# 編集
ALLOWED_HOSTS = ['*']


# 追加
INSTALLED_APPS = [
    ...
    'rest_framework',
    
    # Json Web Token関連のライブラリ
    'rest_framework_jwt',
    'django_filters',
    'k2.apps.K2Config',
]

# 追加
REST_FRAMEWORK = {
    # デフォルトのパーミッションクラス
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
    ],
    # デフォルトの認証クラス
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    # デフォルトのフィルターバックエンド
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
    ],
    # デフォルトのスロットルクラス
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    # デフォルトのスロットルの制限
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '5/sec',
    }
}

# 追加
if DEBUG:
    # CORS関連の設定追加。
    INSTALLED_APPS += ['corsheaders']
    MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware'] + MIDDLEWARE
    CORS_ORIGIN_ALLOW_ALL = True
```

### DRF: ルーティング

```k2/api/k2/urls.py
from django.urls import path, include
from . import views, viewsets
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('users', viewsets.UserViewSet)

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

```k2/api/api/urls.py
from django.contrib import admin
from django.urls import path, include

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


### DRF: View記述

今回はDjangoデフォルトのUserモデルを使用します。

```k2/api/k2/viewsets.py
from django.contrib.auth.models import User
from serializers import UserSerializer
from rest_framework import (
    viewsets
)
import logging

logger = logging.getLogger(__name__)


class BaseModelViewSet(viewsets.ModelViewSet):

    pass

class UserViewSet(BaseModelViewSet):

    queryset = User.objects.all()
    serializer_class = UserSerializer

    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)
```

```k2/api/k2/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
import logging
import logging

logger = logging.getLogger(__name__)


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = '__all__'
```

これで、```localhost:8000/api/users/```にアクセスすると、```python manage.py createsuperuser```コマンドなどで作成したユーザー一覧が返る。



## Vue側

__やる事__

1. 初期設定
1. ルーティング
1. 画面実装
1. ajax投げて表示


### Vue: 初期設定

main.jsでライブラリ等を読み込む。

```k2/web/src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import http from '@/plugins/http'
import vuetify from './plugins/vuetify'
import eventHub from '@/plugins/eventHub'

import 'boxicons/css/boxicons.min.css'

Vue.config.productionTip = false

Vue.use(http)
Vue.use(eventHub)

new Vue({
  router,
  store,
  vuetify,
  render: h => h(App)
}).$mount('#app')
```

```k2/web/src/App.vue
<template>
    <v-app>
        <router-view/>
    </v-app>
</template>

<script>
    export default {
        name: 'App',
    }
</script>
```

axiosのデフォルト設定をしておく。

```k2/web/src/plugins/http.js
import Vue from 'vue'
import axios from 'axios'
import router from '@/router'


export default {
    install: function (Vue, options) {
        const http = axios.create({
            baseURL: 'http://localhost:8000/',
            xsrfCookieName: 'csrftoken',
            xsrfHeaderName: 'X-CSRFTOKEN',
            timeout: 10000,
        })
        Vue.prototype.$axios = http
    }
}
```

```k2/web/src/plugins/vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
})
```

孫コンポーネントとか遠い関係のコンポーネントのイベントを検知するためのもの。

```k2/web/src/plugins/eventHub.js
import Vue from 'vue'

const eventHub = {
    install: function (Vue, options) {
        Vue.prototype.$eventHub = new Vue()
    }
}

export default eventHub
```

eslintがうるさいので最低限だまらせておく。

```k2/web/.eslintrc.js
  ...
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-tabs': 'off',
    'indent': 'off',
    'comma-dangle': 'off',
    'eol-last': 'off',
    'no-unused-vars': 'off',
    'no-mixed-spaces-and-tabs': 'off',
    'no-unneeded-ternary': 'off',
    'vue/no-unused-components': 'off',
    'no-multi-spaces': 'off'
  },
  ...
```


### Vue: ルーティング

全体のルート

```k2/web/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import pages from './pages'

Vue.use(VueRouter)

const routes = [...pages]

const router = new VueRouter({
    mode: 'history',
    routes
})

export default router
```

ページ関連のルート

```k2/web/src/router/pages.js
import { Home } from '@/views/index'

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

export default routes
```


### Vue: 画面実装

index.jsにコンポーネントを纏める。

```k2/web/src/views/index.js
export { default as Home } from './Home'
```

Home画面のコンポーネント。今回はコンポーネント分けないで直書きしちゃいます。

```k2/web/src/views/Home.vue
<template>
    <div id="home_wrap">
        <v-card
            flat
            tile
        >
            <div v-if="loading">
                <Loading/>
            </div>
            <div v-else>
                <v-card-title>
                    users
                </v-card-title>
                <v-list three-line>
                    <template v-for="(user, i) in users">
                        <v-list-item
                            :key="i"
                        >
                            <v-list-item-avatar>
                                <i class='bx bxs-user'></i>
                            </v-list-item-avatar>

                            <v-list-item-content>
                                <v-list-item-title>
                                    {{ user.username }}
                                </v-list-item-title>
                                <v-list-item-subtitle>
                                    id: {{ user.id }}
                                </v-list-item-subtitle>
                            </v-list-item-content>

                        </v-list-item>
                    </template>
                </v-list>
            </div>
        </v-card>
    </div>
</template>

<script>
  import Loading from '../components/Loading'

  export default {
    name: 'Home',
    components: {
        Loading,
    },
    data: () => ({
        loading: true,
        users: [],
    }),
    mounted () {
        this.getUsers()
    },
    methods: {
        getUsers () {
            this.loading = true
            this.$axios({
                url: '/api/users/',
                method: 'GET',
            })
            .then(res => {
                console.log(res)
                this.users = res.data
                this.loading = false
            })
            .catch(e => {
                console.log(e)
                this.loading = false
            })
        }
    }
  }
</script>

<style lang="scss" scoped>
    #home_wrap {
        width: 1200px;
        margin: 0 auto;
    }
</style>
```

ロード画面のコンポーネント

```k2/web/src/components/Loading.vue
<template>
    <div class="loading_wrap">
        <vue-loading
            type="cylon"
            color="#aaa"
            :size="{ width: '50px', height: '50px' }"
        ></vue-loading>
    </div>
</template>
<script>
    import { VueLoading } from 'vue-loading-template'

    export default {
        name: 'Loading',
        components: {
            VueLoading,
        },
    }
</script>
<style lang="scss" scoped>
    .loading_wrap {
        width: 50px;
        height: 50px;
        margin: 50px auto;
    }
</style>
```


#まとめ

こんな感じの画面になりました。

![k2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324522/3e40f4aa-4280-6670-740c-04d7ed262bad.png)