序章

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、Node v14.4.0、 npm v6.14.5、および nuxt v2.13.0。

ステップ1—バックエンドを設定する

このセクションでは、バックエンドを設定し、起動して実行するために必要なすべてのディレクトリを作成します。したがって、ターミナルの新しいインスタンスを起動し、次のコマンドを実行してプロジェクトのディレクトリを作成します。

  1. mkdir recipes_app

次に、ディレクトリに移動します。

  1. cd recipes_app

次に、Pipを使用してPipenvをインストールします。

  1. pip install pipenv

そして、新しい仮想環境をアクティブ化します。

  1. pipenv shell

注:コンピューターにPipenvが既にインストールされている場合は、最初のコマンドをスキップする必要があります。

Pipenvを使用してDjangoとその他の依存関係をインストールしましょう:

  1. pipenv install django django-rest-framework django-cors-headers

注: Pipenvを使用して新しい仮想環境をアクティブ化すると、ターミナルの各コマンドラインの前に現在の作業ディレクトリの名前が付けられます。 この場合、 (recipes_app).

次に、という新しいDjangoプロジェクトを作成します api:

  1. django-admin startproject api

プロジェクトディレクトリに移動します。

  1. cd api

と呼ばれるDjangoアプリケーションを作成します core:

  1. python manage.py startapp core

登録しましょう core アプリケーションと一緒に rest_frameworkcors-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

追加しました http://localhost:3000 クライアントアプリケーションがそのポートで提供され、 CORS(クロスオリジンリソースシェアリング)エラーを防止したいため、ホワイトリストに追加されます。 また、追加しました 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

ビューの作成

を作成しましょう RecipeViewSet のクラス core/views.py ファイルを作成し、以下のスニペットに完全に置き換えます。

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()

The 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)

次に、を作成します urls.py のファイル core ディレクトリを作成し、以下のスニペットに貼り付けます。

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操作を実行できます。

移行の実行

最近レシピモデルを作成してその構造を定義したので、移行ファイルを作成し、モデルの変更をデータベースに適用する必要があります。次のコマンドを実行してみましょう。

  1. python manage.py makemigrations
  2. python manage.py migrate

次に、管理インターフェースにアクセスするためのスーパーユーザーアカウントを作成します。

  1. python manage.py createsuperuser

スーパーユーザーのユーザー名、電子メール、およびパスワードを入力するように求められます。 すぐに管理ダッシュボードにログインする必要があるため、覚えておくことができる詳細を必ず入力してください。

バックエンドで実行する必要がある構成はこれですべてです。 作成したAPIをテストできるようになったので、Djangoサーバーを起動しましょう。

  1. python manage.py runserver

サーバーが稼働したら、 localhost:8000/api/recipes/ それが機能することを確認するには:

次のインターフェイスを使用して、新しいレシピアイテムを作成できます。

特定のレシピアイテムに対して、それらを使用してDELETE、PUT、およびPATCH操作を実行することもできます。 id 主キー。 これを行うには、この構造のアドレスにアクセスします /api/recipe/{id}. このアドレスで試してみましょう— localhost:8000/api/recipes/1:

アプリケーションのバックエンドは以上です。これで、フロントエンドの具体化に進むことができます。

ステップ2—フロントエンドを設定する

チュートリアルのこのセクションでは、アプリケーションのフロントエンドを構築します。 フロントエンドコードのディレクトリをルートのルートに配置します recipes_app ディレクトリ。 だから、からナビゲートします api このセクションのコマンドを実行する前に、ディレクトリ(または新しいターミナルを起動して前のターミナルと一緒に実行)。

を作成しましょう nuxt と呼ばれるアプリケーション client このコマンドで:

  1. npx create-nuxt-app client

注:create-nuxt-appnpx マシンにまだグローバルにインストールされていない場合は、パッケージをインストールします。

インストールが完了すると、 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 ディレクトリ:

  1. 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を使用する場合は、ストアファイルが含まれます。

もあります nuxt.config.js のファイル client ディレクトリの場合、このファイルにはNuxt.jsアプリのカスタム構成が含まれています。

続行する前に、この画像アセットのzipファイルをダウンロードして解凍し、 images 内部のディレクトリ static ディレクトリ。

ページの構造

このセクションでは、いくつかを追加します .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

A .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コンポーネントです。 それは非常に似ています <router-link> Vueルーターのコンポーネント。

フロントエンド開発サーバーを起動してみましょう(まだ実行されていない場合)。

  1. npm run dev

次に、 localhost:3000 ホームページを観察します。

フロントエンドはまもなくデータの通信を開始するため、Djangoバックエンドサーバーが常にターミナルの別のインスタンスで実行されていることを常に確認してください。

このアプリケーションのすべてのページは Vue ComponentとNuxt.jsは、アプリケーションの開発をシームレスにするための特別な属性と関数を提供します。 これらすべての特別な属性は公式ドキュメントにあります。

このチュートリアルでは、次の2つの関数を使用します。

  • head() -このメソッドは、特定の設定に使用されます <meta> 現在のページのタグ。
  • asyncData() -このメソッドは、ページコンポーネントがロードされる前にデータをフェッチするために使用されます。 返されたオブジェクトは、ページコンポーネントのデータとマージされます。 これは、このチュートリアルの後半で使用します。

レシピリストページの作成

と呼ばれるVue.jsコンポーネントを作成しましょう RecipeCard.vue の中に components ディレクトリを作成し、以下のスニペットで更新します。

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. A 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>

フロントエンド開発サーバーを起動してみましょう(まだ実行されていない場合)。

  1. npm run dev

次に、 localhost:3000/recipes、レシピ一覧ページをご覧ください。

上の画像から、設定しても3枚のレシピカードが表示されていることがわかります recipes コンポーネントのデータセクションの空の配列に。 これの説明は、メソッドが asyncData ページが読み込まれる前に実行され、コンポーネントのデータを更新するオブジェクトを返します。

今、私たちがする必要があるのは、 asyncData を作る方法 api Djangoバックエンドにリクエストし、コンポーネントのデータを結果で更新します。

その前に、構成する必要があります Axios. を開きます nuxt.config.js ファイルを作成し、それに応じて更新します。

client / nuxt.config.js
// add this Axios object
axios: {
  baseURL: "http://localhost:8000/api"
},

注:これは、選択したことを前提としています Axios 使用する場合 create-nuxt-app. そうでない場合は、手動でインストールして構成する必要があります 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

The deleteRecipe() メソッドは特定のレシピを削除し、Djangoバックエンドからレシピの最新のリストをフェッチし、最後にコンポーネントのデータを更新します。

これでフロントエンド開発サーバーを起動でき(まだ実行されていない場合)、レシピカードにDjangoバックエンドからのデータが入力されていることがわかります。

これを機能させるには、Djangoバックエンドサーバーが実行されている必要があり、レシピアイテムに使用できるデータ(管理者インターフェイスから入力)が必要です。

  1. 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()、フォームデータが投稿され、レシピが正常に作成されると、アプリはにリダイレクトされます /recipes/ を使用して this.$router.

単一レシピビューページの作成

ユーザーが単一のレシピアイテムを表示できるビューを作成して、 /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>

紹介します params に見られるキー asyncData() 方法。 この場合、使用しています params 取得するには ID 見たいレシピの 抽出します params から URL ページに表示する前に、データをプリフェッチします。

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 に設定されています。これは、定義したトランジションがすべてのページでアクティブになることを意味します。

トランジションのスタイリングを含めましょう。 と呼ばれるディレクトリを作成します css の中に assets ディレクトリを追加し、 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で入手できます。