如何使用Nest.js、MongoDB和Vue.js构建博客

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

作为 Write for DOnations 计划的一部分,作者选择了 Public Interest Inc 中的 软件来接收捐赠。

介绍

Nest.js 是一个使用 TypeScript 构建的可扩展的服务器端 JavaScript 框架,它仍然保持与 JavaScript 的兼容性,这使其成为构建高效可靠的后端应用程序的有效工具。 它具有模块化架构,为 Node.js 开发世界提供了成熟的结构化设计模式。

Vue.js 是一个用于构建用户界面的前端 JavaScript 框架。 它具有简单但非常强大的 API 以及出色的性能。 Vue.js 能够为任何 Web 应用程序的前端层和逻辑提供支持,无论其大小如何。 它易于与其他库或现有项目集成,使其成为大多数现代 Web 应用程序的完美选择。

在本教程中,您将构建一个 Nest.js 应用程序,以熟悉其构建块以及构建现代 Web 应用程序的基本原则。 您将通过将应用程序分成两个不同的部分来处理这个项目:前端和后端。 首先,您将专注于使用 Nest.js 构建的 RESTful 后端 API。 然后,您将专注于使用 Vue.js 构建的前端。 这两个应用程序将在不同的端口上运行,并将作为单独的域运行。

您将构建一个博客应用程序,用户可以使用该应用程序创建和保存新帖子、在主页上查看已保存的帖子以及执行其他过程,例如编辑和删除帖子。 此外,您将连接您的应用程序并使用 MongoDB 保存其数据,这是一个可以接收和存储 JSON 文档的无模式 NoSQL 数据库。 本教程的重点是在开发环境中构建您的应用程序。 对于生产环境,您还应该考虑应用程序的用户身份验证。

先决条件

要完成本教程,您需要:

注意:本教程使用macOS机器进行开发。 如果您使用的是其他操作系统,则可能需要在整个教程中对 npm 命令使用 sudo


第 1 步 — 安装 Nest.js 和其他依赖项

在本节中,您将通过在本地计算机上安装应用程序及其所需的依赖项来开始使用 Nest.js。 您可以使用 Nest.js 提供的 CLI 或从 GitHub 安装启动项目轻松安装 Nest.js。 出于本教程的目的,您将使用 CLI 来设置应用程序。 首先,从终端运行以下命令以将其全局安装在您的机器上:

npm i -g @nestjs/cli

您将看到类似于以下内容的输出:

Output@nestjs/cli@5.8.0
added 220 packages from 163 contributors in 49.104s

要确认 Nest CLI 的安装,请从终端运行以下命令:

nest --version

您将看到显示您机器上安装的当前版本的输出:

Output5.8.0

您将使用 nest 命令来管理您的项目并使用它来生成相关文件——例如控制器、模块和提供程序。

要开始本教程的项目,请使用 nest 命令创建一个名为 blog-backend 的新 Nest.js 项目,方法是从终端运行以下命令:

nest new blog-backend

运行命令后,nest 会立即提示您提供一些基本信息,例如 descriptionversionauthor。 继续并提供适当的详细信息。 响应每个提示后,在您的计算机上点击 ENTER 以继续。

接下来,您将选择一个包管理器。 就本教程而言,选择 npm 并点击 ENTER 开始安装 Nest.js。

这将在本地开发文件夹的 blog-backend 文件夹中生成一个新的 Nest.js 项目。

接下来,从终端导航到新项目的文件夹:

cd blog-backend

运行以下命令以安装其他服务器依赖项:

npm install --save @nestjs/mongoose mongoose

您已经安装了 @nestjs/mongoose,这是一个 Nest.js 专用包,用于 MongoDB 的对象建模工具,以及 mongoose,这是一个用于 Mongoose 的包。

现在您将使用以下命令启动应用程序:

npm run start

现在,如果您从您喜欢的浏览器导航到 http://localhost:3000,您将看到您的应用程序正在运行。

您已经利用 Nest CLI 命令的可用性成功生成了项目。 之后,您继续运行该应用程序并在本地计算机上的默认端口 3000 上访问它。 在下一部分中,您将通过设置数据库连接的配置来进一步使用应用程序。

第 2 步 — 配置和连接数据库

在此步骤中,您将配置 MongoDB 并将其集成到您的 Nest.js 应用程序中。 您将使用 MongoDB 为您的应用程序存储数据。 MongoDB 将其数据存储在 documents 作为 field : value 对。 要访问此数据结构,您将使用 Mongoose,它是一种对象文档建模 (ODM),允许您定义表示 MongoDB 数据库存储的数据类型的模式。

要启动 MongoDB,请打开一个单独的终端窗口,以便应用程序可以继续运行,然后执行以下命令:

sudo mongod

这将启动 MongoDB 服务并在您的机器后台运行数据库。

在文本编辑器中打开项目 blog-backend 并导航到 ./src/app.module.ts。 您可以通过在根 ApplicationModule 中包含已安装的 MongooseModule 来建立与数据库的连接。 为此,请使用以下突出显示的行更新 app.module.ts 中的内容:

~/blog-backend/src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/nest-blog', { useNewUrlParser: true }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

在此文件中,您使用 forRoot() 方法提供到数据库的连接。 完成编辑后保存并关闭文件。

有了这个,您已经使用 MongoDB 的 Mongoose 模块设置了数据库连接。 在下一部分中,您将使用 Mongoose 库、TypeScript 接口和数据传输对象 (DTO) 模式创建数据库模式。

第 3 步 — 创建数据库模式、接口和 DTO

在这一步中,您将使用 Mongoose 为您的数据库创建一个 schema接口 和一个 数据传输对象 。 Mongoose 有助于管理数据之间的关系,并为数据类型提供模式验证。 为了帮助定义应用程序数据库中数据的结构和数据类型,您将创建确定以下内容的文件:

  • 数据库模式:这是一种数据组织,作为定义数据库需要存储的数据的结构和类型的蓝图。
  • interfaces:TypeScript 接口用于类型检查。 它可用于定义应为应用程序传递的数据类型。
  • 数据传输对象:这是一个对象,它定义了如何通过网络发送数据并在进程之间传输数据。

首先,回到应用程序当前正在运行的终端并使用 CTRL + C 停止进程,然后导航到 ./src/ 文件夹:

cd ./src/

然后,创建一个名为 blog 的目录,并在其中创建一个 schemas 文件夹:

mkdir -p blog/schemas

schemas 文件夹中,创建一个名为 blog.schema.ts 的新文件并使用文本编辑器打开它。 然后,添加以下内容:

~/blog-backend/src/blog/schemas/blog.schema.ts

import * as mongoose from 'mongoose';

export const BlogSchema = new mongoose.Schema({
    title: String,
    description: String,
    body: String,
    author: String,
    date_posted: String
})

在这里,您使用 Mongoose 定义了将存储在数据库中的数据类型。 您已指定所有字段都将存储并且只接受字符串值。 完成编辑后保存并关闭文件。

现在,确定了数据库模式后,您可以继续创建接口。

首先,导航回 blog 文件夹:

cd ~/blog-backend/src/blog/

创建一个名为 interfaces 的新文件夹并移入其中:

mkdir interfaces

interfaces 文件夹中,创建一个名为 post.interface.ts 的新文件并使用文本编辑器打开它。 添加以下内容以定义 Post 的数据类型:

~/blog-backend/src/blog/interfaces/post.interface.ts

import { Document } from 'mongoose';

export interface Post extends Document {
    readonly title: string;
    readonly description: string;
    readonly body: string;
    readonly author: string;
    readonly date_posted: string
}

在此文件中,您已成功将 Post 类型的数据类型定义为字符串值。 保存并退出文件。

由于您的应用程序将执行将数据发布到数据库的功能,因此您将创建一个数据传输对象,该对象将定义如何通过网络发送数据。

为此,请在 ./src/blog 文件夹内创建一个文件夹 dto。 在新创建的文件夹中,创建另一个名为 create-post.dto.ts 的文件

导航回 blog 文件夹:

cd ~/blog-backend/src/blog/

然后创建一个名为 dto 的文件夹并移入其中:

mkdir dto

dto 文件夹中,创建一个名为 create-post.dto.ts 的新文件并使用文本编辑器打开它,添加以下内容:

~/blog-backend/src/blog/dto/create-post.dto.ts

export class CreatePostDTO {
    readonly title: string;
    readonly description: string;
    readonly body: string;
    readonly author: string;
    readonly date_posted: string
}

您已将 CreatePostDTO 类中的每个单独属性标记为具有 string 的数据类型和 readonly 以避免不必要的突变。 完成编辑后保存并退出文件。

在这一步中,您已经为数据库创建了一个数据库模式、一个接口以及一个用于数据库将存储的数据的数据传输对象。 接下来,您将为您的博客生成一个模块、控制器和服务。

第 4 步 — 为博客创建模块、控制器和服务

在这一步中,您将通过为您的博客创建一个模块来改进应用程序的现有结构。 该模块将组织您的应用程序的文件结构。 接下来,您将创建一个控制器来处理路由并处理来自客户端的 HTTP 请求。 总而言之,您将设置一个服务来处理对于应用程序的控制器来说太复杂而无法处理的所有业务逻辑。

生成模块

与 Angular 前端 Web 框架类似,Nest.js 使用模块化语法。 Nest.js 应用程序具有模块化设计; 它安装了一个根模块,通常对于小型应用程序来说已经足够了。 但是当应用程序开始增长时,Nest.js 建议采用多模块组织,将代码拆分为相关功能。

Nest.js 中的 模块@Module() 装饰器标识,并接收具有 controllersproviders 等属性的对象。 这些属性中的每一个都分别采用 controllersproviders 的数组。

您将为这个博客应用程序生成一个新模块,以使结构更有条理。 首先,仍然在 ~/blog-backend 文件夹中,执行以下命令:

nest generate module blog

您将看到类似于以下内容的输出:

OutputCREATE /src/blog/blog.module.ts

UPDATE /src/app.module.ts

该命令为应用程序生成了一个名为 blog.module.ts 的新模块,并将新创建的模块导入到应用程序的根模块中。 这将允许 Nest.js 知道除根模块之外的另一个模块。

在此文件中,您将看到以下代码:

~/blog-backend/src/blog/blog.module.ts

import { Module } from '@nestjs/common';

@Module({})
export class BlogModule {}

您将在本教程后面使用所需的属性更新此 BlogModule。 保存并退出文件。

生成服务

service,也可以称为 Nest.js 中的提供程序,旨在从控制器中删除逻辑,这些逻辑仅用于处理 HTTP 请求并将更复杂的任务重定向到服务。 服务是简单的 JavaScript 类,在它们之上带有 @Injectable() 装饰器。 要生成新服务,请在您仍在项目目录中时从终端运行以下命令:

nest generate service blog

您将看到类似于以下内容的输出:

[secondary_label Output]  
CREATE /src/blog/blog.service.spec.ts (445 bytes)

CREATE /src/blog/blog.service.ts (88 bytes)

UPDATE /src/blog/blog.module.ts (529 bytes)

此处使用的 nest 命令创建了一个 blog.service.spec.ts 文件,您可以使用该文件进行测试。 它还创建了一个新的 blog.service.ts 文件,它将保存此应用程序的所有逻辑,并处理向 MongoDB 数据库添加和检索文档。 此外,它会自动导入新创建的服务并添加到 blog.module.ts。

该服务处理应用程序中的所有逻辑,负责与数据库交互,并将适当的响应返回给控制器。 为此,请在文本编辑器中打开 blog.service.ts 文件并将内容替换为以下内容:

~/blog-backend/src/blog/blog.service.ts

import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Post } from './interfaces/post.interface';
import { CreatePostDTO } from './dto/create-post.dto';

@Injectable()
export class BlogService {

    constructor(@InjectModel('Post') private readonly postModel: Model<Post>) { }

    async getPosts(): Promise<Post[]> {
        const posts = await this.postModel.find().exec();
        return posts;
    }

    async getPost(postID): Promise<Post> {
        const post = await this.postModel
            .findById(postID)
            .exec();
        return post;
    }

    async addPost(createPostDTO: CreatePostDTO): Promise<Post> {
        const newPost = await this.postModel(createPostDTO);
        return newPost.save();
    }

    async editPost(postID, createPostDTO: CreatePostDTO): Promise<Post> {
        const editedPost = await this.postModel
            .findByIdAndUpdate(postID, createPostDTO, { new: true });
        return editedPost;
    }

    async deletePost(postID): Promise<any> {
        const deletedPost = await this.postModel
            .findByIdAndRemove(postID);
        return deletedPost;
    }

}

在此文件中,您首先从 @nestjs/commonmongoose@nestjs/mongoose 导入所需的模块。 您还导入了一个名为 Post 的接口和一个数据传输对象 CreatePostDTO

constructor 中,您添加了 @InjectModel(``'``Post``'``),这会将 Post 模型注入到此 BlogService 类中。 您现在可以使用这个注入模型来检索所有帖子、获取单个帖子并执行其他与数据库相关的活动。

接下来,您创建了以下方法:

  • getPosts():从数据库中获取所有帖子。
  • getPost():从数据库中检索单个帖子。
  • addPost():添加新帖子。
  • editPost():更新单个帖子。
  • deletePost():删除特定帖子。

完成后保存并退出文件。

您已完成设置和创建几个方法,这些方法将处理从后端 API 与 MongoDB 数据库的正确交互。 现在,您将创建所需的路由来处理来自前端客户端的 HTTP 调用。

生成控制器

在巢中。 在 Node.js 中,controllers 负责处理来自应用程序客户端的任何传入请求并返回适当的响应。 与大多数其他 Web 框架类似,应用程序侦听请求并对其做出响应很重要。

为了满足博客应用程序的所有 HTTP 请求,您将利用 nest 命令生成新的控制器文件。 确保您仍在项目目录 blog-backend 中,然后运行以下命令:

nest generate controller blog

您将看到类似于以下内容的输出:

OutputCREATE /src/blog/blog.controller.spec.ts (474 bytes)

CREATE /src/blog/blog.controller.ts (97 bytes)

UPDATE /src/blog/blog.module.ts (483 bytes)

输出表明此命令在 src/blog 目录中创建了两个新文件。 它们是 blog.controller.spec.tsblog.controller.ts。 前者是一个文件,您可以使用它来为新创建的控制器编写自动化测试。 后者是控制器文件本身。 Nest.js 中的控制器是用 @Controller 元数据修饰的 TypeScript 文件。 该命令还导入了新创建的控制器并添加到博客模块中。

接下来,使用文本编辑器打开 blog.controller.ts 文件并使用以下内容更新它:

~/blog-backend/src/blog/blog.controller.ts

import { Controller, Get, Res, HttpStatus, Param, NotFoundException, Post, Body, Query, Put, Delete } from '@nestjs/common';
import { BlogService } from './blog.service';
import { CreatePostDTO } from './dto/create-post.dto';
import { ValidateObjectId } from '../shared/pipes/validate-object-id.pipes';


@Controller('blog')
export class BlogController {

    constructor(private blogService: BlogService) { }

    @Get('posts')
    async getPosts(@Res() res) {
        const posts = await this.blogService.getPosts();
        return res.status(HttpStatus.OK).json(posts);
    }

    @Get('post/:postID')
    async getPost(@Res() res, @Param('postID', new ValidateObjectId()) postID) {
        const post = await this.blogService.getPost(postID);
        if (!post) throw new NotFoundException('Post does not exist!');
        return res.status(HttpStatus.OK).json(post);

    }

    @Post('/post')
    async addPost(@Res() res, @Body() createPostDTO: CreatePostDTO) {
        const newPost = await this.blogService.addPost(createPostDTO);
        return res.status(HttpStatus.OK).json({
            message: "Post has been submitted successfully!",
            post: newPost
        })
    }
}

在此文件中,您首先导入了必要的模块来处理来自 @nestjs/common 模块的 HTTP 请求。 然后,您导入了三个新模块,它们是:BlogServiceCreatePostDTOValidateObjectId。 之后,您通过构造函数将 BlogService 注入到控制器中,以便访问和使用 BlogService 文件中已经定义的函数。 这是一种在 Nest.js 中被视为 依赖注入 的模式,用于提高效率并增强应用程序的模块化。

最后,您创建了以下异步方法:

  • getPosts():此方法将执行从客户端接收 HTTP GET 请求以从数据库中获取所有帖子然后返回适当响应的功能。 它装饰有 @Get(``'``posts``'``)
  • getPost():这将 postID 作为参数并从数据库中获取单个帖子。 除了传递给此方法的 postID 参数之外,您还实现了添加一个名为 ValidateObjectId() 的额外方法。 该方法实现了 Nest.js 中的 PipeTransform 接口。 其目的是验证并确保可以在数据库中找到 postID 参数。 您将在下一节中定义此方法。
  • addPost():此方法将处理 POST HTTP 请求以将新帖子添加到数据库。

为了能够编辑和删除特定帖子,您需要向 blog.controller.ts 文件添加另外两个方法。 为此,请在您之前添加到 blog.controller.tsaddPost() 方法之后直接包含以下 editPost()deletePost() 方法:

~/blog-backend/src/blog/blog.controller.ts

...
@Controller('blog')
export class BlogController {
    ...
    @Put('/edit')
    async editPost(
        @Res() res,
        @Query('postID', new ValidateObjectId()) postID,
        @Body() createPostDTO: CreatePostDTO
    ) {
        const editedPost = await this.blogService.editPost(postID, createPostDTO);
        if (!editedPost) throw new NotFoundException('Post does not exist!');
        return res.status(HttpStatus.OK).json({
            message: 'Post has been successfully updated',
            post: editedPost
        })
    }


    @Delete('/delete')
    async deletePost(@Res() res, @Query('postID', new ValidateObjectId()) postID) {
        const deletedPost = await this.blogService.deletePost(postID);
        if (!deletedPost) throw new NotFoundException('Post does not exist!');
        return res.status(HttpStatus.OK).json({
            message: 'Post has been deleted!',
            post: deletedPost
        })
    }
}

在这里您添加了:

  • editPost():该方法接受postID的查询参数,将执行更新单个帖子的功能。 它还利用 ValidateObjectId 方法为您需要编辑的帖子提供适当的验证。
  • deletePost():此方法将接受 postID 的查询参数,并将从数据库中删除特定帖子。

BlogController 类似,您在此处定义的每个异步方法都有一个元数据装饰器,并接受 Nest.js 用作路由机制的前缀。 它控制哪个控制器接收哪些请求,并分别指向应该处理请求和返回响应的方法。

例如,您在本节中创建的 BlogController 具有前缀 blog 和一个名为 getPosts() 的方法,该方法采用 posts 前缀。 这意味着任何发送到 blog/posts (http:localhost:3000/blog/posts) 端点的 GET 请求都将由 getPosts() 方法处理。 此示例类似于其他方法处理 HTTP 请求的方式。

保存并退出文件。

有关完整的 blog.controller.ts 文件,请访问此应用程序的 DO 社区存储库

在本节中,您创建了一个模块以使应用程序更有条理。 您还创建了一个服务,通过与数据库交互并返回适当的响应来处理应用程序的业务逻辑。 最后,您生成了一个控制器并创建了处理来自客户端的 HTTP 请求(例如 GETPOSTPUTDELETE)所需的方法。 在下一步中,您将完成后端设置。

第 5 步 — 为 Mongoose 创建额外的验证

您可以通过唯一 ID(也称为 PostID)识别博客应用程序中的每个帖子。 这意味着获取帖子需要您将此 ID 作为查询参数传递。 要验证此 postID 参数并确保该帖子在数据库中可用,您需要创建一个可重用函数,该函数可以从 BlogController 中的任何方法初始化。

要进行配置,请导航到 ./src/blog 文件夹:

cd ./src/blog/

然后,创建一个名为 shared 的新文件夹:

mkdir -p shared/pipes

pipes 文件夹中,使用文本编辑器创建一个名为 validate-object-id.pipes.ts 的新文件并打开它。 添加以下内容来定义接受的postID数据:

~/blog-backend/src/blog/shared/pipes/validate-object-id.pipes.ts

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import * as mongoose from 'mongoose';

@Injectable()
export class ValidateObjectId implements PipeTransform<string> {
   async transform(value: string, metadata: ArgumentMetadata) {
       const isValid = mongoose.Types.ObjectId.isValid(value);
       if (!isValid) throw new BadRequestException('Invalid ID!');
       return value;
   }
}

ValidateObjectId() 类实现了 @nestjs/common 模块中的 PipeTransform 方法。 它有一个名为 transform() 的方法,该方法将值作为参数——在本例中为 postID。 使用上述方法,任何来自该应用程序前端的带有postID且在数据库中找不到的HTTP请求都将被视为无效。 保存并关闭文件。

在创建服务和控制器后,您需要设置基于 BlogSchemaPost 模型。 此配置可以在根 ApplicationModule 中设置,但在这种情况下,在 BlogModule 中构建模型将维护应用程序的组织。 打开 ./src/blog/blog.module.ts 并使用以下突出显示的行更新它:

~/blog-backend/src/blog/blog.module.ts

import { Module } from '@nestjs/common';
import { BlogController } from './blog.controller';
import { BlogService } from './blog.service';
import { MongooseModule } from '@nestjs/mongoose';
import { BlogSchema } from './schemas/blog.schema';

@Module({
 imports: [
   MongooseModule.forFeature([{ name: 'Post', schema: BlogSchema }])
],
 controllers: [BlogController],
 providers: [BlogService]
})
export class BlogModule { }

该模块使用 MongooseModule.forFeature() 方法来定义应该在模块中注册哪些模型。 没有这个,使用 @injectModel() 装饰器在 BlogService 中注入 PostModel 将不起作用。 添加完内容后保存并关闭文件。

在这一步中,您已经使用 Nest.js 创建了完整的后端 RESTful API,并将其与 MongoDB 集成。 在下一节中,您将配置服务器以允许来自另一台服务器的 HTTP 请求,因为您的前端应用程序将在不同的端口上运行。

第 6 步 - 启用 CORS

默认情况下,从一个域到另一个域的 HTTP 请求通常被阻止,除非服务器指定允许它。 为了让您的前端应用程序向后端服务器发出请求,您必须启用 跨域资源共享 (CORS),这是一种允许在网页上请求受限资源的技术。

在 Nest.js 中启用 CORS,您需要在 main.ts 文件中添加一个方法。 在位于 ./src/main.ts 的文本编辑器中打开此文件,并使用以下突出显示的内容对其进行更新:

~/blog-backend/src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 app.enableCors();
 await app.listen(3000);
}
bootstrap();

保存并退出文件。

现在您已经完成了后端设置,您将把注意力转移到前端并使用 Vue.js 来使用目前构建的 API。

第 7 步 — 创建 Vue.js 前端

在本节中,您将使用 Vue.js 创建前端应用程序。 Vue CLI 是一个标准工具,可让您轻松快速生成和安装新的 Vue.js 项目。

首先,您首先需要在您的机器上全局安装 Vue CLI。 打开另一个终端,而不是从 blog-backend 文件夹工作,导航到本地项目的开发文件夹并运行:

npm install -g @vue/cli

安装过程完成后,您将使用 vue 命令创建一个新的 Vue.js 项目:

vue create blog-frontend

输入此命令后,您会看到一个简短的提示。 选择 manually select features 选项,然后通过在计算机上按 SPACE 选择您需要的功能,以突出显示多个功能。 您将选择 BabelRouterLinter / Formatter

对于接下来的说明,键入 y 以使用路由器的历史模式; 这将确保在路由器文件中启用历史模式,该文件将自动为该项目生成。 此外,选择 ESLint with error prevention only 选择 linter/formatter 配置。 接下来,选择 Lint on save 以获取其他 Lint 功能。 然后选择将您的配置保存在 dedicated config file 中以供将来的项目使用。 输入预设的名称,例如 vueconfig

然后 Vue.js 将开始在名为 blog-frontend 的目录中创建应用程序及其所有必需的依赖项。

安装过程完成后,在 Vue.js 应用程序中导航:

cd blog-frontend

然后,使用以下命令启动开发服务器:

npm run serve

您的应用程序将在 http://localhost:8080 上运行。

由于您将在此应用程序中执行 HTTP 请求,因此您需要安装 Axios,它是一个基于 Promise 的浏览器 HTTP 客户端。 您将在此处使用 Axios 来执行来自应用程序中不同组件的 HTTP 请求。 通过在计算机上的终端点击 CTRL + C 来停止前端应用程序,然后运行以下命令:

npm install axios --save

您的前端应用程序将从应用程序内的不同组件对特定域上的后端 API 进行 API 调用。 为了确保此应用程序的结构正确,您可以创建一个 helper 文件并定义服务器 baseURL

首先,从仍在 blog-frontend 内的终端导航到 ./src/ 文件夹:

cd ./src/

创建另一个名为 utils 的文件夹:

mkdir utils

utils 文件夹中,使用文本编辑器创建一个名为 helper.js 的新文件并打开它。 添加以下内容,为后端 Nest.js 项目定义 baseURL

~blog-frontend/src/utils/helper.js

export const server = {

baseURL: 'http://localhost:3000'

}

通过定义 baseURL,您将能够从 Vue.js 组件文件中的任何位置调用它。 如果您需要更改 URL,在此文件中更新 baseURL 比在您的应用程序中更容易。

在本节中,您安装了 Vue CLI,这是一个用于创建新 Vue.js 应用程序的工具。 您使用此工具制作了 blog-frontend 应用程序。 此外,您运行了应用程序并安装了一个名为 Axios 的库,只要应用程序中有 HTTP 调用,您就会使用该库。 接下来,您将为应用程序创建组件。

第 8 步——创建可重用的组件

现在您将为您的应用程序创建可重用的组件,这是 Vue.js 应用程序的标准结构。 Vue.js 中的组件系统使开发人员可以构建一个单一的、独立的界面单元,该单元可以拥有自己的状态、标记和样式。 这使得 Vue.js 中的组件可以重用。

每个 Vue.js 组件都包含三个不同的部分:

  • <template>:包含 HTML 内容
  • <script>:包含所有基本前端逻辑并定义功能
  • <style>:每个单独组件的样式表

首先,您将首先创建一个组件来创建新帖子。 为此,请在 ./src/components 文件夹中创建一个名为 post 的新文件夹,该文件夹将包含帖子所需的可重用组件。 然后使用您的文本编辑器,在新创建的 post 文件夹中,创建另一个文件并将其命名为 Create.vue。 打开新文件并添加以下代码,其中包含提交帖子所需的输入字段:

~blog-frontend/src/components/post/Create.vue

<template>
  <div>
       <div class="col-md-12 form-wrapper">
         <h2> Create Post </h2>
         <form id="create-post-form" @submit.prevent="createPost">
              <div class="form-group col-md-12">
               <label for="title"> Title </label>
               <input type="text" id="title" v-model="title" name="title" class="form-control" placeholder="Enter title">
              </div>
             <div class="form-group col-md-12">
                 <label for="description"> Description </label>
                 <input type="text" id="description" v-model="description" name="description" class="form-control" placeholder="Enter Description">
             </div>
             <div class="form-group col-md-12">
                 <label for="body"> Write Content </label>
                 <textarea id="body" cols="30" rows="5" v-model="body" class="form-control"></textarea>
             </div>
             <div class="form-group col-md-12">
                 <label for="author"> Author </label>
                 <input type="text" id="author" v-model="author" name="author" class="form-control">
             </div>

             <div class="form-group col-md-4 pull-right">
                 <button class="btn btn-success" type="submit"> Create Post </button>
             </div>          
         </form>
       </div>
   </div>
</template>

这是 CreatePost 组件的 <template> 部分。 它包含创建新帖子所需的 HTML 输入元素。 每个输入字段都有一个 v-model 指令作为输入属性。 这是为了保证每一个表单输入的双向数据绑定,方便Vue.js获取用户的输入。

接下来,将 <script> 部分添加到同一个文件中,直接跟在前面的内容后面:

~blog-frontend/src/components/post/Create.vue

...
<script>
import axios from "axios";
import { server } from "../../utils/helper";
import router from "../../router";
export default {
 data() {
   return {
     title: "",
     description: "",
     body: "",
     author: "",
     date_posted: ""
   };
 },
 created() {
   this.date_posted = new Date().toLocaleDateString();
 },
 methods: {
   createPost() {
     let postData = {
       title: this.title,
       description: this.description,
       body: this.body,
       author: this.author,
       date_posted: this.date_posted
     };
     this.__submitToServer(postData);
   },
   __submitToServer(data) {
     axios.post(`${server.baseURL}/blog/post`, data).then(data => {
       router.push({ name: "home" });
     });
   }
 }
};
</script>

在这里,您添加了一个名为 createPost() 的方法来创建新帖子并使用 Axios 将其提交到服务器。 用户创建新帖子后,应用程序将重定向回主页,用户可以在其中查看已创建帖子的列表。

您将在本教程后面配置 vue-router 以实现重定向。

完成编辑后保存并关闭文件。 有关完整的 Create.vue 文件,请访问此应用程序的 DO 社区存储库

现在,您需要创建另一个组件来编辑特定帖子。 导航到 ./src/components/post 文件夹并创建另一个文件并将其命名为 Edit.vue。 将以下包含 <template> 部分的代码添加到其中:

~blog-frontend/src/components/post/Edit.vue

<template>
<div>
      <h4 class="text-center mt-20">
       <small>
         <button class="btn btn-success" v-on:click="navigate()"> View All Posts </button>
       </small>
    </h4>
        <div class="col-md-12 form-wrapper">
          <h2> Edit Post </h2>
          <form id="edit-post-form" @submit.prevent="editPost">
            <div class="form-group col-md-12">
                <label for="title"> Title </label>
                <input type="text" id="title" v-model="post.title" name="title" class="form-control" placeholder="Enter title">
            </div>
            <div class="form-group col-md-12">
                <label for="description"> Description </label>
                <input type="text" id="description" v-model="post.description" name="description" class="form-control" placeholder="Enter Description">
            </div>
            <div class="form-group col-md-12">
                <label for="body"> Write Content </label>
                <textarea id="body" cols="30" rows="5" v-model="post.body" class="form-control"></textarea>
            </div>
            <div class="form-group col-md-12">
                <label for="author"> Author </label>
                <input type="text" id="author" v-model="post.author" name="author" class="form-control">
            </div>

            <div class="form-group col-md-4 pull-right">
                <button class="btn btn-success" type="submit"> Edit Post </button>
            </div>
          </form>
        </div>
    </div>
</template>

此模板部分包含与 CreatePost() 组件类似的内容; 唯一的区别是它包含需要编辑的特定帖子的详细信息。

接下来,在 Edit.vue 中的 </template> 部分之后直接添加 <script> 部分:

~blog-frontend/src/components/post/Edit.vue

...
<script>
import { server } from "../../utils/helper";
import axios from "axios";
import router from "../../router";
export default {
  data() {
    return {
      id: 0,
      post: {}
    };
  },
  created() {
    this.id = this.$route.params.id;
    this.getPost();
  },
  methods: {
    editPost() {
      let postData = {
        title: this.post.title,
        description: this.post.description,
        body: this.post.body,
        author: this.post.author,
        date_posted: this.post.date_posted
      };

      axios
        .put(`${server.baseURL}/blog/edit?postID=${this.id}`, postData)
        .then(data => {
          router.push({ name: "home" });
        });
    },
    getPost() {
      axios
        .get(`${server.baseURL}/blog/post/${this.id}`)
        .then(data => (this.post = data.data));
    },
    navigate() {
      router.go(-1);
    }
  }
};
</script>

在这里,您获得了路由参数 id 来识别特定的帖子。 然后,您创建了一个名为 getPost() 的方法来从数据库中检索此帖子的详细信息并使用它更新页面。 最后,您创建了一个 editPost() 方法,通过 PUT HTTP 请求将编辑后的帖子提交回后端服务器。

保存并退出文件。 有关完整的 Edit.vue 文件,请访问此应用程序的 DO 社区存储库

现在,您将在 ./src/components/post 文件夹中创建一个新组件并将其命名为 Post.vue。 这将允许您从主页查看特定帖子的详细信息。 在Post.vue中添加如下内容:

~blog-frontend/src/components/post/Post.vue

<template>
    <div class="text-center">
        <div class="col-sm-12">
      <h4 style="margin-top: 30px;"><small><button class="btn btn-success" v-on:click="navigate()"> View All Posts </button></small></h4>
      <hr>
      <h2>{{ post.title }}</h2>
      <h5><span class="glyphicon glyphicon-time"></span> Post by {{post.author}}, {{post.date_posted}}.</h5>
      <p> {{ post.body }} </p>

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

此代码呈现帖子的详细信息,其中包括 titleauthor 和帖子 body

现在,直接在 </template> 之后,将以下代码添加到文件中:

~blog-frontend/src/components/post/Post.vue

...
<script>
import { server } from "../../utils/helper";
import axios from "axios";
import router from "../../router";
export default {
  data() {
    return {
      id: 0,
      post: {}
    };
  },
  created() {
    this.id = this.$route.params.id;
    this.getPost();
  },
  methods: {
    getPost() {
      axios
        .get(`${server.baseURL}/blog/post/${this.id}`)
        .then(data => (this.post = data.data));
    },
    navigate() {
      router.go(-1);
    }
  }
};
</script>

与编辑帖子组件的 <script> 部分类似,您获得了路由参数 id 并使用它来检索特定帖子的详细信息。

添加完内容后保存并关闭文件。 有关完整的 Post.vue 文件,请访问此应用程序的 DO 社区存储库

接下来,为了向用户显示所有创建的帖子,您将创建一个新组件。 如果您导航到 src/views 中的 views 文件夹,您将看到一个 Home.vue 组件 - 如果此文件不存在,请使用您的文本编辑器创建它,添加以下内容代码:

~blog-frontend/src/views/Home.vue

<template>
    <div>

      <div class="text-center">
        <h1>Nest Blog Tutorial</h1>
       <p> This is the description of the blog built with Nest.js, Vue.js and MongoDB</p>

       <div v-if="posts.length === 0">
            <h2> No post found at the moment </h2>
        </div>
      </div>

        <div class="row">
           <div class="col-md-4" v-for="post in posts" :key="post._id">
              <div class="card mb-4 shadow-sm">
                <div class="card-body">
                   <h2 class="card-img-top">{{ post.title }}</h2>
                  <p class="card-text">{{ post.body }}</p>
                  <div class="d-flex justify-content-between align-items-center">
                    <div class="btn-group" style="margin-bottom: 20px;">
                      <router-link :to="{name: 'Post', params: {id: post._id}}" class="btn btn-sm btn-outline-secondary">View Post </router-link>
                       <router-link :to="{name: 'Edit', params: {id: post._id}}" class="btn btn-sm btn-outline-secondary">Edit Post </router-link>
                       <button class="btn btn-sm btn-outline-secondary" v-on:click="deletePost(post._id)">Delete Post</button>
                    </div>
                  </div>

                  <div class="card-footer">
                    <small class="text-muted">Posted on: {{ post.date_posted}}</small><br/>
                    <small class="text-muted">by: {{ post.author}}</small>
                  </div>

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

在这里,在 <template> 部分中,您使用 <router-link> 通过将 post._id 作为查询参数传递来创建用于编辑和查看帖子的链接。 您还使用 v-if 指令有条件地为用户呈现帖子。 如果数据库中没有帖子,用户只会看到以下文本:目前没有找到帖子

保存并退出文件。 有关完整的 Home.vue 文件,请访问此应用程序的 DO 社区存储库

现在,直接在 Home.vue 中的 </template> 部分之后,添加以下 </script> 部分:

~blog-frontend/src/views/Home.vue

...
<script>
// @ is an alias to /src
import { server } from "@/utils/helper";
import axios from "axios";

export default {
  data() {
    return {
      posts: []
    };
  },
  created() {
    this.fetchPosts();
  },
  methods: {
    fetchPosts() {
      axios
        .get(`${server.baseURL}/blog/posts`)
        .then(data => (this.posts = data.data));
    },
    deletePost(id) {
      axios.delete(`${server.baseURL}/blog/delete?postID=${id}`).then(data => {
        console.log(data);
        window.location.reload();
      });
    }
  }
};
</script>

在此文件的 <script> 部分中,您创建了一个名为 fetchPosts() 的方法来从数据库中获取所有帖子,并使用从服务器返回的数据更新了页面。

现在,您将更新前端应用程序的 App 组件,以创建指向 HomeCreate 组件的链接。 打开 src/App.vue 并使用以下内容更新它:

~blog-frontend/src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/create">Create</router-link>
    </div>
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
#nav {
  padding: 30px;
  text-align: center;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

除了包含 HomeCreate 组件的链接之外,您还包含了 <Style> 部分,它是该组件的样式表,并包含一些样式的定义页面上的元素。 保存并退出文件。

您已在此步骤中为您的应用程序创建了所有必需的组件。 接下来,您将配置路由器文件。

第 9 步 — 设置路由

创建所有必要的可重用组件后,您现在可以通过使用指向您创建的所有组件的链接更新其内容来正确配置路由器文件。 这将确保前端应用程序中的所有端点都映射到特定组件以进行适当的操作。 导航到 ./src/router.js 并将其内容替换为以下内容:

~blog-frontend/src/router.js

import Vue from 'vue'
import Router from 'vue-router'
import HomeComponent from '@/views/Home';
import EditComponent from '@/components/post/Edit';
import CreateComponent from '@/components/post/Create';
import PostComponent from '@/components/post/Post';

Vue.use(Router)

export default new Router({
 mode: 'history',
 routes: [
   { path: '/', redirect: { name: 'home' } },
   { path: '/home', name: 'home', component: HomeComponent },
   { path: '/create', name: 'Create', component: CreateComponent },
   { path: '/edit/:id', name: 'Edit', component: EditComponent },
   { path: '/post/:id', name: 'Post', component: PostComponent }
 ]
});

您从 vue-router 模块导入 Router 并通过传递 moderoutes 参数对其进行实例化。 vue-router 的默认模式是散列模式,它使用 URL 散列来模拟完整的 URL,这样当 URL 发生变化时页面不会重新加载。 为了使散列变得不必要,您在这里使用了历史模式来实现 URL 导航而无需重新加载页面。 最后,在 routes 选项中,您指定了端点的路径 — 路由的名称和在应用程序中调用路由时应呈现的组件。 保存并退出文件。

现在您已经设置了到应用程序的路由,您需要包含 Bootstrap 文件以帮助为应用程序的用户界面预先构建样式。 为此,请在文本编辑器中打开 ./public/index.html 文件,并通过将以下内容添加到文件中来包含 Bootstrap 的 CDN 文件:

~blog-frontend/public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  ...
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
  <title>blog-frontend</title>
</head>
<body>
   ...
</body>
</html>

保存并退出文件,然后使用 npm run serve 为您的 blog-frontend 重新启动应用程序(如果当前未运行)。

注意:确保后端服务器和MongoDB实例都在运行。 否则,从另一个终端导航到 blog-backend 并运行 npm run start。 此外,通过从新终端运行 sudo mongod 来启动 MongoDB 服务。


导航到您的应用程序:http://localhost:8080。 现在您可以通过创建和编辑帖子来测试您的博客。

单击应用程序上的 Create 以查看 Create Post 屏幕,该屏幕与 CreateComponent 文件相关并呈现。 在输入字段中输入值,然后单击 创建帖子 按钮提交帖子。 完成后,应用程序会将您重定向回主页。

应用程序的主页呈现 HomeComponent。 这个组件有一个方法,它发送一个 HTTP 调用以从数据库中获取所有帖子并将它们显示给用户。

单击特定帖子的编辑帖子按钮将带您进入编辑页面,您可以在其中合并任何更改并保存您的帖子。

在本节中,您为应用程序配置和设置了路由。 有了这些,您的博客应用程序就准备好了。

结论

在本教程中,您探索了一种使用 Nest.js 构建 Node.js 应用程序的新方法。 您使用 Nest.js 创建了一个简单的博客应用程序来构建后端 RESTful API,并使用 Vue.js 来处理所有前端逻辑。 此外,您还将 MongoDB 集成为 Nest.js 应用程序的数据库。

要了解有关如何向应用程序添加身份验证的更多信息,您可以使用 Passport.js,一个流行的 Node.js 身份验证库。 您可以在 Nest.js 文档 中了解 Passport.js 集成。

您可以在 GitHub 上找到此项目 的完整源代码。 关于 Nest.js 的更多信息,可以访问【X54X】官方文档【X80X】。