学习目标

  • 简单地整合[nestjs][]框架与typeorm
  • 实现基本的CRUD数据操作
  • 使用class-validator验证请求数据
  • 更换更加快速的fastify适配器
  • 使用postman对测试接口

预装依赖

~ pnpm add class-transformer \
  @nestjs/platform-fastify \
  class-validator \
  lodash \
  @nestjs/swagger \
  fastify-swagger \
  mysql2 \
  typeorm \
  @nestjs/typeorm

 ~ pnpm add @types/lodash cross-env -D

生命周期

要合理的编写应用必须事先了解清楚整个程序的访问流程,本教程会讲解如何一步步演进每一次访问流,作为第一步课时,我们的访问流非常简单,可以参考下图

安装Mysql

这里我们只在macos里演示,所以使用brew install mysql安装,其它系统都差不多,比如我日常用的比较多的另一个系统是manjaro,直接sudo pacman -Syy mysql,而windows则更简单,一个phpstudy就什么都有了,安装之后使用如下命令初始化一下

文件结构

我们通过整合typeorm来连接mysql实现一个基本的CRUD应用,首先我们需要创建一下文件结构
1. 创建模块
2. 编写模型
3. 编写Repository(如果有需要的话)
4. 编写观察者(如果有需要的话)
5. 编写数据验证的DTO
6. 编写服务
7. 编写控制器
8. 在每个以上代码各自的目录下建立一个index.ts并导出它们
9. 在各自的Module里进行注册提供者,导出等
10. 在AppModule中导入这个模块

编写好之后的目录结构如下

.
├── app.module.ts                           # 引导模块           
├── config                                  # 配置文件目录
│   ├── database.config.ts                  # 数据库配置
│   └── index.ts
├── core                                    # 核心模块
│   ├── core.module.ts
│   └── index.ts
├── main.ts                                 # 应用启动器
├── modules
│   ├── content                             # 内容模块目录
│   │   ├── content.module.ts               # 内容模块
│   │   ├── controllers                     # 控制器
│   │   ├── dtos                            # DTO访问数据验证
│   │   ├── entities                        # 数据实体模型
|   |   ├── index.ts              
│   │   ├── repositories                    # 自定义Repository
│   │   ├── services                        # 服务
│   └── user                                # 用户模块目录
│       ├── index.ts
│       └── user.module.ts

应用编码

在开始编码之前需要先更改一下package.jsonnestjs-cli.json两个文件

package.json中修改一下启动命令,以便每次启动可以自动配置运行环境并兼容windows环境

"prebuild": "cross-env rimraf dist",
"start": "cross-env NODE_ENV=development nest start",
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",

为了在每次重新编译前自动删除上次的产出,在nestjs-cli.json中配置"deleteOutDir": true

main.ts

把适配器由express换成更快的fastify,并把监听的IP改成0.0.0.0方便外部访问.为了在class-validator中也可以注入nestjs容器的依赖,需要添加useContainer

// main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(
    AppModule,
    new FastifyAdapter()
  );
  useContainer(app.select(AppModule), { fallbackOnErrors: true });
  await app.listen(3000,'0.0.0.0');
}
bootstrap();

连接配置

创建一个src/config/database.config.ts文件

export const database: () => TypeOrmModuleOptions = () => ({
    // ...
    // 此处entites设置为空即可,我们直接通过在模块内部使用`forFeature`来注册模型
    // 后续魔改框架的时候,我们会通过自定义的模块创建函数来重置entities,以便给自己编写的CLI使用
    // 所以这个配置后面会删除
    entities: [], 
    // 自动加载模块中注册的entity
    autoLoadEntities: true,
    // 可以在webpack热更新时保持连接,后续我们使用swc.js+nodemon热更新
    // 所以用不到hot-reload,这个配置对于我们没作用,加不加无所谓
    keepConnectionAlive: true,
    // 可以在开发环境下同步entity的数据结构到数据库
    // 后面教程会使用自定义的迁移命令来代替,以便在生产环境中使用,所以以后这个选项会永久false
    synchronize: process.env.NODE_ENV !== 'production',
});

CoreModule

核心模块用于挂载一些全局类服务,比如整合typeorm的“TypeormModule`

注意: 需要添加@Global()装饰器使CoreModule中导出的服务metadata全局可用

// src/core/core.module.ts
@Global()
@Module({
    imports: [TypeOrmModule.forRoot(database())],
})
export class CoreModule {}

AppModule导入该模块

// src/app.module.ts
@Module({
    imports: [CoreModule],
  ...
})
export class AppModule {}

ContentModule

内容模块用于存放CRUD操作的逻辑代码

// src/modules/content/content.module.ts
@Module({})
export class ContentModule {}

AppModule中注册

// src/app.module.ts
@Module({
    imports: [CoreModule,ContentModule],
  ...
})
export class AppModule {}

实体模型

创建一个PostEntity用于文章数据表

PostEntity继承“BaseEntity,这样做是为了我们可以进行ActiveRecord操作,例如PostEntity.save(post),因为纯DataMapper`的方式有时候代码会显得啰嗦,具体请查看此处

@CreateDateColumn@UpdateDateColumn是自动字段,会根据创建和更新数据的时间自动产生,写入后不必关注

// src/modules/content/entities/post.entity.ts
// 'content_posts'是表名称
@Entity('content_posts')
export class PostEntity extends BaseEntity {
...
    @CreateDateColumn({
        comment: '创建时间',
    })
    createdAt!: Date;

    @UpdateDateColumn({
        comment: '更新时间',
    })
    updatedAt!: Date;
}

存储类

本节存储类是一个空类,后面会添加各种操作方法

// src/modules/content/repositories/post.repository.ts
@EntityRepository(PostEntity)
export class PostRepository extends Repository {}

注册模型和存储类

在编写好entityrepository之后我们还需要通过Typeorm.forFeature这个静态方法进行注册,并把存储类导出为提供者以便在其它模块注入

// src/modules/content/content.module.ts
@Module({
    imports: [TypeOrmModule.forFeature([PostEntity, PostRepository])],
    exports: [TypeOrmModule.forFeature([PostRepository])],
})
export class ContentModule {}

DTO验证

DTO配合管道(PIPE)用于控制器的数据验证,验证器则使用class-validator

class-validator是基于validator.js的封装,所以一些规则可以通过validator.js的文档查找,后面教程中我们会编写大量的自定义的验证规则,这节先尝试基本的用法

其基本的使用方法就是给DTO类的属性添加一个验证装饰器,如下

groups选项用于配置验证组

// src/modules/content/dtos/create-post.dto.ts
@Injectable()
export class CreatePostDto {
    @MaxLength(255, {
        always: true,
        message: '文章标题长度最大为$constraint1',
    })
    @IsNotEmpty({ groups: ['create'], message: '文章标题必须填写' })
    @IsOptional({ groups: ['update'] })
    title!: string;
    ...
}

更新验证类UpdatePostDto继承自CreatePostDto,为了使CreatePostDto中的属性变成可选,需要使用[@nestjs/swagger][]包中的PartialType方法,请查阅此处文档

// src/modules/content/dtos/update-post.dto.ts
@Injectable()
export class UpdatePostDto extends PartialType(CreatePostDto) {
    @IsUUID(undefined, { groups: ['update'], message: '文章ID格式错误' })
    @IsDefined({ groups: ['update'], message: '文章ID必须指定' })
    id!: string;
}

服务类

服务一共包括5个简单的方法,通过调用PostRepository来操作数据

// src/modules/content/services/post.service.ts
@Injectable()
export class PostService {
    // 此处需要注入`PostRepository`的依赖
    constructor(private postRepository: PostRepository) {}
    // 查询文章列表
    async findList() 
    // 查询一篇文章的详细信息
    async findOne(id: string)
    // 添加文章
    async create(data: CreatePostDto)
    // 更新文章
    async update(data: UpdatePostDto)
    // 删除文章
    async delete(id: string)
}

控制器

控制器的方法通过@GET,@POST,@PUT,@PATCH,@Delete等装饰器对外提供接口,并且通过注入PostService服务来操作数据.在控制器的方法上使用框架自带的ValidationPipe管道来验证请求中的body数据,ParseUUIDPipe来验证params数据

// 控制器URL的前缀
@Controller('posts')
export class PostController {
    constructor(protected postService: PostService) {}

    ...
   // 其它方法请自行查看源码
    @Get(':post')
    async show(@Param('post', new ParseUUIDPipe()) post: string) {
        return this.postService.findOne(post);
    }

    @Post()
    async store(
        @Body(
            new ValidationPipe({
                transform: true,
                forbidUnknownValues: true,
                validationError: { target: false },
                groups: ['create'],
            }),
        )
        data: CreatePostDto,
    ) {
        return this.postService.create(data);
    }
}

注册控制器等

  • 为了后面“DTO中可能会导入服务,需要把DTO,同样注册为提供者并且改造一下main.ts,把容器加入到class-containter`中
  • PostService服务可能后续会被UserModule等其它模块使用,所以此处我们也直接导出
// src/modules/content/content.module.ts
@Module({
    imports: [TypeOrmModule.forFeature([PostEntity, PostRepository])],
    providers: [PostService, CreatePostDto, UpdatePostDto],
    controllers: [PostController],
    exports: [PostService, TypeOrmModule.forFeature([PostRepository])],
})
export class ContentModule {}
// src/main.ts
...
async function bootstrap() {
    const app = await NestFactory.create(
        AppModule,
        new FastifyAdapter(),
    );
    useContainer(app.select(AppModule), { fallbackOnErrors: true });
    await app.listen(3000, '0.0.0.0');
}

最后启动应用在postman中测试接口