Nuxt.jsとDjangoを使用してユニバーサルアプリケーションを構築する方法
序章
React.jsやVue.jsなどの最新のJavaScriptライブラリの出現により、フロントエンドWeb開発が改善されました。 これらのライブラリには、ブラウザに完全にリロードせずにWebページのコンテンツを動的にロードするSPA(シングルページアプリケーション)などの機能が付属しています。
ほとんどのシングルページアプリケーションの背後にある概念は、クライアント側のレンダリングです。 クライアント側のレンダリングでは、コンテンツの大部分はJavaScriptを使用してブラウザーでレンダリングされます。 ページの読み込み時に、JavaScriptが完全にダウンロードされ、サイトの残りの部分がレンダリングされるまで、コンテンツは最初に読み込まれません。
クライアント側のレンダリングは比較的最近の概念であり、その使用にはトレードオフがあります。 注目すべきマイナス面は、JavaScriptを使用してページが更新されるまでコンテンツが正確にレンダリングされないため、検索エンジンがクロールするデータがほとんどないため、WebサイトのSEO(検索エンジン最適化)が損なわれることです。
一方、サーバー側のレンダリングは、ブラウザでHTMLページをレンダリングする従来の方法です。 古いサーバー側のレンダリングされたアプリケーションでは、WebアプリケーションはPHPなどのサーバー側の言語を使用して構築されます。 ブラウザからWebページが要求されると、リモートサーバーは(動的な)コンテンツを追加し、入力されたHTMLページを配信します。
クライアント側のレンダリングには欠点があるのと同じように、サーバー側のレンダリングでは、ブラウザがサーバーリクエストを頻繁に送信し、同様のデータに対してフルページのリロードを繰り返し実行します。 最初にSSR(Server-Side Rendering)ソリューションを使用してWebページをロードし、次にフレームワークを使用してさらに動的ルーティングを処理し、必要なデータのみをフェッチできるJavaScriptフレームワークがあります。 結果として得られるアプリケーションは、ユニバーサルアプリケーションと呼ばれます。
要約すると、ユニバーサルアプリケーションは、クライアント側とサーバー側で実行できるJavaScriptコードを記述するために使用されます。 この記事では、Nuxt.jsを使用してユニバーサルレシピアプリケーションを構築します。
Nuxt.jsは、UniversalVue.jsアプリケーションを開発するための高レベルのフレームワークです。 その作成は、Reactの Next.js に触発され、サーバー側でレンダリングされたVue.jsアプリケーションのセットアップで発生する問題(サーバー構成とクライアントコードの配布)を抽象化するのに役立ちます。 Nuxt.jsには、非同期データ、ミドルウェア、レイアウトなど、クライアント側とサーバー側の間の開発を支援する機能も付属しています。
注:シングルページアプリケーションを作成するときにVue.jsはデフォルトでクライアント側レンダリングをすでに実装しているため、ビルドするアプリケーションをサーバー側レンダリング(SSR)と呼ぶことができます。 実際、このアプリケーションはユニバーサルアプリケーションです。
この記事では、DjangoとNuxt.jsを使用してユニバーサルアプリケーションを作成する方法を説明します。 Djangoはバックエンド操作を処理し、DRF(Django Rest Framework)を使用してAPIを提供し、Nuxt.jsはフロントエンドを作成します。
最終的なアプリケーションのデモは次のとおりです。
最終的なアプリケーションは、CRUD操作を実行するレシピアプリケーションであることがわかります。
前提条件
このチュートリアルを実行するには、マシンに次のものをインストールする必要があります。
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
- このプロジェクトでは、Pythonをローカル環境にインストールする必要があります。
- このプロジェクトはPipenvを利用します。 すべてのパッケージングの世界の最高のものをPythonの世界にもたらすことを目的とした、本番環境に対応したツール。 Pipfile、pip、およびvirtualenvを1つのコマンドに利用します。
チュートリアルでは、読者が次のものを持っていることを前提としています。
- DjangoおよびDjangoRESTFrameworkの基本的な実務知識。
- Vue.jsの基本的な実務知識。
このチュートリアルは、Python v3.7.7、Django v3.0.7、ノードv14.4.0、npm
v6.14.5、およびnuxt
v2.13.0で検証されました。
ステップ1—バックエンドを設定する
このセクションでは、バックエンドを設定し、起動して実行するために必要なすべてのディレクトリを作成します。したがって、ターミナルの新しいインスタンスを起動し、次のコマンドを実行してプロジェクトのディレクトリを作成します。
mkdir recipes_app
次に、ディレクトリに移動します。
cd recipes_app
次に、Pipを使用してPipenvをインストールします。
pip install pipenv
そして、新しい仮想環境をアクティブ化します。
pipenv shell
注:コンピューターにPipenvが既にインストールされている場合は、最初のコマンドをスキップする必要があります。
Pipenvを使用してDjangoとその他の依存関係をインストールしましょう:
pipenv install django django-rest-framework django-cors-headers
注: Pipenvを使用して新しい仮想環境をアクティブ化すると、ターミナルの各コマンドラインの前に現在の作業ディレクトリの名前が付けられます。 この場合は(recipes_app)
です。
次に、api
という新しいDjangoプロジェクトを作成します。
django-admin startproject api
プロジェクトディレクトリに移動します。
cd api
core
という名前のDjangoアプリケーションを作成します。
python manage.py startapp core
core
アプリケーションを、rest_framework
およびcors-headers
と一緒に登録して、Djangoプロジェクトが認識できるようにします。 api/settings.py
ファイルを開き、それに応じて更新します。
api / 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', # add this 'corsheaders', # add this 'core' # add this ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # add this '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', ] # add this block below MIDDLEWARE CORS_ORIGIN_WHITELIST = ( 'http://localhost:3000', ) # ... # add the following just below STATIC_URL MEDIA_URL = '/media/' # add this MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # add this
クライアントアプリケーションがそのポートで提供され、 CORS(クロスオリジンリソースシェアリング)エラーを防ぎたいため、http://localhost:3000
をホワイトリストに追加しました。 また、MEDIA_URL
とMEDIA_ROOT
を追加しました。これは、アプリケーションで画像を提供するときに必要になるためです。
レシピモデルの定義
レシピアイテムをデータベースに保存する方法を定義するモデルを作成し、core/models.py
ファイルを開いて、以下のスニペットに完全に置き換えてみましょう。
api / core / models.py
from django.db import models # Create your models here. class Recipe(models.Model): DIFFICULTY_LEVELS = ( ('Easy', 'Easy'), ('Medium', 'Medium'), ('Hard', 'Hard'), ) name = models.CharField(max_length=120) ingredients = models.CharField(max_length=400) picture = models.FileField() difficulty = models.CharField(choices=DIFFICULTY_LEVELS, max_length=10) prep_time = models.PositiveIntegerField() prep_guide = models.TextField() def __str_(self): return "Recipe for {}".format(self.name)
上記のコードスニペットは、レシピモデルの6つのプロパティについて説明しています。
name
ingredients
picture
difficulty
prep_time
prep_guide
レシピモデルのシリアライザーの作成
フロントエンドが受信したデータを処理できるように、モデルインスタンスをJSONに変換するシリアライザーが必要です。 core/serializers.py
ファイルを作成し、次のように更新します。
api / core / serializers.py
from rest_framework import serializers from .models import Recipe class RecipeSerializer(serializers.ModelSerializer): class Meta: model = Recipe fields = ("id", "name", "ingredients", "picture", "difficulty", "prep_time", "prep_guide")
上記のコードスニペットでは、使用するモデルとJSONに変換するフィールドを指定しました。
管理パネルの設定
Djangoは、すぐに使用できる管理インターフェイスを提供します。 このインターフェースにより、作成したばかりのRecipeモデルでCRUD操作を簡単にテストできますが、最初に少し構成を行います。
core/admin.py
ファイルを開き、以下のスニペットに完全に置き換えます。
api / core / admin.py
from django.contrib import admin from .models import Recipe # add this # Register your models here. admin.site.register(Recipe) # add this
ビューの作成
core/views.py
ファイルにRecipeViewSet
クラスを作成し、以下のスニペットに完全に置き換えてみましょう。
api / core / views.py
from rest_framework import viewsets from .serializers import RecipeSerializer from .models import Recipe class RecipeViewSet(viewsets.ModelViewSet): serializer_class = RecipeSerializer queryset = Recipe.objects.all()
viewsets.ModelViewSet
は、デフォルトでCRUD操作を処理するメソッドを提供します。 シリアライザークラスとqueryset
を指定するだけです。
URLの設定
api/urls.py
ファイルに移動し、以下のコードに完全に置き換えます。 このコードは、APIのURLパスを指定します。
api / api / urls.py
from django.contrib import admin from django.urls import path, include # add this from django.conf import settings # add this from django.conf.urls.static import static # add this urlpatterns = [ path('admin/', admin.site.urls), path("api/", include('core.urls')) # add this ] # add this if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
次に、core
ディレクトリにurls.py
ファイルを作成し、以下のスニペットに貼り付けます。
api / core / urls.py
from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import RecipeViewSet router = DefaultRouter() router.register(r'recipes', RecipeViewSet) urlpatterns = [ path("", include(router.urls)) ]
上記のコードでは、router
クラスは次のURLパターンを生成します。
/recipes/
-このルートでCREATEおよびREAD操作を実行できます。/recipes/{id}
-このルートでは、READ、UPDATE、およびDELETE操作を実行できます。
移行の実行
最近レシピモデルを作成してその構造を定義したので、移行ファイルを作成し、モデルの変更をデータベースに適用する必要があります。次のコマンドを実行してみましょう。
python manage.py makemigrations python manage.py migrate
次に、管理インターフェースにアクセスするためのスーパーユーザーアカウントを作成します。
python manage.py createsuperuser
スーパーユーザーのユーザー名、電子メール、およびパスワードを入力するように求められます。 すぐに管理ダッシュボードにログインする必要があるため、覚えておくことができる詳細を必ず入力してください。
バックエンドで実行する必要がある構成はこれですべてです。 作成したAPIをテストできるようになったので、Djangoサーバーを起動しましょう。
python manage.py runserver
サーバーが実行されたら、localhost:8000/api/recipes/
に移動して、サーバーが機能することを確認します。
次のインターフェイスを使用して、新しいレシピアイテムを作成できます。
id
主キーを使用して、特定のレシピアイテムに対してDELETE、PUT、およびPATCH操作を実行することもできます。 これを行うには、この構造/api/recipe/{id}
のアドレスにアクセスします。 このアドレスで試してみましょう— localhost:8000/api/recipes/1
:
アプリケーションのバックエンドは以上です。これで、フロントエンドの具体化に進むことができます。
ステップ2—フロントエンドを設定する
チュートリアルのこのセクションでは、アプリケーションのフロントエンドを構築します。 フロントエンドコードのディレクトリをrecipes_app
ディレクトリのルートに配置します。 したがって、このセクションのコマンドを実行する前に、api
ディレクトリから移動します(または、新しいターミナルを起動して前のターミナルと一緒に実行します)。
次のコマンドを使用して、client
というnuxt
アプリケーションを作成しましょう。
npx create-nuxt-app client
注: create-nuxt-app
の前にnpx
を付けると、パッケージがまだグローバルにマシンにインストールされていない場合にインストールされます。
インストールが完了すると、create-nuxt-app
は追加する追加のツールについていくつか質問します。 このチュートリアルでは、次の選択が行われました。
? Project name: client ? Programming language: JavaScript ? Package manager: Npm ? UI framework: Bootstrap Vue ? Nuxt.js modules: Axios ? Linting tools: ? Testing framework: None ? Rendering mode: Universal (SSR / SSG) ? Deployment target: Server (Node.js hosting) ? Development tools:
これにより、選択したパッケージマネージャーを使用して依存関係のインストールがトリガーされます。
client
ディレクトリに移動します。
cd client
次のコマンドを実行して、アプリケーションを開発モードで起動してみましょう。
npm run dev
開発サーバーが起動したら、localhost:3000
に移動してアプリケーションを確認します。
それでは、client
ディレクトリのディレクトリ構造を見てみましょう。
├── client ├── assets/ ├── components/ ├── layouts/ ├── middleware/ ├── node_modules/ ├── pages/ ├── plugins/ ├── static/ └── store/
これらのディレクトリの目的の内訳は次のとおりです。
- Assets -画像、CSS、Sass、JavaScriptファイルなどのコンパイルされていないファイルが含まれています。
- コンポーネント-Vue.jsコンポーネントが含まれています。
- Layouts-アプリケーションのレイアウトが含まれています。 レイアウトは、ページの外観を変更するために使用され、複数のページに使用できます。
- ミドルウェア-アプリケーションのミドルウェアが含まれています。 ミドルウェアは、ページがレンダリングされる前に実行されるカスタム関数です。
- Pages-アプリケーションのビューとルートが含まれます。 Nuxt.jsは、このディレクトリ内のすべての
.vue
ファイルを読み取り、その情報を使用してアプリケーションのルーターを作成します。 - プラグイン-ルートVue.jsアプリケーションがインスタンス化される前に実行されるJavaScriptプラグインが含まれています。
- Static -静的ファイル(変更される可能性が低いファイル)が含まれ、これらのファイルはすべて、アプリケーションのルートである
/
にマップされます。 - Store -Nuxt.jsでVuexを使用する場合は、ストアファイルが含まれます。
client
ディレクトリにはnuxt.config.js
ファイルもあります。このファイルには、Nuxt.jsアプリのカスタム構成が含まれています。
続行する前に、この画像アセットのzipファイルをダウンロードして解凍し、static
ディレクトリ内にimages
ディレクトリを配置します。
ページの構造
このセクションでは、いくつかの.vue
ファイルをpages
ディレクトリに追加して、アプリケーションが5ページになるようにします。
- ホームページ
- すべてのレシピリストページ
- 単一レシピビューページ
- 単一レシピ編集ページ
- レシピページを追加
次の.vue
ファイルとフォルダをpages
ディレクトリに追加して、この正確な構造にします。
├── pages/ ├── recipes/ ├── _id/ └── edit.vue └── index.vue └── add.vue └── index.vue └── index.vue
上記のファイル構造は、次のルートを生成します。
/
→pages/index.vue
によって処理されます/recipes/add
→pages/recipes/add.vue
によって処理されます/recipes/
→pages/recipes/index.vue
によって処理されます/recipes/{id}/
→pages/recipes/_id/index.vue
によって処理されます/recipes/{id}/edit
→pages/recipes/_id/edit.vue
によって処理されます
.vue
ファイルまたはディレクトリの前にアンダースコアを付けると、動的ルートが作成されます。 これは、ID(recipes/1/
、recipes/2/
など)に基づいてさまざまなレシピを簡単に表示できるため、アプリケーションで役立ちます。
ホームページの作成
Nuxt.jsでは、アプリケーションのルックアンドフィールを変更する場合に、レイアウトが非常に役立ちます。 これで、Nuxt.jsアプリケーションの各インスタンスにデフォルトのレイアウトが付属しているので、アプリケーションに干渉しないようにすべてのスタイルを削除します。
layouts/default.vue
ファイルを開き、以下のスニペットに置き換えます。
client / layouts / default.vue
<template> <div> <nuxt/> </div> </template> <style> </style>
pages/index.vue
ファイルを次のコードで更新しましょう。
client / pages / index.vue
<template> <header> <div class="text-box"> <h1>La Recipes ?</h1> <p class="mt-3">Recipes for the meals we love ❤️</p> <nuxt-link class="btn btn-outline btn-large btn-info" to="/recipes"> View Recipes <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.9), rgba(0, 0, 0, 0.4) ), 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: cursive; font-size: 5rem; } .text-box p { font-size: 2rem; font-weight: lighter; } </style>
上記のコードから、<nuxt-link>
はページ間を移動するために使用できるNuxt.jsコンポーネントです。 これは、 VueRouterの<router-link>
コンポーネントと非常によく似ています。
フロントエンド開発サーバーを起動してみましょう(まだ実行されていない場合)。
npm run dev
次に、localhost:3000
にアクセスして、ホームページを確認します。
フロントエンドはまもなくデータの通信を開始するため、Djangoバックエンドサーバーが常にターミナルの別のインスタンスで実行されていることを常に確認してください。
このアプリケーションのすべてのページはVue
コンポーネントになり、Nuxt.jsはアプリケーションの開発をシームレスにするための特別な属性と機能を提供します。 これらすべての特別な属性は公式ドキュメントにあります。
このチュートリアルでは、次の2つの関数を使用します。
head()
-このメソッドは、現在のページに特定の<meta>
タグを設定するために使用されます。asyncData()
-このメソッドは、ページコンポーネントがロードされる前にデータをフェッチするために使用されます。 返されたオブジェクトは、ページコンポーネントのデータとマージされます。 これは、このチュートリアルの後半で使用します。
レシピリストページの作成
components
ディレクトリにRecipeCard.vue
というVue.jsコンポーネントを作成し、以下のスニペットで更新してみましょう。
client / components / RecipeCard.vue
<template> <div class="card recipe-card"> <img :src="recipe.picture" class="card-img-top" > <div class="card-body"> <h5 class="card-title">{{ recipe.name }}</h5> <p class="card-text"> <strong>Ingredients:</strong> {{ recipe.ingredients }} </p> <div class="action-buttons"> <nuxt-link :to="`/recipes/${recipe.id}/`" class="btn btn-sm btn-success">View</nuxt-link> <nuxt-link :to="`/recipes/${recipe.id}/edit/`" class="btn btn-sm btn-primary">Edit</nuxt-link> <button @click="onDelete(recipe.id)" class="btn btn-sm btn-danger">Delete</button> </div> </div> </div> </template> <script> export default { props: ["recipe", "onDelete"] }; </script> <style> .recipe-card { box-shadow: 0 1rem 1.5rem rgba(0,0,0,.6); } </style>
上記のコンポーネントは、2つの小道具を受け入れます。
- 特定のレシピに関する情報を含む
recipe
オブジェクト。 onDelete
メソッドは、ユーザーがボタンをクリックしてレシピを削除するたびにトリガーされます。
次に、pages/recipes/index.vue
を開き、以下のスニペットで更新します。
client / pages / recipes / 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>La Recipes</h3> <nuxt-link to="/recipes/add" class="btn btn-info">Add Recipe</nuxt-link> </div> </div> <template v-for="recipe in recipes"> <div :key="recipe.id" class="col-lg-3 col-md-4 col-sm-6 mb-4"> <recipe-card :onDelete="deleteRecipe" :recipe="recipe"></recipe-card> </div> </template> </div> </main> </template> <script> import RecipeCard from "~/components/RecipeCard.vue"; const sampleData = [ { id: 1, name: "Jollof Rice", picture: "/images/food-1.jpeg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 2, name: "Macaroni", picture: "/images/food-2.jpeg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 3, name: "Fried Rice", picture: "/images/banner.jpg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " } ]; export default { head() { return { title: "Recipes list" }; }, components: { RecipeCard }, asyncData(context) { let data = sampleData; return { recipes: data }; }, data() { return { recipes: [] }; }, methods: { deleteRecipe(recipe_id) { console.log(deleted `${recipe.id}`) } } }; </script> <style scoped> </style>
フロントエンド開発サーバーを起動してみましょう(まだ実行されていない場合)。
npm run dev
次に、localhost:3000/recipes
にアクセスし、レシピリストページを確認します。
上の画像から、コンポーネントのデータセクションでrecipes
を空の配列に設定したにもかかわらず、3枚のレシピカードが表示されていることがわかります。 これについての説明は、メソッドasyncData
がページがロードされる前に実行され、コンポーネントのデータを更新するオブジェクトを返すことです。
これで、asyncData
メソッドを変更して、Djangoバックエンドに対してapi
リクエストを作成し、その結果でコンポーネントのデータを更新するだけです。
その前に、Axios
を構成する必要があります。 nuxt.config.js
ファイルを開き、それに応じて更新します。
client / nuxt.config.js
// add this Axios object axios: { baseURL: "http://localhost:8000/api" },
注:これは、create-nuxt-app
を使用するときにAxios
を選択したことを前提としています。 そうでない場合は、modules
アレイを手動でインストールして構成する必要があります。
次に、pages/recipes/index.vue
ファイルを開き、<script>
セクションを次のセクションに置き換えます。
client / pages / recipes / index.vue
[...] <script> import RecipeCard from "~/components/RecipeCard.vue"; export default { head() { return { title: "Recipes list" }; }, components: { RecipeCard }, async asyncData({ $axios, params }) { try { let recipes = await $axios.$get(`/recipes/`); return { recipes }; } catch (e) { return { recipes: [] }; } }, data() { return { recipes: [] }; }, methods: { async deleteRecipe(recipe_id) { try { await this.$axios.$delete(`/recipes/${recipe_id}/`); // delete recipe let newRecipes = await this.$axios.$get("/recipes/"); // get new list of recipes this.recipes = newRecipes; // update list of recipes } catch (e) { console.log(e); } } } }; </script> [...]
上記のコードでは、asyncData()
はcontext
というオブジェクトを受け取り、それを分解して$axios
を取得します。 公式ドキュメントでコンテキストのすべての属性を確認できます。
asyncData()
をtry...catch
ブロックでラップします。これは、バックエンドサーバーが実行されておらず、Axiosがデータの取得に失敗した場合に発生するバグを防ぎたいためです。 その場合は常に、recipes
は代わりに空の配列に設定されます。
このコード行:
let recipes = await $axios.$get("/recipes/")
の短いバージョンです:
let response = await $axios.get("/recipes") let recipes = response.data
deleteRecipe()
メソッドは、特定のレシピを削除し、Djangoバックエンドから最新のレシピのリストをフェッチして、最後にコンポーネントのデータを更新します。
これでフロントエンド開発サーバーを起動でき(まだ実行されていない場合)、レシピカードにDjangoバックエンドからのデータが入力されていることがわかります。
これを機能させるには、Djangoバックエンドサーバーが実行されている必要があり、レシピアイテムに使用できるデータ(管理者インターフェイスから入力)が必要です。
npm run dev
localhost:3000/recipes
にアクセスしてみましょう:
レシピアイテムを削除して、それに応じて更新されるのを確認することもできます。
新しいレシピの追加
すでに説明したように、アプリケーションのフロントエンドから新しいレシピを追加できるようにしたいので、pages/recipes/add/
ファイルを開き、次のスニペットで更新します。
client / pages / recipes / add.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">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview" alt > <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" src="@/static/images/placeholder.png" > </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name"> </div> <div class="form-group"> <label for>Ingredients</label> <input v-model="recipe.ingredients" type="text" class="form-control"> </div> <div class="form-group"> <label for>Food picture</label> <input type="file" name="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.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> Prep time <small>(minutes)</small> </label> <input v-model="recipe.prep_time" type="number" class="form-control"> </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </main> </template> <script> export default { head() { return { title: "Add Recipe" }; }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0]; this.createImage(files[0]); }, createImage(file) { // let image = new Image(); let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in this.recipe) { formData.append(data, this.recipe[data]); } try { let response = await this.$axios.$post("/recipes/", formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style scoped> </style>
submitRecipe()
では、フォームデータが投稿され、レシピが正常に作成されると、アプリはthis.$router
を使用して/recipes/
にリダイレクトされます。
単一レシピビューページの作成
ユーザーが単一のレシピアイテムを表示できるビューを作成し、/pages/recipes/_id/index.vue
ファイルを開いて、以下のスニペットに貼り付けてみましょう。
client / pages / recipes / _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">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture" alt > </div> <div class="col-md-6"> <div class="recipe-details"> <h4>Ingredients</h4> <p>{{ recipe.ingredients }}</p> <h4>Preparation time ⏱</h4> <p>{{ recipe.prep_time }} mins</p> <h4>Difficulty</h4> <p>{{ recipe.difficulty }}</p> <h4>Preparation guide</h4> <textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled /> </div> </div> </div> </main> </template> <script> export default { head() { return { title: "View Recipe" }; }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" } }; } }; </script> <style scoped> </style>
asyncData()
メソッドで見られるparams
キーを紹介します。 この場合、params
を使用して、表示するレシピのID
を取得しています。 URL
からparams
を抽出し、そのデータをプリフェッチしてからページに表示します。
Webブラウザで1つのレシピアイテムを観察できます。
単一レシピ編集ページの作成
ユーザーが単一のレシピアイテムを編集および更新できるビューを作成する必要があるため、/pages/recipes/_id/edit.vue
ファイルを開いて、以下のスニペットに貼り付けます。
client / pages / recipes / _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">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="!preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture"> <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview"> </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name" > </div> <div class="form-group"> <label for>Ingredients</label> <input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" > </div> <div class="form-group"> <label for>Food picture</label> <input type="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.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> Prep time <small>(minutes)</small> </label> <input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" > </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-success">Save</button> </form> </div> </div> </main> </template> <script> export default { head(){ return { title: "Edit Recipe" } }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0] this.createImage(files[0]); }, createImage(file) { let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { let editedRecipe = this.recipe if (editedRecipe.picture.name.indexOf("http://") != -1){ delete editedRecipe["picture"] } const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in editedRecipe) { formData.append(data, editedRecipe[data]); } try { let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style scoped> </style>
上記のコードでは、submitRecipe()
メソッドには、画像が変更されていない場合に送信されるデータから編集されたレシピアイテムの画像を削除することを目的とした条件ステートメントがあります。
レシピアイテムが更新されると、アプリケーションはレシピリストページ—/recipes/
にリダイレクトされます。
トランジションの設定
アプリケーションは完全に機能しますが、トランジションを追加することでよりスムーズな外観を与えることができます。これにより、特定の期間にわたってCSSプロパティ値を(ある値から別の値に)スムーズに変更できます。
nuxt.config.js
ファイルでトランジションを設定します。 デフォルトでは、トランジション名はpage に設定されています。これは、定義したトランジションがすべてのページでアクティブになることを意味します。
トランジションのスタイリングを含めましょう。 assets
ディレクトリにcss
というディレクトリを作成し、その中にtransitions.css
ファイルを追加します。 次に、transitions.css
ファイルを開き、以下のスニペットに貼り付けます。
client / Assets / css / transitions.css
.page-enter-active, .page-leave-active { transition: opacity .3s ease; } .page-enter, .page-leave-to { opacity: 0; }
nuxt.config.js
ファイルを開き、それに応じて更新して、作成したCSSファイルをロードします。
nuxt.config.js
/* ** Global CSS */ css: [ '~/assets/css/transitions.css', // update this ],
変更を保存して、ブラウザでアプリケーションを開きます。
これで、アプリケーションは各ナビゲーションのフレームを洗練された方法で変更します。
結論
この記事では、クライアント側とサーバー側のレンダリングされたアプリケーションの違いを学ぶことから始めました。 ユニバーサルアプリケーションとは何かを学び、最後に、Nuxt.jsとDjangoを使用してユニバーサルアプリケーションを構築する方法を学びました。
このチュートリアルのソースコードはGitHubで入手できます。