Nuxt.jsとDjangoを使用してユニバーサルアプリケーションを構築する方法

提供:Dev Guides
移動先:案内検索

序章

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つのコマンドに利用します。

チュートリアルでは、読者が次のものを持っていることを前提としています。

  1. DjangoおよびDjangoRESTFrameworkの基本的な実務知識。
  2. Vue.jsの基本的な実務知識。

このチュートリアルは、Python v3.7.7、Django v3.0.7、ノードv14.4.0、npm v6.14.5、およびnuxtv2.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_URLMEDIA_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/addpages/recipes/add.vueによって処理されます
  • /recipes/pages/recipes/index.vueによって処理されます
  • /recipes/{id}/pages/recipes/_id/index.vueによって処理されます
  • /recipes/{id}/editpages/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">&rarr;</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つの小道具を受け入れます。

  1. 特定のレシピに関する情報を含むrecipeオブジェクト。
  2. 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で入手できます。