前言
在上一篇《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
问题记录
-
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
-
模糊查询
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}%` })
- 以下接口定义,用装饰器@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) {}
-
装饰器@Put,当请求体较大时前端请求发不出来,会报错:network error
-
用 TypeOrmModule.forRootAsync 注册 mongo 连接会报错:can’t resolve dependencies of the ArticleService
结束语
因个人知识面的局限性,如有不正确或异议的地方,望大家指正。
以后的学习笔记和实践示例将持续添加到《Remy 学习与应用》。
感兴趣的同学可以订阅专栏,关注博主,一起学习进步。
如若转载,请注明出处:https://blog.csdn.net/remy0817/article/details/133319551