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

来自菜鸟教程
跳转至:导航、​搜索

介绍

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

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

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

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

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

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

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

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


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

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

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

先决条件

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

  • Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
  • 该项目需要在本地环境中安装Python。
  • 该项目将使用 Pipenv。 一个可用于生产的工具,旨在将所有打包世界中最好的带到 Python 世界。 它将 Pipfile、pip 和 virtualenv 整合到一个命令中。

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

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

本教程已使用 Python v3.7.7、Django v3.0.7、Node 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_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/核心/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/核心/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 操作。

运行迁移

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

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 目录(或启动一个新终端以与前一个终端一起运行)。

让我们使用以下命令创建一个名为 clientnuxt 应用程序:

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

client 目录下还有一个 nuxt.config.js 文件,该文件包含 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并观察首页:

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

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

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

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

创建食谱列表页面

让我们在 components 目录中创建一个名为 RecipeCard.vue 的 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>

上面的组件接受两个道具:

  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 方法以向 Django 后端发出 api 请求,并使用结果更新组件的数据。

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

客户端/nuxt.config.js

// add this Axios object
axios: {
  baseURL: "http://localhost:8000/api"
},

注意: 这假设您在使用 create-nuxt-app 时选择了 Axios。 如果没有,则需要手动安装和配置 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。 可以在官方文档【X81X】中查看【X18X】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 后端服务器必须正在运行,并且必须有一些数据(从管理界面输入)可用于配方项。

npm run dev

让我们访问localhost:3000/recipes

您还可以尝试删除食谱项目并观看它们相应的更新。

添加新食谱

正如我们已经讨论过的,我们希望能够从应用程序的前端添加新配方,因此打开 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() 中,一旦表单数据发布并成功创建配方,应用程序将使用 this.$router 重定向到 /recipes/

创建单一配方视图页面

让我们创建允许用户查看单个配方项目的视图,打开 /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>

我们介绍在 asyncData() 方法中看到的 params 键。 在这种情况下,我们使用 params 来获取我们想要查看的配方的 ID。 我们从 URL 中提取 params 并在页面上显示之前预取其数据。

我们可以在网络浏览器上观察一个单一的食谱项目。

创建单一配方编辑页面

我们需要创建允许用户编辑和更新单个配方项目的视图,因此打开 /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() 方法有一个条件语句,其目的是如果图片没有更改,则从要提交的数据中删除已编辑的食谱项的图片。

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

设置过渡

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

我们将在 nuxt.config.js 文件中设置过渡。 默认情况下,transition 名称设置为 page,这意味着我们定义的transition 将在所有页面上都处于活动状态。

让我们包括过渡的样式。 在 assets 目录下创建一个名为 css 的目录,并在其中添加一个 transitions.css 文件。 现在打开 transitions.css 文件并粘贴到下面的代码片段中:

客户端/资产/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 上找到。