Django + Vue.js + Nuxt.js でSSRアプリを作る
遅ればせながら、新年明けましておめでとうございます。
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から依存関係をインストールします。
※ 私はプロジェクト毎に使い捨てのコンテナを立ち上げているため直接インストールしています。
適宜 pyenv
や pipenv
などの仮想環境を使用してください。
$ 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_URL
と MEDIA_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.py
に api/
配下の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等を使用してください。
画面下部にある 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を持ちます。
waterplant
– 特定の水草に関する情報を含むオブジェクト。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にアップしています。