Nestjs与使用

前言

在上一篇《Astro+Vue3+Nest+TS 搭建服务端渲染 SSR》中,我们已经搭建好了基础工程,这篇将详细介绍如何使用 Nestjs 构建《Remy 学习与应用》的 server 端。

Nestjs

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,构建并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 进行编码)并结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数式响应式编程)的元素。想了解更多,请查阅《Nest 中文文档》

  • 什么是渐进式?简单说,就一开始你不需要了解它的全部功能,能快速上手,有些功能特性不用也可以正常使用。
  • OOP 面向对象编程,万物皆可用对象来描述,如: class Dog{ say(return ‘one one!’)}
  • FP 函数式编程,以函数作为入口,而不是去声明一个对象类,如: say(‘one one!’)
  • RP 响应式编程,一种面向数据流和变化传播的编程范式,如:a = 5; b=6; c=a+b; 当 a 或 b 变化的时候,c 会随之变化
  • FRP 函数式响应式编程,依赖数据流的函数式编程,如:str=‘one one~’; say(str), 当 str 变化,会自动触发 say

server 目录结构

/
├── server/
│   ├── module/
│   │   ├── dict/
│   │   │   ├── dto/
│   │   │   │   └── *Dict.dto.ts
│   │   │   └── dict.*.ts
│   │   └── */
│   ├── filter/
│   │   └── *.filter.ts
│   ├── guard/
│   │   └── *.guard.ts
│   ├── interceptor/
│   │   └── *.interceptor.ts
│   ├── pipe/
│   │   └── *.pipe.ts
│   ├── app.module.ts
│   └── main.ts
├── .env[.development/production]
├── nest-cli.json
├── tsconfig.nest.json
└── package.json

server/module/ 放各功能模块,通常一张表对应一个模块,每个模块中通常有 entity、service、controller、module、dto。以 dict 模块为例:dict.entity.ts、dict.service.ts、dict.controller.ts、dict.module.ts、createDict.dto.ts、updateDict.dto.ts、queryPageDict.dto.ts、responseDict.dto.ts

server/filter/ 过滤器,如异常捕获

server/guard/ 守卫,如身份认证

server/interceptor/ 拦截器,如响应内容反转义、响应分析日志

server/pipe/ 管道,如请求参数转义

server/app.module.ts 应用模块管理

server/main.ts 应用入口

业务模块编写

仍以 dict 模块为例,先上代码

# dict.entity.ts
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm'

@Entity()
export default class Dict {
  /**
   * 字典id
   */
  @PrimaryGeneratedColumn('increment')
  id: number

  /**
   * 字典编码
   */
  @Column({ name: 'dict_code' })
  dictCode: string

  /**
   * 字典名称
   */
  @Column({ name: 'dict_name' })
  dictName: string

  /**
   * 父级字典id
   */
  @Column({ name: 'parent_id' })
  parentId?: number

  /**
   * 逻辑删除标识
   */
  @Column({ name: 'is_delete', default: false, select: false })
  isDelete: boolean

  /**
   * 定义字典表与笔记表一对多的关系
   */
  @OneToMany(() => Note, (note) => note.dict)
  notes: (typeof Note)[]

  /**
   * 定义字典表与示例表一对多的关系
   */
  @OneToMany(() => Example, (example) => example.dict)
  examples: (typeof Example)[]
}
# dict.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator'

export class CreateDictDto {
  @IsNotEmpty()
  @IsString()
  dictCode: string

  @IsNotEmpty()
  @IsString()
  dictName: string

  @IsNumber()
  parentId?: number
}

export class UpdateDictDto {
  @IsNotEmpty()
  @IsNumber()
  id: number

  @IsNotEmpty()
  @IsString()
  dictCode: string

  @IsNotEmpty()
  @IsString()
  dictName: string

  @IsNumber()
  parentId?: number
}

export class QueryDictPageDto extends QueryPageDto {
  key: string // 模糊查询dictCode dictName
  dictCode: string
  dictName: string
  parentId: number
}

export class ResponseDictDto {
  id: number

  dictCode: string

  dictName: string

  parentId: number

  createTime: string

  constructor(dict: Dict) {
    Object.assign(this, dict)
    this.createTime = moment(this.createTime).format('yyyy-MM-DD HH:mm:ss')
  }
}
# dict.service.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Brackets, DataSource, Repository } from 'typeorm'

@Injectable()
export default class DictService {
  constructor(
    // dataSource和dictRepository任选一种
    private readonly dataSource: DataSource,

    @InjectRepository(Dict)
    private readonly dictRepository: Repository<Dict>
  ) {}

  // 新增
  async create(createDictDto: CreateDictDto): Promise<number> {
    const queryRunner = this.dataSource.createQueryRunner()

    // 连接
    await queryRunner.connect()
    // 开启事务
    await queryRunner.startTransaction()

    let dict: Dict

    try {
      // 保存
      dict = await queryRunner.manager.save(Dict, {
        ...createDictDto,
        createTime: moment().format('YYYY-MM-DD HH:mm:ss'),
      })

      // 提交事务
      await queryRunner.commitTransaction()
    } catch (err) {
      // 遇到错误时回滚事务
      await queryRunner.rollbackTransaction()
      // 向上抛出异常
      throw err
    } finally {
      // 释放
      await queryRunner.release()
    }
    return dict.id
  }

  // 修改
  async update(updateDictDto: UpdateDictDto): Promise<boolean> {
    const dict = await this.findOne(updateDictDto.id)
    if (!dict) {
      throw new Error('字典不存在')
    }

    const queryRunner = this.dataSource.createQueryRunner()

    // 连接
    await queryRunner.connect()
    // 开启事务
    await queryRunner.startTransaction()

    try {
      // 更新
      await queryRunner.manager.update(Dict, updateDictDto.id, {
        dictCode: updateDictDto.dictCode,
        dictName: updateDictDto.dictName,
        parentId: updateDictDto.parentId,
      })

      // 提交事务
      await queryRunner.commitTransaction()
    } catch (err) {
      // 遇到错误时回滚事务
      await queryRunner.rollbackTransaction()
      // 向上抛出异常
      throw err
    } finally {
      // 释放
      await queryRunner.release()
    }
    return true
  }

  // 删除
  async deleteOne(id: number): Promise<boolean> {
    const dict = await this.findOne(id)
    if (!dict) {
      throw new Error('字典不存在')
    }

    const queryRunner = this.dataSource.createQueryRunner()

    // 连接
    await queryRunner.connect()
    // 开启事务
    await queryRunner.startTransaction()

    try {
      // 更新
      await queryRunner.manager.update(Dict, id, {
        isDelete: true,
      })

      // 提交事务
      await queryRunner.commitTransaction()
    } catch (err) {
      // 遇到错误时回滚事务
      await queryRunner.rollbackTransaction()
      // 向上抛出异常
      throw err
    } finally {
      // 释放
      await queryRunner.release()
    }
    return true
  }

  // 详情
  findOne(id: number): Promise<ResponseDictDto | null> {
    return this.dataSource
      .getRepository(Dict)
      .findOneBy({ id })
      .then((res) => {
        return res ? new ResponseDictDto(res) : null
      })
  }

  // 分页查询
  async findPage(
    query: QueryDictPageDto
  ): Promise<PageInfo<ResponseDictDto[]>> {
    const currentPage = query.getCurrentPage()
    const pageSize = query.getPageSize()
    const offset = query.getOffset()
    const sortField = query.getMysqlSortField('createTime')
    const sortType = query.getSortType('DESC')

    const [dicts, count] = await this.dictRepository
      .createQueryBuilder('dict')
      .where(
        new Brackets((qb) => {
          const key = `%${query.key}%`
          if (query.key) {
            // 多字段关键字查询
            qb.where('dict.dictCode like :key', { key }).orWhere(
              'dict.dictName like :key',
              { key }
            )
          } else {
            // 单字段模糊查询
            qb.where('dict.dictCode like :dictCode', {
              dictCode: `%${query.dictCode || ''}%`,
            }).andWhere('dict.dictName like :dictName', {
              dictName: `%${query.dictName || ''}%`,
            })
          }
        })
      )
      .andWhere(
        new Brackets((qb) => {
          if (query.parentId) {
            qb.where('dict.parentId = :parentId', query)
          }
        })
      )
      // 默认example.createTime: 'DESC'
      .orderBy('dict.' + sortField, sortType)
      .skip(offset)
      .take(pageSize)
      .getManyAndCount()
    return new PageInfo({
      records: dicts.map((d) => new ResponseDictDto(d)),
      current: currentPage,
      size: pageSize,
      totalCount: count,
    })
  }

  // 查询全部
  findAll(parentId?: number): Promise<ResponseDictDto[]> {
    let result
    if (parentId) {
      result = this.dictRepository.findBy({ parentId })
    }
    result = this.dictRepository.find()

    return result.then((res) => res.map((d) => new ResponseDictDto(d)))
  }
}

# dict.controller.ts
import { Body, Controller, Get, Param, Post, Put, Query } from '@nestjs/common'

@Controller('dict')
export default class DictController {
  constructor(private readonly dictService: DictService) {}

  /**
   * 新增
   * @param createDictDto
   * @returns number
   */
  @Post('add')
  create(@Body() createDictDto: CreateDictDto): Promise<number> {
    return this.dictService.create(createDictDto)
  }

  /**
   * 修改
   * @param updateDictDto
   * @returns boolean
   */
  @Put('update')
  update(@Body() updateDictDto: UpdateDictDto): Promise<boolean> {
    return this.dictService.update(updateDictDto)
  }

  /**
   * 删除
   * @param id
   * @returns boolean
   */
  @Put('delete') // 装饰器@Delete 会报错:`Unexpected token \" in JSON at position 0`,why?
  deleteOne(@Query('id') id: number): Promise<boolean> {
    return this.dictService.deleteOne(id)
  }

  /**
   * 详情
   * @param id
   * @returns ResponseDictDto | null
   */
  @Get('detail/:id')
  findOne(@Param('id') id: number): Promise<ResponseDictDto | null> {
    return this.dictService.findOne(id)
  }

  /**
   * 分页
   * @param queryDictPageDto
   * @returns PageInfo<ResponseDictDto[]>
   */
  @Post('page')
  findPage(
    @Body() queryDictPageDto: QueryDictPageDto
  ): Promise<PageInfo<ResponseDictDto[]>> {
    return this.dictService.findPage(queryDictPageDto)
  }

  /**
   * 全部
   * @param parentId
   * @returns ResponseDictDto[]
   */
  @Get('all')
  findAll(@Query('parentId') parentId?: number): Promise<ResponseDictDto[]> {
    return this.dictService.findAll(parentId)
  }
}
# dict.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  // 导入注册了Dict的存储库
  imports: [TypeOrmModule.forFeature([Dict])],
  providers: [DictService],
  controllers: [DictController],
  // 导出DictService,其他模块可以导入,并以注入的方式使用`@Inject(DictService)`
  exports: [DictService],
})
export default class DictModule {}

以上就是 dict 模块完整的实现代码,下面简单解释一下

dict.entity.ts 定义了 Dict 实体,对应数据库的表结构,和与 Note、Example 之间的关联关系。
Example 中的关系定义:

@ManyToOne(() => Dict, (d) => d.examples)
// name指数据库Example表中的关联字段名;referencedColumnName对应Dict实体中的列名,而不是数据库表的列名
@JoinColumn({ name: 'category_code', referencedColumnName: 'dictCode' })
dict: typeof Dict

CreateDictDto 和 UpdateDictDto 中的装饰器(@IsNotEmpty()、@IsString()、@IsNumber)校验需要搭配全局验证管道

// 添加全局验证管道,用于验证所有接口参数有效性,transform=true时ValidationPipe 可以根据对象的 DTO 类自动将来自网络的有效负载转换为对象类型
app.useGlobalPipes(new ValidationPipe({ transform: true }))

用 ResponseDictDto 对数据做一些加工处理,如:时间格式转换、字段过滤等

dict.service.ts 中的 DataSource 和 Repository 都是用于实体的存储库操作的 api,任选一种即可。事务的使用请结合具体情况,这里仅作用法演示。更多数据库操作,请查阅《TypeORM 中文文档》

dict.controller.ts 中定义了 dict 的接口,接口相对路径如:/dict/add、/dict/update、/dict/detail/:id、/dict/delete?id=

根模块与数据库连接

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ServeStaticModule } from '@nestjs/serve-static'
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
import { ConfigModule, ConfigService } from '@nestjs/config'
import path from 'path'

@Module({
  imports: [
    // 注册静态资源服务
    ServeStaticModule.forRoot({
      rootPath: path.join(path.resolve(), 'dist/client'),
    }),
    // 注册环境配置服务,读取.env/.env.development/.env.production环境配置文件
    ConfigModule.forRoot(),
    // 注册mariadb连接服务
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'mariadb',
        host: configService.get('MARIADB_HOST'),
        port: configService.get('MARIADB_PORT'),
        username: configService.get('MARIADB_USERNAME'),
        password: configService.get('MARIADB_PASSWORD'),
        database: configService.get('MARIADB_DATABASE'),
        entities: [Dict],
      }),
      inject: [ConfigService],
    }),
    // 注册mongodb连接服务
    // 用TypeOrmModule.forRootAsync注册mongo连接会报错:can't resolve dependencies of the ArticleService TODO
    TypeOrmModule.forRoot({
      name: 'article', // 指定连接名
      type: 'mongodb',
      host: host,
      port: port,
      username: username,
      password: password,
      database: database,
      authSource: 'admin',
      entities: [Article],
    }),
    DictModule,
    ArticleModule,
  ],
  // 按需注册provider
  providers: [
    // 注册全局异常过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
    // 注册全局http拦截器
    {
      provide: APP_INTERCEPTOR,
      useClass: GlobalHttpInterceptor,
    },
    // 注册全局转义管道
    {
      provide: APP_PIPE,
      useClass: GlobalEscapePipe,
    },
    // 注册全局身份认证守卫
    {
      provide: APP_GUARD,
      useClass: GlobalAuthGuard,
    },
  ],
})
export default class AppModule {}

这里说明一点,当同时连接多个数据源时需要指定连接名,如上面的 mongo 连接,没有指定 name 的连接默认连接名为 default

# mongo数据源使用
@InjectDataSource('article')
private readonly dataSource: DataSource

server 入口

import { NestFactory } from '@nestjs/core'
// @ts-ignore
import { handler as ssrHandler } from '../server/entry.mjs'
import { ValidationPipe } from '@nestjs/common'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  // 添加全局验证管道,用于验证所有接口参数有效性,transform=true时ValidationPipe 可以根据对象的 DTO 类自动将来自网络的有效负载转换为对象类型
  app.useGlobalPipes(new ValidationPipe({ transform: true }))
  // 将astro ssr处理器作为中间件添加到应用中(包括前端src/pages自动生成的路由)
  app.use(ssrHandler)
  // 配置接口路径全局前缀
  app.setGlobalPrefix(process.env.API_PREFIX || '')
  const port = process.env.SERVER_PORT || 3000
  await app.listen(port)
  console.log(`server open at ${port}...`)
}
bootstrap()

启动 server

直接启动

执行命令 nest start

npm 启动

在 package.json 的 scripts 中添加命令

"nest:start": "nest start",
"nest:start:dev": "nest start --watch",
"neset:build": "nest build"

执行命令 npm run nest:start

问题记录

  1. TypeORMError: Entity metadata for Note#dict was not found. Check if you specified a correct entity object and if it’s connected in the connection options.

    是因为 AppModule 中装饰器@Module 的 imports 没有导入 DictModule

  2. 模糊查询

this.dataSource
  .getRepository(XXX)
  .createQueryBuilder('xxx')
  // 错误示例1
  // .where('xxx.nickname like "%:key%"', { key: nickname })
  // 正确
  .where('xxx.nickname like :key', { key: `%${nickname}%` })
  // 错误示例2: 连续使用where(field, parameters)时,参数parameters中相同的字段会被后执行的覆盖
  // .orWhere('xxx.tag like :key', { key: `%${tag},%` })
  // .orWhere('xxx.tag like :key', { key: `%,${tag},%` })
  // .orWhere('xxx.tag like :key', { key: `%,${tag}%` })
  .orWhere('xxx.tag like :key1', { key1: `%${tag},%` })
  .orWhere('xxx.tag like :key2', { key2: `%,${tag},%` })
  .orWhere('xxx.tag like :key3', { key3: `%,${tag}%` })
  1. 以下接口定义,用装饰器@Delete 会报错:Unexpected token \" in JSON at position 0,换成@Get 能正常访问,why?
@Delete('delete/:id')
async deleteOne(@Param('id') id: number) {}

先换成@Put

@Put('delete')
async deleteOne(@Query('id') id: number) {}
  1. 装饰器@Put,当请求体较大时前端请求发不出来,会报错:network error

  2. 用 TypeOrmModule.forRootAsync 注册 mongo 连接会报错:can’t resolve dependencies of the ArticleService

结束语

因个人知识面的局限性,如有不正确或异议的地方,望大家指正。

以后的学习笔记和实践示例将持续添加到《Remy 学习与应用》

感兴趣的同学可以订阅专栏,关注博主,一起学习进步。

如若转载,请注明出处:https://blog.csdn.net/remy0817/article/details/133319551

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值