介绍
nest.js是一个高效,可扩展的node.js服务器端web框架,node.js提供了一个灵活的运行时环境,而nest.js提供了更高层次的组织架构。
特点:
支持原生typescript的框架;
可以基于express也可以选择fastify(快,更高效),也可以用express直接访问其api
项目入门
-
项目创建(CLI)
npm i -g nestjs/cli
nest new pro-name
// 运行:
npm run start:dev
-
各文件解释:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule); // 创建nestjs app
app.setGlobalPrefix('api'); //设置全局路由前缀: 127.0.0.0:8002/api/...
await app.listen(8002); //开启服务器,开始监听
}
bootstrap();
// app.moudle.ts
// 应用程序的根模块
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';
/* 装饰器Module接收四个属性:
providers: 服务提供者,srevices
controllers: 处理http,将请求委托为providers
imports: 其他模块的服务导入
exports: 导出服务的列表,供其他模块导入使用
*/
@Module({
imports: [PostsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// app.controller.ts
import { Controller, Get, Post, Put } from '@nestjs/common';
import { AppService } from './app.service';
/*
@Controller: 定义控制器
参数:传入'app',则处理路径/app/下的请求。路由控制,表示该控制器的主路径
*/
@Controller('app')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Post('list')
create() {}
@Get('user_*')
getUser() {
return 'getUser';
}
@Put('list/:id')
update() {
return 'update';
}
@Put('list/user')
updateUser() {
return 'updateUser';
}
// 若访问127.0.0.0.1:8002/api/app/list/user不会被调用,路径‘list/:id’已经满足,不会接着向下匹配,直接调用update()
}
/*
http方法处理装饰器:
@Get, @Post, @Put,由这些装饰器处理,方法可以响应对应的http请求。
参数接收一个字符串,字符串数组(字符串:路径/通配符): 路由装饰器
*/
// app.service.ts
import { Injectable } from '@nestjs/common';
// 使用Injectable()装饰器后,直接引用,无需实例化
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
-
添加新模块
// 命令: nest g [文件类型] [文件名] [文件目录] 目录不写,默认创建和文件名一样的文件夹
// 在项目根目录下打开终端依次运行以下命令
nest g mo posts
nest g co posts
nest g service posts
注意: 先创建Module
, 再创建Controller
和Service
, 这样创建出来的文件在Module
中自动注册,反之,后创建Module, Controller
和Service
,会被注册到外层的app.module.ts
当前目录结构:
-
连接mysql
1. (安装mysql+navicat premium) → 新建连接:连接名115 → 新建数据库:数据库名blog
2. TypeORM连接数据库
什么是ORM? :(object-relational mapping)对象关系映射 数据库表是一个二维表,如表所示,将它转换为一个js对象: { id: 1, title: "nestjs", content: "文章描述" } (使得读写都是js对象)
使用typeORM操作数据库,首先安装依赖包:
npm install @nestjs/typeorm typeorm mysql2 -S
官方提供的连接数据库地方法有两种,这里仅说明一种
在根目录下新建文件 .env 和 .env.prod:
// .env
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=root
DB_DATABASE=blog
SALT= NestProject
SECRET=test123456
APPID=sssss
APPSECRET= xxxx
// .env.prod
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWD=root
DB_DATABASE=blog
在根目录下创建文件夹config/env.ts
// env.ts
import * as fs from 'fs';
import * as path from 'path';
// const dotenv = require('dotenv');
const isProd = process.env.NODE_ENV === 'production';
function parseEnv() {
const localEnv = path.resolve('.env');
const prodEnv = path.resolve('.env.prod');
if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
throw new Error('缺少环境配置文件');
}
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
return { path: filePath };
}
export default parseEnv();
在app.module.js中添加如下内容,即可成功连接数据库
...
...
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: [envConfig.path] }),
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST', '127.0.0.0'),
port: configService.get('DB_PORT', 3306),
username: configService.get('DB_USER', 'root'),
password: configService.get('DB_PASSWORD', 'root'),
database: configService.get('DB_DATABASE', 'blog'),
// charset: 'utf8mb4',
timezone: '+08:00',
synchronize: false,
autoLoadEntities: true,
}),
}),
PostsModule,
],
...
..
3. CRUD(create, read, update, delete)增删改查
TypeORM是通过实体映射到数据库表,因此接下来我们先建立一个实体PostsEntity,在posts目录下新建文件posts.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
// 表posts对应的实体:PostEntity
@Entity('posts')
export class PostEntity {
@PrimaryGeneratedColumn()
id: number; // 标记为主列,值自动生成
@Column({ length: 50 })
title: string;
@Column({ length: 20 })
author: string;
@Column('text')
content: string; // 列类型为text
@Column({ default: '' })
thumb_url: string;
@Column('tinyint')
type: number;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
create_time: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
update_time: Date;
}
4. 接下来,创建完实体,在posts.service.ts下实现CRUD操作
import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PostsEntity } from './posts.entity';
import { Repository, getRepository } from 'typeorm';
export interface PostRo {
list: PostsEntity[];
count: number;
}
@Injectable()
export class PostsService {
constructor(
@InjectRepository(PostsEntity)
private readonly postsRepository: Repository,
) {}
// 创建文章
async create(post: Partial): Promise {
const { title } = post;
if (!title) {
throw new HttpException('缺少文章标题', 401);
}
const doc = await this.postsRepository.findOne({ where: { title } });
if (doc) {
throw new HttpException('文章已存在', 401);
}
return await this.postsRepository.save(post);
}
// 获取文章列表
async findAll(query): Promise {
const qb = await getRepository(PostsEntity).createQueryBuilder('post');
qb.where('1 = 1'); //没有意义,占位符,允许后续添加更多条件,一般用于动态查询
qb.orderBy('post.create_time', 'DESC'); //时间降序
const count = await qb.getCount();
const { pageNum = 1, pageSize = 10 } = query;
qb.limit(pageSize);
qb.offset(pageSize * (pageNum - 1)); // 实现分页
const posts = await qb.getMany();
return { list: posts, count: count };
}
// 获取指定文章
async findById(id): Promise {
return this.postsRepository.findOne({ where: { id } });
}
// 更新文章
async updateById(id, post): Promise {
const existPost = await this.postsRepository.findOne({ where: { id } });
if (!existPost) {
throw new HttpException(`id为${id}的文章不存在`, 401);
}
const updatePost = this.postsRepository.merge(existPost, post);
return this.postsRepository.save(updatePost);
}
//删除文章
async remove(id) {
const existPost = await this.postsRepository.findOne({ where: { id } });
if (!existPost) {
throw new HttpException(`id为${id}的文章不存在`, 401);
}
return await this.postsRepository.remove(id);
}
}
-
接口格式统一
一般不会根据http状态码来判断接口的成功或失败,而是根据请求返回的数据中的code字段,例如:
{
"code": 0,
"message": "OK",
"data": {}
}
//失败:
{
"code": -1,
"message": "error msg",
"data": {}
}
- 拦截错误请求
主要步骤是:创建过滤器 → 实现过滤器代码 → 注册
1. 创建过滤器: nest g filter core/filter/http-exception
2. 实现过滤器代码:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); //获取请求上下文
const response = ctx.getResponse(); //请求上下文中的response对象
const status = exception.getStatus(); //获取请求的异常状态码
const message = exception.message
? exception.message
: `${status >= 500 ? 'service error' : 'client error'}`;
const errorResponse = {
data: {},
message: message,
code: -1,
};
response.status(status);
response.Header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
3. 注册:在main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); //设置全局路由前缀: 127.0.0.0:8002/api/...
app.useGlobalFilters(new HttpExceptionFilter()); //设置全局错误的过滤器
await app.listen(8002);
}
bootstrap();
- 拦截成功的返回数据
主要步骤是:创建拦截器 → 实现拦截器代码 → 注册
1. 创建拦截器
nest g interceptor core/interceptor/transform
2. 实现拦截器代码
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
// next: 处理程序调用器
// next.handle() 用来调用下一个程序,返回一个Observable,该应用程序的响应流
// pipe()是Rxjs中用于组合多个操作符的函数。在这里允许对Observable的输出进行一列转换和操作
// map() 是Rxjs中的一个操作符,用来对Observable发出的每一个值应用一个函数,并返回一个新的Observable。这里用于将原始响应数据转换为一个新的对象。
return next.handle().pipe(
map((data) => {
return {
data,
code: 0,
msg: '请求成功',
};
}),
);
}
}
3. 注册:在mian.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); //设置全局路由前缀: 127.0.0.0:8002/api/...
app.useGlobalFilters(new HttpExceptionFilter()); //设置全局错误的过滤器
app.useGlobalInterceptors(new TransformInterceptor()); //设置全局拦截器(作用于所有的请求和响应)
await app.listen(8002);
}
bootstrap();
-
配置接口文档Swagger
- 配置
1. 安装: npm install @nestjs/swagger swagger-ui-express -S
2. 设置:在main.ts中设置Swagger文档信息
...
...
// 设置swagger文档
const config = new DocumentBuilder()
.setTitle('管理后台')
.setDescription('管理后台接口文档')
.setVersion('1.0')
.addBearerAuth() // 添加 Bearer 认证的支持,当API 需要进行用户身份验证时,可以使用该方法来确保用户能够在 Swagger UI 中输入他们的 Bearer token,从而访问受保护的资源。
.build();
const document = SwaggerModule.createDocument(app, config); //生成swagger文档对象
SwaggerModule.setup('docs', app, document); // 设置swagger ui为可访问状态,‘docs’指定路径
await app.listen(8002);
3. 访问 http://localhost:8002/docs可以看到生成的文档
- 接口设置
1. 接口标签
接口根据controller来分类,在controller上添加@ApiTags即可
@ApiTags('文章')
@Controller('post')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
...
...
}
2. 接口描述
为每一个接口添加文字说明,使用@ApiOperation装饰器
@ApiTags('文章')
@Controller('post')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@ApiOperation({ summary: '创建文章' })
@Post()
async create(@Body() post: CreatePostDto) {
// @Body()提取请求体数据post
return await this.postsService.create(post);
}
...
...
}
3. 接口传参
在posts下创建文件夹dto,然后新建文件create-post.dot.ts文件:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreatePostDto {
@ApiProperty({ description: '文章标题' })
readonly title: string;
@ApiPropertyOptional({ description: '内容' })
readonly content: string;
@ApiPropertyOptional({ description: '文章封面' })
readonly coverUrl: string;
@ApiPropertyOptional({ description: '文章状态' })
readonly status: string;
@ApiProperty({ description: '文章分类' })
readonly category: number;
@ApiPropertyOptional({ description: '是否推荐' })
readonly isRecommend: boolean;
@ApiPropertyOptional({ description: '文章标签' })
readonly tag: string;
}
最终效果如下:
-
数据验证
数据验证是指对请求接口的入参数数据进行验证和转换的前置操作,通过验证之后才会将请求内容给到对应的路由方法中去,否则进入异常过滤器。
实现:nest.js中的管道就是用来数据转换和验证的,这也是管道的两种类型。
- 转换:将输入数据转换为所需的数据输出。(ParseIntPipe和ParseUUIDPipe)
- 验证:对输入数据进行验证。若验证通过则继续传递;验证失败则抛出异常,由当前上下文的异常过滤器处理。pipe管道中发生异常,controller不会继续执行任何方法。(ValidationPipe + class-validator)
1. 首先安装依赖
npm install class-validator class-transformer -S
2. 在create-post.dto.ts中添加验证
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber } from 'class-validator';
export class CreatePostDto {
@ApiProperty({ description: '文章标题' })
@IsNotEmpty({ message: '文章标题必填' }) //数据验证
readonly title: string;
@ApiPropertyOptional({ description: '内容' })
readonly content: string;
@ApiPropertyOptional({ description: '文章封面' })
readonly coverUrl: string;
@ApiPropertyOptional({ description: '文章状态' })
readonly status: string;
@IsNumber() //数据验证
@ApiProperty({ description: '文章分类' })
readonly category: number;
@ApiPropertyOptional({ description: '是否推荐' })
readonly isRecommend: boolean;
@ApiPropertyOptional({ description: '文章标签' })
readonly tag: string;
}
3. 注册
app.useGlobalPipes(new ValidationPipe()); //注册数据验证管道
概念理解
- modules
模块是具有@Module()装饰器的类,该装饰器提供了元数据,nest使用它来组织应用程序架构。每个nest应用程序至少有一个模块,即根模块,它是nest开始构建应用程序树的地方。@Module()装饰器接受一个描述模块属性的对象:
共享模块:若想共享某个servie: 则需要放到export数组中:(服务的进出都需要通过module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService] //需要共享的模块
})
export class CatsModule {}
全局模块: @Global,只注册一次,一般为根模块或核心模块。全局模块不需要在imports数中导入。但是,将所有内容全局化并不是一个好的策略
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
动态模块:DynamicModule,可以创建自定义模块,例如下面的例子:forRoot静态方法允许在模块导入时传递entities(数据库相关的实体),以及其他配置。这是nest.js一个强大的功能
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
// 在其他模块调用:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule],
})
export class AppModule {}
- controllers:
控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作。
- providers:
直观来看,providers只是一个被@Injectable()装饰器注释的类,许多基本的类都可以被视为nest providers,一般遵循SOLID原则。调用时直接在constructor中注入依赖。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
//依赖注入
constructor(private readonly catsService: CatsService) {}
}
生命周期一般与应用程序的生命周期同步,启动时,解析每个service,关闭时,销毁每个providers。
可选providers: 注入依赖'HTTP_OPTIONS'到httpClient中,如果不存在则为undefined
contructor(
@Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
) {}