nuxt.js使用教程_使用Nuxt.js和Django构建通用应用程序

本文介绍了如何使用Nuxt.js和Django构建一个通用应用程序。Nuxt.js是一个用于开发Universal Vue.js应用的框架,结合Django处理后端操作和API。文章详细阐述了设置前后端的过程,包括创建Django项目和应用、定义模型、创建序列化器、设置管理面板、创建视图和URL,以及Nuxt.js端的布局、页面创建和数据获取。通过这个教程,读者将了解到如何结合两者来构建一个既能客户端渲染又能服务器端渲染的全栈应用。
摘要由CSDN通过智能技术生成

nuxt.js使用教程

介绍 ( Introduction )

The advent of modern JavaScript libraries such as React.js and Vue.js has transmogrified Front-end web development for the better. These libraries ship with wonderful features including SPA (Single Page Applications) which is basically the dynamic loading of the content in web pages without a full reload to the browser.

诸如React.js和Vue.js之类的现代JavaScript库的出现使前端Web开发变得更加完善。 这些库具有出色的功能,包括SPA(单页应用程序),该功能基本上是在不完全重新加载到浏览器的情况下动态加载网页中的内容。

The concept behind most Single Page Applications is Client-Side Rendering. In Client-Side Rendering, the majority of content is rendered in a browser using JavaScript; on page load, the content doesn’t load initially until the JavaScript has been fully downloaded and renders the rest of the site.

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

Client-Side Rendering is a relatively recent concept and there are trade-offs associated with its use. A notable negative side is that, since the content is not exactly rendered until the page is updated using JavaScript, SEO for the website will suffer as there will hardly be any data for search engines to crawl.

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

Server-Side Rendering, on the other hand, is the conventional way of getting HTML pages rendered on a browser. In the older Server-Side Rendered applications, the web application is built using a Server-Side language such as PHP. When a web page is requested by a browser, the remote server adds the (dynamic) content and delivers a populated HTML page.

另一方面,服务器端渲染是在浏览器上渲染HTML页面的常规方法。 在较早的服务器端渲染的应用程序中,Web应用程序是使用服务器端语言(例如PHP)构建的。 当浏览器请求网页时,远程服务器添加(动态)内容并交付已填充HTML页面。

Just as there are downsides to Client-Side Rendering, Server-Side rendering makes the browser send server requests too frequently and perform repetitions of full page reloads for similar data.

就像客户端渲染存在缺点一样,服务器端渲染会使浏览器过于频繁地发送服务器请求,并重复执行整个页面重新加载以重复类似数据。

At this point, you must be thinking: “what if we could initially load the web page with an SSR (Server-Side Rendering) solution, then use a framework to handle the further dynamic routing and fetch only necessary data?”

此时,您必须考虑:“如果最初可以使用SSR(服务器端渲染)解决方案加载网页,然后使用框架处理进一步的动态路由并仅获取必要的数据,那该怎么办?”

Great thinking! There are JavaScript frameworks that already implement this solution and the resulting applications are called Universal Applications.

好主意! 已经有JavaScript框架已实现此解决方案,并且产生的应用程序称为通用应用程序。

It is correct to say: Universal app = SSR + SPA.

正确地说:通用应用程序= SSR + SPA。

In summary, a universal application is used to describe JavaScript code that can execute on the client and the server side. In this article, we will build a Universal Recipe application using Nuxt.js.

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

Nuxt.js is a higher-level framework for developing Universal Vue.js applications. Its creation was inspired by React’s Next.js and it helps to abstract the difficulties (server configuration and client code distribution) that arise in setting up Server-Side Rendered Vue.js applications. Nuxt.js also ships with lots of features that aid development between client side and server side such as async data, middleware, layouts etc.

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

Note: We can refer to the application we build as Server-Side rendered (SSR) because Vue.js already implements Client-Side rendering by default when we create a Single Page Application. The application is, in fact, a Universal application.

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

In this article, we will see how to create a Universal application using Django and Nuxt.js. Django will handle the backend operations and provide the APIs using the (DRF) Django Rest Framework, while Nuxt.js will create the frontend.

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

Here’s a demo of the final application:

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

From the image above, we see that the final application is a basic Recipes application that performs CRUD operations.

The source code for this tutorial is available here on GitHub.

本教程的源代码可以在这里 GitHub上。

先决条件 ( Prerequisites )

To follow along with this tutorial, you will need the following installed on your machine:

要继续学习本教程,您需要在计算机上安装以下软件:

  1. Python3.

    Python3。
  2. Pip.

    点子
  3. Npm.

    Npm。

Pipenv is a production-ready tool that aims to bring the best of all packaging worlds to the Python world. It harnesses Pipfile, pip, and virtualenv into one single command.

Pipenv是可用于生产的工具,旨在将所有包装领域的精华带入Python世界。 它将Pipfile,pip和virtualenv整合到一个命令中。

The tutorial assumes that the reader has possession of the following:

本教程假定读者拥有以下内容:

  1. Basic working knowledge of Django and Django Rest Framework.

    DjangoDjango Rest Framework的基本工作知识
  2. Basic working knowledge of Vue.js.

    的基本操作知识V UE js

Let’s dive right in!

让我们潜入吧!

设置后端 ( Setting up the Backend )

In this section, we will set up the backend and create all the folders that we need to get things up and running, so launch a new instance of a terminal and create the project’s directory by running this command:

在本节中,我们将设置后端并创建启动和运行所需的所有文件夹,因此启动终端的新实例并通过运行以下命令创建项目的目录:

$mkdir recipes_app

Next, we will navigate into the directory:

接下来,我们将导航到目录:

cd recipes_app

Now we will install Pipenv using Pip and activate a new virtual environment:

现在,我们将使用Pip安装Pipenv并激活一个新的虚拟环境:

$ pipinstall pipenv
$ pipenv shell

Note: You should skip the first command if you already have Pipenv installed on your computer.

注意:如果您的计算机上已经安装了Pipenv,则应跳过第一个命令。

Let’s install Django and other dependencies using Pipenv:

让我们使用Pipenv安装Django和其他依赖项:

(recipes_app) $ pipenv install django django-rest-framework django-cors-headers

Note: After activating a new virtual environment using Pipenv, each command line in the terminal will be prefixed with the name of the present working directory. In this case, it is (recipes_app).

注意:使用Pipenv激活新的虚拟环境后,终端中的每个命令行都将以当前工作目录的名称作为前缀。 在这种情况下,它是(recipes_app)。

Now, we will create a new Django project called api and a Django application called core:

现在,我们将创建一个名为api的新Django项目和一个名为core的Django应用程序:

(recipes_app) $ django-admin startproject api
(recipes_app) $ cd api
(recipes_app) $ python manage.py startapp core

Let’s register the core application, together with rest_framework and cors-headers, so that the Django project recognises it. Open the api/settings.py file and update it accordingly:

让我们将core应用程序与rest_frameworkcors-headers,一起注册cors-headers,以便Django项目可以识别它。 打开api/settings.py文件并进行相应更新:

# 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 = (
    'localhost:3000',
)

# add the following just below STATIC_URL
MEDIA_URL = '/media/' # add this
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # add this

Note: We added localhost:3000 to the white-list because the client application will be served on that port and we want to prevent CORS errors. We also added MEDIA_URL and MEDIA_ROOT because we will need them when serving images in the application.

注意:我们将localhost:3000添加到白名单中,因为将在该端口上提供客户端应用程序,并且我们希望防止CORS错误。 我们还添加了MEDIA_URL和MEDIA_ROOT,因为在应用程序中提供图像时将需要它们。

定义配方模型 (Defining the Recipe model)

Let's create a model to define how the Recipe items should be stored in the database, open the core/models.py file and completely replace it with the snippet below:

让我们创建一个模型来定义如何将食谱项存储在数据库中,打开core/models.py文件,并用下面的代码段完全替换它:

# 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(maxlength=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)

The code snippet above describes six properties on the Recipe model:

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

  • Name

    名称
  • Ingredients

    配料
  • Picture

    图片
  • Difficulty

    困难
  • Prep_time

    准备时间
  • Prep_guide

    准备指南

为配方模型创建序列化器 (Creating serializers for the Recipe model)

We need serializers to convert model instances to JSON so that the frontend can easily work with the received data. We will create a core/serializers.py file and update it with the following:

我们需要序列化程序将模型实例转换为JSON,以便前端可以轻松处理接收到的数据。 我们将创建一个core/serializers.py文件,并使用以下内容对其进行更新:

# 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")

In the code snippet above, we specified the model to work with and the fields we want to be converted to JSON.

在上面的代码片段中,我们指定了要使用的模型以及要转换为JSON的字段。

设置管理面板 (Setup the Admin panel)

Django provides us with an admin interface out of the box; the interface will make it easy to test CRUD operations on the Recipe model we just created, but first, we will do a little configuration.

Django提供了一个开箱即用的管理界面。 该界面将使我们可以轻松地在刚刚创建的Recipe模型上测试CRUD操作,但是首先,我们需要进行一些配置。

Open the core/admin.py file and completely replace it with the snippet below:

打开core/admin.py文件,并用下面的代码段完全替换它:

# core/admin.py

from django.contrib import admin
from .models import Recipe  # add this
# Register your models here.

admin.site.register(Recipe) # add this

创建视图 (Creating the Views)

Let’s create a RecipeViewSet class in the core/views.py file, completely replace it with the snippet below:

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

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

Note: The viewsets.ModelViewSet provides methods to handle CRUD operations by default. We just need to do specify the serializer class and the queryset.

注意:默认情况下,viewsets.ModelViewSet提供了处理CRUD操作的方法。 我们只需要指定序列化器类和queryset。

设置URL (Setting up the URLs)

Head over to the api/urls.py file and completely replace it with the code below. This code specifies the URL path for the API:

转至api/urls.py文件,并将其完全替换为以下代码。 此代码指定API的URL路径:

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

Now create a urls.py file in the core directory and paste in the snippet below:

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

# 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))
]

In the code above, the router class generates the following URL patterns:

在上面的代码中, router类生成以下URL模式:

  • /recipes/ - Create and Read operations can be performed on this route.

    / recipes /-可以在此路由上执行创建和读取操作。
  • /recipes/{id} - Read, Update and Delete operations can be performed on this route.

    / recipes / {id}-可以在此路由上执行读取,更新和删除操作。

运行迁移 (Running Migrations)

Because we recently created a Recipe model and defined its structure, we need to make a Migration file and apply the changes on the model to the database, so let’s run the following commands:

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

(recipes_app) $ python manage.py makemigrations
(recipes_app) $ python manage.py migrate

Now, we will create a superuser account to access the admin interface:

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

(recipes_app) $ python manage.py createsuperuser

You will be prompted to enter a username, email and password for the superuser. Be sure to enter details that you can remember because you will need them to log in to the admin dashboard shortly.

系统将提示您输入超级用户的用户名,电子邮件和密码。 确保输入您可以记住的详细信息,因为您将需要它们很快登录到管理仪表板。

Hooray! That’s all the configuration that needs to be done on the backend. We can now test the APIs we created, so let’s start the Django server:

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

(recipes_app) $ python manage.py runserver

Once the server is running, head over to http://localhost:8000/api/recipes/ to ensure it works:

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

We can create a new Recipe item using the interface:

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

We can also perform DELETE, PUT, and PATCH operations on specific Recipe items using their
id primary keys. To do this, we will visit an address with this structure /api/recipe/{id}. Let’s try with this address — http://localhost:8000/api/recipes/1:

id主键对特定配方项目执行DELETE,PUT和PATCH操作。 为此,我们将访问具有/api/recipe/{id}.结构的地址/api/recipe/{id}. 让我们尝试使用该地址-http:// localhost:8000 / api / recipes / 1

That’s all for the backend of the application, now we can move on to fleshing out the frontend.

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

设置前端 ( Setting up the frontend )

In this section of the tutorial, we will build the front-end of the application. We want to place the folder for the front-end code in the root of the recipes_app directory. So, navigate out of the api directory (or spin up a fresh terminal to run alongside the previous one) before running the commands in this section.

在本教程的这一部分中,我们将构建应用程序的前端。 我们希望将前端代码文件夹放置在recipes_app目录的根目录中。 因此,在运行本节中的命令之前,请跳出api目录(或旋转一个新的终端以与上一个终端一起运行)。

Let’s create a nuxt application called client with this command:

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

$ npx create-nuxt-app client

Note: Preceding create-nuxt-app with npx installs the package if it is not already installed globally on your machine.

注意:如果您的计算机上尚未全局安装软件包,则在npx之前将create-nuxt-app安装会安装该软件包。

Once the installation is complete, create-nuxt-app will ask a few questions about extra tools to be added. We will answer them as follows:

安装完成后, create-nuxt-app将询问一些有关要添加的其他工具的问题。 我们将回答如下:

  • Enter a project name or just hit enter for default

    输入项目名称或按回车键默认
  • Enter a project description or just hit enter for default

    输入项目描述或直接按Enter
  • Select none for custom server framework

    为定制服务器框架选择
  • Select PWA support for features to install

    选择PWA支持以安装功能
  • Select bootstrap for UI framework

    选择UI框架的引导程序
  • Select none for test framework

    对于测试框架中选择
  • Select Universal for rendering mode

    选择通用作为渲染模式
  • Enter an author name or just hit enter for default

    输入作者姓名或按回车键默认
  • Select npm for package manager

    选择npm作为包管理器

This will trigger the installation of dependencies using the selected package manager and finally, you will be presented with a screen like this:

Let’s run the following commands to start the application in development mode:

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

$cd client
$ npm run dev

Once the development server has started, head over to http://localhost:3000 to see the application:

开发服务器启动后,转到http:// localhost:3000查看该应用程序:

Awesome! Now let’s take a look at the directory structure of the client folder:

太棒了! 现在,让我们看一下client文件夹的目录结构:

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

Here’s a breakdown of what these directories are for:

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

  • Assets - Contains uncompiled files such as images, CSS, SASS and JavaScript files.

    资产 -包含未编译的文件,例如图像,CSS,SASS和JavaScript文件。
  • Components - Contains Vue.js components.

    组件 -包含Vue.js组件。
  • Layouts - Contain’s the application’s Layouts; Layouts are used to change the appearance of a page and can be used for multiple pages.

    布局 -包含应用程序的布局; 布局用于更改页面的外观,并且可以用于多个页面。
  • Middleware - Contain’s the application’s Middleware; Middleware are custom functions that are run before a page is rendered.

    中间件 -包含应用程序的中间件; 中间件是在呈现页面之前运行的自定义功能。
  • Pages - Contains the application’s Views and Routes. Nuxt.js reads all the .vue files in this directory and uses the information to create the application’s router.

    页面 -包含应用程序的视图和路线。 Nuxt.js读取此目录中的所有.vue文件,并使用该信息创建应用程序的路由器。
  • Plugins - Contains JavaScript plugins to be run before the root Vue.js application is instantiated.

    插件 -包含要在实例化根Vue.js应用程序之前运行JavaScript插件。
  • Static - Contains static files (files that are unlikely to change) and all these files are mapped to the root of the application, which is /.

    静态 -包含静态文件(不太可能更改的文件),所有这些文件都映射到应用程序的根目录/
  • Store - Contains store files for if we intend to use Vuex with Nuxt.js.

    商店 -包含存储文件,因为如果我们打算使用Vuex与Nuxt.js.

There is also a nuxt.config.js file in the client folder, this file contains custom configuration for the Nuxt.js app. Before we continue, download this zip file, extract it and put the images/ folder inside the static/ directory.

client文件夹中还有一个nuxt.config.js文件,该文件包含Nuxt.js应用程序的自定义配置。 在继续之前,请下载此zip文件 ,解压缩后将images/文件夹放在static/目录中。

页面结构 (Structure of the pages)

Remember how we said that Nuxt.js reads all the .vue files in the pages/ directory and uses the information to create the application’s router? In this section, we will add some .vue files to the pages/ directory so that our application will have five pages as so:

还记得我们怎么说Nuxt.js读取pages/目录中的所有.vue文件并使用该信息来创建应用程序的路由器吗? 在本节中,我们将一些.vue文件添加到pages/目录,以便我们的应用程序具有五个页面:

  • Homepage

    主页
  • All Recipes list page

    所有食谱列表页面
  • Single Recipe view page

    单一配方查看页面
  • Single Recipe edit page

    单一配方编辑页面
  • Add Recipe page

    添加配方页面

Let’s add the following .vue files and folders to the pages/ directory so we have this exact structure:

让我们将以下.vue文件和文件夹添加到pages/目录,以便我们具有确切的结构:

├── pages/
   ├── recipes/
     ├── _id/
       └── edit.vue
       └── index.vue
     └── add.vue
     └── index.vue
  └── index.vue

The file structure above will generate the following routes:

上面的文件结构将生成以下路由:

  • / → handled by pages/index.vue

    /→由pages/index.vue处理
  • /recipes/add → handled by pages/recipes/add.vue

    / recipes / add→由pages/recipes/add.vue
  • /recipes/ → handled by pages/recipes/index.vue

    / recipes /→由pages/recipes/index.vue
  • /recipes/{id}/ → handled by pages/recipes/_id/index.vue

    / recipes / {id} /→由pages/recipes/_id/index.vue
  • /recipes/{id}/edit → handled by pages/recipes/_id/edit.vue

    / recipes / {id} / edit→由pages/recipes/_id/edit.vue

Note: A .vue file or directory prefixed by an underscore will create a dynamic route. This is useful in our application as it will make it easy to display different recipes based on their IDs e.g recipes/1/, recipes/2/, etc

注意:带下划线前缀的.vue文件或目录将创建动态路由。 这在我们的应用程序中很有用,因为它可以轻松地根据其ID显示不同的配方,例如配方1 /,配方2 /等。

建立首页 (Creating the homepage)

In Nuxt.js, Layouts are a great help when you want to change the look and feel of your application. Now, each instance of a Nuxt.js application ship with a default Layout, we want to remove all the styles so they do not interfere with our application.

在Nuxt.js中,当您想要更改应用程序的外观时,布局是一个很好的帮助。 现在,Nuxt.js应用程序的每个实例都带有默认的Layout,我们希望删除所有样式,以免它们干扰我们的应用程序。

Open the layouts/default.vue file and replace it with the following snippet below:

打开layouts/default.vue文件,并将其替换为以下代码片段:

<template>
  <div>
    <nuxt/>
  </div>
</template>

<style>
</style>

Let’s update the pages/index.vue file with the code below:

让我们使用以下代码更新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>

From the code above, <nuxt-link> is a Nuxt.js component which can be used to navigate between pages. It is very similar to the <router-link> component from Vue Router.

从上面的代码中, <nuxt-link>是一个Nuxt.js组件,可用于在页面之间导航。 它与Vue Router中<router-link>组件非常相似。

Let's start the front-end development server (if it isn’t already running), visit http://localhost:3000/, and see what the Homepage looks like:

让我们启动前端开发服务器(如果尚未运行),请访问http:// localhost:3000 / ,然后查看首页的外观:

npm run dev

Note: Always ensure that the Django backend server is always running in another instance of the terminal because the front-end will begin communicating with it for data shortly.

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

Every page in this application will be a Vue Component and Nuxt.js provides special attributes and functions to make the development of applications seamless. You can find documentation on all these special attributes here.

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

For the sake of this tutorial, we will make use of two of these functions:

为了本教程的缘故,我们将使用以下两个功能:

  • head() - This method is used to set specific <meta> tags for the current page.

    head() -此方法用于为当前页面设置特定的<meta>标签。
  • asyncData() - This method is used to fetch data before the page component is loaded. The object returned is then merged with the page component’s data. We will make use of this later in this tutorial.

    asyncData() -此方法用于在加载页面组件之前获取数据。 然后将返回的对象与页面组件的数据合并。 我们将在本教程的后面部分中使用它。

创建食谱列表页面 (Creating the Recipes List page)

Let’s create a Vue.js component called RecipeCard.vue in the components/ directory and update it with the snippet below:

让我们在components/目录中创建一个名为RecipeCard.vue的Vue.js组件,并使用以下代码片段对其进行更新:

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

The component above accepts two props:

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

  1. A recipe object which contains information about a particular recipe.

    recipe对象,其中包含有关特定配方的信息。
  2. An onDelete method which will be triggered whenever a user clicks on the button to delete a recipe.

    一个onDelete方法,只要用户单击按钮以删除配方,就会触发该方法。

Next, open the pages/recipes/index.vue and update it with the snippet below:

接下来,打开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>

Let's start the front-end development server (if it isn’t already running), visit http://localhost:3000/recipes, and see this recipes listing page:

让我们启动前端开发服务器(如果尚未运行),请访问http:// localhost:3000 / recipes ,然后查看此食谱列表页面:

npm run dev

From the image above, we see that three recipe cards appear even though we set recipes to an empty array in the component’s data section. The explanation for this is that the method asyncData is executed before the page loads and it returns an object which updates the component’s data.

从上图中,即使将recipes设置为组件数据部分中的空数组,我们recipes看到三个配方卡。 对此的解释是,方法asyncData在页面加载之前执行,并且它返回一个更新组件数据的对象。

Now, all we need to do is modify the asyncData method to make an api request to the Django backend and update the component’s data with the result.

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

Before we do that, we have to install and configure Axios:

在此之前,我们必须安装和配置Axios

npm install -s @nuxtjs/axios

Once Axios is installed, open the nuxt.config.js file and update it accordingly:

安装Axios ,打开nuxt.config.js文件并进行相应的更新:

// client/nuxt.config.js

  /_
  ** Nuxt.js modules
  _/
  modules: [,
    // Doc: https://bootstrap-vue.js.org/docs/
    'bootstrap-vue/nuxt',
    '@nuxtjs/axios' // add this
  ],

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

Now, open the pages/recipes/index.vue file and replace the <script> section with the one below:

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

[...]

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

[...]

In the code above, asyncData()receives an object called context, which we destructure to get $axios . You can check out all the attributes of context here.

在上面的代码中, asyncData()接收一个称为context的对象,我们对其进行$axios以获取$axios 。 您可以在此处检查context所有属性。

Note: We wrap asyncData() in a try...block because we want to prevent the bug which will occur if the backend server isn't running and axios fails to retrieve data. Whenever that happens, recipes is just set to an empty array instead.

注意:我们将asyncData()包装在try ... block中,因为我们想防止在后端服务器未运行且axios无法检索数据时发生的错误。 每当发生这种情况时,配方只会设置为一个空数组。

This line of code — let recipes = await $axios.$get("/recipes/") — is a shorter version of:

这行代码- let recipes = await $axios.$get("/recipes/") -是以下代码的let recipes = await $axios.$get("/recipes/")版本:

let response = await $axios.get("/recipes")
let recipes = response.data

The deleteRecipe() method deletes a particular recipe, fetches the most recent list of recipes from the Django backend and finally updates the component’s data.

deleteRecipe ()方法删除特定的配方,从Django后端获取配方的最新列表,最后更新组件的数据。

We can start the front-end development server (if it isn’t already running) now and we will see that the recipe cards are now being populated with data from the Django backend.

现在,我们可以启动前端开发服务器(如果尚未运行),并且我们将看到配方卡中正在填充来自Django后端的数据。

Note: For this to work, the Django backend server has to be running and there must be some data (entered from the admin interface) available for the Recipe items.

注意:要使此工作正常,Django后端服务器必须正在运行,并且配方项必须有一些数据(从管理界面输入)。

Let’s visit http://localhost:3000/recipes, and take a peek:

让我们访问http:// localhost:3000 / recipes ,然后看看:

npm run dev

You can also try deleting recipe items and watch them update accordingly.

您也可以尝试删除配方项目,并观看它们的相应更新。

添加新食谱 (Adding new Recipes)

As we already discussed, we want to be able to add new recipes from the front-end of the application so open the pages/recipes/add/ file and update it with the following snippet:

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

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

In submitRecipe(), once the form data has been posted and the recipe is created successfully, the app is redirected to /recipes/ using this.$router.

submitRecipe() ,一旦表单数据已经过帐并成功创建了配方,则使用this.$router将应用程序重定向到/recipes/

创建单一配方视图页面 (Create the Single Recipe View page)

Let’s create the view that allows a user to view a single Recipe item, open the /pages/recipes/_id/index.vue file and paste in the snippet below:

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

The code here is pretty straight forward. The new thing we introduce here is the params key seen in the asyncData() method. In this case we are using params to get the ID of the recipe we want to view. We extract params from the URL and prefetch its data before displaying it on the page.

这里的代码非常简单。 我们在这里引入的新内容是在asyncData()方法中看到的params键。 在这种情况下,我们使用params来获取要查看的配方的ID 。 我们从URL提取params ,并在将其显示在页面上之前预取其数据。

We can view a single Recipe item on the web browser now and see a similar screen:

我们现在可以在网络浏览器上查看一个食谱项目,并看到类似的屏幕:

创建单个配方编辑页面 (Create the Single Recipe Edit Page)

We need to create the view that allows the user to edit and update a single Recipe item, so open the /pages/recipes/_id/edit.vue file and paste in the snippet below:

我们需要创建允许用户编辑和更新单个Recipe项目的视图,因此打开/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.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>
</style>

In the code above, the submitRecipe() method has a conditional statement whose purpose is to remove the picture of an edited Recipe item from the data to be submitted if the picture was not changed.

在上面的代码中, submitRecipe()方法具有条件语句,其目的是在未更改图片的情况下,从要提交的数据中删除已编辑配方项目的图片。

Once the Recipe item has been updated, the application is redirected to the Recipes list page — /recipes/.

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

设置过渡 (Setting up Transitions)

Congratulations for making it this far! The application is fully functional and that’s great, however, we can give it a smoother look by adding transitions.

恭喜! 该应用程序具有完整的功能,这很棒,但是,我们可以通过添加过渡来使其外观更加流畅。

Transitions allow us to smoothly change CSS property values (from one value to another), over a given duration.

过渡允许我们在给定的时间内平稳地更改CSS属性值(从一个值更改为另一个值)。

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

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

Let’s include the styling for the transition. Create a folder 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:

让我们包括过渡的样式。 在assets/目录中创建一个名为css/的文件夹,并在其中添加一个transitions.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文件并进行相应更新以加载我们刚刚创建CSS文件:

// nuxt.config.js

module.exports = {   /_
  ** Global CSS
  _/
  css: ['~/assets/css/transitions.css'], // update this
}

Voila! See how easy it is to add transitions to the application? Now our application will change frames on each navigation in a sleek way 😋:

瞧! 看到向应用程序添加过渡有多么容易? 现在我们的应用程序将以一种光滑的方式更改每个导航上的帧frames:

结论 ( Conclusion )

In this article, we started out by learning the differences between Client-Side and Server-Side rendered applications. We went on to learn what a Universal application is and finally, we saw how to build a Universal application using Nuxt.js and Django.

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

The source code for this tutorial is available here on GitHub.

本教程的源代码可以在这里 GitHub上。

翻译自: https://scotch.io/tutorials/building-a-universal-application-with-nuxtjs-and-django

nuxt.js使用教程

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值