Django + Vue.js + Nuxt.js でSSRアプリを作る

Python,TIPS,Vue.jsDjango,Nuxt

遅ればせながら、新年明けましておめでとうございます。
12月、1月とお仕事でリリースしなければならないものもあり気づけば中旬に差し掛かってしまいました。

さて、前回はNuxt.jsを使用したSSRアプリケーションの概要について投稿しましたが今回はDjangoとNuxtを使用し実際にSSRアプリケーションのサンプルを作成してみます。

今回の構成はバックエンドのDjangoはDRF(Django Rest Framework)を使用しAPIを提供、フロントエンドはVueベースのフレームワークであるNuxt.jsで作成していきます。

今回作成するサンプルアプリです。

今回作成するコードはGitHubから取得できます。


バックエンドの準備

まずは今回のプロジェクトディレクトリを作成し、移動しましょう。

$ mkdir django-nuxt-ssr && cd django-nuxt-ssr

続いてpipから依存関係をインストールします。

※ 私はプロジェクト毎に使い捨てのコンテナを立ち上げているため直接インストールしています。
 適宜 pyenvpipenv などの仮想環境を使用してください。

$ pip3 install --user django django-rest-framework django-cors-headers

次に api というDjangoプロジェクトを作成し、 core というDjangoアプリケーションを作成します。

$ django-admin startproject api
$ cd api
$ python3 manage.py startapp core


settings.py にお決まりのアプリ登録を行います。

api/settings.py

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # 追加
    'corsheaders', # 追加
    'core' # 追加
  ] 

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',
]

 # MIDDLEWARE の直下で追加
CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
)

...
...

# STATIC_URL の直下で追加
MEDIA_URL = '/media/' # 追加
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 追加

今回はホワイトリストに localhost:3000 を追加しましたがご自身の使われてる環境に合わせて適宜変更をしてください。
また、今回作成するアプリは画像を取り扱うため MEDIA_URLMEDIA_ROOT も記述しています。

Model の定義

それでは早速 Model を定義していきます。

core/models.py

from django.db import models


class WaterPlants(models.Model):
    CARBON_DIOXIDE = (
        ("Low", "Low"),
        ("Middle", "Middle"),
        ("High", "High"),
    )
    DIFFICULTY_LEVELS = (
        ("Easy", "Easy"),
        ("Medium", "Medium"),
        ("Hard", "Hard"),
    )
    name = models.CharField(max_length=120)
    position = models.CharField(max_length=50)
    picture = models.FileField()
    difficulty = models.CharField(choices=DIFFICULTY_LEVELS, max_length=10)
    addition_amount = models.CharField(choices=CARBON_DIOXIDE, max_length=10)
    leaf_length = models.PositiveIntegerField()
    water_quality = models.TextField()

    def __str_(self):
        return "{} の水草情報".format(self.name)

水草を管理するClassとして以下のプロパティを定義しました。

  • 名前
  • 配置場所
  • 写真
  • 育成難易度
  • CO2添加量
  • 葉長
  • 水質

シリアライザーの作成

フロント側が受信したデータを簡単に処理できる様にModelインスタンスをJSONに変換させるシリアライザーを作成します。

core/serializers.py

from rest_framework import serializers
from .models import WaterPlants


class WaterPlantsSerializer(serializers.ModelSerializer):
    class Meta:
        model = WaterPlants
        fields = (
            "id",
            "name",
            "position",
            "picture",
            "difficulty",
            "addition_amount",
            "leaf_length",
            "water_quality",
        )

管理画面の編集

Djangoには標準で用意された管理インターフェイスが存在しています。
この管理画面を使用し、先ほど作成した水草モデルのテストをしてみましょう。
まず最初に管理画面の構成を少し変更します。

core/admin.py

from django.contrib import admin
from .models import WaterPlants  # 追加

# Register your models here.

admin.site.register(WaterPlants)  # 追加

Viewの作成

viewsets.ModelViewSet を使用してCRUD処理を用意します。
これは先ほど作成したシリアライザーと、シリアライザーに引き渡すためのクエリセットを指定するだけ使用することができます。

core/views.py

from rest_framework import viewsets
from .serializers import WaterPlantsSerializer
from .models import WaterPlants


class WaterPlantsViewSet(viewsets.ModelViewSet):
    serializer_class = WaterPlantsSerializer
    queryset = WaterPlants.objects.all()

URLの設定

続いてエンドポイントの準備を行います。
まずはプロジェクトルートの urls.py を次の様に編集しアプリルートの urls.pyapi/ 配下のURIを委任するよう定義します。

api/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  # 追加

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("core.urls")),  # 追加
]

# 追加
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


次にアプリルート内に urls.py を作成します。

core/urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import WaterPlantsViewSet

router = DefaultRouter()
router.register(r"waterplants", WaterPlantsViewSet)

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

上記のコードは以下の様にURLパターンを生成してくれます。

  • /waterplants/
    このルートで新規水草の作成や読み取りが実行可能
  • /waterplants/{id}
    このルートで既存水草の読み取り・更新・削除が実行可能

マイグレーション

これまでの工程で水草モデルを作成し構造を定義したため、マイグレーション用のファイルを作成しモデルの変更をデータベースに反映させる必要があります。

$ python3 manage.py makemigrations
$ python3 manage.py migrate


最後に管理画面にアクセスするためのスーパーユーザを作成します。

$ python3 manage.py createsuperuser

これでバックエンド側の準備は完了です。
それでは早速開発サーバを起動してみましょう。

APIのテスト

次のコマンドを実行し開発サーバを起動します。

$ python3 manage.py runserver

サーバが起動したら http://localhost:8000/api/waterplants/ をブラウザで開いてください。

DRFは簡易的なテストインターフェイスを備えているのでブラウザ上から直接APIをテストすることができますが、お好みに合わせてcURLやPostman等を使用してください。

※ この画像ではポートフォワーディングによりポートが8000とは異なっています。
ローカルで起動した場合はデフォルトでは8000でアクセスしてください。

画面下部にある Raw data もしくは HTML data を使用して実際にデータ投入することができます。
試しに適当なデータを一件POSTしてみましょう。

ニューラージパールグラスを投入してみました。

このPOSTによりSequentialで自動採番された id を使用して DELETE / PUT 及び PATCH を実行することができます。
一先ず正常にGET出来ることを試すため http://localhost:8000/api/waterplants/1 にアクセスしてみましょう。

無事に取得することができました。
これにてバックエンド側の準備は完了です、続いてフロント側の構築をしていきます。


フロントエンドの準備

まず最初に今回のトップディレクトリである django-nuxt-ssr にNuxtアプリを作成します。

$ cd /path/to/django-nuxt-ssr
$ npx create-nuxt-app client

もし上記コマンドでエラーが発生した際は npm install -g create-nuxt-app で最初にダウンロードしてから再度 npx コマンドを実行してみてください。

正常に実行するといくつかの質問がインタラクティブに表示されますので以下の様に回答してください。

  • client と入力してEnter
  • 適当な説明を入力するか、空欄のままEnter
  • 適当な名前を入力するか、空欄のままEnter
  • Npmをスペースで選択しEnter
  • Bootstrap Vueをスペースで選択肢Enter
  • Noneを選択しEnter
  • Axios, PWA Support をスペースで選択しEnter
  • ESLintをスペースで選択しEnter
  • Noneを選択しEnter
  • Universal(SSR)を選択しEnter
  • VSCode を使用しているなら選択しEnter

回答が完了すると依存関係のインストールが実行されます。
インストールが終わったら次のコマンドを実行し開発モードでアプリケーションを起動してみましょう。

$ cd client
$ npm run dev

サーバーの起動後 http://localhost:3000 にブラウザからアクセスすると次の様な画面の後、

この様にWelcomeページが表示されます。

生成された client フォルダーのディレクトリ構造については前回の投稿で解説していますので今回は省略します。

ページの作成

まず最初に pages/ ディレクトリに単一ファイルコンポーネントを設置し次の5ページを作成していきましょう。

  • ホームページ
  • 全ての水草一覧ページ
  • 水草情報の詳細ページ
  • 水草情報の編集ページ
  • 水草情報の追加ページ

次のコマンドを実行しフォルダーとファイルを作成してください。

cd pages
touch index.vue
mkdir waterplants && cd waterplants
touch add.vue
touch index.vue
mkdir _id && cd _id
touch edit.vue
touch index.vue

 pagesディレクトリはこのようになります。

そしてこのディレクトリ構成を元にNuxtで生成されるルートは次の通りです。

  • /
    pages/index.vue により生成
  • /waterplants/add
    pages/waterplants/add.vue により生成
  • /waterplants/
    pages/waterpants/index.vue により生成
  • /waterplants/{id}/
    pages/waterplants/_id/index.vue により生成
  • /waterplants/{id}/edit
    pages/waterplants/_id/edit.vue により生成

ホームページの作成

Nuxt.jsアプリケーションの各インスタンスにはデフォルトのレイアウトが付属していますが、今回は干渉しないよう削除しておきます。

layouts/default.vue

<template>
  <div>
    <nuxt/>
  </div>
</template>

<style>
</style>


次にホームページの単一ファイルを編集します。

pages/index.vue

<template>
  <header>
    <div class="text-box">
      <h1>自分で作る水草図鑑</h1>
      <p class="mt-3">
        自分が育てた水草を記録してみよう!
      </p>
      <nuxt-link class="btn btn-outline btn-large btn-info" to="/waterplants">
        水草一覧
        <span class="ml-2">→</span>
      </nuxt-link>
    </div>
  </header>
</template>
<script>
export default {
  head () {
    return {
      title: 'Home page'
    }
  }
}
</script>
<style>
header {
  min-height: 100vh;
  background-image: linear-gradient(
      to right,
      rgba(0, 0, 0, 0.7),
      rgba(0, 0, 0, 0.2)
    ),
    url("/images/banner.jpg");
  background-position: center;
  background-size: cover;
  position: relative;
}
.text-box {
  position: absolute;
  top: 50%;
  left: 10%;
  transform: translateY(-50%);
  color: #fff;
}
.text-box h1 {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  font-size: 5rem;
}
.text-box p {
  font-size: 2rem;
  font-weight: lighter;
}
</style>

<nuxt-link>はページ間を移動するために使用できるNuxt.jsコンポーネントで、Vue Routerの<router-link>コンポーネントととてもよく似ています。

また、head() という関数を使用しました。
このメソッドは現在のページに特定の<meta>タグを設定するために使用します。

それではホームページがどの様に表示されるのか実際に確認してみましょう。
npm run dev を実行し http://localhost:3000 にアクセスします。

いい感じですね。
それでは他のページについても作成していきます。

次からは最初に作成したバックエンド側と通信をさせていきますので、停止している場合はDjangoの開発サーバを起動しておいてください。

水草一覧ページの作成

まずは components/ ディレクトリに WaterPlantCard.vue というVue.jsコンポーネントを作成しましょう。

components/WaterPlantCard.vue

<template>
  <div class="card water-plant-card">
    <img :src="waterplant.picture" class="card-img-top">
    <div class="card-body">
      <h5 class="card-title">
        {{ waterplant.name }}
      </h5>
      <p class="card-text">
        <strong>配置場所:</strong> {{ waterplant.position }}
      </p>
      <div class="action-buttons">
        <nuxt-link :to="`/waterplants/${waterplant.id}/`" class="btn btn-sm btn-success">
          詳細
        </nuxt-link>
        <nuxt-link :to="`/waterplants/${waterplant.id}/edit/`" class="btn btn-sm btn-primary">
          編集
        </nuxt-link>
        <button @click="onDelete(waterplant.id)" class="btn btn-sm btn-danger">
          削除
        </button>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  props: ['waterplant', 'onDelete']
}
</script>
<style>
.water-plant-card {
    box-shadow: 0 1rem 1.5rem rgba(0,0,0,.6);
}
</style>

このコンポーネントは2つのpropsを持ちます。

  1. waterplant – 特定の水草に関する情報を含むオブジェクト。
  2. onDelete – ユーザーが水草を削除するためにボタンをクリックすることでトリガーされる。

次に pages/waterplants/index.vue を更新します。

<template>
  <main class="container mt-5">
    <div class="row">
      <div class="col-12 text-right mb-4">
        <div class="d-flex justify-content-between">
          <h3>育てた水草一覧</h3>
          <nuxt-link to="/waterplants/add" class="btn btn-info">
            水草を追加する
          </nuxt-link>
        </div>
      </div>
      <template v-for="waterplant in waterplants">
        <div :key="waterplant.id" class="col-lg-3 col-md-4 col-sm-6 mb-4">
          <water-plant-card :onDelete="deleteWaterPlant" :waterplant="waterplant"></water-plant-card>
        </div>
      </template>
    </div>
  </main>
</template>
<script>

import WaterPlantCard from '~/components/WaterPlantCard.vue'

const sampleData = [
  {
    id: 1,
    name: 'パールグラス',
    position: '中景〜後景',
    picture: '/images/banner.jpg',
    difficulty: 'Easy',
    addition_amount: 'High',
    leaf_length: 2,
    water_quality: '弱酸性~中性  軟水~弱硬水'
  },
  {
    id: 2,
    name: 'キューバパールグラス',
    position: '前景',
    picture: '/images/banner.jpg',
    difficulty: 'Hard',
    addition_amount: 'High',
    leaf_length: 1,
    water_quality: '弱酸性~中性  軟水~弱硬水'
  },
  {
    id: 3,
    name: 'ニューラージパールグラス',
    position: '前景',
    picture: '/images/banner.jpg',
    difficulty: 'Medium',
    addition_amount: 'High',
    leaf_length: 1,
    water_quality: '弱酸性~中性  軟水~弱硬水'
  }
]

export default {
  head () {
    return {
      title: 'waterplants list'
    }
  },
  components: {
    WaterPlantCard
  },
  data () {
    return {
      waterplants: []
    }
  },
  asyncData (context) {
    let data = sampleData
    return {
      waterplants: data
    }
  },
  methods: {
    deleteWaterPlant (waterplant_id) {
      console.log(deleted`${waterplant.id}`)
    }
  }
}
</script>
<style scoped>
</style>

この画像を見てください。
先ほどのコードでは waterplants コンポーネントの data () で空の配列を設定しましたが3つのカードが表示されています。
これは asyncData メソッドがページのロード前に実行されることによります。

では早速先ほど作成したDjangoのAPIに create-nuxt-app を実行した際に選択した Axios を使用してリクエストを投げ、そのレスポンス結果でコンポーネントのデータを更新してみましょう。

まず configファイルを更新します。

client/nuxt.config.js


export default {
  mode: 'universal',

  ...
  ...
  
  /*
  ** Axios module configuration
  ** See https://axios.nuxtjs.org/options
  */
  axios: {
    baseURL: "http://localhost:8000/api" // 追加
  },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    }
  }
}


次に、pages/waterplants/index.vue ファイルを開き <script> タグ内を次のコードに置き換えます。

pages/waterplants/index.vue

export default {
  head () {
    return {
      title: '水草一覧'
    }
  },
  components: {
    WaterPlantCard
  },
  data () {
    return {
      waterplants: []
    }
  },
  async asyncData ({ $axios, params }) {
    try {
      const waterplants = await $axios.$get(`/waterplants/`)
      return { waterplants }
    } catch (e) {
      return { waterplants: [] }
    }
  },
  methods: {
    async deleteWaterPlant (waterplant_id) { // eslint-disable-line
      try {
        await this.$axios.$delete(`/waterplants/${waterplant_id}/`) // eslint-disable-line
        const newWaterPlants = await this.$axios.$get('/waterplants/')
        this.waterplants = newWaterPlants
      } catch (e) {
        console.log(e)
      }
    }
  }
}

deleteWaterPlants() メソッドは特定の水草を削除し、Djangoのバックエンドから最新の水草リストを取得しコンポーネントを更新します。

それでは http://localhost:3000/ にアクセスして確認してみましょう。

無事にDjangoからデータを取得することが出来ました。
続いて新規追加機能を実装しましょう。

新しい水草を追加する

pages/recipes/add/ を編集していきます。

<template>
  <main class="container my-5">
    <div class="row">
      <div class="col-12 text-center my-3">
        <h2 class="mb-3 display-4 text-uppercase">
          {{ waterplant.name }}
        </h2>
      </div>
      <div class="col-md-6 mb-4">
        <img
          v-if="preview"
          :src="preview"
          class="img-fluid"
          style="width: 400px border-radius: 10px box-shadow: 0 1rem 1rem rgba(0,0,0,.7)"
          alt
        >
        <img
          v-else
          src="@/static/images/banner.jpg"
          class="img-fluid"
          style="width: 400px border-radius: 10px box-shadow: 0 1rem 1rem rgba(0,0,0,.7)"
        >
      </div>
      <div class="col-md-4">
        <form @submit.prevent="submitWaterPlant">
          <div class="form-group">
            <label for>水草名称</label>
            <input v-model="waterplant.name" type="text" class="form-control">
          </div>
          <div class="form-group">
            <label for>植える位置</label>
            <input v-model="waterplant.position" type="text" class="form-control">
          </div>
          <div class="form-group">
            <label for>水草の写真</label>
            <input @change="onFileChange" type="file" name="file">
          </div>
          <div class="row">
            <div class="col-md-6">
              <div class="form-group">
                <label for>育成難易度</label>
                <select v-model="waterplant.difficulty" class="form-control">
                  <option value="Easy">
                    Easy
                  </option>
                  <option value="Medium">
                    Medium
                  </option>
                  <option value="Hard">
                    Hard
                  </option>
                </select>
              </div>
            </div>
            <div class="col-md-6">
              <div class="form-group">
                <label for>二酸化炭素添加量</label>
                <select v-model="waterplant.addition_amount" class="form-control">
                  <option value="Low">
                    Low
                  </option>
                  <option value="Middle">
                    Middle
                  </option>
                  <option value="High">
                    High
                  </option>
                </select>
              </div>
            </div>
            <div class="col-md-6">
              <div class="form-group">
                <label for>
                  葉長
                  <small>(mm)</small>
                </label>
                <input v-model="waterplant.leaf_length" type="number" class="form-control">
              </div>
            </div>
          </div>
          <div class="form-group mb-3">
            <label for>
              水質
            </label>
            <input v-model="waterplant.water_quality" type="text" class="form-control">
          </div>
          <button type="submit" class="btn btn-primary">
            登録
          </button>
        </form>
      </div>
    </div>
  </main>
</template>
<script>
export default {
  head () {
    return {
      title: '水草追加'
    }
  },
  data () {
    return {
      waterplant: {
        name: '',
        position: '',
        picture: '',
        difficulty: '',
        addition_amount: '',
        leaf_length: '',
        water_quality: ''
      },
      preview: ''
    }
  },
  methods: {
    onFileChange (e) {
      const files = e.target.files || e.dataTransfer.files
      if (!files.length) {
        return
      }
      this.waterplant.picture = files[0]
      this.createImage(files[0])
    },
    createImage (file) {
      const reader = new FileReader()
      const vm = this
      reader.onload = (e) => {
        vm.preview = e.target.result
      }
      reader.readAsDataURL(file)
    },
    async submitWaterPlant () {
      const config = {
        headers: { 'content-type': 'multipart/form-data' }
      }
      const formData = new FormData()
      for (const data in this.waterplant) {
        formData.append(data, this.waterplant[data])
      }
      try {
        const response = await this.$axios.$post('/waterplants/', formData, config)
        this.$router.push('/waterplants/')
      } catch (e) {
        console.log(e)
      }
    }
  }
}
</script>
<style scoped>
</style>

submitWaterPlant() でフォームのデータが投稿され、水草情報が正常に作成されるとthis.$router を使用して一覧画面へリダイレクトを行います。

詳細ページを作成する

次はユーザーが特定の水草を表示できるように /pages/waterplants/_id/index.vue を更新します。

<template>
  <main class="container my-5">
    <div class="row">
      <div class="col-12 text-center my-3">
        <h2 class="mb-3 display-4 text-uppercase">
          {{ waterplant.name }}
        </h2>
      </div>
      <div class="col-md-6 mb-4">
        <img
          class="img-fluid"
          :src="waterplant.picture"
          style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
          alt
        >
      </div>
      <div class="col-md-6">
        <div class="waterplant-details">
          <h4>植える位置</h4>
          <p>{{ waterplant.position }}</p>
          <h4>育成難易度</h4>
          <p>{{ waterplant.difficulty }}</p>
          <h4>二酸化炭素添加量</h4>
          <p>{{ waterplant.addition_amount }}</p>
          <h4>葉長</h4>
          <p>{{ waterplant.leaf_length }} mm</p>
          <h4>水質</h4>
          <p>{{ waterplant.water_quality }}</p>
        </div>
      </div>
    </div>
  </main>
</template>
<script>
export default {
  head () {
    return {
      title: '水草詳細ページ'
    }
  },
  data () {
    return {
      waterplant: {
        name: '',
        position: '',
        picture: '',
        difficulty: '',
        addition_amount: '',
        leaf_length: '',
        water_quality: ''
      }
    }
  },
  async asyncData ({ $axios, params }) {
    try {
      const waterplant = await $axios.$get(`/waterplants/${params.id}`)
      return { waterplant }
    } catch (e) {
      return { waterplant: [] }
    }
  }
}
</script>
<style scoped>
</style>

詳細の表示はとてもシンプルに実装できます。
表示したい水草のIDを取得するために asyncData() の中で params を使用しており、これによりURLからパラメータで渡される水草のIDを取得しDangoから該当のデータを取得します。

実際にブラウザで開いて見ると以下の様に水草情報が表示されました。

編集ページを作成する

一度登録した水草の情報を修正できないのは問題があるため最後に編集ページを作成します。
/pages/waterplants/_id/edit.vue を作り込みます。

<template>
  <main class="container my-5">
    <div class="row">
      <div class="col-12 text-center my-3">
        <h2 class="mb-3 display-4 text-uppercase">
          {{ waterplant.name }}
        </h2>
      </div>
      <div class="col-md-6 mb-4">
        <img v-if="!preview" :src="waterplant.picture" class="img-fluid" style="width: 400px border-radius: 10px box-shadow: 0 1rem 1rem rgba(0,0,0,.7)">
        <img v-else :src="preview" class="img-fluid" style="width: 400px border-radius: 10px box-shadow: 0 1rem 1rem rgba(0,0,0,.7)">
      </div>
      <div class="col-md-4">
        <form @submit.prevent="submitWaterPlant">
          <div class="form-group">
            <label for>水草の名称</label>
            <input v-model="waterplant.name" type="text" class="form-control">
          </div>
          <div class="form-group">
            <label for>植える位置</label>
            <input v-model="waterplant.position" type="text" class="form-control" name="Positions">
          </div>
          <div class="form-group">
            <label for>水草の写真</label>
            <input @change="onFileChange" type="file">
          </div>
          <div class="row">
            <div class="col-md-6">
              <div class="form-group">
                <label for>
                  育成難易度
                </label>
                <select v-model="waterplant.difficulty" class="form-control">
                  <option value="Easy">
                    Easy
                  </option>
                  <option value="Medium">
                    Medium
                  </option>
                  <option value="Hard">
                    Hard
                  </option>
                </select>
              </div>
            </div>
            <div class="col-md-6">
              <div class="form-group">
                <label for>二酸化炭素添加量</label>
                <select v-model="waterplant.addition_amount" class="form-control">
                  <option value="Low">
                    Low
                  </option>
                  <option value="Middle">
                    Middle
                  </option>
                  <option value="High">
                    High
                  </option>
                </select>
              </div>
            </div>
            <div class="col-md-6">
              <div class="form-group">
                <label for>葉長<small>(mm))</small></label>
                <input v-model="waterplant.leaf_length" type="text" class="form-control" name="Positions">
              </div>
            </div>
          </div>
          <div class="form-group mb-3">
            <label for>水質</label>
            <input v-model="waterplant.water_quality" type="text" class="form-control" name="Positions">
          </div>
          <button type="submit" class="btn btn-success">
            更新
          </button>
        </form>
      </div>
    </div>
  </main>
</template>
<script>
export default {
  head () {
    return {
      title: '水草編集ページ'
    }
  },
  data () {
    return {
      waterplant: {
        name: '',
        position: '',
        picture: '',
        difficulty: '',
        addition_amount: '',
        leaf_length: '',
        water_quality: ''
      },
      preview: ''
    }
  },
  async asyncData ({ $axios, params }) {
    try {
      const waterplant = await $axios.$get(`/waterplants/${params.id}`)
      return { waterplant }
    } catch (e) {
      return { waterplant: [] }
    }
  },
  methods: {
    onFileChange (e) {
      const files = e.target.files || e.dataTransfer.files
      if (!files.length) {
        return
      }
      this.waterplant.picture = files[0]
      this.createImage(files[0])
    },
    createImage (file) {
      const reader = new FileReader()
      const vm = this
      reader.onload = (e) => {
        vm.preview = e.target.result
      }
      reader.readAsDataURL(file)
    },
    async submitWaterPlant () {
      const editedWaterPlant = this.waterplant
      if (editedWaterPlant.picture.includes('http://') !== -1) {
        delete editedWaterPlant.picture
      }
      const config = {
        headers: { 'content-type': 'multipart/form-data' }
      }
      const formData = new FormData()
      for (const data in editedWaterPlant) {
        formData.append(data, editedWaterPlant[data])
      }
      try {
        const response = await this.$axios.$patch(`/waterplants/${editedWaterPlant.id}/`, formData, config)
        this.$router.push('/waterplants/')
        console.log(response)
      } catch (e) {
        console.log(e)
      }
    }
  }
}
</script>

<style>
</style>

ワンポイントとして、上記の submitWaterPlant() メソッドでは下記条件付きステートメントを設定しています。

if (editedWaterPlant.picture.includes('http://') !== -1) {
  delete editedWaterPlant.picture
}

これはユーザによって画像が変更されなかった場合、送信されるデータから waterplant.picture を削除します。

トランジションの設定

機能面はこれでほぼ完成しました。
最後にNuxt.jsが提供している <transition> を使用してページ推移時にトランジションさせてみましょう。

まずはトランジション用の設定ファイルを用意します。
assetsフォルダにcssフォルダを作成し、次のファイルを作成してください。

assets/css/transitions.css

.page-enter-active,
.page-leave-active {
  transition: opacity .3s ease;
}
.page-enter,
.page-leave-to {
  opacity: 0;
}

そして作成したCSSファイルを全ページを対象に読み込むよう設定します。

nuxt.config.js

** Global CSS
*/
css: [
  '~/assets/css/transitions.css' // 追加
],


これだけで完了です。
とても簡単にトランジションを設定することができました。


まとめ

今回の投稿ではDjangoとNuxtを使用してユニバーサルアプリを作成してみました。
ルーティング周りの簡略化などNuxtは非常に優れたフレームワークです。
通常のVueも楽しいですがNuxtを導入してみるのもお勧めします。

今回作成したソースコードは、GitHubにアップしています。

Python,TIPS,Vue.jsDjango,Nuxt

Posted by Kenny