数据库
Nest 与数据库无关,允许您轻松地与任何 SQL 或 NoSQL 数据库集成。根据您的偏好,您有许多可用的选项。一般来说,将 Nest 连接到数据库只需为数据库加载一个适当的 Node.js 驱动程序,就像使用 Express 或 Fastify 一样。
您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM ,例如 Sequelize (recipe)、knexjs (tutorial)`和 TypeORM ,以在更高的抽象级别上进行操作。
为了方便起见,Nest 还提供了与现成的 TypeORM 与 @nestjs/typeorm 的紧密集成,我们将在本章中对此进行介绍,而与 @nestjs/mongoose 的紧密集成将在这一章中介绍。这些集成提供了附加的特定于 nestjs 的特性,比如模型/存储库注入、可测试性和异步配置,从而使访问您选择的数据库更加容易。
TypeORM 集成
为了与 SQL和 NoSQL 数据库集成,Nest 提供了 @nestjs/typeorm 包。Nest 使用TypeORM是因为它是 TypeScript 中最成熟的对象关系映射器( ORM )。因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成。
为了开始使用它,我们首先安装所需的依赖项。在本章中,我们将演示如何使用流行的关系型数据库 Mysql , TypeORM 提供了对许多关系数据库的支持,比如 PostgreSQL 、Oracle、Microsoft SQL Server、SQLite,甚至像 MongoDB 这样的 NoSQL 数据库。我们在本章中介绍的步骤对于 TypeORM 支持的任何数据库都是相同的。您只需为所选数据库安装相关的客户端 API 库。
$ npm install --save @nestjs/typeorm typeorm mysql2
安装过程完成后,我们可以将 TypeOrmModule 导入AppModule 。
app.module.ts
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';@Module({imports: [TypeOrmModule.forRoot({type: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',entities: [],synchronize: true,}),],})export class AppModule {}
!> 警告:设置 synchronize: true 不能被用于生产环境,否则您可能会丢失生产环境数据
forRoot() 方法支持所有TypeORM包中createConnection()函数暴露出的配置属性。其他一些额外的配置参数描述如下:
| 参数 | 说明 |
|---|---|
| retryAttempts | 重试连接数据库的次数(默认:10) |
| retryDelay | 两次重试连接的间隔(ms)(默认:3000) |
| autoLoadEntities | 如果为true,将自动加载实体(默认:false) |
?> 更多关于数据源选项见这里
一旦完成,TypeORM 的DataSource和 EntityManager 对象就可以在整个项目中注入(不需要导入任何模块),例如:
app.module.ts
import { DataSource } from 'typeorm';@Dependencies(DataSource)@Module({imports: [TypeOrmModule.forRoot(), UsersModule],})export class AppModule {constructor(dataSource) {this.dataSource = dataSource;}}
存储库模式
TypeORM 支持存储库设计模式,因此每个实体都有自己的存储库。可以从数据库连接获得这些存储库。
为了继续这个示例,我们需要至少一个实体。我们来定义User 实体。
user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';@Entity()export class User {@PrimaryGeneratedColumn()id: number;@Column()firstName: string;@Column()lastName: string;@Column({ default: true })isActive: boolean;}
?> 关于实体的更多内容见TypeORM 文档。
该 User 实体在 users 目录下。这个目录包含了和 UsersModule模块有关的所有文件。你可以决定在哪里保存模型文件,但我们推荐在他们的域中就近创建,即在相应的模块目录中。
要开始使用 user 实体,我们需要在模块的forRoot()方法的选项中(除非你使用一个静态的全局路径)将它插入entities数组中来让 TypeORM知道它的存在。
app.module.ts
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { User } from './users/user.entity';@Module({imports: [TypeOrmModule.forRoot({type: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',entities: [User],synchronize: true,}),],})export class AppModule {}
现在让我们看一下 UsersModule:
user.module.ts
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { UsersService } from './users.service';import { UsersController } from './users.controller';import { User } from './user.entity';@Module({imports: [TypeOrmModule.forFeature([User])],providers: [UsersService],controllers: [UsersController],})export class UsersModule {}
此模块使用 forFeature() 方法定义在当前范围中注册哪些存储库。这样,我们就可以使用 @InjectRepository()装饰器将 UsersRepository 注入到 UsersService 中:
users.service.ts
import { Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './user.entity';@Injectable()export class UsersService {constructor(@InjectRepository(User)private usersRepository: Repository<User>) {}findAll(): Promise<User[]> {return this.usersRepository.find();}findOne(id: string): Promise<User> {return this.usersRepository.findOne(id);}async remove(id: string): Promise<void> {await this.usersRepository.delete(id);}}
?> 不要忘记将 UsersModule 导入根 AppModule。
如果要在导入TypeOrmModule.forFeature 的模块之外使用存储库,则需要重新导出由其生成的提供程序。 您可以通过导出整个模块来做到这一点,如下所示:
users.module.ts
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { User } from './user.entity';@Module({imports: [TypeOrmModule.forFeature([User])],exports: [TypeOrmModule],})export class UsersModule {}
现在,如果我们在 UserHttpModule 中导入 UsersModule ,我们可以在后一个模块的提供者中使用 @InjectRepository(User)。
users-http.module.ts
import { Module } from '@nestjs/common';import { UsersModule } from './user.module';import { UsersService } from './users.service';import { UsersController } from './users.controller';@Module({imports: [UsersModule],providers: [UsersService],controllers: [UsersController],})export class UserHttpModule {}
关系
关系是指两个或多个表之间的联系。关系基于每个表中的常规字段,通常包含主键和外键。
关系有三种:
| 名称 | 说明 |
|---|---|
| 一对一 | 主表中的每一行在外部表中有且仅有一个对应行。使用@OneToOne()装饰器来定义这种类型的关系 |
| 一对多/多对一 | 主表中的每一行在外部表中有一个或多的对应行。使用@OneToMany()和@ManyToOne()装饰器来定义这种类型的关系 |
| 多对多 | 主表中的每一行在外部表中有多个对应行,外部表中的每个记录在主表中也有多个行。使用@ManyToMany()装饰器来定义这种类型的关系 |
使用对应的装饰器来定义实体的关系。例如,要定义每个User可以有多个Photo,可以使用@OneToMany()装饰器。
user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';import { Photo } from '../photos/photo.entity';@Entity()export class User {@PrimaryGeneratedColumn()id: number;@Column()firstName: string;@Column()lastName: string;@Column({ default: true })isActive: boolean;@OneToMany((type) => Photo, (photo) => photo.user)photos: Photo[];}
?> 要了解 TypeORM 中关系的内容,可以查看TypeORM 文档。
自动载入实体
手动将实体一一添加到连接选项的entities数组中的工作会很无聊。此外,在根模块中涉及实体破坏了应用的域边界,并可能将应用的细节泄露给应用的其他部分。针对这一情况,可以使用静态全局路径(例如, dist/*/.entity{.ts,.js})。
注意,webpack不支持全局路径,因此如果你要在单一仓库(Monorepo)中构建应用,可能不能使用全局路径。针对这一问题,有另外一个可选的方案。在配置对象的属性中(传递给forRoot()方法的)设置autoLoadEntities属性为true来自动载入实体,示意如下:
app.module.ts
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';@Module({imports: [TypeOrmModule.forRoot({...autoLoadEntities: true,}),],})export class AppModule {}
通过配置这一选项,每个通过forFeature()注册的实体都会自动添加到配置对象的entities数组中。
?> 注意,那些没有通过forFeature()方法注册,而仅仅是在实体中被引用(通过关系)的实体不能通过autoLoadEntities配置被包含。
事务
数据库事务代表在数据库管理系统(DBMS)中针对数据库的一组操作,这组操作是有关的、可靠的并且和其他事务相互独立的。一个事务通常可以代表数据库中的任何变更(了解更多)。
在TypeORM 事务中有很多不同策略来处理事务,我们推荐使用QueryRunner类,因为它对事务是完全可控的。
首先,我们需要将DataSource对象以正常方式注入:
@Injectable()export class UsersService {constructor(private dataSource: DataSource) {}}
?> DataSource类需要从typeorm包中导入
现在,我们可以使用这个对象来创建一个事务。
async createMany(users: User[]) {const queryRunner = this.dataSource.createQueryRunner();await queryRunner.connect();await queryRunner.startTransaction();try {await queryRunner.manager.save(users[0]);await queryRunner.manager.save(users[1]);await queryRunner.commitTransaction();} catch (err) {//如果遇到错误,可以回滚事务await queryRunner.rollbackTransaction();} finally {//你需要手动实例化并部署一个queryRunnerawait queryRunner.release();}}
?> 注意dataSource仅用于创建QueryRunner。然而,要测试这个类,就需要模拟整个DataSource对象(它暴露出来的几个方法),因此,我们推荐采用一个帮助工厂类(也就是QueryRunnerFactory)并且定义一个包含仅限于维持事务需要的方法的接口。这一技术让模拟这些方法变得非常直接。
可选地,你可以使用一个DataSource对象的回调函数风格的transaction方法(阅读更多)。
async createMany(users: User[]) {await this.dataSource.transaction(async manager => {await manager.save(users[0]);await manager.save(users[1]);});}
不推荐使用装饰器来控制事务(@Transaction()和@TransactionManager())。
订阅者
使用 TypeORM订阅者,你可以监听特定的实体事件。
import {DataSource,EntitySubscriberInterface,EventSubscriber,InsertEvent,} from 'typeorm';import { User } from './user.entity';@EventSubscriber()export class UserSubscriber implements EntitySubscriberInterface<User> {constructor(dataSource: DataSource) {dataSource.subscribers.push(this);}listenTo() {return User;}beforeInsert(event: InsertEvent<User>) {console.log(`BEFORE USER INSERTED: `, event.entity);}}
!> 事件订阅者不能是请求范围的。
现在,将UserSubscriber类添加到providers数组。
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { User } from './user.entity';import { UsersController } from './users.controller';import { UsersService } from './users.service';import { UserSubscriber } from './user.subscriber';@Module({imports: [TypeOrmModule.forFeature([User])],providers: [UsersService, UserSubscriber],controllers: [UsersController],})export class UsersModule {}
?> 更多实体订阅者内容见这里。
迁移
迁移提供了一个在保存数据库中现有数据的同时增量升级数据库使其与应用中的数据模型保持同步的方法。TypeORM 提供了一个专用CLI 命令行工具用于生成、运行以及回滚迁移。
迁移类和Nest应用源码是分开的。他们的生命周期由TypeORM CLI管理,因此,你不能在迁移中使用依赖注入和其他Nest专有特性。在TypeORM 文档 中查看更多关于迁移的内容。
多个数据库
某些项目可能需要多个数据库连接。这也可以通过本模块实现。要使用多个连接,首先要做的是创建这些连接。在这种情况下,连接命名成为必填项。
假设你有一个Album 实体存储在他们自己的数据库中。
const defaultOptions = {type: 'postgres',port: 5432,username: 'user',password: 'password',database: 'db',synchronize: true,};@Module({imports: [TypeOrmModule.forRoot({...defaultOptions,host: 'user_db_host',entities: [User],}),TypeOrmModule.forRoot({...defaultOptions,name: 'albumsConnection',host: 'album_db_host',entities: [Album],}),],})export class AppModule {}
?> 如果未为连接设置任何 name ,则该连接的名称将设置为 default。请注意,不应该有多个没有名称或同名的连接,否则它们会被覆盖。
?> 如果您正在使用 TypeOrmModule.forRootAsync,您必须在外面设置数据源名称 useFactory 。示例:
TypeOrmModule.forRootAsync({name: 'albumsConnection',useFactory: ...,inject: ...,}),
有关详细信息,请参阅 此问题
此时,您的User 和 Album 实体中的每一个都已在各自的连接中注册。通过此设置,您必须告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪种连接。如果不传递任何连接名称,则使用 default 连接。
@Module({imports: [TypeOrmModule.forFeature([User]), TypeOrmModule.forFeature([Album], 'albumsConnection')],})export class AppModule {}
您也可以为给定的数据源注入 DataSource 或 EntityManager:
@Injectable()export class AlbumsService {constructor(@InjectConnection('albumsConnection')private dataSource: DataSource,@InjectEntityManager('albumsConnection')private entityManager: EntityManager,) {}}
也可以向提供者注入任何 DataSource:
@Module({providers: [{provide: AlbumsService,useFactory: (albumsConnection: DataSource) => {return new AlbumsService(albumsConnection);},inject: [getDataSourceToken('albumsConnection')],},],})export class AlbumsModule {}
测试
在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,从而使我们的测试适合于独立,并使它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的存储库。那是什么?解决方案是创建假存储库。为了实现这一点,我们设置了[自定义提供者]。事实上,每个注册的存储库都由 entitynamereposition 标记表示,其中 EntityName 是实体类的名称。
@nestjs/typeorm 包提供了基于给定实体返回准备好 token 的 getRepositoryToken() 函数。
@Module({providers: [UsersService,{provide: getRepositoryToken(User),useValue: mockRepository,},],})export class UsersModule {}
现在, 将使用mockRepository 作为 UsersRepository。每当任何提供程序使用 @InjectRepository() 装饰器请求 UsersRepository 时, Nest 会使用注册的 mockRepository 对象。
定制存储库
TypeORM 提供称为自定义存储库的功能。要了解有关它的更多信息,请访问此页面。基本上,自定义存储库允许您扩展基本存储库类,并使用几种特殊方法对其进行丰富。
要创建自定义存储库,请使用 @EntityRepository() 装饰器和扩展 Repository 类。
@EntityRepository(Author)export class AuthorRepository extends Repository<Author> {}
?> @EntityRepository() 和 Repository 来自 typeorm 包。
创建类后,下一步是将实例化责任移交给 Nest。为此,我们必须将 AuthorRepository 类传递给 TypeOrm.forFeature() 函数。
@Module({imports: [TypeOrmModule.forFeature([AuthorRepository])],controller: [AuthorController],providers: [AuthorService],})export class AuthorModule {}
之后,只需使用以下构造注入存储库:
@Injectable()export class AuthorService {constructor(private readonly authorRepository: AuthorRepository) {}}
异步配置
通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 forRootAsync() 函数,提供了几种处理异步数据的方法。
第一种可能的方法是使用工厂函数:
TypeOrmModule.forRootAsync({useFactory: () => ({type: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',entities: [__dirname + '/**/*.entity{.ts,.js}'],synchronize: true,}),});
我们的工厂的行为与任何其他异步提供者一样(例如,它可以是异步的,并且它能够通过inject注入依赖)。
TypeOrmModule.forRootAsync({imports: [ConfigModule],useFactory: (configService: ConfigService) => ({type: 'mysql',host: configService.get<string>('HOST'),port: configService.get<string>('PORT'),username: configService.get<string>('USERNAME'),password: configService.get<string>('PASSWORD'),database: configService.get<string>('DATABASE'),entities: [__dirname + '/**/*.entity{.ts,.js}'],synchronize: true,}),inject: [ConfigService],});
或者,您可以使用useClass语法。
TypeOrmModule.forRootAsync({useClass: TypeOrmConfigService,});
上面的构造将 TypeOrmConfigService 在内部进行实例化 TypeOrmModule,并将利用它来创建选项对象。在 TypeOrmConfigService 必须实现 TypeOrmOptionsFactory 的接口。
上面的构造将在TypeOrmModule内部实例化TypeOrmConfigService,并通过调用createTypeOrmOptions()
@Injectable()class TypeOrmConfigService implements TypeOrmOptionsFactory {createTypeOrmOptions(): TypeOrmModuleOptions {return {type: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',entities: [__dirname + '/**/*.entity{.ts,.js}'],synchronize: true,};}}
为了防止在 TypeOrmModule 中创建 TypeOrmConfigService 并使用从不同模块导入的提供程序,可以使用 useExisting 语法。
TypeOrmModule.forRootAsync({imports: [ConfigModule],useExisting: ConfigService,});
这个构造与 useClass 的工作原理相同,但有一个关键的区别 — TypeOrmModule 将查找导入的模块来重用现有的 ConfigService,而不是实例化一个新的 ConfigService。
示例
这儿有一个可用的例子。
Sequelize 集成
另一个使用TypeORM的选择是使用@nestjs/sequelize包中的Sequelize ROM。额外地,我们使用sequelize-typescript包来提供一系列额外的装饰器以声明实体。
要开始使用它,我们首先安装需要的依赖。在本章中,我们通过流行的MySQL关系数据库来进行说明。Sequelize支持很多种关系数据库,例如PostgreSQL,MySQL,Microsoft SQL Server,SQLite以及MariaDB。本章中的步骤也适合其他任何Sequelize支持的数据库。你只要简单地安装所选数据库相应的客户端 API 库就可以。
$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2$ npm install --save-dev @types/sequelize
安装完成后,就可以将SequelizeModule导入到根AppModule中。
app.module.ts
import { Module } from '@nestjs/common';import { SequelizeModule } from '@nestjs/sequelize';@Module({imports: [SequelizeModule.forRoot({dialect: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',models: [],}),],})export class AppModule {}
forRoot()方法支持所有Sequelize构造器(了解更多)暴露的配置属性。下面是一些额外的配置属性。
| 名称 | 说明 |
|---|---|
| retryAttempts | 尝试连接数据库的次数(默认:10) |
| retryDelay | 两次连接之间间隔时间(ms)(默认:3000) |
| autoLoadModels | 如果为true,模型将自动载入(默认:false) |
| keepConnectionAlive | 如果为true,在应用关闭后连接将不会关闭(默认:false) |
| synchronize | 如果为true,自动载入的模型将同步(默认:false) |
一旦这些完成了,Sequelize对象就可以注入到整个项目中(不需要在任何模块中再引入),例如:
app.service.ts
import { Injectable } from '@nestjs/common';import { Sequelize } from 'sequelize-typescript';@Injectable()export class AppService {constructor(private sequelize: Sequelize) {}}
模型
Sequelize采用活动记录(Active Record)模式,在这一模式下,你可以使用模型类直接和数据库交互。要继续该示例,我们至少需要一个模型,让我们定义这个User模型:
user.model.ts
import { Column, Model, Table } from 'sequelize-typescript';@Tableexport class User extends Model<User> {@ColumnfirstName: string;@ColumnlastName: string;@Column({ defaultValue: true })isActive: boolean;}
?> 查看更多的可用装饰器。
User模型文件在users目录下。该目录包含了和UsersModule有关的所有文件。你可以决定在哪里保存模型文件,但我们推荐在他们的域中就近创建,即在相应的模块目录中。
要开始使用User模型,我们需要通过将其插入到forRoot()方法选项的models数组中来让Sequelize知道它的存在。
app.module.ts
import { Module } from '@nestjs/common';import { SequelizeModule } from '@nestjs/sequelize';import { User } from './users/user.model';@Module({imports: [SequelizeModule.forRoot({dialect: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',models: [User],}),],})export class AppModule {}
接下来我们看看UsersModule:
users.module.ts
import { Module } from '@nestjs/common';import { SequelizeModule } from '@nestjs/sequelize';import { User } from './user.model';import { UsersController } from './users.controller';import { UsersService } from './users.service';@Module({imports: [SequelizeModule.forFeature([User])],providers: [UsersService],controllers: [UsersController],})export class UsersModule {}
这个模块使用forFeature()方法来定义哪个模型被注册在当前范围中。我们可以使用@InjectModel()装饰器来把UserModel注入到UsersService中。
users.service.ts
import { Injectable } from '@nestjs/common';import { InjectModel } from '@nestjs/sequelize';import { User } from './user.model';@Injectable()export class UsersService {constructor(@InjectModel(User)private userModel: typeof User) {}async findAll(): Promise<User[]> {return this.userModel.findAll();}findOne(id: string): Promise<User> {return this.userModel.findOne({where: {id,},});}async remove(id: string): Promise<void> {const user = await this.findOne(id);await user.destroy();}}
?> 不要忘记在根AppModule中导入UsersModule。
如果你要在导入SequelizeModule.forFreature的模块之外使用存储库,你需要重新导出其生成的提供者。你可以像这样将整个模块导出:
users.module.ts
import { Module } from '@nestjs/common';import { SequelizeModule } from '@nestjs/sequelize';import { User } from './user.entity';@Module({imports: [SequelizeModule.forFeature([User])],exports: [SequelizeModule],})export class UsersModule {}
现在如果我们在UserHttpModule中引入UsersModule,我们可以在后一个模块的提供者中使用@InjectModel(User)。
users-http.module.ts
import { Module } from '@nestjs/common';import { UsersModule } from './user.module';import { UsersService } from './users.service';import { UsersController } from './users.controller';@Module({imports: [UsersModule],providers: [UsersService],controllers: [UsersController],})export class UserHttpModule {}
关系
关系是指两个或多个表之间的联系。关系基于每个表中的常规字段,通常包含主键和外键。
关系有三种:
| 名称 | 说明 |
|---|---|
| 一对一 | 主表中的每一行在外部表中有且仅有一个对应行。 |
| 一对多/多对一 | 主表中的每一行在外部表中有一个或多的对应行。 |
| 多对多 | 主表中的每一行在外部表中有多个对应行,外部表中的每个记录在主表中也有多个行。 |
使用对应的装饰器来定义实体的关系。例如,要定义每个User可以有多个Photo,可以使用@HasMany()装饰器。
user.entity.ts
import { Column, Model, Table, HasMany } from 'sequelize-typescript';import { Photo } from '../photos/photo.model';@Tableexport class User extends Model<User> {@ColumnfirstName: string;@ColumnlastName: string;@Column({ defaultValue: true })isActive: boolean;@HasMany(() => Photo)photos: Photo[];}
?> 阅读本章了解更多关于Sequelize的内容。
自动载入模型
手动将模型一一添加到连接选项的models数组中的工作会很无聊。此外,在根模块中涉及实体破坏了应用的域边界,并可能将应用的细节泄露给应用的其他部分。针对这一情况,在配置对象的属性中(传递给forRoot()方法的)设置autoLoadModels和synchronize属性来自动载入模型,示意如下:
app.module.ts
import { Module } from '@nestjs/common';import { SequelizeModule } from '@nestjs/sequelize';@Module({imports: [SequelizeModule.forRoot({...autoLoadModels: true,synchronize: true,}),],})export class AppModule {}
通过配置这一选项,每个通过forFeature()注册的实体都会自动添加到配置对象的models数组中。
?> 注意,这不包含那些没有通过forFeature()方法注册,而仅仅是在实体中被引用(通过关系)的模型。
事务
数据库事务代表在数据库管理系统(DBMS)中针对数据库的一组操作,这组操作是有关的、可靠的并且和其他事务相互独立的。一个事务通常可以代表数据库中的任何变更(了解更多)。
在Sequelize事务中有很多不同策略来处理事务,下面是一个管理事务的示例(自动回调)。
首先,我们需要将Sequelize对象以正常方式注入:
@Injectable()export class UsersService {constructor(private sequelize: Sequelize) {}}
?> Sequelize类需要从sequelize-typescript包中导入
现在,我们可以使用这个对象来创建一个事务。
async createMany() {try {await this.sequelize.transaction(async t => {const transactionHost = { transaction: t };await this.userModel.create({ firstName: 'Abraham', lastName: 'Lincoln' },transactionHost,);await this.userModel.create({ firstName: 'John', lastName: 'Boothe' },transactionHost,);});} catch (err) {// 一旦发生错误,事务会回滚}}
?> 注意Sequelize仅用于开始一个事务。然而,要测试这个类,就需要模拟整个Sequelize对象(它暴露出来的几个方法),因此,我们推荐采用一个帮助工厂类(也就是TransactionRunner)并且定义一个包含仅限于维持事务需要的方法的接口。这一技术让模拟这些方法变得非常直接。
可选地,你可以使用一个Connection对象的回调函数风格的transaction方法(阅读更多)。
async createMany(users: User[]) {await this.connection.transaction(async manager => {await manager.save(users[0]);await manager.save(users[1]);});}
不推荐使用装饰器来控制事务(@Transaction()和@TransactionManager())。
迁移
迁移提供了一个在保存数据库中现有数据的同时增量升级数据库使其与应用中的数据模型保持同步的方法。Sequelize提供了一个专用CLI 命令行工具用于生成、运行以及回滚迁移。
迁移类和Nest应用源码是分开的。他们的生命周期由TypeORM CLI管理,因此,你不能在迁移中使用依赖注入和其他Nest专有特性。在Sequelize文档 中查看更多关于迁移的内容。
多个数据库
某些项目可能需要多个数据库连接。这也可以通过本模块实现。要使用多个连接,首先要做的是创建这些连接。在这种情况下,连接命名成为必填项。
假设你有一个Album 实体存储在他们自己的数据库中。
const defaultOptions = {dialect: 'postgres',port: 5432,username: 'user',password: 'password',database: 'db',synchronize: true,};@Module({imports: [SequelizeModule.forRoot({...defaultOptions,host: 'user_db_host',models: [User],}),SequelizeModule.forRoot({...defaultOptions,name: 'albumsConnection',host: 'album_db_host',models: [Album],}),],})export class AppModule {}
?> 如果未为连接设置任何 name ,则该连接的名称将设置为 default。请注意,不应该有多个没有名称或同名的连接,否则它们会被覆盖。
此时,您的User 和 Album 实体中的每一个都已在各自的连接中注册。通过此设置,您必须告诉 SequelizeModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪种连接。如果不传递任何连接名称,则使用 default 连接。
@Module({imports: [SequelizeModule.forFeature([User]), SequelizeModule.forFeature([Album], 'albumsConnection')],})export class AppModule {}
您也可以为给定的连接注入 Sequelize:
@Injectable()export class AlbumsService {constructor(@InjectConnection('albumsConnection')private sequelize: Sequelize) {}}
测试
在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,从而使我们的测试适合于独立,并使它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的存储库。那是什么?解决方案是创建假模型。为了实现这一点,我们设置了[自定义提供者]。事实上,每个注册的模型都由 <ModelName>Model 令牌自动表示,其中 ModelName 是模型类的名称。
@nestjs/sequelize 包提供了基于给定模型返回准备好 token 的 getModelToken() 函数。
@Module({providers: [UsersService,{provide: getModelToken(User),useValue: mockModel,},],})export class UsersModule {}
现在, 将使用mockModel 作为 UsersModel。每当任何提供程序使用 @InjectModel() 装饰器请求 UserModel 时, Nest 会使用注册的 mockModel 对象。
异步配置
通常,您可能希望异步传递SequelizeModule选项,而不是事先静态传递它们。在这种情况下,使用 forRootAsync() 函数,提供了几种处理异步数据的方法。
第一种可能的方法是使用工厂函数:
SequelizeModule.forRootAsync({useFactory: () => ({dialect: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',models: [],}),});
我们的工厂的行为与任何其他异步提供者一样(例如,它可以是异步的,并且它能够通过inject注入依赖)。
SequelizeModule.forRootAsync({imports: [ConfigModule],useFactory: (configService: ConfigService) => ({dialect: 'mysql',host: configService.get<string>('HOST'),port: configService.get<string>('PORT'),username: configService.get<string>('USERNAME'),password: configService.get<string>('PASSWORD'),database: configService.get<string>('DATABASE'),models: [],}),inject: [ConfigService],});
或者,您可以使用useClass语法。
SequelizeModule.forRootAsync({useClass: SequelizeConfigService,});
上面的构造将 SequelizeConfigService 在SequelizeModule内部进行实例化 ,并通过调用createSequelizeOptions()来创建一个选项对象。注意,这意味着 SequelizeConfigService 必须实现 SequelizeOptionsFactory 的接口。如下所示:
@Injectable()class SequelizeConfigService implements SequelizeOptionsFactory {createSequelizeOptions(): SequelizeModuleOptions {return {dialect: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'root',database: 'test',models: [],};}}
为了防止在 SequelizeModule 中创建 SequelizeConfigService 并使用从不同模块导入的提供程序,可以使用 useExisting 语法。
SequelizeModule.forRootAsync({imports: [ConfigModule],useExisting: ConfigService,});
这个构造与 useClass 的工作原理相同,但有一个关键的区别 — SequelizeModule 将查找导入的模块来重用现有的 ConfigService,而不是实例化一个新的 ConfigService。
示例
这儿有一个可用的例子。
Mongo
Nest支持两种与 MongoDB 数据库集成的方式。既使用内置的TypeORM 提供的 MongoDB 连接器,或使用最流行的 MongoDB 对象建模工具 Mongoose。在本章后续描述中我们使用专用的@nestjs/mongoose包。
首先,我们需要安装所有必需的依赖项:
$ npm install --save @nestjs/mongoose mongoose
安装过程完成后,我们可以将其 MongooseModule 导入到根目录 AppModule 中。
app.module.ts
import { Module } from '@nestjs/common';import { MongooseModule } from '@nestjs/mongoose';@Module({imports: [MongooseModule.forRoot('mongodb://localhost/nest')],})export class AppModule {}
该 forRoot() 和 mongoose 包中的 mongoose.connect() 一样的参数对象。参见。
模型注入
在Mongoose中,一切都源于 Scheme,每个 Schema 都会映射到 MongoDB 的一个集合,并定义集合内文档的结构。Schema 被用来定义模型,而模型负责从底层创建和读取 MongoDB 的文档。
Schema 可以用 NestJS 内置的装饰器来创建,或者也可以自己动手使用 Mongoose的常规方式。使用装饰器来创建 Schema 会极大减少引用并且提高代码的可读性。
我们先定义CatSchema:
schemas/cat.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';import { Document } from 'mongoose';export type CatDocument = Cat & Document;@Schema()export class Cat extends Document {@Prop()name: string;@Prop()age: number;@Prop()breed: string;}export const CatSchema = SchemaFactory.createForClass(Cat);
注意你也可以通过使用
DefinitionsFactory类(可以从@nestjs/mongoose导入)来生成一个原始Schema,这将允许你根据被提供的元数据手动修改生成的Schema定义。这对于某些很难用装饰器体现所有的极端例子非常有用。
@Schema 装饰器标记一个类作为Schema 定义,它将我们的 Cat 类映射到 MongoDB 同名复数的集合 Cats,这个装饰器接受一个可选的 Schema 对象。将它想象为那个你通常会传递给 mongoose.Schema 类的构造函数的第二个参数(例如, new mongoose.Schema(_, options)))。
更多可用的 Schema 选项可以 看这里。
@Prop 装饰器在文档中定义了一个属性。举个例子,在上面的 Schema 定义中,我们定义了三个属性,分别是:name ,age 和 breed。得益于 TypeScript 的元数据(还有反射),这些属性的 Schema类型会被自动推断。然而在更复杂的场景下,有些类型例如对象和嵌套数组无法正确推断类型,所以我们要向下面一样显式的指出。
@Prop([String])tags: string[];
另外的 @Prop 装饰器接受一个可选的参数,通过这个,你可以指示这个属性是否是必须的,是否需要默认值,或者是标记它作为一个常量,下面是例子:
@Prop({ required: true })name: string;
最后的,原始 Schema 定义也可以被传递给装饰器。这也非常有用,举个例子,一个属性体现为一个嵌套对象而不是一个定义的类。要使用这个,需要从像下面一样从 @nestjs/mongoose 包导入 raw()。
@Prop(raw({firstName: { type: String },lastName: { type: String }}))details: Record<string, any>;
或者,如果你不喜欢使用装饰器,你可以使用 mongoose.Schema 手动定义一个 Schema。下面是例子:
schemas/cat.schema.ts
import * as mongoose from 'mongoose';export const CatSchema = new mongoose.Schema({name: String,age: Number,breed: String,});
该 cat.schema 文件在 cats 目录下。这个目录包含了和 CatsModule模块有关的所有文件。你可以决定在哪里保存Schema文件,但我们推荐在他们的域中就近创建,即在相应的模块目录中。
我们来看看CatsModule:
cats.module.ts
import { Module } from '@nestjs/common';import { MongooseModule } from '@nestjs/mongoose';import { CatsController } from './cats.controller';import { CatsService } from './cats.service';import { Cat, CatSchema } from './schemas/cat.schema';@Module({imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],controllers: [CatsController],providers: [CatsService],})export class CatsModule {}
MongooseModule提供了forFeature()方法来配置模块,包括定义哪些模型应该注册在当前范围中。如果你还想在另外的模块中使用这个模型,将MongooseModule添加到CatsModule的exports部分并在其他模块中导入CatsModule。
注册Schema后,可以使用 @InjectModel() 装饰器将 Cat 模型注入到 CatsService 中:
cats.service.ts
import { Model } from 'mongoose';import { Injectable } from '@nestjs/common';import { InjectModel } from '@nestjs/mongoose';import { Cat, CatDocument } from './schemas/cat.schema';import { CreateCatDto } from './dto/create-cat.dto';@Injectable()export class CatsService {constructor(@InjectModel('Cat') private catModel: Model<CatDocument>) {}async create(createCatDto: CreateCatDto): Promise<Cat> {const createdCat = new this.catModel(createCatDto);return createdCat.save();}async findAll(): Promise<Cat[]> {return this.catModel.find().exec();}}
连接
有时你可能需要连接原生的Mongoose 连接对象,你可能在连接对象中想使用某个原生的 API。你可以使用如下的@InjectConnection()装饰器来注入 Mongoose 连接。
import { Injectable } from '@nestjs/common';import { InjectConnection } from '@nestjs/mongoose';import { Connection } from 'mongoose';@Injectable()export class CatsService {constructor(@InjectConnection() private connection: Connection) {}}
多数据库
有的项目需要多数据库连接,可以在这个模块中实现。要使用多连接,首先要创建连接,在这种情况下,连接必须**要有名称。
app.module.ts
import { Module } from '@nestjs/common';import { MongooseModule } from '@nestjs/mongoose';@Module({imports: [MongooseModule.forRoot('mongodb://localhost/test', {connectionName: 'cats',}),MongooseModule.forRoot('mongodb://localhost/users', {connectionName: 'users',}),],})export class AppModule {}
?> 你不能在没有名称的情况下使用多连接,也不能对多连接使用同一个名称,否则会被覆盖掉。
在设置中,要告诉MongooseModule.forFeature()方法应该使用哪个连接。
@Module({imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }], 'cats')],})export class AppModule {}
也可以向一个给定的连接中注入Connection。
import { Injectable } from '@nestjs/common';import { InjectConnection } from '@nestjs/mongoose';import { Connection } from 'mongoose';@Injectable()export class CatsService {constructor(@InjectConnection('cats') private connection: Connection) {}}
钩子(中间件)
中间件(也被称作预处理(pre)和后处理(post)钩子)是在执行异步函数时传递控制的函数。中间件是针对Schema层级的,在写插件(源码)时非常有用。在 Mongoose 编译完模型后使用pre()或post()不会起作用。要在模型注册前注册一个钩子,可以在使用一个工厂提供者(例如 useFactory)是使用MongooseModule中的forFeatureAsync()方法。使用这一技术,你可以访问一个 Schema 对象,然后使用pre()或post()方法来在那个 schema 中注册一个钩子。示例如下:
@Module({imports: [MongooseModule.forFeatureAsync([{name: 'Cat',useFactory: () => {const schema = CatsSchema;schema.pre('save', () => console.log('Hello from pre save'));return schema;},},]),],})export class AppModule {}
和其他工厂提供者一样,我们的工厂函数是异步的,可以通过inject注入依赖。
@Module({imports: [MongooseModule.forFeatureAsync([{name: 'Cat',imports: [ConfigModule],useFactory: (configService: ConfigService) => {const schema = CatsSchema;schema.pre('save', () => console.log(`${configService.get<string>('APP_NAME')}: Hello from pre save`));return schema;},inject: [ConfigService],},]),],})export class AppModule {}
插件
要向给定的 schema 中注册插件,可以使用forFeatureAsync()方法。
@Module({imports: [MongooseModule.forFeatureAsync([{name: 'Cat',useFactory: () => {const schema = CatsSchema;schema.plugin(require('mongoose-autopopulate'));return schema;},},]),],})export class AppModule {}
要向所有 schema 中立即注册一个插件,调用Connection对象中的.plugin()方法。你可以在所有模型创建前访问连接。使用connectionFactory来实现:
app.module.ts
import { Module } from '@nestjs/common';import { MongooseModule } from '@nestjs/mongoose';@Module({imports: [MongooseModule.forRoot('mongodb://localhost/test', {connectionFactory: (connection) => {connection.plugin(require('mongoose-autopopulate'));return connection;},}),],})export class AppModule {}
测试
在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,使我们的测试套件独立并尽可能快地执行它们。但是我们的类可能依赖于从连接实例中提取的模型。如何处理这些类呢?解决方案是创建模拟模型。
为了简化这一过程,@nestjs/mongoose 包公开了一个 getModelToken() 函数,该函数根据一个 token 名称返回一个准备好的[注入token](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals)。使用此 token,你可以轻松地使用任何标准自定义提供者技术,包括 useClass、useValue 和 useFactory。例如:
@Module({providers: [CatsService,{provide: getModelToken('Cat'),useValue: catModel,},],})export class CatsModule {}
在本例中,每当任何使用者使用 @InjectModel() 装饰器注入模型时,都会提供一个硬编码的 Model<Cat> (对象实例)。
异步配置
通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 forRootAsync() 方法,Nest提供了几种处理异步数据的方法。
第一种可能的方法是使用工厂函数:
MongooseModule.forRootAsync({useFactory: () => ({uri: 'mongodb://localhost/nest',}),});
与其他工厂提供程序一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖。
MongooseModule.forRootAsync({imports: [ConfigModule],useFactory: async (configService: ConfigService) => ({uri: configService.getString('MONGODB_URI'),}),inject: [ConfigService],});
或者,您可以使用类而不是工厂来配置 MongooseModule,如下所示:
MongooseModule.forRootAsync({useClass: MongooseConfigService,});
上面的构造在 MongooseModule中实例化了 MongooseConfigService,使用它来创建所需的 options 对象。注意,在本例中,MongooseConfigService 必须实现 MongooseOptionsFactory 接口,如下所示。 MongooseModule 将在提供的类的实例化对象上调用 createMongooseOptions() 方法。
@Injectable()class MongooseConfigService implements MongooseOptionsFactory {createMongooseOptions(): MongooseModuleOptions {return {uri: 'mongodb://localhost/nest',};}}
为了防止 MongooseConfigService 内部创建 MongooseModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。
MongooseModule.forRootAsync({imports: [ConfigModule],useExisting: ConfigService,});
例子
一个可用的示例见这里。
配置
应用程序通常在不同的环境中运行。根据环境的不同,应该使用不同的配置设置。例如,通常本地环境依赖于特定的数据库凭据,仅对本地 DB 实例有效。生产环境将使用一组单独的 DB 凭据。由于配置变量会更改,所以最佳实践是将配置变量存储在环境中。
外部定义的环境变量通过 process.env global 在Node.js 内部可见。 我们可以尝试通过在每个环境中分别设置环境变量来解决多个环境的问题。 这会很快变得难以处理,尤其是在需要轻松模拟或更改这些值的开发和测试环境中。
在 Node.js 应用程序中,通常使用 .env 文件,其中包含键值对,其中每个键代表一个特定的值,以代表每个环境。 在不同的环境中运行应用程序仅是交换正确的.env 文件的问题。
在 Nest 中使用这种技术的一个好方法是创建一个 ConfigModule ,它暴露一个 ConfigService ,根据 $NODE_ENV 环境变量加载适当的 .env 文件。虽然您可以选择自己编写这样的模块,但为方便起见,Nest 提供了开箱即用的@ nestjs/config软件包。 我们将在本章中介绍该软件包。
安装
要开始使用它,我们首先安装所需的依赖项。
$ npm i --save @nestjs/config
?> 注意 @nestjs/config 内部使用 dotenv 实现。
开始使用
安装完成之后,我们需要导入ConfigModule模块。通常,我们在根模块AppModule中导入它,并使用.forRoot()静态方法导入它的配置。
import { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';@Module({imports: [ConfigModule.forRoot()],})export class AppModule {}
上述代码将从默认位置(项目根目录)载入并解析一个.env文件,从.env文件和process.env合并环境变量键值对,并将结果存储到一个可以通过ConfigService访问的私有结构。forRoot()方法注册了ConfigService提供者,后者提供了一个get()方法来读取这些解析/合并的配置变量。由于@nestjs/config依赖dotenv,它使用该包的规则来处理冲突的环境变量名称。当一个键同时作为环境变量(例如,通过操作系统终端如export DATABASE_USER=test导出)存在于运行环境中以及.env文件中时,以运行环境变量优先。
一个样例.env文件看起来像这样:
DATABASE_USER=testDATABASE_PASSWORD=test
自定义 env 文件路径
默认情况下,程序在应用程序的根目录中查找.env文件。 要为.env文件指定另一个路径,请配置forRoot()的配置对象 envFilePath 属性(可选),如下所示:
ConfigModule.forRoot({envFilePath: '.development.env',});
您还可以像这样为.env 文件指定多个路径:
ConfigModule.forRoot({envFilePath: ['.env.development.local', '.env.development'],});
如果在多个文件中发现同一个变量,则第一个变量优先。
禁止加载环境变量
如果您不想加载.env 文件,而是想简单地从运行时环境访问环境变量(如 OS shell 导出,例如export DATABASE_USER = test),则将options对象的ignoreEnvFile属性设置为true,如下所示 :
ConfigModule.forRoot({ignoreEnvFile: true,});
全局使用
当您想在其他模块中使用ConfigModule时,需要将其导入(这是任何 Nest 模块的标准配置)。 或者,通过将options对象的isGlobal属性设置为true,将其声明为全局模块,如下所示。 在这种情况下,将ConfigModule加载到根模块(例如AppModule)后,您无需在其他模块中导入它。
ConfigModule.forRoot({isGlobal: true,});
自定义配置文件
对于更复杂的项目,您可以利用自定义配置文件返回嵌套的配置对象。 这使您可以按功能对相关配置设置进行分组(例如,与数据库相关的设置),并将相关设置存储在单个文件中,以帮助独立管理它们
自定义配置文件导出一个工厂函数,该函数返回一个配置对象。配置对象可以是任意嵌套的普通 JavaScript 对象。process.env对象将包含完全解析的环境变量键/值对(具有如上所述的.env文件和已解析和合并的外部定义变量)。因为您控制了返回的配置对象,所以您可以添加任何必需的逻辑来将值转换为适当的类型、设置默认值等等。例如:
// config/configuration.tsexport default () => ({port: parseInt(process.env.PORT, 10) || 3000,database: {host: process.env.DATABASE_HOST,port: parseInt(process.env.DATABASE_PORT, 10) || 5432}});
我们使用传递给ConfigModule.forRoot()方法的 options 对象的load属性来加载这个文件:
import configuration from './config/configuration';@Module({imports: [ConfigModule.forRoot({load: [configuration],}),],})export class AppModule {}
ConfigModule 注册一个 ConfigService ,并将其导出为在其他消费模块中可见。此外,我们使用 useValue 语法(参见自定义提供程序)来传递到 .env 文件的路径。此路径将根据 NODE_ENV 环境变量中包含的实际执行环境而不同(例如,’开发’、’生产’等)。 > info 注意 分配给load属性的值是一个数组,允许您加载多个配置文件 (e.g. load: [databaseConfig, authConfig])
使用 ConfigService
现在您可以简单地在任何地方注入 ConfigService ,并根据传递的密钥检索特定的配置值。 要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。与任何提供程序一样,我们需要将其包含模块ConfigModule导入到将使用它的模块中(除非您将传递给ConfigModule.forRoot()方法的 options 对象中的isGlobal属性设置为true)。 如下所示将其导入功能模块。
// feature.module.ts@Module({imports: [ConfigModule],...})
然后我们可以使用标准的构造函数注入:
constructor(private configService: ConfigService) {}
在我们的类中使用它:
要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。
// get an environment variableconst dbUser = this.configService.get<string>('DATABASE_USER');// get a custom configuration valueconst dbHost = this.configService.get<string>('database.host');
如上所示,使用configService.get()方法通过传递变量名来获得一个简单的环境变量。您可以通过传递类型来执行 TypeScript 类型提示,如上所示(例如,get<string>(…))。get()方法还可以遍历一个嵌套的自定义配置对象(通过自定义配置文件创建,如上面的第二个示例所示)。get()方法还接受一个可选的第二个参数,该参数定义一个默认值,当键不存在时将返回该值,如下所示:
// use "localhost" when "database.host" is not definedconst dbHost = this.configService.get<string>('database.host', 'localhost');
配置命名空间
ConfigModule模块允许您定义和加载多个自定义配置文件,如上面的自定义配置文件所示。您可以使用嵌套的配置对象来管理复杂的配置对象层次结构,如本节所示。或者,您可以使用registerAs()函数返回一个“带名称空间”的配置对象,如下所示:
export default registerAs('database', () => ({host: process.env.DATABASE_HOST,port: process.env.DATABASE_PORT || 5432,}));
与自定义配置文件一样,在您的registerAs()工厂函数内部,process.env对象将包含完全解析的环境变量键/值对(带有.env文件和已定义并已合并的外部定义变量)
?> 注意 registerAs 函数是从 @nestjs/config 包导出的。
使用forRoot()的load方法载入命名空间的配置,和载入自定义配置文件方法相同:
// config/database.config.tsimport databaseConfig from './config/database.config';@Module({imports: [ConfigModule.forRoot({load: [databaseConfig],}),],})export class AppModule {}
然后我们可以使用标准的构造函数注入,并在我们的类中使用它: 现在,要从数据库命名空间获取host的值,请使用符号.。使用'database'作为属性名称的前缀,该属性名称对应于命名空间的名称(作为传递给registerAs()函数的第一个参数)
const dbHost = this.configService.get<string>('database.host');
一个合理的替代方案是直接注入'database'的命名空间,我们将从强类型中获益:
constructor(@Inject(databaseConfig.KEY)private dbConfig: ConfigType<typeof databaseConfig>,) {}
?> 注意 ConfigType 函数是从 @nestjs/config 包导出的。
部分注册
到目前为止,我们已经使用forRoot()方法在根模块(例如,AppModule)中处理了配置文件。也许您有一个更复杂的项目结构,其中特定于功能的配置文件位于多个不同的目录中。与在根模块中加载所有这些文件不同,@nestjs/config包提供了一个称为部分注册的功能,它只引用与每个功能模块相关联的配置文件。使用特性模块中的forFeature()静态方法来执行部分注册,如下所示:
import databaseConfig from './config/database.config';@Module({imports: [ConfigModule.forFeature(databaseConfig)],})export class DatabaseModule {}
?> 您可以选择将 ConfigModule 声明为全局模块,而不是在每个模块中导入 ConfigModule。 > info 警告在某些情况下,您可能需要使用onModuleInit()钩子通过部分注册来访问加载的属性,而不是在构造函数中。这是因为forFeature()方法是在模块初始化期间运行的,而模块初始化的顺序是不确定的。如果您以这种方式访问由另一个模块在构造函数中加载的值,则配置所依赖的模块可能尚未初始化。onModuleInit()方法只在它所依赖的所有模块被初始化之后运行,因此这种技术是安全的
Schema验证
一个标准实践是如果在应用启动过程中未提供需要的环境变量或它们不满足特定的验证规则时抛出异常。@nestjs/config包让我们可以使用Joi npm 包来提供这种类型验证。使用 Joi,你可以定义一个对象Schema对象并验证对应的JavaScript对象。
Install Joi (and its types, for TypeScript users):
安装 Joi(Typescript 用户还需要安装其类型申明)
$ npm install --save @hapi/joi$ npm install --save-dev @types/hapi__joi
!> 注意 最新版本的“@hapi/joi”要求您运行 Node v12 或更高版本。对于较老版本的 node,请安装“v16.1.8”。这主要是在“v17.0.2”发布之后,它会在构建期间导致错误。更多信息请参考他们的文档和github issue
现在,我们可以定义一个 Joi 验证模式,并通过forRoot()方法的options对象的validationSchema属性传递它,如下所示
// app.module.tsimport * as Joi from '@hapi/joi';@Module({imports: [ConfigModule.forRoot({validationSchema: Joi.object({NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),PORT: Joi.number().default(3000),}),}),],})export class AppModule {}
由于我们为 NODE_ENV 和 PORT 设置了默认值,因此如果不在环境文件中提供这些变量,验证将不会失败。然而, 我们需要明确提供 API_AUTH_ENABLED。如果我们的 .env 文件中的变量不是模式( schema )的一部分, 则验证也会引发错误。此外,Joi 还会尝试将 env 字符串转换为正确的类型。
默认情况下,允许使用未知的环境变量(其键不在模式中出现的环境变量),并且不会触发验证异常。默认情况下,将报告所有验证错误。您可以通过通过forRoot() options 对象的validationOptions键传递一个 options 对象来更改这些行为。此选项对象可以包含由 Joi 验证选项提供的任何标准验证选项属性。例如,要反转上面的两个设置,像这样传递选项:
// app.module.tsimport * as Joi from '@hapi/joi';@Module({imports: [ConfigModule.forRoot({validationSchema: Joi.object({NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),PORT: Joi.number().default(3000),}),validationOptions: {allowUnknown: false,abortEarly: true,},}),],})export class AppModule {}
@nestjs/config包使用默认设置:
allowUnknown:控制是否允许环境变量中未知的键。默认为true。abortEarly:如果为true,在遇到第一个错误时就停止验证;如果为false,返回所有错误。默认为false。
注意,一旦您决定传递validationOptions对象,您没有显式传递的任何设置都将默认为Joi标准默认值(而不是@nestjs/config默认值)。例如,如果在自定义validationOptions对象中保留allowUnknowns未指定,它的Joi默认值将为false。因此,在自定义对象中指定这两个设置可能是最安全的。
自定义 getter 函数
ConfigService定义了一个通用的get()方法来通过键检索配置值。我们还可以添加getter函数来启用更自然的编码风格:
@Injectable()export class ApiConfigService {constructor(private configService: ConfigService) {}get isAuthEnabled(): boolean {return this.configService.get('AUTH_ENABLED') === 'true';}}
现在我们可以像下面这样使用getter函数:
// app.service.ts@Injectable()export class AppService {constructor(apiConfigService: ApiConfigService) {if (apiConfigService.isAuthEnabled) {// Authentication is enabled}}}
扩展变量
@nestjs/config包支持环境变量扩展。使用这种技术,您可以创建嵌套的环境变量,其中一个变量在另一个变量的定义中引用。例如:
APP_URL=mywebsite.comSUPPORT_EMAIL=support@${APP_URL}
通过这种构造,变量SUPPORT_EMAIL解析为support@mywebsite.com。注意${…}语法来触发解析变量APP_URL在SUPPORT_EMAIL定义中的值。
?> 提示 对于这个特性,@nestjs/config 包内部使用dotenv-expand实现。
使用传递给ConfigModule的forRoot()方法的 options 对象中的expandVariables属性来启用环境变量展开,如下所示:
// app.module.ts@Module({imports: [ConfigModule.forRoot({// ...expandVariables: true,}),],})export class AppModule {}
在main.ts中使用
虽然我们的配置是存储在服务中的,但它仍然可以在 main.ts 文件中使用。通过这种方式,您可以使用它来存储诸如应用程序端口或 CORS 主机之类的变量。
要访问它,您必须使用app.get()方法,然后是服务引用:
const configService = app.get(ConfigService);
然后你可以像往常一样使用它,通过调用带有配置键的 get 方法:
const port = configService.get('PORT');
验证
验证网络应用中传递的任何数据是一种最佳实践。为了自动验证传入请求, Nest 提供了几个开箱即用的管道。
ValidationPipeParseIntPipeParseBoolPipeParseArrayPipeParseUUIDPipe
ValidationPipe 使用了功能强大的 class-validator 包及其声明性验证装饰器。 ValidationPipe 提供了一种对所有传入的客户端有效负载强制执行验证规则的便捷方法,其中在每个模块的本地类或者 DTO 声明中使用简单的注释声明特定的规则。
概览
在 Pipes 一章中,我们完成了构建简化验证管道的过程。为了更好地了解我们在幕后所做的工作,我们强烈建议您阅读本文。在这里,我们将重点讨论 ValidationPipe 的各种实际用例,并使用它的一些高级定制特性。
使用内置的ValidationPipe
在开始使用之前,我们先安装依赖。
$ npm i --save class-validator class-transformer
?> ValidationPipe 从 @nestjs/common 包导入。
由于此管道使用了 class-validator 和 class-transformer 库,因此有许多可用的选项。通过传递给管道的配置对象来进行配置。依照下列内置的选项:
export interface ValidationPipeOptions extends ValidatorOptions {transform?: boolean;disableErrorMessages?: boolean;exceptionFactory?: (errors: ValidationError[]) => any;}
所有可用的class-validator选项(继承自ValidatorOptions接口):
| 选项 | 类型 | 描述 |
|---|---|---|
enableDebugMessages |
boolean |
如果设置为 true ,验证器会在出问题的时候打印额外的警告信息 |
skipUndefinedProperties |
boolean |
如果设置为 true ,验证器将跳过对所有验证对象中值为 null 的属性的验证 |
skipNullProperties |
boolean |
如果设置为 true ,验证器将跳过对所有验证对象中值为 null 或 undefined 的属性的验证 |
skipMissingProperties |
boolean |
如果设置为 true ,验证器将跳过对所有验证对象中缺失的属性的验证 |
whitelist |
boolean |
如果设置为 true ,验证器将去掉没有使用任何验证装饰器的属性的验证(返回的)对象 |
forbidNonWhitelisted |
boolean |
如果设置为 true ,验证器不会去掉非白名单的属性,而是会抛出异常 |
forbidUnknownValues |
boolean |
如果设置为 true ,尝试验证未知对象会立即失败 |
disableErrorMessage |
boolean |
如果设置为 true ,验证错误不会返回给客户端 |
errorHttpStatusCode |
number |
这个设置允许你确定在错误时使用哪个异常类型。默认抛出 BadRequestException |
exceptionFactory |
Function |
接受一个验证错误数组并返回一个要抛出的异常对象 |
groups |
string[] |
验证对象时使用的分组 |
always |
boolean |
设置装饰器选项 always 的默认值。默认值可以在装饰器的选项中被覆写 |
strictGroups |
boolean |
忽略在任何分组内的装饰器,如果 groups 没有给出或者为空 |
dismissDefaultMessages |
boolean |
如果设置为 true ,将不会使用默认消息验证,如果不设置,错误消息会始终是 undefined |
validationError.target |
boolean |
确定目标是否要在 ValidationError 中暴露出来 |
validationError.value |
boolean |
确定验证值是否要在 ValidationError 中暴露出来 |
stopAtFirstError |
boolean |
如果设置为 true ,对于给定的属性的验证会在触发第一个错误之后停止。默认为 false |
?> 更多关于class-validator包的内容见项目仓库。
自动验证
为了本教程的目的,我们将绑定 ValidationPipe 到整个应用程序,因此,将自动保护所有接口免受不正确的数据的影响。
async function bootstrap() {const app = await NestFactory.create(ApplicationModule);app.useGlobalPipes(new ValidationPipe());await app.listen(3000);}bootstrap();
要测试我们的管道,让我们创建一个基本接口。
@Post()create(@Body() createUserDto: CreateUserDto) {return 'This action adds a new user';}
?> 由于 Typescript 没有保存 泛型或接口 的元数据。当你在你的 DTO 中使用他们的时候。 ValidationPipe 可能不能正确验证输入数据。出于这种原因,可以考虑在你的 DTO 中使用具体的类。
?> 当你导入你的 DTO 时,你不能使用仅类型的导入,因为类型会在运行时被擦除,记得用 import { CreateUserDto } 而不是 import type { CreateUserDto } 。
现在我们可以在 CreateUserDto 中添加一些验证规则。我们使用 class-validator 包提供的装饰器来实现这一点,这里有详细的描述。以这种方式,任何使用 CreateUserDto 的路由都将自动执行这些验证规则。
import { IsEmail, IsNotEmpty } from 'class-validator';export class CreateUserDto {@IsEmail()email: string;@IsNotEmpty()password: string;}
有了这些规则,当某人使用无效 email 执行对我们的接口的请求时,则应用程序将自动以 400 Bad Request 代码以及以下响应正文进行响应:
{"statusCode": 400,"error": "Bad Request","message": ["email must be an email"]}
除了验证请求主体之外,ValidationPipe 还可以与其他请求对象属性一起使用。假设我们希望接受端点路径中的 id 。为了确保此请求参数只接受数字,我们可以使用以下结构:
@Get(':id')findOne(@Param() params: FindOneParams) {return 'This action returns a user';}
与 DTO 一样,FindOneParams 只是一个使用 class-validator 定义验证规则的类。它是这样的:
import { IsNumberString } from 'class-validator';export class FindOneParams {@IsNumberString()id: number;}
禁用详细错误
错误消息有助于解释请求中的错误。然而,一些生产环境倾向于禁用详细的错误。通过向 ValidationPipe 传递一个选项对象来做到这一点:
app.useGlobalPipes(new ValidationPipe({disableErrorMessages: true,}));
现在,不会将错误消息返回给最终用户。
剥离属性
我们的 ValidationPipe 还可以过滤掉方法处理程序不应该接收的属性。在这种情况下,我们可以对可接受的属性进行白名单,白名单中不包含的任何属性都会自动从结果对象中删除。例如,如果我们的处理程序需要 email 和 password,但是一个请求还包含一个 age 属性,那么这个属性可以从结果 DTO 中自动删除。要启用这种行为,请将 whitelist 设置为 true 。
app.useGlobalPipes(new ValidationPipe({whitelist: true,}));
当设置为 true 时,这将自动删除非白名单属性(在验证类中没有任何修饰符的属性)。
或者,您可以在出现非白名单属性时停止处理请求,并向用户返回错误响应。要启用此选项,请将 forbidNonWhitelisted 选项属性设置为 true ,并将 whitelist 设置为 true。
负载对象转换(Transform)
来自网络的有效负载是普通的 JavaScript 对象。ValidationPipe 可以根据对象的 DTO 类自动将有效负载转换为对象类型。若要启用自动转换,请将 transform 设置为 true。这可以在方法级别使用:
cats.control.ts
@Post()@UsePipes(new ValidationPipe({ transform: true }))async create(@Body() createCatDto: CreateCatDto) {this.catsService.create(createCatDto);}
要在全局启用这一行为,将选项设置到一个全局管道中:
app.useGlobalPipes(new ValidationPipe({transform: true,}));
要使能自动转换选项,ValidationPipe将执行简单类型转换。在下述示例中,findOne()方法调用一个从地址参数中解析出的id参数。
@Get(':id')findOne(@Param('id') id: number) {console.log(typeof id === 'number'); // truereturn 'This action returns a user';}
默认地,每个地址参数和查询参数在网络传输时都是 string 类型。在上述示例中,我们指定 id 参数为 number (在方法签名中)。因此,ValidationPipe会自动将 string 类型转换为 number 。
显式转换
在上述部分,我们演示了 ValidationPipe 如何基于期待类型隐式转换查询和路径参数,然而,这一特性需要开启自动转换功能。
可选地(在不开启自动转换功能的情况下),你可以使用 ParseIntPipe 或者 ParseBoolPipe 显式处理值(注意,没有必要使用 ParseStringPipe ,这是因为如前所述的,网络中传输的路径参数和查询参数默认都是 string 类型)。
@Get(':id')findOne(@Param('id', ParseIntPipe) id: number,@Query('sort', ParseBoolPipe) sort: boolean,) {console.log(typeof id === 'number'); // trueconsole.log(typeof sort === 'boolean'); // truereturn 'This action returns a user';}
?> ParseIntPipe和ParseBoolPipe从@nestjs/common包中导出。
映射类型
当你在编写如增删改查(新增/删除/修改/查询)的新功能的时候,你会经常基于一个实体类型来构造一个变种。 Nest 提供了一些可以进行类型转换的功能函数来让这种任务更加方便。
!> 如果你的应用使用了 @nestjs/swagger 包,请看这一章节来了解更多有关映射类型的信息。类似地,如果你使用了 @nestjs/graphql 包请看这一章节。这几个包都十分依赖类型所以需要分开导入以使用。因此,如果你使用了 @nestjs/mapped-types (而不是合适的包,根据你应用的类型是 @nestjs/swagger 或者 @nestjs/graphql ),你可能会碰到各种各样的没有被文档记录的副作用。
当构造输入验证类型(也称为 DTO )时,你往往会在同一个类型上构造 创建 和 更新 变种。举个例子, 创建 变种可能要求全部的字段都被填写,但是 更新 变种可能会把全部的字段变成可选的。
Nest 提供了 PartialType() 函数来让这个任务变得简单,同时也可以减少样板代码。
PartialType() 函数返回一个类型(一个类)包含被设置成可选的所有输入类型的属性。假设我们有一个 创建 的类型:
export class CreateCatDto {name: string;age: number;breed: string;}
在默认情况下,所有的字段都是被需要的。使用 PartialType() 并把类引用( CreateCatDto )当作参数传入就可以创造一个有着相同字段但是每一个字段都是可选的新类型:
export class UpdateCatDto extends PartialType(CreateCatDto) {}
?> PartialType() 函数是从 @nestjs/mapped-types 包导入的。
PickType() 函数通过挑出输入类型的一组属性构造一个新的类型(类)。假设我们有以下的类型:
export class CreateCatDto {name: string;age: number;breed: string;}
我们可以使用 PickType() 函数从这个类中挑出一组属性:
export class UpdateCatAgeDto extends PickType(CreateCatDto, ['age'] as const) {}
?> PickType() 函数是从 @nestjs/mapped-types 包导入的。
OmitType() 函数通过挑出输入类型中的全部属性,然后移除一组特定的属性构造一个类型。假设我们有以下的类型:
export class CreateCatDto {name: string;age: number;breed: string;}
如下所示,我们可以生成一个派生的拥有除了 name 以外的所有属性的类型。在这个结构中,给 OmitType() 的第二个参数是一个包含了属性名的数组:
export class UpdateCatDto extends OmitType(CreateCatDto, ['name'] as const) {}
?> OmitType() 函数是从 @nestjs/mapped-types 包导入的。
IntersectionType() 函数将两个类型合并成一个类型。假设我们有以下的两个类型:
export class CreateCatDto {name: string;breed: string;}export class AdditionalCatInfo {color: string;}
我们可以生成一个合并了两个类型中所有属性的新类型:
export class UpdateCatDto extends IntersectionType(CreateCatDto,AdditionalCatInfo,) {}
?> IntersectionType() 函数是从 @nestjs/mapped-types 包导入的。
这些映射类型函数是可以组合的。下面的例子会创造一个拥有除了 name 属性以外所有的 CreateCatDto 的属性,而且这些属性是可选的:
export class UpdateCatDto extends PartialType(OmitType(CreateCatDto, ['name'] as const),) {}
转换和验证数组
TypeScript 不存储泛型或接口的元数据,因此当你在 DTO 中使用它们的时候, ValidationPipe 可能不能正确验证输入数据。例如,在下列代码中, createUserDto 不能正确验证。
@Post()createBulk(@Body() createUserDtos: CreateUserDto[]) {return 'This action adds new users';}
要验证数组,创建一个包裹了该数组的专用类,或者使用 ParseArrayPipe 。
@Post()createBulk(@Body(new ParseArrayPipe({ items: CreateUserDto }))createUserDtos: CreateUserDto[],) {return 'This action adds new users';}
此外, ParseArrayPipe 可能需要手动解析查询参数。让我们考虑一个返回作为查询参数传递的标识的 users 的 findByIds() 方法:
@Get()findByIds(@Query('id', new ParseArrayPipe({ items: Number, separator: ',' }))ids: number[],) {return 'This action returns users by ids';}
这个构造用于验证一个来自如下形式带参数的 GET 请求:
GET /?ids=1,2,3
Websockets 和 微服务
尽管本章展示了使用 HTTP 风格的应用程序的例子(例如,Express或 Fastify ), ValidationPipe 对于 WebSockets 和微服务是一样的,不管使用什么传输方法。
学到更多
要阅读有关由 class-validator 提供的自定义验证器,错误消息和可用装饰器的更多信息,请访问此页面。
高速缓存(Caching)
缓存是一项伟大而简单的技术,可以帮助提高应用程序的性能。它充当临时数据存储,提供高性能的数据访问。
安装
我们首先需要安装所需的包:
$ npm install cache-manager$ npm install -D @types/cache-manager
内存缓存
Nest为各种缓存存储提供程序提供了统一的 API。内置的是内存中的数据存储。但是,您可以轻松地切换到更全面的解决方案,比如 Redis 。为了启用缓存,首先导入 CacheModule 并调用它的 register() 方法。
import { CacheModule, Module } from '@nestjs/common';import { AppController } from './app.controller';@Module({imports: [CacheModule.register()],controllers: [AppController],})export class ApplicationModule {}
与缓存存储的交互
为了和缓存管理器实例进行交互,需要使用CACHE_MANAGER标记将其注入到你的类,如下所示:
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
?> Cache类是从cache-manager中导入的,而CACHE_MANAGER则是从@nestjs/common包中导入的。
Cache实例(来自cache-manager包)上的get方法被用来从缓存中检索键值。如果该键在缓存中不存在,则返回null。
const value = await this.cacheManager.get('key');
使用set方法将一个键值对添加到缓存中:
await this.cacheManager.set('key', 'value');
缓存的默认过期时间是5秒。
你可以为特定的键手动指定一个TTL(过期时间,以秒为单位),如下所示:
await this.cacheManager.set('key', 'value', { ttl: 1000 });
如果要让缓存永不过期,请将配置的ttl属性设置为0。
await this.cacheManager.set('key', 'value', { ttl: 0 });
使用del方法从缓存中删除一个键值对:
await this.cacheManager.del('key');
使用reset方法清空整个缓存:
await this.cacheManager.reset();
自动缓存响应
!> 在 GraphQL 应用中,拦截器针对每个字段解析器分别运行,因此,CacheModule(使用拦截器来缓存响应)将无法正常工作。
要启用自动缓存响应,只需在想缓存数据的地方绑定CacheInterceptor。
@Controller()@UseInterceptors(CacheInterceptor)export class AppController {@Get()findAll(): string[] {return [];}}
!> 警告: 只有使用 GET 方式声明的节点会被缓存。此外,注入本机响应对象( @Res() )的 HTTP 服务器路由不能使用缓存拦截器。有关详细信息,请参见响应映射。
全局缓存
为了减少重复代码量,可以将CacheInterceptor全局绑定到每个端点(endpoints):
import { CacheModule, Module, CacheInterceptor } from '@nestjs/common';import { AppController } from './app.controller';import { APP_INTERCEPTOR } from '@nestjs/core';@Module({imports: [CacheModule.register({isGlobal: true,})],controllers: [AppController],providers: [{provide: APP_INTERCEPTOR,useClass: CacheInterceptor,},],})export class AppModule {}
定制缓存
所有缓存的数据有其自己的过期时间(TTL)。要个性化不同值,将选项对象传递给register()方法。
CacheModule.register({ttl: 5, //秒max: 10, //缓存中最大和最小数量});
全局缓存重载
使能全局缓存后,缓存入口存储在基于路径自动生成的Cachekey中。你可能需要基于每个方法重载特定的缓存设置(@CacheKey()和@CacheTTL()),允许为独立控制器方法自定义缓存策略。这在使用不同存储缓存时是最有意义的。
@Controller()export class AppController {@CacheKey('custom_key')@CacheTTL(20)findAll(): string[] {return [];}}
?> @CacheKey()和@CacheTTL()装饰器从@nestjs/common包导入。
@CacheKey()装饰器可以有或者没有一个对应的@CacheTTL()装饰器,反之亦然。你可以选择仅覆盖@CacheKey()或@CacheTTL()。没有用装饰器覆盖的设置将使用全局注册的默认值(见自定义缓存)。
WebSockets 和 微服务
显然,您可以毫不费力地使用 CacheInterceptor WebSocket 订阅者模式以及 Microservice 的模式(无论使用何种服务间的传输方法)。
?> 译者注: 微服务架构中服务之间的调用需要依赖某种通讯协议介质,在 nest 中不限制你是用消息队列中间件,RPC/gRPC 协议或者对外公开 API 的 HTTP 协议。
@CacheKey('events')@UseInterceptors(CacheInterceptor)@SubscribeMessage('events')handleEvent(client: Client, data: string[]): Observable<string[]> {return [];}
然而,需要一个附加的@CacheKey()装饰器来指定一个用于依次存储并获取缓存数据的键。注意,你不应该缓存所有的内容。永远也不要去缓存那些用于实现业务逻辑也不是简单地查询数据的行为。
此外,你可以使用@CacheTTL()装饰器来指定一个缓存过期时间(TTL),用于覆盖全局默认的 TTL 值。
@CacheTTL(10)@UseInterceptors(CacheInterceptor)@SubscribeMessage('events')handleEvent(client: Client, data: string[]): Observable<string[]> {return [];}
?> @CacheTTL()装饰器可以@CacheKey()装饰器同时或者不同时使用。
不同的存储
服务在底层使用缓存管理器(cache-manager)。cache-manager包支持一个宽范围的可用存储,例如,Redis存储。一个完整的支持存储列表见这里。要设置Redis存储,简单地将该包和相应的选项传递给register()方法。
import * as redisStore from 'cache-manager-redis-store';import { CacheModule, Module } from '@nestjs/common';import { AppController } from './app.controller';@Module({imports: [CacheModule.register({store: redisStore,host: 'localhost',port: 6379,}),],controllers: [AppController],})export class ApplicationModule {}
调整追踪
默认地,Nest使用请求 URL(在一个HTTPapp 中)或者缓存键(在websockets和microservices应用中,通过@CacheKey()装饰器设置)来联系缓存记录和路径。然而,有时你可能想要根据不同要素设置追踪,例如HTTP headers(比如,确定合适profile路径的Authorization)。
为了达到这个目的,创建一个CacheInterceptor的子类并覆盖trackBy()方法。
@Injectable()class HttpCacheInterceptor extends CacheInterceptor {trackBy(context: ExecutionContext): string | undefined {return 'key';}}
异步配置
你可能想异步传递模块选项来代替在编译时静态传递。在这种情况下,可以使用registerAsync()方法,它提供了不同的处理异步配置的方法。
一个方法是使用工厂函数:
CacheModule.registerAsync({useFactory: () => ({ttl: 5,}),});
我们的工厂行为和其他异步模块工厂一样(它可以使用inject异步注入依赖)。
CacheModule.registerAsync({imports: [ConfigModule],useFactory: async (configService: ConfigService) => ({ttl: configService.getString('CACHE_TTL'),}),inject: [ConfigService],});
此外,你也可以使用useClass方法:
CacheModule.registerAsync({useClass: CacheConfigService,});
上述构造器将在CacheModule内部实例化CacheConfigService并用它来得到选项对象,CacheConfigService需要使用CacheOptionsFactory接口来提供配置选项:
@Injectable()class CacheConfigService implements CacheOptionsFactory {createCacheOptions(): CacheModuleOptions {return {ttl: 5,};}}
如果你希望使用在其他不同模块中导入的现有的配置提供者,使用useExisting语法:
CacheModule.registerAsync({imports: [ConfigModule],useExisting: ConfigService,});
这和useClass工作模式相同,但有一个根本区别——CacheModule将查找导入的模块来重用任何已经创建的ConfigService,以代替自己创实例化。
?> 提示: @CacheKey() 装饰器来源于 @nestjs/common 包。
但是, @CacheKey() 需要附加装饰器以指定用于随后存储和检索缓存数据的密钥。此外,请注意,开发者不应该缓存所有内容。缓存数据是用来执行某些业务操作,而一些简单数据查询是不应该被缓存的。
自定义缓存
所有缓存数据都有自己的到期时间(TTL)。要自定义默认值,请将配置选项填写在 register()方法中。
CacheModule.register({ttl: 5, // secondsmax: 10, // maximum number of items in cache});
不同的缓存库
我们充分利用了缓存管理器。该软件包支持各种实用的商店,例如Redis 商店(此处列出完整列表)。要设置 Redis 存储,只需将包与 correspoding 选项一起传递给 register() 方法即可。
?> 译者注: 缓存方案库目前可选的有 redis, fs, mongodb, memcached 等。
import * as redisStore from 'cache-manager-redis-store';import { CacheModule, Module } from '@nestjs/common';import { AppController } from './app.controller';@Module({imports: [CacheModule.register({store: redisStore,host: 'localhost',port: 6379,}),],controllers: [AppController],})export class ApplicationModule {}
调整跟踪
默认情况下, Nest 通过 @CacheKey() 装饰器设置的请求路径(在 HTTP 应用程序中)或缓存中的 key(在 websockets 和微服务中)来缓存记录与您的节点数据相关联。然而有时您可能希望根据不同因素设置跟踪,例如,使用 HTTP 头部字段(例如 Authorization 字段关联身份鉴别节点服务)。
为此,创建 CacheInterceptor 的子类并覆盖 trackBy() 方法。
@Injectable()class HttpCacheInterceptor extends CacheInterceptor {trackBy(context: ExecutionContext): string | undefined {return 'key';}}
异步配置
通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 registerAsync() 方法,提供了几种处理异步数据的方法。
第一种可能的方法是使用工厂函数:
CacheModule.registerAsync({useFactory: () => ({ttl: 5,}),});
显然,我们的工厂要看起来能让每一个调用用使用。(可以变成顺序执行的同步代码,并且能够通过注入依赖使用)。
CacheModule.registerAsync({imports: [ConfigModule],useFactory: async (configService: ConfigService) => ({ttl: configService.getString('CACHE_TTL'),}),inject: [ConfigService],});
或者,您可以使用类而不是工厂:
CacheModule.registerAsync({useClass: CacheConfigService,});
上面的构造将 CacheConfigService 在内部实例化为 CacheModule ,并将利用它来创建选项对象。在 CacheConfigService 中必须实现 CacheOptionsFactory 的接口。
@Injectable()class CacheConfigService implements CacheOptionsFactory {createCacheOptions(): CacheModuleOptions {return {ttl: 5,};}}
为了防止 CacheConfigService 内部创建 CacheModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。
CacheModule.registerAsync({imports: [ConfigModule],useExisting: ConfigService,});
它和 useClass 的用法有一个关键的相同点: CacheModule 将查找导入的模块以重新使用已创建的 ConfigService 实例,而不是重复实例化。
序列化(Serialization)
序列化(Serialization)是一个在网络响应中返回对象前的过程。 这是一个适合转换和净化要返回给客户的数据的地方。例如,应始终从最终响应中排除敏感数据(如用户密码)。此外,某些属性可能需要额外的转换,比方说,我们只想发送一个实体的子集。手动完成这些转换既枯燥又容易出错,并且不能确定是否覆盖了所有的情况。
?> 译者注: Serialization 实现可类比 composer 库中 fractal ,响应给用户的数据不仅仅要剔除设计安全的属性,还需要剔除一些无用字段如 create_time, delete_time,update_time 和其他属性。在 JAVA 的实体类中定义 N 个属性的话就会返回 N 个字段,解决方法可以使用范型编程,否则操作实体类回影响数据库映射字段。
概要
为了提供一种直接的方式来执行这些操作, Nest 附带了这个 ClassSerializerInterceptor 类。它使用类转换器来提供转换对象的声明性和可扩展方式。基于此类基础下,可以从类转换器中获取方法和调用 classToPlain() 函数返回的值。要这样做,可以将由class-transformer装饰器提供的规则应用在实体/DTO 类中,如下所示:
排除属性
我们假设要从一个用户实体中自动排除password属性。我们给实体做如下注释:
import { Exclude } from 'class-transformer';export class UserEntity {id: number;firstName: string;lastName: string;@Exclude()password: string;constructor(partial: Partial<UserEntity>) {Object.assign(this, partial);}}
然后,直接在控制器的方法中调用就能获得此类的实例。
@UseInterceptors(ClassSerializerInterceptor)@Get()findOne(): UserEntity {return new UserEntity({id: 1,firstName: 'Kamil',lastName: 'Mysliwiec',password: 'password',});}
!> 我们必须返回一个类的实体。如果你返回一个普通的 JavaScript 对象,例如,{user: new UserEntity()},该对象将不会被正常序列化。
?> 提示: @ClassSerializerInterceptor() 装饰器来源于 @nestjs/common 包。
现在当你调用此服务时,将收到以下响应结果:
{"id": 1,"firstName": "Kamil","lastName": "Mysliwiec"}
注意,拦截器可以应用于整个应用程序(见这里)。拦截器和实体类声明的组合确保返回 UserEntity 的任何方法都将确保删除 password 属性。这给你一个业务规则的强制、集中的评估。
公开属性
您可以使用 @Expose() 装饰器来为属性提供别名,或者执行一个函数来计算属性值(类似于 getter 函数),如下所示。
@Expose()get fullName(): string {return `${this.firstName} ${this.lastName}`;}
变换
您可以使用 @Transform() 装饰器执行其他数据转换。例如,您要选择一个名称 RoleEntity 而不是返回整个对象。
@Transform(({value}) => value.name)role: RoleEntity;
传递选项
你可能想要修改转换函数的默认行为。要覆盖默认设置,请使用 @SerializeOptions() 装饰器来将其传递给一个options对象。
@SerializeOptions({excludePrefixes: ['_'],})@Get()findOne(): UserEntity {return {};}
?> 提示: @SerializeOptions() 装饰器来源于 @nestjs/common 包。
通过 @SerializeOptions() 传递的选项作为底层 classToPlain() 函数的第二个参数传递。在本例中,我们自动排除了所有以_前缀开头的属性。
Websockets 和 微服务
虽然本章展示了使用 HTTP 风格的应用程序的例子(例如,Express 或 Fastify ),但是 ClassSerializerInterceptor对于 WebSockets 和微服务的工作方式是一样的,不管使用的是哪种传输方法。
更多
想了解有关装饰器选项的更多信息,请访问此页面。
API 多版本
接口版本
?> 提示: 本章节仅适用于以 HTTP 构建的应用程序.
接口版本可以在同一个应用程序中的控制器或者路由层面支持 不同的版本。 应用程序经常更改,在仍然需要支持以前版本的应用程序的同时,需要进行重大更改的情况并不少见。
Nest.js 支持一下 4 种形式的版本管理:
URI 版本类型 |
版本在请求的 URI 中传递 (默认) |
Header 版本管理 |
在自定义的 Header 中传递版本 |
Media Type 版本管理 |
在 Accept 头部标签中声明版本 |
自定义版本管理 |
请求的任何部分都可用于指定版本,需要提供一个自定义函数来提取所述版本 |
URI 版本类型
URI 版本管理在请求地址中标识版本,比如 https://example.com/v1/route 和 https://example.com/v2/route。
!> 警告: URI 版本管理会在 全局路径前缀 (如果存在)自动添加版本号, 并且在控制器或路由之前.
按照以下操作来使用 URI 版本管理:
@@filename(main)const app = await NestFactory.create(AppModule);// or "app.enableVersioning()"app.enableVersioning({type: VersioningType.URI,});await app.listen(3000);
!> 警告: URI 版本管理默认使用 v 前缀, 并且可以通过设置 prefix 来自定义前缀或者设置 false 来取消使用前缀行为。
?> 提示: VersioningType 中的类型 type 是从 @nestjs/common 引入的枚举。
Header 版本类型
Header 版本管理使用用户指定的自定义头部标签来标明使用的请求版本。
使用 Header 版本管理的示例 HTTP 请求
按照以下步骤为你的应用程序启用 Header 版本管理。
@@filename(main)const app = await NestFactory.create(AppModule);app.enableVersioning({type: VersioningType.HEADER,header: 'Custom-Header',});await app.listen(3000);
header 属性标明要用来传递接口版本的头部标签名。
?> 提示: VersioningType 中的类型 type 是从 @nestjs/common 引入的枚举。
Media Type 版本类型
Media 版本类型使用 Accept 头部标签来声明请求的版本。
在 Accept 头部标签内, 版本将与媒体类型用分号;分隔。它应该包含一个键值对,表示要用于请求的版本,例如 Accept: application/json;v=2。在配置包含键和分隔符的版本时,可以设置 key 的值作为键的前缀值。
要为应用程序启用 媒体类型版本控制,请执行以下操作:
@@filename(main)const app = await NestFactory.create(AppModule);app.enableVersioning({type: VersioningType.MEDIA_TYPE,key: 'v=',});await app.listen(3000);
key 属性应该是包含该版本的键值对的键和分隔符。在示例中 Accept: application/json;v=2,key 属性将设置为 v=。
?> 提示: 版本控制类型 枚举可用于 type 属性,并从 @nestjs/common 包中导入。
自定义版本类型
自定义版本控制使用请求的任何部分来指定一个或多个版本,并使用返回字符串或字符串数组的 extractor 函数分析传入请求。
如果请求者提供了多个版本,则提取器函数 extractor 可以返回一个字符串数组,这些字符串按最大、最高版本到最小、最低版本的顺序排序,版本控制按从高到低的顺序与路由匹配。
如果从 extractor 返回空字符串或数组,则不会匹配任何路由,并返回 404。
例如,如果传入请求指定它支持版本 1、2 和 3,则 提取器 必须 返回 `[3, 2, 1]``,这可确保首先选择最可能的最高路由版本。
如果提取了版本 [3, 2, 1],但仅存在版本 2 和 1 的路由,则选择与版本 2 匹配的路由(自动忽略版本 3)。
!> 警告: 由于设计限制,根据从提取器 extractor 中返回的数组中选择最高匹配版本 不能可靠地 与 Express 适配器使用,单个版本(1 个字符串或 1 个元素的数组)在 Express 中工作正常,而Fastify 能正确支持最高匹配版本选择和单个版本选择。
要为应用程序启用 自定义版本控制,请创建一个 extractor 函数并将其传递到应用程序中,如下所示:
@@filename(main)// Example extractor that pulls out a list of versions from a custom header and turns it into a sorted array.// This example uses Fastify, but Express requests can be processed in a similar way.const extractor = (request: FastifyRequest): string | string[] =>[request.headers['custom-versioning-field'] ?? ''].flatMap(v => v.split(',')).filter(v => !!v).sort().reverse()const app = await NestFactory.create(AppModule);app.enableVersioning({type: VersioningType.CUSTOM,extractor,});await app.listen(3000);
示例
版本控制允许您对控制器、各个路由进行版本控制,并且还为某些资源提供了一种选择退出版本控制的方法。无论应用程序使用哪种版本控制类型,版本控制的用法都是相同的。
!> 警告: 如果为应用程序启用了版本控制,但控制器或路由未指定版本,则对该控制器、路由的任何请求都将返回 404 响应状态。同样,如果收到的请求包含没有相应控制器或路由的版本,则还将返回 404 响应状态。
控制器版本
可以在独立的控制器中指定版本,此设置将影响控制器下的所有路由。
参照以下添加针对单个路由的版本:
@@filename(cats.controller)@Controller({version: '1',})export class CatsControllerV1 {@Get('cats')findAll(): string {return 'This action returns all cats for version 1';}}@@switch@Controller({version: '1',})export class CatsControllerV1 {@Get('cats')findAll() {return 'This action returns all cats for version 1';}}
路由版本
可以在独立的路由中指定版本,此版本将覆盖其他影响路由的版本控制比如设置的控制器版本。
参照以下添加针对单个路由的版本:
@@filename(cats.controller)import { Controller, Get, Version } from '@nestjs/common';@Controller()export class CatsController {@Version('1')@Get('cats')findAllV1(): string {return 'This action returns all cats for version 1';}@Version('2')@Get('cats')findAllV2(): string {return 'This action returns all cats for version 2';}}@@switchimport { Controller, Get, Version } from '@nestjs/common';@Controller()export class CatsController {@Version('1')@Get('cats')findAllV1() {return 'This action returns all cats for version 1';}@Version('2')@Get('cats')findAllV2() {return 'This action returns all cats for version 2';}}
多版本
可以在控制器或路由中设置支持多版本,你需要以数组的形式设置多版本支持。
参照以下设置多版本支持
@@filename(cats.controller)@Controller({version: ['1', '2'],})export class CatsController {@Get('cats')findAll(): string {return 'This action returns all cats for version 1 or 2';}}@@switch@Controller({version: ['1', '2'],})export class CatsController {@Get('cats')findAll() {return 'This action returns all cats for version 1 or 2';}}
无影响的路由
有的控制器或路由不关心接口版本并且无论版本如何,都将具有相同的功能,为了适应这种情况,可以将版本设置为 VERSION_NEUTRAL。
传入的请求将映射到 VERSION_NEUTRAL 控制器或路由,而不管请求中发送的版本如何,或者请求中根本不包含版本信息。
!> 警告: 对于 URI 版本控制,VERSION_NEUTRAL 不会在 URI 中指定版本信息。
要添加非特定版本控制器或路由,请执行以下操作:
@@filename(cats.controller)import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';@Controller({version: VERSION_NEUTRAL,})export class CatsController {@Get('cats')findAll(): string {return 'This action returns all cats regardless of version';}}@@switchimport { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';@Controller({version: VERSION_NEUTRAL,})export class CatsController {@Get('cats')findAll() {return 'This action returns all cats regardless of version';}}
全局默认版本
如果你不想为每一个控制器或路由指定版本,或者你想为每一个控制器、路由指定默认的版本而不指定具体的版本号,你可以参照以下设置 defaultVersion:
@@filename(main)app.enableVersioning({// ...defaultVersion: '1'// ordefaultVersion: ['1', '2']// ordefaultVersion: VERSION_NEUTRAL});
定时任务
定时任务允许你按照指定的日期/时间、一定时间间隔或者一定时间后单次执行来调度(scheduling)任意代码(方法/函数)。在Linux世界中,这经常通过操作系统层面的cron包等执行。在Node.js应用中,有几个不同的包可以模拟 cron 包的功能。Nest 提供了@nestjs/schedule包,其集成了流行的 Node.js 的node-cron包,我们将在本章中应用该包。
安装
我们首先从安装需要的依赖开始。
$ npm install --save @nestjs/schedule
要激活工作调度,从根AppModule中导入ScheduleModule并运行forRoot()静态方法,如下:
app.module.ts
import { Module } from '@nestjs/common';import { ScheduleModule } from '@nestjs/schedule';@Module({imports: [ScheduleModule.forRoot()],})export class AppModule {}
.forRoot()调用初始化调度器并且注册在你应用中任何声明的cron jobs,timeouts和intervals。注册开始于onApplicationBootstrap生命周期钩子发生时,保证所有模块都已经载入,任何计划工作已经声明。
声明计时工作(cron job)
一个计时工作调度任何函数(方法调用)以自动运行, 计时工作可以:
- 单次,在指定日期/时间
- 重复循环:重复工作可以在指定周期中指定执行(例如,每小时,每周,或者每 5 分钟)
在包含要运行代码的方法定义前使用@Cron()装饰器声明一个计时工作,如下:
import { Injectable, Logger } from '@nestjs/common';import { Cron } from '@nestjs/schedule';@Injectable()export class TasksService {private readonly logger = new Logger(TasksService.name);@Cron('45 * * * * *')handleCron() {this.logger.debug('Called when the current second is 45');}}
在这个例子中,handleCron()方法将在当前时间为45秒时定期执行。换句话说,该方法每分钟执行一次,在第 45 秒执行。
@Cron()装饰器支持标准的cron patterns:
- 星号通配符 (也就是 *)
- 范围(也就是 1-3,5)
- 步长(也就是 */2)
在上述例子中,我们给装饰器传递了45 * * * * *,下列键展示了每个位置的计时模式字符串的意义:
* * * * * *| | | | | || | | | | day of week| | | | month| | | day of month| | hour| minutesecond (optional)
一些示例的计时模式包括:
| 名称 | 含义 |
|---|---|
| * * * * * * | 每秒 |
| 45 * * * * * | 每分钟第 45 秒 |
| 10 * * * | 每小时,从第 10 分钟开始 |
| 0 /30 9-17 * * | 上午 9 点到下午 5 点之间每 30 分钟 |
| 0 30 11 * * 1-5 | 周一至周五上午 11:30 |
@nestjs/schedule包提供一个方便的枚举
import { Injectable, Logger } from '@nestjs/common';import { Cron, CronExpression } from '@nestjs/schedule';@Injectable()export class TasksService {private readonly logger = new Logger(TasksService.name);@Cron(CronExpression.EVERY_45_SECONDS)handleCron() {this.logger.debug('Called every 45 seconds');}}
在本例中,handleCron()方法每45秒执行一次。
可选地,你可以为将一个JavaScript的Date对象传递给@Cron()装饰器。这样做可以让工作在指定日期执行一次。
?> 使用JavaScript日期算法来关联当前日期和计划工作。@Cron(new Date(Date.now()+10*1000))用于在应用启动 10 秒后运行。
你可以在声明后访问并控制一个定时任务,或者使用动态 API动态创建一个定时任务(其定时模式在运行时定义)。要通过 API 声明定时任务,你必须通过将选项对象中的name属性作为可选的第二个参数传递给装饰器,从而将工作和名称联系起来。
@Cron('* * 8 * * *', {name: 'notifications',})triggerNotifications() {}
声明间隔
要声明一个以一定间隔运行的方法,使用@Interval()装饰器前缀。以毫秒单位的number传递间隔值,如下:
@Interval(10000)handleInterval() {this.logger.debug('Called every 10 seconds');}
?> 本机制在底层使用JavaScript的setInterval()函数。你也可以使用定期调度工作来应用一个定时任务。
如果你希望在声明类之外通过动态 API控制你声明的时间间隔。使用下列结构将名称与间隔关联起来。
@Interval('notifications', 2500)handleInterval() {}
动态 API 也支持动态创建时间间隔,间隔属性在运行时定义,可以列出和删除他们。
声明延时任务
要声明一个在指定时间后运行(一次)的方法,使用@Timeout()装饰器前缀。将从应用启动的相关时间偏移量(毫秒)传递给装饰器,如下:
@Timeout(5000)handleTimeout() {this.logger.debug('Called once after 5 seconds');}
?> 本机制在底层使用 JavaScript 的setTimeout()方法
如果你想要在声明类之外通过动态 API 控制你声明的超时时间,将超时时间和一个名称以如下结构关联:
@Timeout('notifications', 2500)handleTimeout() {}
动态 API 同时支持创建动态超时时间,超时时间在运行时定义,可以列举和删除他们。
动态规划模块 API
@nestjs/schedule模块提供了一个支持管理声明定时、超时和间隔任务的动态 API。该 API 也支持创建和管理动态定时、超时和间隔,这些属性在运行时定义。
动态定时任务
使用SchedulerRegistryAPI 从你代码的任何地方获取一个CronJob实例的引用。首先,使用标准构造器注入ScheduleRegistry。
constructor(private schedulerRegistry: SchedulerRegistry) {}
?> 从@nestjs/schedule包导入SchedulerRegistry。
使用下列类,假设通过下列定义声明一个定时任务:
@Cron('* * 8 * * *', {name: 'notifications',})triggerNotifications() {}
如下获取本工作:
const job = this.schedulerRegistry.getCronJob('notifications');job.stop();console.log(job.lastDate());
getCronJob()方法返回一个命名的定时任务。然后返回一个包含下列方法的CronJob对象:
- stop()-停止一个按调度运行的任务
- start()-重启一个停止的任务
- setTime(time:CronTime)-停止一个任务,为它设置一个新的时间,然后再启动它
- lastDate()-返回一个表示工作最后执行日期的字符串
- nextDates(count:number)-返回一个
moment对象的数组(大小count),代表即将执行的任务日期
?> 在moment对象中使用toDate()来渲染成易读的形式。
使用SchedulerRegistry.addCronJob()动态创建一个新的定时任务,如下:
addCronJob(name: string, seconds: string) {const job = new CronJob(`${seconds} * * * * *`, () => {this.logger.warn(`time (${seconds}) for job ${name} to run!`);});this.scheduler.addCronJob(name, job);job.start();this.logger.warn(`job ${name} added for each minute at ${seconds} seconds!`,);}
在这个代码中,我们使用cron包中的CronJob对象来创建定时任务。CronJob构造器采用一个定时模式(类似@Cron()装饰器作为其第一个参数,以及一个将执行的回调函数作为其第二个参数。SchedulerRegistry.addCronJob()方法有两个参数:一个CronJob名称,以及一个CronJob对象自身。
!> 记得在使用前注入SchedulerRegistry,从cron包中导入 CronJob。
使用SchedulerRegistry.deleteCronJob()方法删除一个命名的定时任务,如下:
deleteCron(name: string) {this.scheduler.deleteCronJob(name);this.logger.warn(`job ${name} deleted!`);}
使用SchedulerRegistry.getCronJobs()方法列出所有定时任务,如下:
getCrons() {const jobs = this.scheduler.getCronJobs();jobs.forEach((value, key, map) => {let next;try {next = value.nextDates().toDate();} catch (e) {next = 'error: next fire date is in the past!';}this.logger.log(`job: ${key} -> next: ${next}`);});}
getCronJobs()方法返回一个map。在这个代码中,我们遍历该map并且尝试获取每个CronJob的nextDates()方法。在CronJobAPI 中,如果一个工作已经执行了并且没有下一次执行的日期,将抛出异常。
动态间隔
使用SchedulerRegistry.getInterval()方法获取一个时间间隔的引用。如上,使用标准构造注入SchedulerRegistry。
constructor(private schedulerRegistry: SchedulerRegistry) {}
如下使用:
const interval = this.schedulerRegistry.getInterval('notifications');clearInterval(interval);
使用SchedulerRegistry.addInterval()方法创建一个新的动态间隔,如下:
addInterval(name: string, seconds: string) {const callback = () => {this.logger.warn(`Interval ${name} executing at time (${seconds})!`);};const interval = setInterval(callback, seconds);this.scheduler.addInterval(name, interval);}
在该代码中,我们创建了一个标准的 JavaScript 间隔,然后将其传递给ScheduleRegistry.addInterval()方法。该方法包括两个参数:一个时间间隔的名称,和时间间隔本身。
如下使用SchedulerRegistry.deleteInterval()删除一个命名的时间间隔:
deleteInterval(name: string) {this.scheduler.deleteInterval(name);this.logger.warn(`Interval ${name} deleted!`);}
使用SchedulerRegistry.getIntervals()方法如下列出所有的时间间隔:
getIntervals() {const intervals = this.scheduler.getIntervals();intervals.forEach(key => this.logger.log(`Interval: ${key}`));}
动态超时
使用SchedulerRegistry.getTimeout()方法获取一个超时引用,如上,使用标准构造注入SchedulerRegistry:
constructor(private schedulerRegistry: SchedulerRegistry) {}
并如下使用:
const timeout = this.schedulerRegistry.getTimeout('notifications');clearTimeout(timeout);
使用SchedulerRegistry.addTimeout()方法创建一个新的动态超时,如下:
addTimeout(name: string, seconds: string) {const callback = () => {this.logger.warn(`Timeout ${name} executing after (${seconds})!`);});const timeout = setTimeout(callback, seconds);this.scheduler.addTimeout(name, timeout);}
在该代码中,我们创建了个一个标准的 JavaScript 超时任务,然后将其传递给ScheduleRegistry.addTimeout()方法,该方法包含两个参数:一个超时的名称,以及超时对象自身。
使用SchedulerRegistry.deleteTimeout()方法删除一个命名的超时,如下:
deleteTimeout(name: string) {this.scheduler.deleteTimeout(name);this.logger.warn(`Timeout ${name} deleted!`);}
使用SchedulerRegistry.getTimeouts()方法列出所有超时任务:
getTimeouts() {const timeouts = this.scheduler.getTimeouts();timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));}
示例
一个可用的例子见这里。
队列
队列是一种有用的设计模式,可以帮助你处理一般应用规模和性能的挑战。一些队列可以帮助你处理的问题示例包括:
- 平滑输出峰值。例如,如果用户可以在任何时间创建资源敏感型任务,你可以将其添加到一个消息队列中而不是同步执行。然后你可以通过工作者进程从队列中以一个可控的方式取出进程。在应用规模增大时,你可以轻松添加新的队列消费者来提高后端任务处理能力。
- 将可能阻塞
Node.js事件循环的整体任务打碎。例如,如果一个用户请求是 CPU 敏感型工作,例如音频转码,你可以将其委托给其他进程,从而保证用户接口进程保持响应。 - 在不同的服务间提供一个可信的通讯通道。例如,你可以将任务(工作)加入一个进程或服务,并由另一个进程或服务来消费他们。你可以在由其他任何进程或服务执行的工作完成、错误或者其他状态变化时得到通知(通过监听状态事件)。当队列生产者或者消费者失败时,他们的状态会被保留,任务将在 node 重启后自动重启。
Nest 提供了@nestjs/bull包,这是Bull包的一个包装器,Bull 是一个流行的、支持良好的、高性能的基于 Nodejs 的消息队列系统应用。该包将 Bull 队列以 Nest 友好的方式添加到你的应用中。
Bull 使用Redis持久化工作数据,因此你需要在你的系统中安装 Redis。因为他是基于 Redis 的,你的队列结构可以是完全分布式的并且和平台无关。例如,你可以有一些队列生产者、消费者和监听者,他们运行在 Nest 的一个或多个节点上,同时,其他生产者、消费者和监听者在其他 Node.js 平台或者其他网络节点上。
本章使用@nestjs/bull包,我们同时推荐阅读BUll 文档来获取更多背景和应用细节。
安装
要开始使用,我们首先安装需要的依赖:
$ npm install --save @nestjs/bull bull$ npm install --save-dev @types/bull
一旦安装过程完成,我们可以在根AppModule中导入BullModule。
app.module.ts
import { Module } from '@nestjs/common';import { BullModule } from '@nestjs/bull';@Module({imports: [BullModule.registerQueue({name: 'audio',redis: {host: 'localhost',port: 6379,},}),],})export class AppModule {}
registerQueue()方法用于实例化并/或注册队列。队列在不同的模块和进程之间共享,在底层则通过同样的凭据连接到同样的 Redis 数据库。每个队列由其name属性区分(如下),当共享队列(跨模块/进程)时,第一个registerQueue()方法同时实例化该队列并向模块注册它。其他模块(在相同或者不同进程下)则简单地注册队列。队列注册创建一个injection token,它可以被用在给定 Nest 模块中获取队列。
针对每个队列,传递一个包含下列属性的配置对象:
-name:string- 一个队列名称,它可以被用作injection token(用于将队列注册到控制器/提供者),也可以作为装饰器参数来将消费者类和监听者与队列联系起来。是必须的。 -limiter:RateLimiter-该选项用于确定消息队列处理速率,查看RateLimiter获取更多信息。可选的。 -redis:RedisOpts-该选项用于配置 Redis 连接,查看RedisOpts获取更多信息。可选的。 -prefix: string-队列所有键的前缀。可选的。 -defaultJobOptions: JobOpts-选项用以控制新任务的默认属性。查看JobOpts获取更多信息。可选的。 -settings: AdvancedSettings-高级队列配置设置。这些通常不需要改变。查看AdvancedSettings获取更多信息。可选的。
注意,name属性是必须的。其他选项是可选的,为队列行为提供更细节的控制。这些会直接传递给 Bull 的Queue构造器。在这里阅读更多选项。当在第二个或者子模块中注册一个队列时,最佳时间是省略配置对象中除name属性之外的所有选项。这些选项仅应该在实例化队列的模块中确定。
?> 在registerQueue()方法中传递多个逗号分隔的选项对象来创建多个队列。
由于任务在 Redis 中是持久化的,每次当一个特定名称的队列被实例化时(例如,当一个 app 启动/重启时),它尝试处理任何可能在前一个旧的任务遗留未完成的session。
每个队里可能有一个或很多生产者、消费者以及监听者。消费者从一个特定命令队列中获取任务:FIFO(默认,先进先出),LIFO(后进先出)或者依据优先级。
控制队列处理命令在这里讨论。
生产者
任务生产者添加任务到队列中。生产者是典型的应用服务(Nest 提供者)。要添加工作到一个队列,首先注册队列到服务中:
import { Injectable } from '@nestjs/common';import { Queue } from 'bull';import { InjectQueue } from '@nestjs/bull';@Injectable()export class AudioService {constructor(@InjectQueue('audio') private audioQueue: Queue) {}}
?> @InjectQueue()装饰器由其名称指定队列,像它在registerQueue()方法中提供的那样(例如,audio)。
现在,通过调用队列的add()方法添加一个任务,传递一个用户定义的任务对象。任务表现为序列化的JavaScript对象(因为它们被存储在 Redis 数据库中)。你传递的任务形式是可选的;用它来在语义上表示你任务对象:
const job = await this.audioQueue.add({foo: 'bar',});
命名的任务
任务需要独一无二的名字。这允许你创建专用的消费者,这将仅处理给定名称的处理任务。
const job = await this.audioQueue.add('transcode', {foo: 'bar',});
!> 当使用命名任务时,你必须为每个添加到队列中的特有名称创建处理者,否则队列会反馈缺失了给定任务的处理器。查看这里阅读更多关于消费命名任务的信息。
任务选项
任务可以包括附加选项。在Quene.add()方法的job参数之后传递选项对象。任务选项属性有:
priority: number-选项优先级值。范围从 1(最高优先)到 MAX_INT(最低优先)。注意使用属性对性能有轻微影响,因此要小心使用。delay: number- 任务执行前等待的时间(毫秒)。注意,为了精确延时,服务端和客户端时钟应该同步。attempts: number-任务结束前总的尝试次数。repeat: RepeatOpts-按照定时设置重复任务记录,查看RepeatOpts。backoff: number | BackoffOpts- 如果任务失败,自动重试闪避设置,查看BackoffOpts。lifo: boolean-如果为true,从队列右端添加任务以替代从左边添加(默认为 false)。timeout: number-任务超时失败的毫秒数。jobId: number | string- 覆盖任务 ID-默认地,任务 ID 是唯一的整数,但你可以使用该参数覆盖它。如果你使用这个选项,你需要保证jobId是唯一的。如果你尝试添加一个包含已有 id 的任务,它不会被添加。removeOnComplete: boolean | number-如果为true,当任务完成时移除任务。一个数字用来指定要保存的任务数。默认行为是将完成的工作保存在已完成的设置中。removeOnFail: boolean | number-如果为true,当所有尝试失败时移除任务。一个数字用来指定要保存的任务数。默认行为是将失败的任务保存在已失败的设置中。stackTraceLimit: number-限制在stacktrace中保存的堆栈跟踪线。
这里是一些带有任务选项的自定义任务示例。
要延迟任务的开始,使用delay配置属性:
const job = await this.audioQueue.add({foo: 'bar',},{ delay: 3000 } // 3 seconds delayed);
要从右端添加任务到队列(以 LIFO(后进先出)处理任务),设置配置对象的lifo属性为true。
const job = await this.audioQueue.add({foo: 'bar',},{ lifo: true });
要优先一个任务,使用priority属性。
const job = await this.audioQueue.add({foo: 'bar',},{ priority: 2 });
消费者
消费者是一个类,定义的方法要么处理添加到队列中的任务,要么监听队列的事件,或者两者皆有。使用@Processor()装饰器来定义消费者类,如下:
import { Processor } from '@nestjs/bull';@Processor('audio')export class AudioConsumer {}
装饰器的字符串参数(例如,audio)是和类方法关联的队列名称。
在消费者类中,使用@Process()装饰器来装饰任务处理者。
import { Processor, Process } from '@nestjs/bull';import { Job } from 'bull';@Processor('audio')export class AudioConsumer {@Process()async transcode(job: Job<unknown>) {let progress = 0;for (i = 0; i < 100; i++) {await doSomething(job.data);progress += 10;job.progress(progress);}return {};}}
装饰器方法(例如transcode()) 在工作空闲或者队列中有消息要处理的时候被调用。该处理器方法接受job对象作为其仅有的参数。处理器方法的返回值被保存在任务对象中,可以在之后被访问,例如,在用于完成事件的监听者中。
Job对象有多个方法,允许你和他们的状态交互。例如,上述代码使用progress()方法来更新工作进程。查看这里以了解完整的Job对象 API 参照。
你可以指定一个任务处理方法,仅处理指定类型(包含特定name的任务)的任务,这可以通过如下所述的将name传递给@Process()装饰器完成。你在一个给定消费者类中可以有多个@Process()处理器,以反应每个任务类型(name),确保每个name有相应的处理者。
@Process('transcode')async transcode(job: Job<unknown>) { ... }
事件监听者
当队列和/或任务状态改变时,Bull生成一个有用的事件集合。Nest 提供了一个装饰器集合,允许订阅一系列标准核心事件集合。他们从@nestjs/bull包中导出。
事件监听者必须在一个消费者类中声明(通过@Processor()装饰器)。要监听一个事件,使用如下表格之一的装饰器来声明一个事件处理器。例如,当一个任务进入audio队列活跃状态时,要监听其发射的事件,使用下列结构:
import { Processor, Process } from '@nestjs/bull';import { Job } from 'bull';@Processor('audio')export class AudioConsumer {@OnQueueActive()onActive(job: Job) {console.log(`Processing job ${job.id} of type ${job.name} with data ${job.data}...`,);}
鉴于 BUll 运行于分布式(多 node)环境,它定义了本地事件概念。该概念可以辨识出一个由完整的单一进程触发的事件,或者由不同进程共享的队列。一个本地事件是指在本地进程中触发的一个队列行为或者状态变更。换句话说,当你的事件生产者和消费者是本地单进程时,队列中所有事件都是本地的。
当一个队列在多个进程中共享时,我们可能要遇到全局事件。对一个由其他进程触发的事件通知器进程的监听者来说,它必须注册为全局事件。
当相应事件发射时事件处理器被唤醒。该处理器被下表所示的签名调用,提供访问事件相关的信息。我们讨论下面签名中本地和全局事件处理器。
| 本地事件监听者 | 全局事件监听者 | 处理器方法签名/当触发时 | |
|---|---|---|---|
| @OnQueueError() | @OnGlobalQueueError() | handler(error: Error) - 当错误发生时,error包括触发错误 |
|
| @OnQueueWaiting() | @OnGlobalQueueWaiting() | handler(jobId: number \ | string) - 一旦工作者空闲就等待执行的任务,jobId包括进入此状态的 id |
| @OnQueueActive() | @OnGlobalQueueActive() | handler(job: Job)-job任务已启动 |
|
| @OnQueueStalled() | @OnGlobalQueueStalled() | handler(job: Job)-job任务被标记为延迟。这在时间循环崩溃或暂停时进行调试工作时是很有效的 |
|
| @OnQueueProgress() | @OnGlobalQueueProgress() | handler(job: Job, progress: number)-job任务进程被更新为progress值 |
|
| @OnQueueCompleted() | @OnGlobalQueueCompleted() | handler(job: Job, result: any) job任务进程成功以result结束 |
|
| @OnQueueFailed() | @OnGlobalQueueFailed() | handler(job: Job, err: Error)job任务以err原因失败 |
|
| @OnQueuePaused() | @OnGlobalQueuePaused() | handler()队列被暂停 | |
| @OnQueueResumed() | @OnGlobalQueueResumed() | handler(job: Job)队列被恢复 | |
| @OnQueueCleaned() | @OnGlobalQueueCleaned() | handler(jobs: Job[], type: string) 旧任务从队列中被清理,job是一个清理任务数组,type是要清理的任务类型 |
|
| @OnQueueDrained() | @OnGlobalQueueDrained() | handler()在队列处理完所有等待的任务(除非有些尚未处理的任务被延迟)时发射出 | |
| @OnQueueRemoved() | @OnGlobalQueueRemoved() | handler(job: Job)job任务被成功移除 |
当监听全局事件时,签名方法可能和本地有一点不同。特别地,本地版本的任何方法签名接受job对象的方法签名而不是全局版本的jobId(number)。要在这种情况下获取实际的job对象的引用,使用Queue#getJob方法。这种调用可能需要等待,因此处理者应该被声明为async,例如:
@OnGlobalQueueCompleted()async onGlobalCompleted(jobId: number, result: any) {const job = await this.immediateQueue.getJob(jobId);console.log('(Global) on completed: job ', job.id, ' -> result: ', result);}
?> 要获取一个Queue对象(使用getJob()调用),你当然必须注入它。同时,队列必须注册到你要注入的模块中。
在特定事件监听器装饰器之外,你可以使用通用的@OnQueueEvent()装饰器与BullQueueEvents或者BullQueueGlobalEvents枚举相结合。在这里阅读更多有关事件的内容。
队列管理
队列有一个 API 来实现管理功能比如暂停、恢复、检索不同状态的任务数量等。你可以在这里找到完整的队列 API。直接在Queue对象上调用这些方法,如下所示的暂停/恢复示例。
使用pause()方法调用来暂停队列。一个暂停的队列在恢复前将不会处理新的任务,但会继续处理完当前执行的任务。
await audioQueue.pause();
要恢复一个暂停的队列,使用resume()方法,如下:
await audioQueue.resume();
异步配置
你可能需要异步而不是静态传递队列选项。在这种情况下,使用registerQueueAsync()方法,可以提供不同的异步配置方法。
一个方法是使用工厂函数:
BullModule.registerQueueAsync({name: 'audio',useFactory: () => ({redis: {host: 'localhost',port: 6379,},}),});
我们的工厂函数方法和其他异步提供者(它可以是async的并可以使用inject来注入)方法相同。
BullModule.registerQueueAsync({name: 'audio',imports: [ConfigModule],useFactory: async (configService: ConfigService) => ({redis: {host: configService.get('QUEUE_HOST'),port: +configService.get('QUEUE_PORT'),},}),inject: [ConfigService],});
可选的,你可以使用useClass语法。
BullModule.registerQueueAsync({name: 'audio',useClass: BullConfigService,});
上述结构在BullModule中实例化BullConfigService,并通过调用createBullOptions()来用它提供一个选项对象。注意这意味着BullConfigService要实现BullOptionsFactory工厂接口,如下:
@Injectable()class BullConfigService implements BullOptionsFactory {createBullOptions(): BullModuleOptions {return {redis: {host: 'localhost',port: 6379,},};}}
要阻止在BullModule中创建BullConfigService并使用一个从其他模块导入的提供者,可以使用useExisting语法。
BullModule.registerQueueAsync({name: 'audio',imports: [ConfigModule],useExisting: ConfigService,});
这个结构和useClass有一个根本区别——BullModule将查找导入的模块来重用现有的ConfigServie而不是实例化一个新的。
示例
一个可用的示例见这里。
日志
Nest 附带一个默认的内部日志记录器实现,它在实例化过程中以及在一些不同的情况下使用,比如发生异常等等(例如系统记录)。这由 @nestjs/common 包中的 Logger 类实现。你可以全面控制如下的日志系统的行为:
- 完全禁用日志
- 指定日志系统详细水平(例如,展示错误,警告,调试信息等)
- 覆盖默认日志记录器的时间戳(例如使用 ISO8601 标准作为日期格式)
- 完全覆盖默认日志记录器
- 通过扩展自定义默认日志记录器
- 使用依赖注入来简化编写和测试你的应用
你也可以使用内置日志记录器,或者创建你自己的应用来记录你自己应用水平的事件和消息。
更多高级的日志功能,可以使用任何 Node.js 日志包,比如Winston,来生成一个完全自定义的生产环境水平的日志系统。
基础自定义
要禁用日志,在(可选的)Nest 应用选项对象中向 NestFactory.create() 传递第二个参数设置 logger 属性为 false 。
const app = await NestFactory.create(ApplicationModule, {logger: false,});await app.listen(3000);
你也可以只启用特定日志级别,设置一个字符串形式的 logger 属性数组以确定要显示的日志水平,如下:
const app = await NestFactory.create(ApplicationModule, {logger: ['error', 'warn'],});await app.listen(3000);
数组中的字符串可以是以下字符串的任意组合: log , error , warn , debug 和 verbose 。
?>你可以通过设置 NO_COLOR 环境变量为非空字符串来禁用默认日志信息的颜色
自定义应用
你可以提供一个自定义日志记录器应用,并由 Nest 作为系统记录使用,这需要设置logger 属性到一个满足 LoggerService 接口的对象。例如,你可以告诉 Nest 使用内置的全局 JavaScript console 对象(其实现了 LoggerService 接口),如下:
const app = await NestFactory.create(ApplicationModule, {logger: console,});await app.listen(3000);
应用你的自定义记录器很简单。只要简单实现以下 LoggerService 接口中的每个方法就可以:
import { LoggerService } from '@nestjs/common';export class MyLogger implements LoggerService {/*** Write a 'log' level log.*/log(message: any, ...optionalParams: any[]) {}/*** Write an 'error' level log.*/error(message: any, ...optionalParams: any[]) {}/*** Write a 'warn' level log.*/warn(message: any, ...optionalParams: any[]) {}/*** Write a 'debug' level log.*/debug?(message: any, ...optionalParams: any[]) {}/*** Write a 'verbose' level log.*/verbose?(message: any, ...optionalParams: any[]) {}}
你可以通过 logger 属性为 Nest 应用的选项对象提供一个 MyLogger 实例:
const app = await NestFactory.create(ApplicationModule, {logger: new MyLogger(),});await app.listen(3000);
这个技术虽然很简单,但是没有为 MyLogger 类应用依赖注入。这会带来一些挑战,尤其在测试方面,同时也限制了 MyLogger 的重用性。更好的解决方案参见如下的依赖注入部分。
扩展内置的日志类
很多实例操作需要创建自己的日志。你不必完全重新发明轮子。只需继承内置 ConsoleLogger 类以部分覆盖默认实现,并使用 super 将调用委托给父类。
import { ConsoleLogger } from '@nestjs/common';export class MyLogger extends ConsoleLogger {error(message: any, stack?: string, context?: string) {// add your tailored logic heresuper.error.apply(this, arguments);}}
你可以按如下使用应用记录器来记录部分所述,从你的特征模块中使用扩展记录器。
你可以把你的扩展日志记录器的实例传递到应用选项对象的 logger 属性来让 Nest 使用你的日志记录器记录系统日志(如自定义应用所述),也可以按照如下的依赖注入部分。如果你这样做,你在调用 super 时要小心,如上述代码示例,要委托一个特定的日志方法,调用其父(内置)类,以便 Nest 可以依赖需要的内置特征。
依赖注入
你可能需要利用依赖注入的优势来使用高级的日志记录功能。例如,你可能想把 ConfigService 注入到你的记录器中来对它自定义,然后把自定义记录器注入到其他控制器和/或提供者中。要为你的自定义记录器启用依赖注入,创建一个实现 LoggerService 的类并将其作为提供者注册在某些模块中,例如,你可以:
- 定义一个
MyLogger类来继承内置的ConsoleLogger或者完全覆盖它,如前节所述。注意一定要实现LoggerService接口。 - 创建一个
LoggerModule如下所示,从该模块中提供MyLogger。
import { Module } from '@nestjs/common';import { MyLogger } from './my-logger.service.ts';@Module({providers: [MyLogger],exports: [MyLogger],})export class LoggerModule {}
通过这个结构,你现在可以提供你的自定义记录器供其他任何模块使用。因为你的 MyLogger 类是模块的一部分,它也可以使用依赖注入(例如,注入一个 ConfigService )。提供自定义记录器供使用还需要一个技术,即 Nest 的系统记录(例如,供 bootstrapping 和 error handling )。
由于应用实例化( NestFactory.create() )在任何模块上下文之外发生,它不能参与初始化时正常的依赖注入阶段。因此我们必须保证至少一个应用模块导入了 LoggerModule 来触发 Nest ,从而生成一个我们的 MyLogger 类的单例。
我们可以在之后按照下列知道来告诉 Nest 使用同一个 MyLogger 实例。
const app = await NestFactory.create(ApplicationModule, {bufferLogs: true,});app.useLogger(app.get(MyLogger));await app.listen(3000);
?>在上面的例子中,我们把 bufferLogs 设置为 true 以确保所有的日志都会被放入缓冲区直到一个自定义的日志记录器被接入(在上面的例子中是 MyLogger )并且应用初始化成功或者失败。如果初始化失败,Nest 会回退到原始的 ConsoleLogger 以打印出错误信息。你也可以将 autoFlushLogs 设置为 false (默认为 true )来手动刷新日志缓冲区(使用 Logger#flush() 方法)。
在这里我们在 NestApplication 实例中用了 get() 方法以获取 MyLogger 对象的单例。这个技术在根本上是个“注入”一个日志记录器的实例供 Nest 使用的方法。 app.get() 调用获取 MyLogger 单例,并且像之前所述的那样依赖于第一个注入到其他模块的实例。
你也可以在你的特征类中注入这个 MyLogger 提供者,从而保证 Nest 系统记录和应用记录行为一致。参考为应用记录使用记录器和注入一个自定义日志记录器章节以获取更多信息。
为应用记录使用记录器
我们可以组合上述几种技术来提供一致性的行为和格式化以保证我们的应用事件/消息记录和 Nest 系统记录一致。
一个很好的实践是在每个提供者内实例化 @nestjs/common 内的 Logger 类。我们可以将提供者的名字当作 context 参数传入 Logger 的构造函数,就像这样:
import { Logger, Injectable } from '@nestjs/common';@Injectable()class MyService {private readonly logger = new Logger(MyService.name);doSomething() {this.logger.log('Doing something...');}}
在默认的日志记录器实现中, context 是包裹在方括号中被打印出来,就像下面例子中的 NestFactory :
[Nest] 19096 - 12/08/2019, 7:12:59 AM [NestFactory] Starting Nest application...
如果我们通过 app.useLogger() 提供一个自定义日志记录器,那么它会在 Nest 内部被使用。这就意味着我们的代码可以保持与实现无关,因为我们可以简单地调用 app.useLogger() 用默认日志记录器来代替自定义的那个。
如果我们跟着前面章节一步步做下来并且调用了 app.useLogger(app.get(MyLogger)) ,那么接下来在 MyService 中对 this.logger.log() 的调用会造成对 MyLogger 实例中方法 log 的调用。
这个应该在大多数情况下都适用。但是你如果想要更深入的自定义(比如增加或者调用自定义方法),请看下一章节。
注入自定义日志记录器
通过像下面一样的扩展内置的日志记录器来开始这一章节。我们传入 scope 选项来配置 ConsoleLogger 的元数据,通过指定瞬态作用域来保证在每个模块内都有独一无二的 MyLogger 的实例。在下面的例子中,我们没有扩展每个单独的 ConsoleLogger 方法(比如 log() , warn() 之类),尽管你可能会选择去这么做。
import { Injectable, Scope, ConsoleLogger } from '@nestjs/common';@Injectable({ scope: Scope.TRANSIENT })export class MyLogger extends ConsoleLogger {customLog() {this.log('Please feed the cat!');}}
接下来,我们采用如下结构创建一个 LoggerModule 。
import { Module } from '@nestjs/common';import { MyLogger } from './my-logger.service';@Module({providers: [MyLogger],exports: [MyLogger],})export class LoggerModule {}
然后,在你的模块中导入 LoggerModule 。因为我们继承了默认的 Logger ,所以我们可以很方便地调用 setContext 方法。之后我们就可以像下面一样开始使用这个包含了上下文的日志记录器了。
@Injectable()export class CatsService {private readonly cats: Cat[] = [];constructor(private myLogger: MyLogger) {// 因为瞬态作用域的原因, CatService 有属于自己的独一无二的 MyLogger 实例,// 所以在这里设置上下文不会影响到其他提供者的实例this.myLogger.setContext('CatsService');}findAll(): Cat[] {// 你可以调用所有的默认方法this.myLogger.warn('About to return cats!');// 当然还有你的自定义方法this.myLogger.customLog();return this.cats;}}
最后,和下面一样在你的 main.ts 文件中让 Nest 使用自定义日志记录器的实例。当然这只是个例子,我们还没有真正自定义日志记录器的行为(通过扩展像 log() , warn() 这些 Logger 的方法),所以这一步并不一定需要。但是如果你给这些方法增加了自定义的逻辑而且你想让 Nest 去使用这个实现,那么你还是会需要这一步。
const app = await NestFactory.create(ApplicationModule, {bufferLogs: true,});app.useLogger(new MyLogger());await app.listen(3000);
?>除了把 bufferLogs 设置为true,你也可以声明 logger: false 来临时禁用日志记录器。需要注意的是如果你给 NestFactory.create 提供了 logger: false ,在你调用 useLogger 以前没有东西会被记录进日志,所以你可能会错过一些重要的初始化错误。如果你不在意一些初始化信息会使用默认日志记录器来记录,那你可以直接设置 logger: false 。
使用外部记录器
生产环境应用通常包括特定的记录需求,包括高级过滤器,格式化和中心化记录。Nest 的内置记录器用于监控 Nest 系统状态,在开发时也可以为你的特征模块提供实用的基础的文本格式的记录,但生产环境可能更倾向于使用类似Winston的模块,这是一个标准的 Node.js 应用,你可以在 Nest 中体验到类似模块的优势。
Cookies
一个HTTP cookie是指存储在用户浏览器中的一小段数据。Cookies被设计为创建一种可靠的机制让网站来记录状态信息。当用户再次访问网站时,发出的请求会自带cookie。
在Express中使用(默认)
首先安装需要的包(以及TypeScript用户需要的类型包):
$ npm i cookie-parser$ npm i -D @types/cookie-parser
安装完成后,将cookie-parser配置为全局中间件(例如在main.ts文件中)。
import * as cookieParser from 'cookie-parser';// somewhere in your initialization fileapp.use(cookieParser());
可以向cookieParser中间件中传递一些参数:
secret: 一个字符串或者数组,用来给cookie签名。如果不指定这个选项,将不解析签名的cookie。如果提供了一个字符串,那么它会被用来作为secret。如果提供了一个数组,将尝试依次使用其元素来作为secret解析cookie。option:一个作为第二个参数传递给cookie.parse的对象,参见cookie来了解更多内容。
该中间件将从请求的头文件中解析Cookie并将其数据作为req.cookies暴露出来。如果提供了secret,将暴露为req.signedCookies。这些属性以cookie名称和属性的键值对保存。
当提供了secret时,该中间件将解析并验证所有签名的cookie并将其值从req.cookies移动到req.signedCookies。签名cookie是指包含s:前缀的cookie。验证失败的签名cookie值会被替换为false而不是被篡改过的值。
当这些完成后,就可以从路径处理程序中读取cookie了,例如:
@Get()findAll(@Req() request: Request) {console.log(request.cookies); // or "request.cookies['cookieKey']"// or console.log(request.signedCookies);}
?> @Req()装饰器从@nestjs/common中引入,Request从express中引入。
要在输出的响应中附加cookie,使用Response#cookie()方法:
@Get()findAll(@Res({ passthrough: true }) response: Response) {response.cookie('key', 'value')}
!> 如果你想把相应处理逻辑留给框架,需要将passthrough参数设置为true,如上所示。参见这里
?> @Res()装饰器从@nestjs/common中引入,Response从express中引入。
在Fastify中使用
首先安装需要的包:
npm i fastify-cookie···安装完成后,注册`fastify-cookie`插件。```TypeScriptimport fastifyCookie from 'fastify-cookie';// somewhere in your initialization fileconst app = await NestFactory.create<NestFastifyApplication>(AppModule,new FastifyAdapter(),);app.register(fastifyCookie, {secret: 'my-secret', // for cookies signature});
当这些完成后,就可以从路径处理程序中读取cookie了,例如:
@Get()findAll(@Req() request: FastifyRequest) {console.log(request.cookies); // or "request.cookies['cookieKey']"}}
?> @Req()装饰器从@nestjs/common中引入,FastifyRequest从fastify中引入。
要在输出的响应中附加cookie,使用FastifyReply#setCookie()方法:
@Get()findAll(@Res({ passthrough: true }) response: FastifyReply) {response.setCookie('key', 'value')}
要了解更多FastifyReply#setCookie(),可参见这里。
!> 如果你想把相应处理逻辑留给框架,需要将passthrough参数设置为true,如上所示。参见这里
?> @Res()装饰器从@nestjs/common中引入,FastifyReply从fastify中引入。
创建一个自定义装饰器(跨平台)
可以创建一个自定义装饰器,来提供一个方便易用、声明清晰的方式来处理cookie。
import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {const request = ctx.switchToHttp().getRequest();return data ? request.cookies?.[data] : request.cookies;},);
这个@Cookie()装饰器将解析所有cookie,或者从req.cookie对象中解析一个命名的cookie并用其值来填充解析的参数。
现在就可以使用该装饰器,从路径处理程序中读取cookie了,例如:
@Get()findAll(@Cookies('name') name: string) {}
事件
Event Emitter 事件发射器 包(@nestjs/event-emitter)提供了一个简单的观察者实现,允许你订阅和监听在你应用中发生的不同事件。事件服务器是将应用程序的不同部分解耦的一个伟大的方法,因为一个事件可以被不同的监听者监听,且他们之间并不互相依赖。
EventEmitterModule在内部使用eventmitter2包。
开始
首先安装依赖。
$ npm i --save @nestjs/event-emitter
安装完成后,在root的AppModule中引入EventEmitterModule并且在forRoot()静态方法中运行:
import { Module } from '@nestjs/common';import { EventEmitterModule } from '@nestjs/event-emitter';@Module({imports: [EventEmitterModule.forRoot()],})export class AppModule {}
.forRoot()调用与初始化事件发射器并注册应用程序中各个声明的事件监听器。注册发生在onApplicationBootstrap生命周期钩子开始的时候,以确保所有模块都已加载,所有定时任务已声明。
配置文件在EventEmitter实例中,在forRoot()方法中传递配置对象,如下:
EventEmitterModule.forRoot({// set this to `true` to use wildcardswildcard: false,// the delimiter used to segment namespacesdelimiter: '.',// set this to `true` if you want to emit the newListener eventnewListener: false,// set this to `true` if you want to emit the removeListener eventremoveListener: false,// the maximum amount of listeners that can be assigned to an eventmaxListeners: 10,// show event name in memory leak message when more than maximum amount of listeners is assignedverboseMemoryLeak: false,// disable throwing uncaughtException if an error event is emitted and it has no listenersignoreErrors: false,});
分派事件
要分派一个事件(例如 fire), 使用标准构造函数注入EventEmitter2。
constructor(private eventEmitter: EventEmitter2) {}
?> EventEmitter2 从 @nestjs/event-emitter 包中导入。
然后在类中使用它:
this.eventEmitter.emit('order.created',new OrderCreatedEvent({orderId: 1,payload: {},}),);
监听事件
要声明一个事件监听器,用@OnEvent()装饰器装饰一个方法,装饰器在包含要执行代码的方法之前,如下:
@OnEvent('order.created')handleOrderCreatedEvent(payload: OrderCreatedEvent) {// handle and process "OrderCreatedEvent" event}
!> 事件订阅器不能是请求范围的。
在简单的事件发射器中,第一个参数可以是字符串或者符号,在通配的事件发射器中,可以是字符串|符号|数组<字符串|符号>。第二个参数(可选的)是一个监听器的选项对象(参见这里)。
@OnEvent('order.created', { async: true })handleOrderCreatedEvent(payload: OrderCreatedEvent) {// handle and process "OrderCreatedEvent" event}
要使用命名空间或者通配符,传递wildcard选项到EventEmitterModule#forRoot()方法中。当命名空间/通配符启用时,事件可以是句点隔开的(foo.bar)形式或者数组(['foo','bar']),句点也可以配置为一个配置属性(delimiter)。命名空间启用时,你可以使用通配符订阅事件。
@OnEvent('order.*')handleOrderEvents(payload: OrderCreatedEvent | OrderRemovedEvent | OrderUpdatedEvent) {// handle and process an event}
注意,这样的通配符仅对一个块有效。参数order.*将匹配例如order.creted和order.shipped事件,但不会匹配order.delayed.out_of_stock。要监听这样的事件,使用多层通配符模式(例如**)。见EventEmitter2文档。
@OnEvent('**')handleEverything(payload: any) {// handle and process an event}
?> EventEmitter2类提供了一些有用的方法来和事件交互,例如waitFor和onAny,参见这里。
压缩
压缩可以大大减小响应主体的大小,从而提高 Web 应用程序的速度。
在大业务量的生产环境网站中,强烈推荐将压缩功能从应用服务器中卸载——典型做法是使用反向代理(例如 Nginx)。在这种情况下,你不应该使用压缩中间件。
配合 Express 使用(默认)
使用压缩中间件启用 gzip 压缩。
首先,安装所需的包:
$ npm i --save compression
安装完成后,将其应用为全局中间件。
import * as compression from 'compression';// somewhere in your initialization fileapp.use(compression());
配合 Fastify 使用
如果你在使用的是 FastifyAdapter,请考虑使用 fastify-compress。
$ npm i --save fastify-compress
安装完成后,将其应用为全局中间件。
import * as compression from 'fastify-compress';// somewhere in your initialization fileapp.register(compression);
默认地,如果浏览器支持编码,fastify-compress使用Brotli压缩(Node>=11.7.0)。Brotli在压缩比方面非常有效,但也非常慢。鉴于此,你可能想告诉fastify-compress仅使用deflate和gzip来压缩相应,你最终会得到一个较大的相应但是可以传输的更快。
要指定编码,向app.register提供第二个参数:
app.register(compression, { encodings: ['gzip', 'deflate'] });
上述内容告诉fastify-compress仅使用 gzip 和 deflate 编码,如果客户端同时支持两种,则以 gzip 优先。
文件上传
为了处理文件上传,Nest 提供了一个内置的基于 multer 中间件包的 Express 模块。Multer 处理以 multipart/form-data 格式发送的数据,该格式主要用于通过 HTTP POST 请求上传文件。这个模块是完全可配置的,您可以根据您的应用程序需求调整它的行为。
!> Multer无法处理不是受支持的多部分格式( multipart/form-data )的数据。 另外,请注意此程序包与 FastifyAdapter 不兼容。
为了更好的类型安全,我们来安装 Multer 的类型声明包:
$ npm i -D @types/multer
只要这个模块被安装,我们就可以使用 Express.Multer.File 这个类型(你可以通过 import { Express } from 'express' 导入这个类型)。
基本实例
当我们要上传单个文件时, 我们只需将 FileInterceptor() 与处理程序绑定在一起, 然后使用 @UploadedFile() 装饰器从 request 中取出 file。
@Post('upload')@UseInterceptors(FileInterceptor('file'))uploadFile(@UploadedFile() file: Express.Multer.File) {console.log(file);}
?> FileInterceptor() 装饰器是 @nestjs/platform-express 包提供的, @UploadedFile() 装饰器是 @nestjs/common 包提供的。
FileInterceptor() 接收两个参数:
一个
fieldName(指向包含文件的 HTML 表单的字段)可选
options对象, 类型为MulterOptions。这个和被传入 multer 构造函数 (此处有更多详细信息) 的对象是同一个对象。
!> FileInterceptor() 可能不兼容诸如 Google Firebase 之类的第三方云服务商。
文件数组
为了上传文件数组,我们使用 FilesInterceptor()。请使用 FilesInterceptor() 装饰器(注意装饰器名称中的复数文件)。这个装饰器有三个参数:
fieldName:(保持不变)maxCount:可选的数字,定义要接受的最大文件数options:可选的MulterOptions对象 ,如上所述
使用 FilesInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件。
@Post('upload')@UseInterceptors(FilesInterceptor('files'))uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {console.log(files);}
?> FilesInterceptor() 装饰器是 @nestjs/platform-express 包提供的, @UploadedFiles() 装饰器是 @nestjs/common 包提供的。
多个文件
要上传多个文件(全部使用不同的键),请使用 FileFieldsInterceptor() 装饰器。这个装饰器有两个参数:
uploadedFields:对象数组,其中每个对象指定一个必需的name属性和一个指定字段名的字符串值(如上所述),以及一个可选的maxCount属性(如上所述)options: 可选的MulterOptions对象,如上所述
使用 FileFieldsInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件。
@Post('upload')@UseInterceptors(FileFieldsInterceptor([{ name: 'avatar', maxCount: 1 },{ name: 'background', maxCount: 1 },]))uploadFile(@UploadedFiles() files: { avatar?: Express.Multer.File[], background?: Express.Multer.File[] }) {console.log(files);}
任何文件
要使用任意字段名称键上载所有字段,请使用 AnyFilesInterceptor() 装饰器。该装饰器可以接受如上所述的可选选项对象。
使用 AnyFilesInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件。
@Post('upload')@UseInterceptors(AnyFilesInterceptor())uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {console.log(files);}
默认选项
您可以像上面描述的那样在文件拦截器中指定 multer 选项。要设置默认选项,可以在导入 MulterModule 时调用静态 register() 方法,传入受支持的选项。您可以使用这里列出的所有选项。
MulterModule.register({dest: '/upload',});
?> MulterModule 类是 @nestjs/platform-express 包提供的。
异步配置
当需要异步而不是静态地设置 MulterModule 选项时,请使用 registerAsync() 方法。与大多数动态模块一样,Nest 提供了一些处理异步配置的技术。
第一种可能的方法是使用工厂函数:
MulterModule.registerAsync({useFactory: () => ({dest: '/upload',}),});
与其他工厂提供程序一样,我们的工厂函数可以是异步的,并且可以通过 inject 选项注入依赖。
MulterModule.registerAsync({imports: [ConfigModule],useFactory: async (configService: ConfigService) => ({dest: configService.getString('MULTER_DEST'),}),inject: [ConfigService],});
或者,您可以使用类而不是工厂来配置 MulterModule,如下所示:
MulterModule.registerAsync({useClass: MulterConfigService,});
上面的构造在 MulterModule 中实例化 MulterConfigService ,使用它来创建所需的选项对象。注意,在本例中,MulterConfigService 必须实现 MulterOptionsFactory 接口,如下所示。MulterModule 将在提供的类的实例化对象上调用 createMulterOptions() 方法。
@Injectable()class MulterConfigService implements MulterOptionsFactory {createMulterOptions(): MulterModuleOptions {return {dest: '/upload',};}}
如果你想要重复使用一个已经存在的选项提供者而不是在 MulterModule 内创建一个私有的拷贝,使用 useExisting 语法。
MulterModule.registerAsync({imports: [ConfigModule],useExisting: ConfigService,});
例子
一个能够运行的样例在这里。
流处理文件
!> 这个章节将向你展示如何在HTTP应用中流处理文件。以下例子不适用于GraphQL或者微服务应用
有时你可能想从你的REST API向客户端发送文件,想要在Nest中做到这一点,你可能会像下面这样做:
@Controller('file')export class FileController {@Get()getFile(@Res() res: Response) {const file = createReadStream(join(process.cwd(), 'package.json'));file.pipe(res);}}
但是这样一来,你就会丢失控制器之后的拦截器逻辑。想要避免这一点,你可以返回一个StreamableFile实例,框架会帮你使用管道传输响应。
Streamable File类
StreamableFile是一个持有要返回的流的类。你可以传入一个Buffer或者Stream到StreamableFile类的构造函数来创建一个新的StreamableFile实例。
?> StreamableFile类可以从@nestjs/common中导入
跨平台支持
默认情况下,Fastify服务器可以不通过stream.pipe(res)直接发送文件,所以你并不需要使用StreamableFile类。但是,Nest仍然支持在所有这些类型的平台上使用StreamableFile,所以即使你需要在Express和Fastify之间切换,也不需要担心这两个引擎上的兼容性问题。
例子
下面是一个作为文件而不是JSON返回package.json的例子,当然,它也适用于图片、文档和所有其他类型的文件。
import { Controller, Get, StreamableFile } from '@nestjs/common';import { createReadStream } from 'fs';import { join } from 'path';@Controller('file')export class FileController {@Get()getFile(): StreamableFile {const file = createReadStream(join(process.cwd(), 'package.json'));return new StreamableFile(file);}}
返回的默认类型是 application/octet-stream,如果你需要自定义响应类型,你可以使用 res.set 方法。
import { Controller, Get, StreamableFile, Response } from '@nestjs/common';import { createReadStream } from 'fs';import { join } from 'path';@Controller('file')export class FileController {@Get()getFile(@Response({ passthrough: true }) res): StreamableFile {const file = createReadStream(join(process.cwd(), 'package.json'));res.set({'Content-Type': 'application/json','Content-Disposition': 'attachment; filename="package.json"',});return new StreamableFile(file);}}
HTTP 模块
Axios 是功能很丰富的 HTTP 客户端, 广泛应用于许多应用程序中。这就是为什么 Nest 包装这个包, 并以内置模块 HttpModule 的形式暴露它。HttpModule 导出 HttpService, 它只是暴露了基于 Axios 的方法来执行 HTTP 请求, 而且还将返回类型转换为 Observables。
?>你也可以直接使用包括 got 或 undici 在内的任何通用的 Node.js 的 HTTP 客户端。
安装
我们先安装需要的依赖来开始使用它
$ npm i --save @nestjs/axios
开始使用
安装完成之后,想要使用 HttpService ,我们需要导入 HttpModule 。
@Module({imports: [HttpModule],providers: [CatsService],})export class CatsModule {}
接下来,使用构造函数来注入 HttpService。
?> HttpModule 和 HttpService 是 @nestjs/axios 包提供的
@Injectable()export class CatsService {constructor(private readonly httpService: HttpService) {}findAll(): Observable<AxiosResponse<Cat[]>> {return this.httpService.get('http://localhost:3000/cats');}}
?> AxiosResponse 是 axios 包( $ npm i axios )暴露的接口。
所有 HttpService 的方法都返回一个包裹在 Observable 对象内的 AxiosResponse 。
配置
Axios 提供了许多选项,您可以利用这些选项来增加您的 HttpService 功能。在这里阅读更多相关信息。要配置底层的 Axios 实例,请使用 HttpModule 的 register() 方法。所有这些属性都将直接传递给底层的 Axios 构造函数。
@Module({imports: [HttpModule.register({timeout: 5000,maxRedirects: 5,}),],providers: [CatsService],})export class CatsModule {}
异步配置
如果你要给模块异步地传递选项,就使用 registerAsync() 方法。就像大多数动态模块一样, Nest 提供了几种处理异步数据的方法。
一种方法是使用工厂函数:
HttpModule.registerAsync({useFactory: () => ({timeout: 5000,maxRedirects: 5,}),});
就像其他工厂提供者一样,我们的工厂函数可以是异步的而且可以通过 inject 参数注入依赖。
HttpModule.registerAsync({imports: [ConfigModule],useFactory: async (configService: ConfigService) => ({timeout: configService.getString('HTTP_TIMEOUT'),maxRedirects: configService.getString('HTTP_MAX_REDIRECTS'),}),inject: [ConfigService],});
或者,你可以使用类而不是工厂来配置 HttpModule ,如下面所示。
HttpModule.registerAsync({useClass: HttpConfigService,});
上面的构造将在 HttpModule 中实例化 HttpConfigService,并利用它来创建一个选项对象。注意这个例子, HttpConfigService 必须和下面所示的一样实现 HttpModuleOptionsFactory 接口。 HttpModule 会调用被提供的类的实例上的 createHttpOptions() 方法。
@Injectable()class HttpConfigService implements HttpModuleOptionsFactory {createHttpOptions(): HttpModuleOptions {return {timeout: 5000,maxRedirects: 5,};}}
如果你想要重复使用一个已经存在的选项提供者而不是在 HttpModule 内创建一个私有的拷贝,使用 useExisting 语法。
HttpModule.registerAsync({imports: [ConfigModule],useExisting: ConfigService,});
Session(会话)
HTTP session提供了一个用于在不同请求间存储信息的方法,这在MVC架构应用中非常有用。
在Express中使用(默认)
首先安装需要的包(以及TypeScript用户需要的类型包):
$ npm i express-session$ npm i -D @types/express-session
安装完成后,将express-session配置为全局中间件(例如在main.ts文件中)。
import * as session from 'express-session';// somewhere in your initialization fileapp.use(session({secret: 'my-secret',resave: false,saveUninitialized: false,}),);
!> 在生产环境中,有意的默认不在服务器端提供会话存储。因为这在很多场合下会造成内存泄漏,不能扩展到单个进程,因此仅用于调试和开发环境。参见官方仓库。
secret用于加密该会话 IDcookie,它可以是一个字符串用于单一加密,或者数组用来多重加密。如果提供了一个数组,只有第一个元素被用来加密会话 IDcookie,其他元素将被用于验证签名请求。密码本身不应该过于容易被人工解析,最好使用一组随机字符。
使能resave选项会强制重新保存会话即使在请求过程中它未被修改过。其默认值为true,但不赞成使用默认值,在未来这个默认值将被修改。
类似地,使能saveUninitialized选项将强制存储一个未初始化的会话。一个未初始化的会话可能是一个新的尚未修改的会话。配置为false用于登陆会话是很有用的,可以减少服务器存储,或者遵循法律规定在存储用户cookie前需要获得用户授权。配置为false在一个客户端在无会话情况下建立多个请求的状况下会很有用。参见这里。
还可以给session中间件传递更多参数,参见API 文档。
?> 注意secure:true是推荐选项。然而,它需要启用了https的网站,也就是说,HTTPS对安全cookie来说是必须的。如果配置了secure,但是通过HTTP访问网站,将不会保存cookie。如果你的node.js在代理之后,并且启用了secure:true选项,你需要在express中配置trust proxy选项。
当这些完成后,就可以从路径处理程序中读取session了,例如:
@Get()findAll(@Req() request: Request) {req.session.visits = req.session.visits ? req.session.visits + 1 : 1;}
?> @Req()装饰器从@nestjs/common中引入,@Request从express中引入。
另外,你也可以使用@Session()装饰器来从请求解压一个session对象:
@Get()findAll(@Session() session: Record<string, any>) {session.visits = session.visits ? session.visits + 1 : 1;}
在Fastify中使用
首先安装需要的包:
$ npm i fastify-secure-session···安装完成后,注册`fastify-secure-session`插件。```TypeScriptimport secureSession from 'fastify-secure-session';// somewhere in your initialization fileconst app = await NestFactory.create<NestFastifyApplication>(AppModule,new FastifyAdapter(),);app.register(secureSession, {secret: 'averylogphrasebiggerthanthirtytwochars',salt: 'mq9hDxBVDbspDR6n',});
?> 你也可以预先生成一个key(参见指南)或者使用key 变化。
当这些完成后,就可以从路径处理程序中读取session了,例如:
@Get()findAll(@Req() request: FastifyRequest) {const visits = request.session.get('visits');request.session.set('visits', visits ? visits + 1 : 1);}
?> @Req()装饰器从@nestjs/common中引入,FastifyRequest从fastify中引入。
另外,你也可以使用@Session()装饰器来从请求解压一个session对象:
@Get()findAll(@Session() session: secureSession.Session) {const visits = session.get('visits');session.set('visits', visits ? visits + 1 : 1);}
?> @Session()装饰器从@nestjs/common中引入,secureSession.Session从fastify-secure-session中引入。(引入语句: import * as secureSession from 'fastify-secure-session').
MVC(模型-视图=控制器)
Nest 默认使用 Express 库,因此,在 Express 中使用 MVC (模型 - 视图 - 控制器)模式的每一种技术同样适用于 Nest 。首先,让我们使用 CLI 工具搭建一个简单的 Nest 应用程序:
$ npm i -g @nestjs/cli$ nest new project
为了创建一个简单的 MVC 应用程序,我们必须安装一个模板引擎:
$ npm install --save hbs
我们决定使用 hbs 引擎,但您可以使用任何符合您要求的内容。安装过程完成后,我们需要使用以下代码配置 express 实例:
main.ts
import { NestFactory } from '@nestjs/core';import { NestExpressApplication } from '@nestjs/platform-express';import { join } from 'path';import { AppModule } from './app.module';async function bootstrap() {const app = await NestFactory.create<NestExpressApplication>(AppModule);app.useStaticAssets(join(__dirname, '..', 'public'));app.setBaseViewsDir(join(__dirname, '..', 'views'));app.setViewEngine('hbs');await app.listen(3000);}bootstrap();
我们告诉 express,该 public 目录将用于存储静态文件, views 将包含模板,并且 hbs 应使用模板引擎来呈现 HTML 输出。
app.useStaticAssets 还支持第二个参数来设置虚拟目录。
app.useStaticAssets(join(__dirname, '..', 'public'), {prefix: '/static',});
模板渲染
现在,让我们在该文件夹内创建一个 views 目录和一个 index.hbs 模板。在模板内部,我们将打印从控制器传递的 message:
index.hbs
<!DOCTYPE html><html><head><meta charset="utf-8" /><title>App</title></head><body>{{ message }}</body></html>
然后, 打开 app.controller 文件, 并用以下代码替换 root() 方法:
app.controller.ts
import { Get, Controller, Render } from '@nestjs/common';@Controller()export class AppController {@Get()@Render('index')root() {return { message: 'Hello world!' };}}
在这个代码中,我们指定模板使用@Render()装饰器,同时将路径处理器方法的返回值被传递给要渲染的模板。注意,该返回值是一个包含message属性的对象,和我们之前创建模板中的message占位符对应。
在应用程序运行时,打开浏览器访问 http://localhost:3000/ 你应该看到这个 Hello world! 消息。
动态模板渲染
如果应用程序逻辑必须动态决定要呈现哪个模板,那么我们应该使用 @Res()装饰器,并在路由处理程序中提供视图名,而不是在 @Render() 装饰器中:
?> 当 Nest 检测到 @Res() 装饰器时,它将注入特定于库的响应对象。我们可以使用这个对象来动态呈现模板。在这里了解关于响应对象 API 的更多信息。
app.controller.ts
import { Get, Controller, Render } from '@nestjs/common';import { Response } from 'express';import { AppService } from './app.service';@Controller()export class AppController {constructor(private readonly appService: AppService) {}@Get()root(@Res() res: Response) {return res.render(this.appService.getViewName(), { message: 'Hello world!' });}}
这里有一个可用的例子。
Fastify
如本章所述,我们可以将任何兼容的 HTTP 提供程序与 Nest 一起使用。比如 Fastify 。为了创建具有 fastify 的 MVC 应用程序,我们必须安装以下包:
$ npm i --save fastify point-of-view handlebars
接下来的步骤几乎涵盖了与 express 库相同的内容(差别很小)。安装过程完成后,我们需要打开 main.ts 文件并更新其内容:
main.ts
import { NestFactory } from '@nestjs/core';import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';import { AppModule } from './app.module';import { join } from 'path';async function bootstrap() {const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());app.useStaticAssets({root: join(__dirname, '..', 'public'),prefix: '/public/',});app.setViewEngine({engine: {handlebars: require('handlebars'),},templates: join(__dirname, '..', 'views'),});await app.listen(3000);}bootstrap();
Fastify 的API 略有不同,但这些方法调用背后的想法保持不变。使用 Fastify 时一个明显的需要注意的区别是传递到 @Render() 装饰器中的模板名称包含文件扩展名。
app.controller.ts
import { Get, Controller, Render } from '@nestjs/common';@Controller()export class AppController {@Get()@Render('index.hbs')root() {return { message: 'Hello world!' };}}
在应用程序运行时,打开浏览器并导航至 http://localhost:3000/ 。你应该看到这个 Hello world! 消息。
这里有一个可用的例子。
性能(Fastify)
在底层,Nest 使用了Express框架,但如前所述,它提供了与各种其他库的兼容性,例如 Fastify。Nest应用一个框架适配器,其主要功能是代理中间件和处理器到适当的特定库应用中,从而达到框架的独立性。
?> 注意要应用框架适配器,目标库必须提供在Express 类似的请求/响应管道处理
Fastify 非常适合这里,因为它以与 express 类似的方式解决设计问题。然而,fastify 的速度要快得多,达到了几乎两倍的基准测试结果。问题是,为什么 Nest 仍然使用 express 作为默认的 HTTP 提供程序?因为 express 是应用广泛、广为人知的,而且拥有一套庞大的兼容中间件。
但是由于 Nest 提供了框架独立性,因此您可以轻松地在它们之间迁移。当您对快速的性能给予很高的评价时,Fastify 可能是更好的选择。要使用 Fastify,只需选择 FastifyAdapter本章所示的内置功能。
安装
首先,我们需要安装所需的软件包:
$ npm i --save @nestjs/platform-fastify
适配器(Adapter)
安装 fastify 后,我们可以使用 FastifyAdapter。
import { NestFactory } from '@nestjs/core';import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';import { ApplicationModule } from './app.module';async function bootstrap() {const app = await NestFactory.create<NestFastifyApplication>(ApplicationModule, new FastifyAdapter());await app.listen(3000);}bootstrap();
默认情况下,Fastify仅在 localhost 127.0.0.1 接口上监听(了解更多信息)。如果要接受其他主机上的连接,则应'0.0.0.0'在 listen() 呼叫中指定:
async function bootstrap() {const app = await NestFactory.create<NestFastifyApplication>(ApplicationModule, new FastifyAdapter());await app.listen(3000, '0.0.0.0');}
平台特定的软件包
请记住,当您使用 FastifyAdapter 时,Nest 使用 Fastify 作为 HTTP 提供程序。 这意味着依赖 Express 的每个配方都可能不再起作用。 您应该改为使用 Fastify 等效程序包。
重定向响应
Fastify 处理重定向响应的方式与 Express 有所不同。要使用 Fastify 进行正确的重定向,请同时返回状态代码和 URL,如下所示:
@Get()index(@Res() res) {res.status(302).redirect('/login');}
Fastify 选项
您可以通过构造函数将选项传递给 Fastify的构造 FastifyAdapter 函数。例如:
new FastifyAdapter({ logger: true });
例子
这里有一个工作示例
服务器端事件发送
服务器端事件发送(SSE)是一个服务器推送技术,用来使客户端在HTTP连接下自动接收服务器更新消息。每个消息以一个由一对新行符号作为结束的文字块发送(参见这里)。
使用
要在路径中使能服务器端事件发送(路径在控制器类中注册),用@Sse()装饰器注释该方法处理程序。
@Sse('sse')sse(): Observable<MessageEvent> {return interval(1000).pipe(map((_) => ({ data: { hello: 'world' } })));}
?> @Sse()装饰器从@nestjs/common中导入,Observable, interval, 和 map从rxjs中导入。
!> 服务器端事件发送路径必须返回Observable流。
在上述示例中,我们定义了一个命名的sse来生成实时更新,这些事件可以通过 EventSource API 监听。
sse方法返回一个Observable并发送多个MessageEvent(在本例中,它每秒发射一个新的MessageEvent)。MessageEvent应该与下列接口相匹配。
export interface MessageEvent {data: string | object;id?: string;type?: string;retry?: number;}
这样,我们可以在客户端创建一个EventSource类的实例应用。并将/sse路径(这和我们传递给@Sse()装饰其中的路径字符串一致)作为其构造函数参数。
EventSource实例打开一个和 HTTP 服务器的持久性连接,它以text/event-stream格式发送事件。连接在调用EventSource.close()方法前始终保持。
一旦连接打开,从服务器传来的消息将以事件格式传递给你的代码。如果在传入消息中有事件字段,和事件字段相同的字段将会被触发。如果当前没有该事件字段,则触发一个一般的message事件(参见这里)。
const eventSource = new EventSource('/sse');eventSource.onmessage = ({ data }) => {console.log('New message', JSON.parse(data));};
这里有一个可用的例子。
