start
正常来说,我们一般是在 controller 中书写我们的处理请求代码,但是在 Nest 中 controller 不能写处理请求的代码,而是在 service 中书写我们的代码,故 controller 中只负责调用对应的 service 中的处理函数(有点像路由层~)
# 创建项目
nest new project-name
cd nest
yarn run start
初始化文件说明
src
├── app.controller.spec.ts // app.controller.ts 的测试文件
├── app.controller.ts // 描述路由过程的文件
├── app.module.ts // 通过导入要在项目中使用的模块来构建应用程序
├── app.service.ts // app.controller.ts 中使用的业务逻辑
└── main.ts // 首次执行代码的入口
nest 以模块为单位管理每个功能。
hot load(webpack hmr)
引入 Webpack 相关依赖
yarn add -D webpack-node-externals run-script-webpack-plugin webpack
创建 webpack.config.js
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
module.exports = function (options, webpack) {
return {
...options,
entry: ['webpack/hot/poll?100', options.entry],
externals: [
nodeExternals({
allowlist: ['webpack/hot/poll?100'],
}),
],
plugins: [
...options.plugins,
new webpack.HotModuleReplacementPlugin(),
new webpack.WatchIgnorePlugin({
paths: [/\.js$/, /\.d\.ts$/],
}),
new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
],
};
};
将脚本添加到 package.json
中
{
"start:dev": "webpack --config webpack.config.js --watch",
}
最后,在 main
中添加 webpack 相关命令
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
declare const module: any;
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
集成 swagger
引入依赖
yarn add @nestjs/swagger
修改主入口文件
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle("Swagger example")
.setDescription("The swagger API description")
.setVersion("1.0.0")
.addTag("example")
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("docs", app, document);
await app.listen(3000);
}
bootstrap();
这样可以算是基础的集成了 Swagger,但是要想有更清晰明了的展示方式还需要对 controller
、dto
等进行一些数据声明
controller:
// app.controller
import { CreateUserDto } from "@/dto/app.dto";
import { ApiTags, ApiOperation } from "@nestjs/swagger";
@ApiTags("示例")
@Controller("/example")
export class AppController {
constructor(private readonly appService: AppService) {}
@ApiOperation({ summary: "示例请求" })
@Get()
getHello(@I18nLang() lang: string): string {
return this.appService.getHello(lang);
}
@ApiOperation({ summary: "创建用户" })
@Post("create")
@UseInterceptors(FileInterceptor("file"))
createUser(@Body() userData: CreateUserDto) {
return this.appService.createUser(userData);
}
}
dto:
// app.dto
import { ApiProperty } from "@nestjs/swagger";
export class CreateUserDto {
@ApiProperty({ description: "用户名" })
readonly name: string;
@ApiProperty({ description: "性别" })
readonly gender?: string;
@ApiProperty({ description: "密码" })
readonly pwd: string;
}
现在 swagger 文档最很清晰明了了
使用 Prisma(ora) 进行数据库管理
yarn add prisma -D
# 初始化
yarn prisma init
连接 mysql
datasource db {
// 改成 mysql
provider = "mysql"
url = env("DATABASE_URL")
}
进入 .env
文件
# 初始化
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
# 修改为(大写部分需要替换成本地的数据库)
mysql://USER:PASSWORD@HOST:PORT/DATABASE
验证是否配置成功
yarn prisma validate
生成 SQL 迁移文件并针对数据库运行它们
# init 为迁移名
yarn prisma migrate dev --name init
安装并生成 Prisma 客户端。
在安装过程中,Prisma 会自动为您调用 prisma generate
命令
在每次更改 Prisma 模型后运行此命令来更新生成的 Prisma 客户端。
yarn add @prisma/client
# generate
yarn prisma generate
在 controller
中使用
// 基础使用
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
这里我们将 Prisma 引入 nest
中
// src/db/mysql.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
// OnModuleInit 是可选的 - 如果省略它,Prisma 将在第一次调用数据库时延迟连接
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect()
.then(() => {
console.log("[MYSQL]连接成功...");
})
.catch((err) => {
console.log("[MYSQL]连接失败..."), err);
});
}
}
// 延迟连接
@Injectable()
export class PrismaService extends PrismaClient {
async onModuleInit() {
await this.$connect()
.then(() => {
console.log("[MYSQL]连接成功...");
})
.catch((err) => {
console.log("[MYSQL]连接失败..."), err);
});
}
}
配置路径别名
使用 module-alias
进行配置
# 引入库
yarn add module-alias
对 package.json
进行配置
{
"_moduleAliases": {
"@": "dist"
}
}
这里的 src
需要修改为打包后的目录,比如 dist
然后在入口文件中引入这个库
import "module-alias/register";
到这里我们的路径别名已经配置完成了,但是如果使用的是 ts,还需要在 tsconfig.json
文件中配置
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
loggerMiddler
局部中间件
首先,在 common/middleware
中新建 logger.middler.ts
文件
这里先使用 class 方法实现简单的中间件功能
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { cyan } from "kolorist";
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
await next();
const ms = Date.now() - start;
// example: GET //v1/example/find 200 0ms
console.log(
cyan(`${req.method} ${req.baseUrl}/${req.url} ${res.statusCode} ${ms}ms`),
);
}
}
接着,我们在 app.module
中应用中间件
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes("example");
}
}
到此,我们的局部中间件已经完成了。当然,由于这个中间件功能比较简单(它没有成员,没有其他方法,也没有依赖项),
可以使用简单的函数实现–功能性中间件
import { Request, Response, NextFunction } from "express";
import { cyan } from "kolorist";
export async function LoggerMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const start = Date.now();
await next();
const ms = Date.now() - start;
// example: GET //v1/example/find 200 0ms
console.log(
cyan(`${req.method} ${req.baseUrl}/${req.url} ${res.statusCode} ${ms}ms`),
);
}
全局中间件
要注册中间件,就把上面在 app.module
中应用中间件改为在入口文件中使用
import { LoggerMiddleware } from "@/common/middleware/logger.middleware";
const app = await NestFactory.create(AppModule);
app.use(LoggerMiddleware);
await app.listen(3000);
异常过滤
要使用 filter,我们需要在 common 新建一个 http-exception.filter
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from "@nestjs/common";
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// 获取请求上下文
const ctx = host.switchToHttp();
// 获取请求上下文中的 response 对象
const response = ctx.getResponse();
// 获取异常状态码
const status = exception.getStatus();
const exceptionMsg = exception.getResponse() as any;
const msgLength = exceptionMsg.message.length;
const messageLen = exceptionMsg.message[0].length;
const finalMsg =
msgLength > messageLen ? exceptionMsg.message : exceptionMsg.message[0];
const message = finalMsg
? finalMsg
: `${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);
}
}
在全局注册使用
import { HttpExceptionFilter } from "@/common/filters/http-exception.filter";
app.useGlobalFilters(new HttpExceptionFilter());
守卫
pipe
数据校验比较好的一点就是先对请求的数据进行校验,如果不符规则就直接丢出错误。
这样能够让我们能够更好的专注于逻辑层面,而不是要先对数据进行 if 判断其类型
数据校验主要由 Nest 内置的管道 ValidationPipe 完成
先全局注册管道
yarn add class-validator class-transformer
// main
import { ValidationPipe } from "@nestjs/common";
app.useGlobalPipes(new ValidationPipe());
接着在 controller 中使用,注意我们在这里使用到了 DTO(一种用于描述数据模型和数据传输的对象)
使用 DTO 可以将这些不同的数据格式和方式转换成一种统一的、标准化的方式
import { createUserDto } from "@/common/dto/app.dto";
@Controller("/example")
export class AppController {
constructor(private readonly appService: AppService) {}
@Post("create")
@ApiOperation({ summary: "创建用户" })
@UseInterceptors(FileInterceptor("file"))
createUser(@Body() userData: createUserDto) {
return this.appService.createUser(userData);
}
}
接着,我们来看看那 dto
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, Length } from "class-validator";
export class createUserDto {
@ApiProperty({ description: "用户名" })
@Length(5, 10, {
message: "用户名在 5 个到 10 个之间",
})
readonly name: string;
readonly gender?: string;
@ApiProperty({ description: "密码" })
@IsNotEmpty({ message: "用户密码必填" })
readonly pwd: string;
}
这样就完成了我们的数据校验了
集成 i18n
引入依赖
yarn add nestjs-i18n
添加翻译文件,目录结构如下
src
└── i18n
├── en
│ └── test.json
└── zh
└── test.json
在 app.module
中引入
import { Module } from '@nestjs/common';
import {join} from 'path';
import {
I18nModule,
QueryResolver,
HeaderResolver,
AcceptLanguageResolver,
} from "nestjs-i18n";
@Module({
imports: [
I18nModule.forRoot({
fallbackLanguage: "en",
loaderOptions: {
path: join(__dirname, "/i18n/"),
watch: true,
},
resolvers: [
{ use: QueryResolver, options: ["lang"] },
AcceptLanguageResolver,
new HeaderResolver(["x-lang"]),
],
}),
],
})
接下来就可以使用了
controller
import { Controller, Get } from "@nestjs/common";
import { AppService } from "@/services/app.service";
import { I18nLang } from "nestjs-i18n";
@Controller("/example")
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(@I18nLang() lang: string): string {
// 需要一个语言参数:zh|en
return this.appService.getHello(lang);
}
}
service
import { Injectable } from "@nestjs/common";
import { PrismaService } from "@/db/mysql.service";
import { UserInter } from "@/interface/user.interface";
import { I18nLang, I18nService } from "nestjs-i18n";
@Injectable()
export class AppService {
constructor(
private prisma: PrismaService,
private readonly i18n: I18nService,
) {}
private readonly users: UserInter[];
getHello(@I18nLang() lang: string): string {
return this.i18n.translate("test.Hello", { lang });
}
}
集成了 i18n 后,可以在 apifox
中使用 param(lang)、headers(Accept-Language)返回相应的 i18n 内容
日志
- 基础使用
在 main 中可以对 logger 一些基础的配置,比如是否启用,日志是否需要颜色,日志等级等等
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: true,
// 是否禁用日志记录,以及启用等级
// logger: false,
// logger: ["error", "warn",],
// logger: console,
});
如果日志打印时不需要颜色,只需在 env 中修改变量即可,只要是非空就不会有颜色
# NO_COLOR="1"
- 自定义日志
如果想要使用自定义日志,那么我们就必须要实现 LoggerService 接口
自定义我们的日志系统
import { LoggerService } from "@nestjs/common";
export class MyLogger implements LoggerService {
/**
* 写log'等级日志.
*/
log(message: any, ...optionalParams: any[]) {
console.log(message, optionalParams);
}
/**
* 写'error'等级日志.
*/
error(message: any, ...optionalParams: any[]) {
console.log(message, optionalParams);
}
...
}
这里实例化我们自己的日志
import { MyLogger } from "@/common/utils/logger";
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: new MyLogger(),
});
当然,这里我们需要重写我们的日志,所以我们也可以选择通过扩展内置的ConsoleLogger类并覆盖默认实现的选定行为
import { ConsoleLogger } from "@nestjs/common";
export class MyLogger extends ConsoleLogger {
// log、error...
log(message: any, stack?: string, context?: string) {
// add your tailored logic here
console.log(message, stack, context);
super.error(...arguments);
}
}
- 依赖注入
- 使用记录器进行应用程序日志记录
- 使用扩展 logger
yarn add winston nest-winston winston-daily-rotate-file
使用 minio 作为存储图片
上传图片
使用这个方法上传的时候,需要对已有的文件进行判断是否有相同文件名(或者使用时间戳),如果有重名的文件会对其进行覆盖操作
// minio.service
import { Injectable } from "@nestjs/common";
import * as Minio from "minio";
@Injectable()
export class MinioService {
private readonly minioClient: Minio.Client;
constructor() {
this.minioClient = new Minio.Client({
// 服务ip
endPoint: "192.168.18.24",
// 服务端口
port: 9000,
useSSL: false,
accessKey: "minioadmin",
secretKey: "minioadmin",
});
}
async uploadFile(bucketName: string, objectName: string, stream: Buffer) {
const res = await this.minioClient.bucketExists(bucketName);
console.log(res);
console.log(objectName, stream);
await this.minioClient.putObject(
bucketName,
objectName,
stream,
function (e) {
if (e) {
console.log(e);
return "error";
} else {
// 如果重名也会成功
return "Successfully uploaded the buffer";
}
},
);
}
}
在 controller 中调用文件 minio 的文件上传接口
// minio.controller
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
} from "@nestjs/common";
import { MinioService } from "@/services/minio.service";
import { FileInterceptor } from "@nestjs/platform-express";
@Controller("minio")
export class MinioController {
constructor(private readonly minioService: MinioService) {}
@Post("upload")
@UseInterceptors(FileInterceptor("file"))
uploadFile(@UploadedFile() file: any) {
return this.minioService.uploadFile(
"images",
file.originalname,
file.buffer,
);
}
}
上传文件到本地
引入依赖
yarn add -D @types/multer
在 app.module
中应用 multer 中间件
import { diskStorage } from "multer";
import { Module } from "@nestjs/common";
import { join, extname } from "node:path";
import { PrismaService } from "@/db/mysql.service";
import { AppService } from "@/services/app.service";
import { MinioService } from "@/services/minio.service";
import { MulterModule } from "@nestjs/platform-express";
import { AppController } from "@/controllers/app.controller";
import { MinioController } from "@/controllers/minio.controller";
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
// 指定文件的存储目录
destination: join(__dirname, "/uploads"),
// 时间戳来重命名
filename: (req, file, callback) => {
const fileName = `${
new Date().getTime() + extname(file.originalname)
}`;
return callback(null, fileName);
},
}),
}),
]
})
在 app.controller
中书写调用上传文件 service,这里需要注意的是 UploadedFile
后面不能跟 s(这是单文件上传),否则打印出来的文件将是 undefined
import { FileInterceptor } from "@nestjs/platform-express";
@Post("upload")
@UseInterceptors(FileInterceptor("file"))
upload(@UploadedFile() file: Express.Multer.File) {
return this.appService.upload(file);
}
在 app.service
中返回上传成功逻辑
async upload(file: Express.Multer.File) {
// 到这里文件已经上传到本地了,有后续的存储需求
// 比如要上传到云存储服务中,在这里可以继续处理
console.log("上传成功");
// 返回文件URL
return `http://localhost:9001/uploads/${file.filename}`;
}
全局 config 配置
创建一个 ConfigModule 公开加载相应 .env 文件的公开文件 ConfigService
引入依赖
yarn add @nestjs/config
在顶层模块中导入 ConfigModule
// app.module
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [
ConfigModule.forRoot({
// 从 env 文件中读取常量值
envFilePath: [".env"],
// 开启常量缓存
cache: true,
}),
]
})
main 文件读取值
import { ConfigService } from "@nestjs/config";
const configService = app.get(ConfigService);
const port = configService.get("NEST_PORT");
当然,我们也可以使用自定义的配置文件进行加载。创建 config 文件,将需要读取的变量都从这里导出
// config.ts
export default {
port: parseInt(process.env.NEST_PORT, 10) || 3000,
};
JWT
创建 token
首先,引入相关依赖
yarn add @nestjs/jwt
接着在 app.service 中引入
import { JwtService } from "@nestjs/jwt";
// 在构造函数中引入
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private readonly i18n: I18nService,
) {}
async signIn(data: UserInter) {
let result: string = "";
const { id, pwd } = data;
const userInfo = await this.prisma.user.findUnique({
where: { id: Number(id) },
});
const payload = { sub: id, username: userInfo.name };
if (userInfo.pwd === pwd) {
result = "登录成功";
}
const token = await this.jwtService.signAsync(payload);
// https://docs.nestjs.com/recipes/passport
return { result, userInfo, token };
}
然后,创建 secret 文件并导出
// auth.constants
export const jwtConstants = {
secret: "studTWork",
};
最后,在 app.module 中注册
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [
JwtModule.register({
global: true,
secret: jwtConstants.secret,
// 过期时间
signOptions: { expiresIn: "60s" },
}),
]
})
验证 token
首先,要先创建守卫 auth.guard
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { Request } from "express";
import { JwtService } from "@nestjs/jwt";
import { jwtConstants } from "../utils/auth.constants";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
request["user"] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}
接着,在要验证的 controller 的路由中使用
import { AuthGuard } from "@/common/guards/auth.guard";
@Get("find")
@UseGuards(AuthGuard) //使用 token 验证
@ApiOperation({ summary: "查找用户" })
findUser(@Query() data): Promise<UserInter> {
return this.appService.findUser(Number(data.id));
}
websocket 集成
yarn add @nestjs/websockets @nestjs/platform-socket.io
疑问
为什么 nest 不能正常解析发送过来的 form-data?
解决:
NestJS提供了一个内置 multipart/form-data 的解析器,可以使用. FileInterceptor
import { Body, Post, UseInterceptors } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
@Post("create")
@UseInterceptors(FileInterceptor("file"))
createUser(@Body() userData) {
return this.appService.createUser(userData);
}
另外一个方法:使用 nestjs-form-data
包
获取请求中 param 中的参数
使用 Query
装饰器进行获取
@Get("find")
findUser(@Query() data): Promise<UserInter> {
return this.appService.findUser(Number(data.id));
}
基础概念
IoC
维基:IoC,控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup)
也就是说 IoC 是开发代码的设计原则,DI 则是实现 IoC 的方案:将控制权交给框架
代码层面来说:
- Class A 中用到了 Class B 的对象 b,一般情况下,需要在 A 的代码中显式地用 new 创建 B 的对象。
- 使用 IoC 后,A 的代码只需要定义一个 private 的 B 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 B 对象在外部 new 出来并注入到 A 类里的引用中。
- IoC 将采用依赖注入或依赖查找两种方案去实现
DI
- 依赖注入是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B 对象,通过类型或名称来判断将不同的对象注入到不同的属性中
- 依赖查找是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制
AOP
AOP (Aspect Oriented Programming),面向切面编程(装饰器)。
面向切面程序设计是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。
AOP 主要的思想是将我们对于业务逻辑无关的一些操作,比如日志记录、性能统计、安全控制、异常处理、错误上报等,将这些操作从业务逻辑中剥离出来,将它们放在一些独立的方法中,使得对这些操作做修改的时候就可以不用影响到业务逻辑相关的代码。它主要体现了对代码的低耦合性的追求。
因为 NestJS 使用的 MVC 架构,所以有 AOP 的能力,其中 Nest 的实现主要包括如下五种
- Middleware
- Guard
- Pipe
- Interceptor
- ExceptionFilter
DTO
数据传输对象(DTO - Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)
DTO 本身更像是一个指南, 在使用API时,方便我们了解请求期望的数据类型以及返回的数据对象。先使用一下,可能更方便理解。