乐趣区

关于typescript:Nestjs最佳实践教程2基本数据操作

  • 视频地址: https://www.bilibili.com/vide…

学习指标

  • 简略地整合 nestjs 框架与[typeorm][]
  • 实现根本的 CRUD 数据操作
  • 应用 [class-validator][] 验证申请数据
  • 更换更加疾速的 [fastify][] 适配器
  • 应用 Thunder Client 对测试接口

装置 Mysql

理论生产环境中倡议应用 PostgreSQL, 因为教程以学习为主, 所以间接应用相对来说比拟通用和简略的 Mysql

应用以下命令装置 Mysql

如果本机不是应用 linux(比方应用 wsl2), 请到 mysql 官网点击 download 按钮下载安装包后在 chrome 查看下载地址,而后在开发机用 wget 下载

如果本机应用 MacOS, 应用brew install mysql, 如果本机应用 Arch 系列, 应用sudo pacman -Syy mysql

# 下载镜像包
cd /usr/local/src
sudo wget sudo wget https://repo.mysql.com/mysql-apt-config_0.8.22-1_all.deb
# 增加镜像(其它选项不必管,间接 OK 就能够)
sudo apt-get install ./mysql-apt-config_0.8.22-1_all.deb
# 升级包列表
sudo apt-get update
# 开始装置,输出明码后,有一个明码验证形式,因为是开发用,所以抉择第二个弱验证即可
sudo apt-get install mysql-server 
# 初始化, 在是否加载验证组件时抉择 No, 在是否禁用近程登录时也抉择 No
sudo mysql_secure_installation
# 因为是近程 SSH 连贯开发所以须要开启近程数据库链接,如果是本地或者 wsl2 则不须要开启
mysql -u root -p 
CREATE USER 'root'@'%' IDENTIFIED BY '明码';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

接着应用 Navicat 等客户端就能够连贯了

预装依赖

  • [lodash][]是罕用的工具库
  • [cross-env][]用于跨平台设置环境变量
  • [class-transformer][]用于对申请和返回等数据进行序列化
  • [class-validator][]用于验证申请 dto
  • [typeorm][]一个 TS 编写的[node.js][]ORM
  • [@nestjs/typeorm][]Nestjs 的 TypeOrm 整合模块
  • [@nestjs/platform-fastify][]Fastify 适配器, 用于代替 express
  • [nestjs-swagger][]生成 open api 文档, 目前咱们应用其 PartialType 函数是 UpdateDto 中的属性可选
  • [fastify-swagger][]生成 Fastify 的 Open API
~ pnpm add class-transformer \
  @nestjs/platform-fastify \
  class-validator \
  lodash \
  @nestjs/swagger \
  fastify-swagger \
  mysql2 \
  typeorm \
  @nestjs/typeorm

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

生命周期

要正当的编写利用必须当时理解分明整个程序的拜访流程, 本教程会解说如何一步步演进每一次拜访流, 作为第一步课时, 咱们的拜访流非常简单, 能够参考下图

文件构造

咱们通过整合 [typeorm][] 来连贯 mysql 实现一个根本的 CRUD 利用, 首先咱们须要创立一下文件构造

倡议初学者手动创立,没必要应用 CLI 去创立,这样目录和文件更加清晰

  1. 创立模块
  2. 编写模型
  3. 编写 Repository(如果有需要的话)
  4. 编写数据验证的 DTO
  5. 编写服务
  6. 编写控制器
  7. 在每个以上代码各自的目录下建设一个 index.ts 并导出它们
  8. 在各自的 Module 里进行注册提供者, 导出等
  9. AppModule 中导入这两个模块

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

.
├── app.module.ts                           # 疏导模块           
├── config                                  # 配置文件目录
│   ├── database.config.ts                  # 数据库配置
│   └── index.ts
├── main.ts                                 # 利用启动器
├── modules
    ├── content                             # 内容模块目录
    │   ├── content.module.ts               # 内容模块
    │   ├── controllers                     # 控制器
    │   ├── dtos                            # DTO 拜访数据验证
    │   ├── entities                        # 数据实体模型
    |   ├── index.ts              
    │   ├── repositories                    # 自定义 Repository
    │   ├── services                        # 服务
    └──  core
        ├── constants.ts                    # 常量
        ├── core.module.ts                  # 外围模块
        ├── decorators                      # 装璜器
        └── types.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][] 的DTO类中也能够注入 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<NestFastifyApplication>(
    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,
    // 能够在开发环境下同步 entity 的数据结构到数据库
    // 前面教程会应用自定义的迁徙命令来代替, 以便在生产环境中应用, 所以当前这个选项会永恒 false
    synchronize: process.env.NODE_ENV !== 'production',
});

CoreModule

外围模块用于挂载一些全局类服务, 比方整合 [typeorm][] 的`TypeormModule

留神 : 这里不要应用@Global() 装璜器来构建全局模块,因为前面在 CoreModule 类中增加一些其它办法

返回值中增加 global: true 来注册全局模块, 并导出metadata.

// src/core/core.module.ts
export class CoreModule {public static forRoot(options?: TypeOrmModuleOptions) {const imports: ModuleMetadata['imports'] = [TypeOrmModule.forRoot(options)];
        return {
            global: true,
            imports,
            module: CoreModule,
        };
    }
}

AppModule 导入该模块, 并注册数据库连贯

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

自定义存储类

因为原来用于自定义 Repository 的 @EntityRepository 在 typeorm0.3 版本后曾经不可用, 特地不不便, 所以依据这里的示例来自定义一个 CustomRepository 装璜器

// src/modules/core/constants.ts
// 传入装璜器的 metadata 数据标识
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';

// src/modules/core/decorators/repository.decorator.ts
// 定义装璜器
import {CUSTOM_REPOSITORY_METADATA} from '../constants';
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator =>
    SetMetadata(CUSTOM_REPOSITORY_METADATA, entity);

// src/modules/core/decorators/index.ts
export * from './repository.decorator';

定义静态方法用于注册自定义 Repository

 public static forRepository<T extends Type<any>>(repositories: T[],
        dataSourceName?: string,
    ): DynamicModule {const providers: Provider[] = [];

        for (const Repo of repositories) {const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo);

            if (!entity) {continue;}

            providers.push({inject: [getDataSourceToken(dataSourceName)],
                provide: Repo,
                useFactory: (dataSource: DataSource): typeof Repo => {const base = dataSource.getRepository<ObjectType<any>>(entity);
                    return new Repo(base.target, base.manager, base.queryRunner);
                },
            });
        }

        return {
            exports: providers,
            module: CoreModule,
            providers,
        };
    }

ContentModule

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

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

AppModule 中注册

// src/app.module.ts
@Module({imports: [CoreModule.forRoot(database()),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;
}

存储类

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

这里用到咱们后面定义的自定义 CustomRepository 装璜器

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

注册模型和存储类

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

// src/modules/content/content.module.ts
@Module({
    imports: [TypeOrmModule.forFeature([PostEntity]),
        // 注册自定义 Repository
        CoreModule.forRepository([PostRepository]),
    ],
     exports: [
        // 导出自定义 Repository, 以供其它模块应用
        CoreModule.forRepository([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,
                // 不在谬误中裸露 target
                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]),
        // 注册自定义 Repository
        CoreModule.forRepository([PostRepository]),
    ],
    providers: [PostService, CreatePostDto, UpdatePostDto],
    controllers: [PostController],
    exports: [
        PostService,
        // 导出自定义 Repository, 以供其它模块应用
        CoreModule.forRepository([PostRepository]),
    ],
})
export class ContentModule {}
// src/main.ts
...
async function bootstrap() {
    const app = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        new FastifyAdapter(),);
    useContainer(app.select(AppModule), {fallbackOnErrors: true});
    await app.listen(3000, '0.0.0.0');
}

最初启动利用在 Thunder Client 中测试接口

退出移动版