如何使用 Nuxt.js 和 Django 构建通用应用程序

介绍

现代 JavaScript 库(如 React.js 和 Vue.js)的出现使前端 Web 开发变得更好。这些库附带的功能包括 SPA(单页应用程序),它是在网页中动态加载内容,而无需完全重新加载到浏览器。

大多数单页应用程序背后的概念是客户端呈现。在客户端渲染中,大部分内容使用 JavaScript 在浏览器中渲染;在页面加载时,在 JavaScript 完全下载并呈现网站的其余部分之前,内容最初不会加载。

客户端渲染是一个相对较新的概念,它的使用需要权衡。一个显着的负面影响是,由于在使用 JavaScript 更新页面之前不会完全呈现内容,因此网站的 SEO(搜索引擎优化)将受到影响,因为搜索引擎几乎没有任何数据可供抓取。

另一方面,服务器端呈现是在浏览器上呈现 HTML 页面的传统方式。在较旧的服务器端呈现应用程序中,Web 应用程序是使用服务器端语言(例如 PHP)构建的。当浏览器请求网页时,远程服务器会添加(动态)内容并提供填充的 HTML 页面。

正如客户端渲染的缺点一样,服务器端渲染会使浏览器过于频繁地发送服务器请求,并为类似的数据执行重复的整页重新加载。有一些 JavaScript 框架可以最初使用 SSR(服务器端渲染)解决方案加载网页,然后使用框架来处理进一步的动态路由并仅获取必要的数据。生成的应用程序称为通用应用程序

总之,通用应用程序用于描述可以在客户端和服务器端执行的 JavaScript 代码。在本文中,我们将使用 Nuxt.js 构建一个通用食谱应用程序。

Nuxt.js 是用于开发通用 Vue.js 应用程序的更高级别的框架。它的创建受到 React 的Next.js 的启发,它有助于抽象设置服务器端呈现的 Vue.js 应用程序时出现的困难(服务器配置和客户端代码分发)。Nuxt.js 还附带有助于客户端和服务器端之间开发的功能,例如异步数据、中间件、布局等。

注意:我们可以将我们构建的应用程序称为服务端渲染(SSR),因为在我们创建单页应用程序时,Vue.js 默认已经实现了客户端渲染。该应用程序实际上是一个通用应用程序。

在本文中,我们将看到如何使用 Django 和 Nuxt.js 创建通用应用程序。Django 将处理后端操作并使用 DRF(Django Rest Framework)提供 API,而 Nuxt.js 将创建前端。

这是最终应用程序的演示:

带有用于插入新配方和添加图片的页面的配方应用程序动画

我们看到最终的应用程序是一个执行 CRUD 操作的配方应用程序。

先决条件

要学习本教程,您需要在您的机器上安装以下软件:

本教程假设读者具备以下条件:

  1. DjangoDjango REST Framework 的基本工作知识
  2. Vue.js 的基本工作知识

本教程已通过 Python v3.7.7、Django v3.0.7、Node v14.4.0、npmv6.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)

现在,我们将创建一个名为 的新 Django 项目api

  • django-admin startproject api

导航到项目目录:

  • cd api

创建一个名为 的 Django 应用程序core

  • 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

我们添加http://localhost:3000到白名单是因为客户端应用程序将在该端口上提供服务,并且我们希望防止CORS(跨源资源共享)错误。我们还添加了MEDIA_URLandMEDIA_ROOT因为在应用程序中提供图像时我们将需要它们。

定义配方模型

让我们创建一个模型来定义 Recipe 项应如何存储在数据库中,打开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)

上面的代码片段描述了 Recipe 模型的六个属性:

  • 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/核心/admin.py
from django.contrib import admin
from .models import Recipe  # add this
# Register your models here.

admin.site.register(Recipe) # add this

创建视图

让我们RecipeViewSetcore/views.py文件中创建一个类,用下面的代码片段完全替换它:

api/核心/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)

现在,urls.pycore目录中创建一个文件并粘贴以下代码段:

api/核心/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 操作。

运行迁移

因为我们最近创建了一个 Recipe 模型并定义了它的结构,所以我们需要制作一个迁移文件并将模型上的更改应用到数据库中,所以让我们运行以下命令:

  • python manage.py makemigrations
  • python manage.py migrate

现在,我们将创建一个超级用户帐户来访问管理界面:

  • python manage.py createsuperuser

系统将提示您输入超级用户的用户名、电子邮件和密码。请务必输入您能记住的详细信息,因为您很快就会需要它们来登录管理仪表板。

这就是需要在后端完成的所有配置。我们现在可以测试我们创建的 API,所以让我们启动 Django 服务器:

  • python manage.py runserver

服务器运行后,请转到localhost:8000/api/recipes/以确保其正常工作:

带有配方列表页面的 Django Rest Framework

我们可以使用界面创建一个新的 Recipe 项:

通过界面添加新食谱

我们还可以使用id主键对特定 Recipe 项执行 DELETE、PUT 和 PATCH 操作为此,我们将访问具有此结构的地址/api/recipe/{id}让我们试试这个地址—— localhost:8000/api/recipes/1

通过界面编辑现有配方

这就是应用程序后端的全部内容,现在我们可以继续充实前端。

第 2 步 – 设置前端

在本教程的这一部分中,我们将构建应用程序的前端。我们想将前端代码的目录放在目录的根recipes_app目录中。因此,api在运行本节中的命令之前,导航出目录(或启动一个新的终端以与前一个终端一起运行)。

让我们创建一个使用此命令nuxt调用应用程序client

  • 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目录:

  • cd client

让我们运行以下命令以在开发模式下启动应用程序:

npm run dev

开发服务器启动后,localhost:3000前往查看应用程序:

Nuxt 应用页面显示“配方应用客户端”

现在,我们来看看目录的目录结构client

├── client
  ├── assets/
  ├── components/
  ├── layouts/
  ├── middleware/
  ├── node_modules/
  ├── pages/
  ├── plugins/
  ├── static/
  └── store/

以下是这些目录的用途的细分:

  • 资产– 包含未编译的文件,例如图像、CSS、Sass 和 JavaScript 文件。
  • 组件– 包含 Vue.js 组件。
  • Layouts – 包含应用程序的布局;布局用于更改页面的外观,并可用于多个页面。
  • 中间件– 包含应用程序的中间件;中间件是在呈现页面之前运行的自定义函数。
  • 页面– 包含应用程序的视图和路由。Nuxt.js 读取.vue此目录中的所有文件,并使用这些信息来创建应用程序的路由器。
  • 插件– 包含要在根 Vue.js 应用程序实例化之前运行的 JavaScript 插件。
  • 静态– 包含静态文件(不太可能更改的文件)并且所有这些文件都映射到应用程序的根目录,即/.
  • Store – 如果我们打算将 Vuex 与 Nuxt.js 一起使用,则包含存储文件。

目录中还有一个nuxt.config.js文件client,该文件包含 Nuxt.js 应用程序的自定义配置。

在我们继续之前,下载这个图像资产的 zip 文件,解压它,并将images目录放在static目录中。

页面结构

在本节中,我们将向目录中添加一些.vue文件,pages以便我们的应用程序有五个页面:

  • 主页
  • 所有食谱列表页面
  • 单一配方查看页面
  • 单一配方编辑页面
  • 添加食谱页面

让我们将以下.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文件并将其替换为以下代码段:

客户端/布局/default.vue
<template>
  <div>
    <nuxt/>
  </div>
</template>

<style>
</style>

让我们pages/index.vue使用以下代码更新文件:

客户端/页面/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 组件,可用于在页面之间导航。它与Vue Router 中<router-link>组件非常相似

让我们启动前端开发服务器(如果它还没有运行):

  • npm run dev

然后访问localhost:3000并观察主页:

主页背景为食物图片,标题为“La Recipes”,带有“查看食谱按钮”

始终确保 Django 后端服务器始终在终端的另一个实例中运行,因为前端将很快开始与其通信以获取数据。

这个应用程序中的每个页面都是一个Vue组件,Nuxt.js 提供了特殊的属性和功能,使应用程序的开发变得无缝。您可以在官方文档中找到所有这些特殊属性

为了本教程,我们将使用其中两个函数:

  • head()– 此方法用于<meta>为当前页面设置特定标签。
  • asyncData()– 此方法用于在加载页面组件之前获取数据。然后将返回的对象与页面组件的数据合并。我们将在本教程的后面部分使用它。

创建食谱列表页面

让我们创建一个RecipeCard.vuecomponents目录中调用的 Vue.js 组件并使用以下代码段更新它:

客户端/组件/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>

上面的组件接受两个 props:

  1. recipe包含有关特定配方的信息对象。
  2. 一种onDelete方法将被触发每当在用户点击按钮来删除配方。

接下来,打开pages/recipes/index.vue并使用以下代码段更新它:

客户端/页面/食谱/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在组件的数据部分设置为空数组,也会出现三张配方卡对此的解释是该方法asyncData在页面加载之前执行并返回一个更新组件数据的对象。

现在,我们需要做的就是修改asyncData方法以api向 Django 后端发出请求并使用结果更新组件的数据。

在我们这样做之前,我们必须配置Axios. 打开nuxt.config.js文件并相应地更新它:

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

注意:这假设您Axios在使用create-nuxt-app. 如果没有,则需要手动安装和配置modules阵列。

现在,打开pages/recipes/index.vue文件并将该<script>部分替换为以下部分:

客户端/页面/食谱/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您可以在官方文档中查看 的所有属性context

我们将其包裹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 后端服务器,并且必须有一些数据(从管理界面输入)可用于 Recipe 项目。

  • npm run dev

让我们参观localhost:3000/recipes

食谱页面有 6 个食谱卡和右侧的“添加食谱”按钮

您还可以尝试删除配方项目并观察它们相应地更新。

添加新食谱

正如我们已经讨论过的,我们希望能够从应用程序的前端添加新配方,因此打开pages/recipes/add/文件并使用以下代码段更新它:

客户端/页面/食谱/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.

创建单一配方视图页面

让我们创建允许用户查看单个 Recipe 项的视图,打开/pages/recipes/_id/index.vue文件并粘贴以下代码段:

客户端/页面/食谱/_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看到key asyncData()在这种情况下,我们使用params来获取ID我们想要查看的配方的 。我们params从 中提取URL并预取其数据,然后将其显示在页面上。

单一配方项目薯片。 包括配料、准备时间、难度和准备指南

我们可以在 Web 浏览器上观察到单个 Recipe 项。

创建单一配方编辑页面

我们需要创建允许用户编辑和更新单个 Recipe 项目的视图,因此打开/pages/recipes/_id/edit.vue文件并粘贴以下代码段:

客户端/页面/食谱/_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()方法有一个条件语句,其目的是在图片未更改的情况下从要提交的数据中删除已编辑的 Recipe 项目的图片。

配方项目更新后,应用程序将重定向到配方列表页面 — /recipes/

设置过渡

该应用程序功能齐全,但是,我们可以通过添加过渡使其看起来更平滑,这允许我们在给定的持续时间内平滑地更改 CSS 属性值(从一个值到另一个值)。

We will set up transitions in the nuxt.config.js file. By default, the transition name is set to page, which means that the transitions we define will be active on all pages.

Let’s include the styling for the transition. Create a directory called css in the assets directory and add a transitions.css file within. Now open the transitions.css file and paste in the snippet below:

client/assets/css/transitions.css
.page-enter-active,
.page-leave-active {
  transition: opacity .3s ease;
}
.page-enter,
.page-leave-to {
  opacity: 0;
}

Open the nuxt.config.js file and update it accordingly to load the CSS file we just created:

nuxt.config.js
/*
** Global CSS
*/
css: [
  '~/assets/css/transitions.css', // update this
],

Save your changes and open the application in your browser:

用户浏览应用程序并添加和编辑配方的动画

Now, our application will change frames on each navigation in a sleek way.

Conclusion

在本文中,我们首先了解客户端和服务器端呈现的应用程序之间的差异。我们继续学习什么是通用应用程序,最后,我们看到了如何使用 Nuxt.js 和 Django 构建通用应用程序。

本教程源代码可在 GitHub 上找到

觉得文章有用?

点个广告表达一下你的爱意吧 !😁