背景
全栈
全栈其实可以说是一种野心,发展全栈对公司和个人都大有益处,但最重要的还是对个人的发展如何。个人发展全栈主要还是为了应对未来,主要有几方面: 避免被淘汰、 提升个人价值、 走向管理职位、 实现创业梦。
从个人为出发点,后端语言百花齐放,想要到一个比较深入的程度,那对所使用的开发语言、框架就需要稍微深入了。例如同时要深入typescript、Java的领域,那就会谈到精力问题,对个人来说似乎会很累,不太现实。
NestJS
根据官网介绍:
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
简单来说就是:
优势
NestJS 的一些优势包括:
- 构建在现代 JavaScript 栈之上,因此使用了最新的 JavaScript 技术。
- 基于 Angular 的架构和语法,提供了强大的模块化系统和依赖注入功能。
- 基于 TypeScript,提供了强类型和静态类型检查。
- 提供了丰富的工具和模块,可用于构建各种类型的服务器端应用程序,包括 RESTful API、GraphQL API、WebSocket 服务器等。
- 提供了一组可扩展的构建块,可用于快速构建应用程序。
- 提供了与主流数据库和身份验证系统的集成。
对于前端人员来说,“一招Typescript”吃遍前后端,何乐而不为。
项目搭建
项目初始化
确保已经安装了 Node.js(版本 >= 12,v13 除外)
$ npm i -g @nestjs/cli
$ nest new project-name
运行项目
启动 HTTP 服务监听定义在 src/main.ts 文件中定义的端口号。在浏览器访问[http://localhost:3000](http://localhost:3000/)/
,访问成功代表服务已经OK了;
# dev为热更新模式
$ npm run start:dev
框架约束
NestJS文件约束:
- 每个模块最少有三种文件组合:module、server、controller。
- NestJS允许命令行创建不生成
.spec
(nest g controller product --no-spec
)。
初始化后项目的主要目录结构:
这里涉及到NestJs框架的文件约束:
app.controller.ts | 单个路由的基本控制器(Controller) |
---|---|
app.controller.spec.ts | 针对控制器的单元测试 |
app.module.ts | 应用程序的根模块(Module) |
app.service.ts | 具有单一方法的基本服务(Service) |
main.ts | 应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。 |
在main.ts
中使用工厂函数NestFactory
创建一个 AppModule
应用实例,启动 HTTP 监听器,等待 HTTP 请求。
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
// <NestExpressApplication> | <NestFastifyApplication> | <>
// 一般来说我们选择 NestExpress,因为网上资源多,官方文档有很多例子都是基于Express
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 设置全局路由前缀
// app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();
可选平台Express
是一个众所周知的 node.js 简约 Web 框架。 这是一个经过实战考验,适用于生产的库,拥有大量社区资源。 默认情况下使用 @nestjs/platform-express 包。 许多用户都可以使用 Express ,并且无需采取任何操作即可启用它。
Fastify
是一个高性能,低开销的框架,专注于提供最高的效率和速度。 在这里
阅读如何使用它。
核心概念
模块
模块是用
@Module()
装饰的类。@Module()
装饰器提供了元数据,Nest 用它来组织应用程序结构。
模块主要分4个类别:
- 功能模块
- 共享模块
- 全局模块
- 动态模块
main.ts
只引入了app.module.ts
文件,此为应用程序的根模块(每个 Nest 应用程序至少有一个模块):
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
对装饰器不了解的,可以看走近MidwayJS:初识TS装饰器与IoC机制
@Module()
装饰器接收四个属性:providers
、controllers
、imports
、exports
。
providers |
Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享; |
---|---|
controllers |
处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理; |
imports |
导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入; |
exports |
导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出; |
下面通过一个产品的CRUD例子来进行演示:
NestJs中,具有同一应用程序域的文件英国放到同一个功能模块下:
首先我们可以通过nest-cli提供的命令生成模块:
nest g module products
import { Module } from '@nestjs/common';
@Module({})
export class ProductsModule {}
同时 ProductsModule 也会被自动注册到根模块:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductsModule } from './products/products.module';
@Module({
imports: [ProductsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
这样便完成了一个新的功能模块的注册。
共享模块在 Nest 中,默认情况下,模块是单例的,因此我们可以轻松地在多个模块之间共享同一个提供者实例。
模块本身存在共享性,一个模块一旦创建便可以被任意模块重复使用。例如:
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
// 导出模块供其他模块倒入,所有模块共享catsService实例
exports: [CatsService]
})
export class CatsModule {}
全局模块如果想提供一些通用的模块,如:helper,数据库连接等等,可以创建全局模块,也比较简单,增加@Global
即可,全局模块应该只注册一次,最好由根或核心模块注册。
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 {}
动态模块动态模块在 Nest 中也是非常重要的。此功能使您可以轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。例如:
注册动态模块:DatabaseModule
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])],
})
export class AppModule {}
控制器
同样的,通过nest-cli提供的命令生成控制器:
nest g controller products
在Nest中,使用装饰器
@Controller()
定义一个基本的控制器,该装饰器可以传入一个路径参数
import { Controller } from '@nestjs/common';
@Controller('products')
export class ProductsController {}
同时在 products.module.ts
文件中会自动注册 ProductsController。
服务
一样的操作,使用命令生成
nest g service products
其实可以通过
nest g resource products
一次性生成。
更多创建命令可以通过nest generate --help
查看。
import { Injectable } from '@nestjs/common';
@Injectable()
export class ProductsService {}
此时的 products.module.ts 文件
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}
到目前为止,已经完成了 Products
功能模块的搭建,在增加业务逻辑之前,先来了解一下 NestJS 如何接入数据库。
数据库
这里以TypeORM框架为例与MySQL数据库进行交互。
ORM技术(Object-Relational Mapping),把关系数据库的变结构映射到对象上。
安装依赖:
npm install @nestjs/typeorm typeorm mysql2 -S
在 app.module.ts 中注册:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductsModule } from './products/products.module';
@Module({
imports: [
ProductsModule,
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'store',
autoLoadEntities: true, // 自动加载实体
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
这是最简单的配置方式,当然 **TypeORM **也非常方便地支持:
- 存储库模式:每个实体都有自己的存储库Entity。
- 实体关系:一对一
@OneToOne()
,一对多@OneToMany()
和@ManyToOne()
,多对多@ManyToMany()
- 自动载入实体
- 事务
- 多数据库
- 异步配置
- 等等
详细可参考官方文档。
对了,不忘创建一下 Products 的实体类;
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('products')
export class ProductsEntity {
@PrimaryGeneratedColumn()
id: number; // 标记为主列,值自动生成
@Column({ length: 50 })
title: string;
@Column('text')
desc: string;
@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;
}
完成数据库的打通后,开始增加业务逻辑代码。
HTTP装饰器
HTTP 装饰器使用起来同样非常简单;
Nest 为所有标准的 HTTP 方法提供了相应的装饰器:
@Get()
- @Post( )
@Put()
@Delete()
@Patch()
@Options()
@Head()
-
@All()
用于定义一个用于处理所有 HTTP 请求方法的处理程序。
经过它们装饰的方法,可以对相应的HTTP请求进行响应。
同时它们可以接受一个字符串或一个字符串数组作为参数,这里的字符串可以是固定的路径,也可以是通配符。
import { Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateProductDto } from './dto/create-product.dto';
import { ProductsService } from './products.service';
@ApiTags('产品')
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@ApiOperation({ summary: '创建产品' })
@Post()
async create(@Body() product: CreateProductDto) {
return await this.productsService.create(product);
}
@Get()
async findAll(): Promise<ProductsEntity[]> {
return await this.productsService.findAll();
}
@Get(':id')
async findById(@Param('id') id) {
return await this.productsService.findById(id);
}
@Put(':id')
async update(@Param('id') id, @Body() product) {
return await this.productsService.updateById(id, product);
}
@Delete('id')
async remove(@Param('id') id) {
return await this.productsService.remove(id);
}
}
@Req()、@Res()
Nest 提供了对底层平台(默认为 Express)的请求对象(request)的访问方式。我们可以在待处理函数的签名中增加@Req()
装饰器,Nest 则会自动注入请求对象。
针对HTTP请求,Nest 提供了开箱即用的专有装饰器,如下
装饰器 | 对应参数 |
---|---|
@Request(),@Req() | req |
@Response(),@Res()* | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params/req.params[key] |
@Body(key?: string) | req.body/req.body[key] |
@Query(key?: string) | req.query/req.query[key] |
@Headers(name?: string) | req.headers/req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
另外,Nest还支持较多的装饰器,例如
-
@HttpCode(statusCode)
修改响应状态码 -
@Header(key, value)
自定义响应头 -
@Redirect(url, statusCode|302)
设置重定向
详细可参考官方文档查阅。
Repository
上面介绍了 TypeORM 以及 Entity,这里具体化一下 service;
import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProductsEntity } from './products.entity';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(ProductsEntity)
private readonly productsRepository: Repository<ProductsEntity>,
) {}
// 创建产品
async create(product: Partial<ProductsEntity>): Promise<ProductsEntity> {
const { title } = product;
if (!title) {
throw new HttpException('缺少产品标题', 401);
}
const doc = await this.productsRepository.findOne({ where: { title } });
if (doc) {
throw new HttpException('产品已存在', 401);
}
return await this.productsRepository.save(product);
}
// 获取指定产品
async findById(id): Promise<ProductsEntity> {
return await this.productsRepository.findOneBy({ id: id });
}
// 更新产品
async updateById(id, product): Promise<ProductsEntity> {
const existProduct = await this.productsRepository.findOne(id);
if (!existProduct) {
throw new HttpException(`id为${id}的产品不存在`, 401);
}
const updateProduct = this.productsRepository.merge(existProduct, product);
return this.productsRepository.save(updateProduct);
}
// 刪除产品
async remove(id) {
const existProduct = await this.productsRepository.findOne(id);
if (!existProduct) {
throw new HttpException(`id为${id}的产品不存在`, 401);
}
return await this.productsRepository.remove(existProduct);
}
// 获取产品列表
async findAll(): Promise<ProductsEntity[]> {
return this.productsRepository.find();
}
}
自此一个完成的请求就已经完成了,但为了接口更加规范,还可以再进一步完善;
过滤器
过滤器也是 NestJS 的众多provider之一;这里用过滤器来简单处理错误请求;
nest g filter core/filter/http-exception
代码实现:
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);
}
}
在main.js中注册:
....
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 注册全局错误的过滤器
app.useGlobalFilters(new HttpExceptionFilter());
...
await app.listen(3000);
}
bootstrap();
接下来对请求成功返回的格式进行统一的处理,可以用Nest.js的拦截器来实现。
拦截器
拦截器可以在调用路由处理程序之前和之后访问响应/请求。比如 logger 或者 统一处理响应结果;
老配方,首先使用命令创建一个拦截器:
nest g interceptor core/interceptor/transform
代码实现
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: 200,
msg: '请求成功',
};
}),
);
}
}
注册到main.js
...
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
...
// 全局注册拦截器
app.useGlobalInterceptors(new TransformInterceptor());
...
await app.listen(3000);
}
bootstrap();
接口文档
规范化接口当然少不了接口文档,Nest也是完美支持 swagge,在product
目录下创建一个dto文件夹,再创建一个create-post.dot.ts
文件:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateProductDto {
@ApiProperty({ description: '产品标题' })
readonly title: string;
@ApiProperty({ description: '产品描述' })
readonly desc: string;
@ApiPropertyOptional({ description: '产品缩略图' })
readonly thumb_url: string;
@ApiProperty({ description: '产品类型' })
readonly type: number;
}
完善controller,进行说明:
...
@ApiTags('产品')
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@ApiOperation({ summary: '创建产品' })
@Post()
async create(@Body() product: CreateProductDto) {
return await this.productsService.create(product);
}
...
}
数据验证
在Nest.js中,管道(Pipes)就是专门用来做数据转换的,我们看一下它的定义:
管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。管道有两个类型:
- 转换:管道将输入数据转换为所需的数据输出
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;
管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 异常过滤器 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。
通俗来讲就是,对请求接口的入参进行验证和转换的前置操作,验证好了我才会将内容给到路由对应的方法中去,失败了就进入异常过滤器中。
安装依赖:
npm install class-validator class-transformer -S
Nest.js自带了三个开箱即用的管道:
- ValidationPipe
- ParseIntPipe
- ParseUUIDPipe,
ValidationPipe 配合 class-validator 就可以完美的实现对参数类型进行验证,验证失败抛出异常
管道验证操作通常用在dto这种传输层的文件中,用作验证操作。安装两个需要的依赖包:class-transformer
和class-validator
npm install class-validator class-transformer -S
然后在create-post.dto.ts文件中添加验证, 完善错误信息提示:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber } from 'class-validator';
export class CreateProductDto {
@IsNotEmpty({ message: '产品标题必填' })
@ApiProperty({ description: '产品标题' })
readonly title: string;
@IsNotEmpty({ message: '缺少产品描述' })
@ApiProperty({ description: '产品描述' })
readonly desc: string;
@ApiPropertyOptional({ description: '产品缩略图' })
readonly thumb_url: string;
@IsNumber()
@ApiProperty({ description: '产品类型' })
readonly type: number;
}
上面只编写了一些常用的验证,class-validator还提供了很多的验证方法, 感兴趣可以看官方文档.
最后我们还有一个重要的步骤, 就是在main.ts中全局注册一下管道ValidationPipe:
...
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
...
// 注册校验管道
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();