Nest.js 入门教程 第一章

前言

这里主要介绍一些我们在开发中会常用的工具以及使用。主要看 nestjs中文网,先看每个教程再结合使用。该 demo 会放到 GithubGitee 上,有兴趣可以拉到最底下获取。

一、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 , 源码放在下面:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值