前言
这里主要介绍一些我们在开发中会常用的工具以及使用。主要看 nestjs中文网,先看每个教程再结合使用。该 demo 会放到 Github
和 Gitee
上,有兴趣可以拉到最底下获取。
一、Cookie
一个HTTP cookie是指存储在用户浏览器中的一小段数据。Cookies被设计为创建一种可靠的机制让网站来记录状态信息。当用户再次访问网站时,发出的请求会自带cookie。
安装
npm install cookie-parser
npm install @types/cookie-parser -D # 安装 ts 声明文件
使用
在 main.ts
中使用:
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { NestExpressApplication } from '@nestjs/platform-express'
import * as cookieParser from 'cookie-parser'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule)
const port = process.env.PORT || 3000
// 设置 cookie, key 为密钥,可以不设置,如果需要加密 cookie 信息可以添加
app.use(cookieParser("key"))
await app.listen(port, () => {
console.log(`Server is running on port ${port}, http://localhost:${port}`)
})
}
bootstrap()
以 app.controller.ts
为例子,使用:
import { Controller, Get, Res, Req } from '@nestjs/common'
@Controller()
export class AppController {
// 设置 cookie
@Get('set')
index(@Res() res) {
// 设置 cookie
res.cookie('name', 'test-value', {
maxAge: 1000 * 60 * 10, // cookie 过期时间
httpOnly: false, // 是否限制 JavaScript 访问该 cookie
signed: true // 如果设置了密钥就要写这个参数
})
res.send('get-cookie')
}
// 获取 cookie
@Get('get')
getCookie(@Req() req) {
// 获取 cookie
return req.signedCookies // 如果没有使用密钥那就是: res.cookie 或 res.cookie[key]
}
// 清除 cookie
@Get('clear')
clearCookie(@Res() res) {
res.clearCookie('name')
res.send('clear cookie')
}
}
二、文件上传
为了处理文件上传,Nest 提供了一个内置的基于 multer
中间件包的 Express
模块。Multer
处理以 multipart/form-data
格式发送的数据,该格式主要用于通过 HTTP POST 请求上传文件。这个模块是完全可配置的,您可以根据您的应用程序需求调整它的行为。
安装
安装 Multer 的类型声明包:
npm i -D @types/multer
使用
例子upload.controller.ts
:
import {
Controller,
Post,
Put,
UseInterceptors,
UploadedFile,
UploadedFiles,
Body,
HttpException
} from '@nestjs/common'
import { FileInterceptor, FileFieldsInterceptor } from '@nestjs/platform-express'
import { createWriteStream } from 'fs'
import { join } from 'path'
// 存放图片的地址
function reslovePath(path: string) {
return join(__dirname, '../../public/upload', path)
}
@Controller('upload')
export class UploadController {
// 单文件上传
@Post()
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
try {
// 文件命名
const filename = Date.now() + '-' + file.originalname
// 以流的形式写入文件
const writeStream = createWriteStream(reslovePath(filename))
// 写入文件
writeStream.write(file.buffer)
return {
code: 200,
message: '上传成功',
data: {
url: `http://localhost:3000/static/upload/${filename}`
}
}
} catch (err) {
return {
code: 500,
message: '上传失败',
data: err
}
}
}
// 多文件上传
@Post('list')
@UseInterceptors(
FileFieldsInterceptor(
Array.from({ length: 2 }, (_, i) => ({ name: `files-${i}`, maxCount: 1 }))
)
)
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
try {
const data: string[] = []
for (const key in files) {
const file = files[key][0]
const filename = Date.now() + '-' + key + '.' + file.originalname?.split('.').at(-1)
const writeStream = createWriteStream(reslovePath(filename))
writeStream.write(file.buffer)
data.push(`http://localhost:3000/static/upload/${filename}`)
}
return {
code: 200,
message: '上传成功',
data
}
} catch (err) {
return {
code: 500,
message: '上传失败',
data: err.message
}
}
}
// 只能上传图片
@Post('image')
@UseInterceptors(
FileInterceptor('file', {
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
return cb(null, false)
}
cb(null, true)
}
})
)
uploadImage(@UploadedFile() file: Express.Multer.File) {
// console.log(file)
if (!file) {
return '请上传图片格式为jpg|jpeg|png|gif的文件'
} else {
return 'ok'
}
}
}
前端上传:
import { uploadFile, uploadFileList, uploadImage } from '@/api/upload'
// 处理上传数据
function handleFile(e, isList = false) {
const files = e.target.files
const formData = new FormData()
if (isList) {
for (let i = 0; i < files.length; i++) {
formData.append(`files-${i}`, files[i])
}
} else {
formData.append('file', files[0])
}
return formData
}
function Home() {
// 上传文件
const upload = async (e, isList = false) => {
const files = handleFile(e, isList)
const res = isList ? await uploadFileList(files) : await uploadFile(files)
console.log('上传单文件', res)
}
const uploadImageFn = async (e) => {
const files = handleFile(e)
const res = await uploadImage(files)
console.log('只能上传图片【后端验证】', res)
}
return (
<div className="home-content">
<p>
<span>单文件上传:</span>
<input type="file" accept="image/*" onChange={(e) => upload(e, false)} />
</p>
<p>
<span>多文件上传:</span>
<input type="file" multiple accept="image/*" onChange={(e) => upload(e, true)} />
</p>
<p>
<span>只能上传图片【前端上传不做限制】:</span>
<input type="file" onChange={(e) => uploadImageFn(e)} />
</p>
</div>
)
}
api
方法【request
为封装的 axios
】:
import request from '@/utils/request'
// 单文件
export function uploadFile(data) {
return request({
url: '/upload',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 多文件
export function uploadFileList(data) {
return request({
url: '/upload/list',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export function uploadImage(data) {
return request({
url: '/upload/image',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
三、中间件
中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next() 中间件函数。 next()
中间件函数通常由名为 next
的变量表示。
生成
可以使用快捷命令生成一个中间件
nest g mi common/middleware/logger # 生成的中间件路径在 /src/common/middleware 目录下,文件名叫 logger.middleware.ts
使用
如过你使用的是全局中间件,可以在 main.ts
中引入:
`import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { NestExpressApplication } from '@nestjs/platform-express'
import { loggerMiddleware } from './common/middleware/logger.middleware'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule)
const port = process.env.PORT || 3000
// 设置全局日志中间件
app.use(loggerMiddleware)
await app.listen(port, () => {
console.log(`Server is running on port ${port}, http://localhost:${port}`)
})
}
bootstrap()
logger.middleware.ts
, 默认生成的中间件是类的写法,也可以自己写成函数写法,这样写简洁一些,可以根据自己需求来:
# import { Request, Response, NextFunction } from 'express'
/**
* 全局日志中间件
* @param req 请求
* @param res 响应
* @param next 下一个中间件
*/
export function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(`日志中间件:${req.method} ${req.url}`)
next()
}
如果你要使用的单个控制器的中间件,可以在 app.module.ts
中配置:
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { userMiddleware } from './common/middleware/user.middleware'
@Module({
imports: [],
controllers: [AppController],
providers: [AppService]
})
// 添加以下代码,如果想查看更多请看上面的教程地址
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(userMiddleware).forRoutes(AppController)
}
}
四、管道
管道有两个典型的应用场景:
- 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
生成
可以使用快捷命令生成一个中间件
nest g pi common/pipe/app # 生成的中间件路径在 /src/common/pipe 目录下,文件名叫 app.pipe.ts
使用对象结构验证,教程:
npm install --save joi
npm install --save-dev @types/joi
使用
app.pipe.ts
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'
import { ObjectSchema } from 'joi'
@Injectable()
export class AppPipePipe implements PipeTransform {
private readonly schema: ObjectSchema
constructor(schema: ObjectSchema) {
this.schema = schema
}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value)
// 验证字段
if (error) {
throw new BadRequestException('ValidationError:' + error.message)
}
return value
}
}
在控制器中使用:
import { Controller, Get, Query, UsePipes } from '@nestjs/common'
import { AppService } from './app.service'
import { AppPipePipe } from './common/pipe/app.pipe'
import { PipeSchema } from './shared' // 这个文件主要是验证规则,参考下面 shared
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// 测试管道
@Get('pipe')
@UsePipes(new AppPipePipe(PipeSchema))
pipe(@Query() query): string {
return JSON.stringify(query)
}
}
PipeSchema
规则如下:
import * as joi from 'joi'
export const PipeSchema = joi.object({
id: joi.string().required(),
name: joi.string().min(3).max(10).required()
})
如果传的参数不对会自动检测出来
五、授权守卫
只有当调用者(通常是经过身份验证的特定用户)具有足够的权限时,特定的路由才可用。我们现在要构建的 AuthGuard
假设用户是经过身份验证的(因此,请求头附加了一个 token
)。它将提取和验证 token
,并使用提取的信息来确定请求是否可以继续。
生成
nest g gu common/guard/auth # 生成 auth.guard.ts 文件,路径为 /src/common/guard/auth.guard.ts
使用
在控制器中使用,可以通过守卫来控制路由权限:
import { Controller, Get, Res, Req, UseGuards } from '@nestjs/common'
import { AuthGuard } from 'src/common/guard/auth.guard'
@Controller('user')
// @UseGuards(AuthGuard) // 如果写在这里,那么所有 /user 的路由都会触发守卫
export class UserController {
// 获取用户信息
@Get('info')
@UseGuards(AuthGuard) // 写在里面,当访问 /user/info 是会触发守卫,其他路由不受影响
getInfo(@Req() req) {
return '用户信息'
}
}
在守卫中,可以这么写(auth.guard.ts
),返回 false
将不能通过守卫,无法访问该路由,反之:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Observable } from 'rxjs'
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// 获取 cookie
const request = context.switchToHttp().getRequest()
// 获取 cookie
const cookie = request.signedCookies // 如果没有设置密钥的话,可以是 request.cookies
// 判断 cookie 是否存在
if (!cookie['token']) {
return false
}
// 获取 session
// const session = request.session
return true
}
}
你也可以全局使用,在 main.ts
中添加:
const app = await NestFactory.create(AppModule)
app.useGlobalGuards(new RolesGuard()) # 添加你的权限守卫
六、异常过滤器
内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。
生成
nest g f /common/filter/http # 生成 http.fillter.ts
使用
这里使用自定义异常(http.fillter.ts
):
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
msg: exception.message
})
}
}
过滤器可以在当个控制器中使用,也可以在全局中使用,我这里是在全局中使用(main.ts
):
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { HttpExceptionFilter } from './common/filter/http.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalFilters(new HttpExceptionFilter()) // 添加全局过滤器
await app.listen(3000)
}
bootstrap()
使用全局过滤器后,在控制器中使用:
import { Controller, Get, HttpException } from '@nestjs/common'
@Controller()
export class AppController {
// 测试异常
@Get('exception')
exception(): string {
throw new HttpException('Error Exception', 403)
}
}
请求/exception
返回:
{"code":403,"timestamp":"2023-08-08T03:36:10.077Z","path":"/api/exception","msg":"Error Exception"}
Demo
以上例子整理成 demo , 源码放在下面: