技术

优质
小牛编辑
145浏览
2023-12-01

认证(Authentication)

身份验证是大多数现有应用程序的重要组成部分。有许多不同的方法、策略和方法来处理用户授权。我们最终决定使用什幺取决于特定的应用程序要求,并且与它们的需求密切相关。

passport 是目前最流行的 node.js 认证库,为社区所熟知,并相继应用于许多生产应用中。将此工具与 Nest 框架集成起来非常简单。为了演示,我们将设置 passport-http-bearer 和 passport-jwt 策略。

安装

  1. $ npm install --save @nestjs/passport passport passport-jwt passport-http-bearer jsonwebtoken

承载

首先,我们将实现 passport-http-bearer 库。让我们从创建 AuthService 类开始,它将公开一个方法 validateUser(), 该方法的责任是通过提供的承载令牌查询用户。

auth.service.ts

  1. import { Injectable } from '@nestjs/common';
  2. import { UsersService } from '../users/users.service';
  3. @Injectable()
  4. export class AuthService {
  5. constructor(private readonly usersService: UsersService) {}
  6. async validateUser(token: string): Promise<any> {
  7. return await this.usersService.findOneByToken(token);
  8. }
  9. }

validateUser() 方法将 token 作为参数。此 token 是从与HTTP请求一起传递的授权标头中提取的。findOneByToken() 方法的职责是验证传递的 token 是否确实存在,并与数据库中的所有注册帐户关联。

完成 AuthService 后,我们必须创建相应的策略,passport 将使用该策略来验证请求。

http.strategy.ts

  1. import { BearerStrategy } from 'passport-http-bearer';
  2. import { PassportStrategy } from '@nestjs/passport';
  3. import { Injectable, UnauthorizedException } from '@nestjs/common';
  4. import { AuthService } from './auth.service';
  5. @Injectable()
  6. export class HttpStrategy extends PassportStrategy(BearerStrategy) {
  7. constructor(private readonly authService: AuthService) {
  8. super();
  9. }
  10. async validate(token: any, done: Function) {
  11. const user = await this.authService.validateUser(token);
  12. if (!user) {
  13. return done(new UnauthorizedException(), false);
  14. }
  15. done(null, user);
  16. }
  17. }

HttpStrategy 使用 AuthService 来验证 token。当 token 有效时, passport 允许进行进一步的请求处理。否则, 用户将收到 401 (Unauthorized) 响应。

然后,我们可以创建 AuthModule

auth.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { AuthService } from './auth.service';
  3. import { HttpStrategy } from './http.strategy';
  4. import { UsersModule } from '../users/users.module';
  5. @Module({
  6. imports: [UsersModule],
  7. providers: [AuthService, HttpStrategy],
  8. })
  9. export class AuthModule {}

?> 为了使用 UsersService, AuthModule 导入了 UsersModule。内部实现在这里并不重要。

然后,您可以在想要启用身份验证的任何位置使用 AuthGuard

  1. @Get('users')
  2. @UseGuards(AuthGuard('bearer'))
  3. findAll() {
  4. return [];
  5. }

?> AuthGuard@nestjs/passport 包中提供的。

bearer 是 passport 将使用的策略的名称。此外,AuthGuard 还接受第二个参数,options 对象,您可以通过该对象来确定 passport 行为。

JWT

第二种描述的方法是使用 JSON web token (JWT) 对端点进行身份验证。首先,让我们关注 AuthService 类。我们需要从token验证切换到基于负载的验证逻辑, 并提供一种方法来为特定用户创建 JWT 令牌, 然后可用于对传入请求进行身份验证。

auth.service.ts

  1. import * as jwt from 'jsonwebtoken';
  2. import { Injectable } from '@nestjs/common';
  3. import { UsersService } from '../users/users.service';
  4. import { JwtPayload } from './interfaces/jwt-payload.interface';
  5. @Injectable()
  6. export class AuthService {
  7. constructor(private readonly usersService: UsersService) {}
  8. async createToken() {
  9. const user: JwtPayload = { email: 'user@email.com' };
  10. return jwt.sign(user, 'secretKey', { expiresIn: 3600 });
  11. }
  12. async validateUser(payload: JwtPayload): Promise<any> {
  13. return await this.usersService.findOneByEmail(payload.email);
  14. }
  15. }

?> 在最佳情况下,jwt package 和 token configuration (密钥和到期时间)应注册为 custom providers

为了简化一个示例,我们创建了一个假用户。此外,到期时间和 secretKey 是硬编码的(在实际应用中,您应该考虑使用环境变量)。第二步是创建相应的 JwtStrategy

jwt.strategy.ts

  1. import { ExtractJwt, Strategy } from 'passport-jwt';
  2. import { AuthService } from './auth.service';
  3. import { PassportStrategy } from '@nestjs/passport';
  4. import { Injectable, UnauthorizedException } from '@nestjs/common';
  5. import { JwtPayload } from './interfaces/jwt-payload.interface';
  6. @Injectable()
  7. export class JwtStrategy extends PassportStrategy(Strategy) {
  8. constructor(private readonly authService: AuthService) {
  9. super({
  10. jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  11. secretOrKey: 'secretKey',
  12. });
  13. }
  14. async validate(payload: JwtPayload, done: Function) {
  15. const user = await this.authService.validateUser(payload);
  16. if (!user) {
  17. return done(new UnauthorizedException(), false);
  18. }
  19. done(null, user);
  20. }
  21. }

JwtStrategy 使用 AuthService 来验证解码的有效负载。有效负载有效(用户存在)时,passport允许进一步处理请求。否则, 用户将收到 401 (Unauthorized) 响应

之后,我们可以转到 AuthModule

auth.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { AuthService } from './auth.service';
  3. import { JwtStrategy } from './jwt.strategy';
  4. import { UsersModule } from '../users/users.module';
  5. @Module({
  6. imports: [UsersModule],
  7. providers: [AuthService, JwtStrategy],
  8. })
  9. export class AuthModule {}

?> 为了使用 UsersService, AuthModule 导入了 UsersModule。内部实现在这里并不重要。

然后,您可以在想要启用身份验证的任何位置使用 AuthGuard

  1. @Get('users')
  2. @UseGuards(AuthGuard('jwt'))
  3. findAll() {
  4. return [];
  5. }

?> AuthGuard@nestjs/passport 包中提供的。

jwt 是 passport 将使用的策略的名称。此外,AuthGuard 还接受第二个参数,options 对象,您可以通过该对象来确定 passport 行为。这里提供了一个完整的工作示例。

数据库(TypeORM)

为了减少开始与数据库进行连接所需的样板, Nest 提供了随时可用的 @nestjs/typeorm 软件包。我们选择了 TypeORM, 因为它绝对是 Node.js 中可用的最成熟的对象关系映射器 (ORM)。由于它是用TypeScript编写的,所以它在Nest框架下运行得非常好。

首先,我们需要安装所有必需的依赖关系:

  1. $ npm install --save @nestjs/typeorm typeorm mysql

?> 在本章中,我们将使用MySQL数据库,但TypeORM提供了许多不同的支持,如PostgreSQL,SQLite甚至MongoDB(NoSQL)。

一旦安装完成,我们可以将其 TypeOrmModule 导入到根目录中 ApplicationModule 。

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. @Module({
  4. imports: [
  5. TypeOrmModule.forRoot({
  6. type: 'mysql',
  7. host: 'localhost',
  8. port: 3306,
  9. username: 'root',
  10. password: 'root',
  11. database: 'test',
  12. entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  13. synchronize: true,
  14. }),
  15. ],
  16. })
  17. export class ApplicationModule {}

forRoot() 方法接受与 TypeORM 包中的 createConnection() 相同的配置对象。此外, 我们可以在项目根目录中创建一个 ormconfig.json 文件, 而不是将任何内容传递给它。

ormconfig.json

  1. {
  2. "type": "mysql",
  3. "host": "localhost",
  4. "port": 3306,
  5. "username": "root",
  6. "password": "root",
  7. "database": "test",
  8. "entities": ["src/**/**.entity{.ts,.js}"],
  9. "synchronize": true
  10. }

现在我们可以简单地将圆括号留空:

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. @Module({
  4. imports: [TypeOrmModule.forRoot()],
  5. })
  6. export class ApplicationModule {}

之后,ConnectionEntityManager 将可用于注入整个项目(无需导入任何其他模块),例如以这种方式:

app.module.ts

  1. import { Connection } from 'typeorm';
  2. @Module({
  3. imports: [TypeOrmModule.forRoot(), PhotoModule],
  4. })
  5. export class ApplicationModule {
  6. constructor(private readonly connection: Connection) {}
  7. }

存储库模式

该TypeORM支持库的设计模式,使每个实体都有自己的仓库。这些存储库可以从数据库连接中获取。

首先,我们至少需要一个实体。我们将重用 Photo 官方文档中的实体。

photo/photo.entity.ts

  1. import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
  2. @Entity()
  3. export class Photo {
  4. @PrimaryGeneratedColumn()
  5. id: number;
  6. @Column({ length: 500 })
  7. name: string;
  8. @Column('text')
  9. description: string;
  10. @Column()
  11. filename: string;
  12. @Column('int')
  13. views: number;
  14. @Column()
  15. isPublished: boolean;
  16. }

Photo 实体属于该 photo 目录。这个目录代表了 PhotoModule。这是你决定在哪里保留你的模型文件。从我的观点来看,最好的方法是将它们放在他们的域中, 放在相应的模块目录中。

让我们看看 PhotoModule:

photo/photo.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. import { PhotoService } from './photo.service';
  4. import { PhotoController } from './photo.controller';
  5. import { Photo } from './photo.entity';
  6. @Module({
  7. imports: [TypeOrmModule.forFeature([Photo])],
  8. providers: [PhotoService],
  9. controllers: [PhotoController],
  10. })
  11. export class PhotoModule {}

此模块使用 forFeature() 方法定义定义哪些存储库应在当前范围内注册。

现在, 我们可以使用 @InjectRepository() 修饰器向 PhotoService 注入 PhotoRepository:

photo/photo.service.ts

  1. import { Injectable, Inject } from '@nestjs/common';
  2. import { InjectRepository } from '@nestjs/typeorm';
  3. import { Repository } from 'typeorm';
  4. import { Photo } from './photo.entity';
  5. @Injectable()
  6. export class PhotoService {
  7. constructor(
  8. @InjectRepository(Photo)
  9. private readonly photoRepository: Repository<Photo>,
  10. ) {}
  11. async findAll(): Promise<Photo[]> {
  12. return await this.photoRepository.find();
  13. }
  14. }

?> 不要忘记将 PhotoModule 导入根 ApplicationModule

多个数据库

某些项目可能需要多个数据库连接。幸运的是,这也可以通过本模块实现。要使用多个连接,首先要做的是创建这些连接。在这种情况下,连接命名成为必填项。

假设你有一个 Person 实体和一个 Album 实体,每个实体都存储在他们自己的数据库中。

  1. @Module({
  2. imports: [
  3. TypeOrmModule.forRoot({
  4. type: 'postgres',
  5. host: 'photo_db_host',
  6. port: 5432,
  7. username: 'user',
  8. password: 'password',
  9. database: 'db',
  10. entities: [Photo],
  11. synchronize: true
  12. }),
  13. TypeOrmModule.forRoot({
  14. type: 'postgres',
  15. name: 'personsConnection',
  16. host: 'person_db_host',
  17. port: 5432,
  18. username: 'user',
  19. password: 'password',
  20. database: 'db',
  21. entities: [Person],
  22. synchronize: true
  23. }),
  24. TypeOrmModule.forRoot({
  25. type: 'postgres',
  26. name: 'albumsConnection',
  27. host: 'album_db_host',
  28. port: 5432,
  29. username: 'user',
  30. password: 'password',
  31. database: 'db',
  32. entities: [Album],
  33. synchronize: true
  34. })
  35. ]
  36. })
  37. export class ApplicationModule {}

?> 如果未为连接设置任何 name ,则该连接的名称将设置为 default。请注意,不应该有多个没有名称或同名的连接,否则它们会被覆盖。

此时,您的 PhotoPersonAlbum 实体中的每一个都已在各自的连接中注册。通过此设置,您必须告诉 TypeOrmModule.forFeature() 函数和 @InjectRepository() 装饰器应该使用哪种连接。如果不传递任何连接名称,则使用 default 连接。

  1. @Module({
  2. // ...
  3. TypeOrmModule.forFeature([Photo]),
  4. TypeOrmModule.forFeature([Person], 'personsConnection'),
  5. TypeOrmModule.forFeature([Album], 'albumsConnection')
  6. })
  7. export class ApplicationModule {}

您也可以为给定的连接注入 ConnectionEntityManager

  1. @Injectable()
  2. export class PersonService {
  3. constructor(
  4. @InjectConnection('personsConnection')
  5. private readonly connection: Connection,
  6. @InjectEntityManager('personsConnection')
  7. private readonly entityManager: EntityManager
  8. ) {}
  9. }

测试

在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,从而使我们的测试适合于独立,并使它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的存储库。那是什幺?解决方案是创建假存储库。为了实现这一点,我们应该设置 custom providers。事实上,每个注册的存储库都由 entitynamereposition 标记表示,其中 EntityName 是实体类的名称。

@nestjs/typeorm 包提供了基于给定实体返回准备好token的 getRepositoryToken() 函数。

  1. @Module({
  2. providers: [
  3. PhotoService,
  4. {
  5. provide: getRepositoryToken(Photo),
  6. useValue: mockRepository,
  7. },
  8. ],
  9. })
  10. export class PhotoModule {}

现在, 将使用硬编码 mockRepository 作为 PhotoRepository。每当任何提供程序使用 @InjectRepository() 修饰器请求 PhotoRepository 时, Nest 会使用注册的 mockRepository 对象。

这儿有一个可用的例子。

Mongo

文件上传

为了处理文件上传,Nest使用了multer中间件。这个中间件是完全可配置的,您可以根据您的应用需求调整其行为。

基本实例

当我们要上传单个文件时, 我们只需将 FileInterceptor () 与处理程序绑定在一起, 然后使用 @UploadedFile () 装饰器从 request 中取出 file

  1. @Post('upload')
  2. @UseInterceptors(FileInterceptor('file'))
  3. uploadFile(@UploadedFile() file) {
  4. console.log(file);
  5. }

?> FileInterceptor()@UploadedFile() 装饰都是 @nestjs/common 包提供的。

FileInterceptor() 接收两个参数, 一个 fieldName (指向包含文件的 HTML 表单的字段) 和可选 options 对象。这些 MulterOptions 等效于传入 multer 构造函数 (此处有更多详细信息)

多个文件

为了同时上传多个文件,我们使用 FilesInterceptor()。这个拦截器需要三个参数。fieldName(保持不变)、可同时上载的最大文件数 maxCount 以及可选的 MulterOptions 对象。此外,要从 request 对象中选择文件,我们使用 @UploadedFiles() 装饰器

  1. @Post('upload')
  2. @UseInterceptors(FilesInterceptor('files'))
  3. uploadFile(@UploadedFiles() files) {
  4. console.log(files);
  5. }

?> FilesInterceptor()@UploadedFiles() 装饰都是 @nestjs/common 包提供的。

日志记录(Logger)

Nest附带了一个默认的内部 Logger 实现,它在实例化过程中使用,并且在几个不同的情况下使用,例如 occurred exception 等。但有时,您可能希望完全禁用日志记录,或者提供自定义实现并自行处理消息。为了关闭 logger,我们使用 Nest 的 options 对象。

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: false,
  3. });
  4. await app.listen(3000);

不过, 我们可能希望在底层使用不同的 logger, 而不是禁用整个日志机制。为了达到这个目的, 我们必须传递一个满足 LoggerService 接口的对象。例如, 可以是内置的 console

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: console,
  3. });
  4. await app.listen(3000);

但这不是个好主意。但是,我们可以轻松创建自己的记录器。

  1. import { LoggerService } from '@nestjs/common';
  2. export class MyLogger implements LoggerService {
  3. log(message: string) {}
  4. error(message: string, trace: string) {}
  5. warn(message: string) {}
  6. }

然后,我们可以直接应用 MyLogger 实例:

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: new MyLogger(),
  3. });
  4. await app.listen(3000);

CORS

跨源资源共享(CORS)是一种允许从另一个域请求资源的机制。在引擎盖下,Nest 使用了 cors 软件包,该软件包提供了一些选项,您可以根据自己的要求进行自定义。为了启用CORS,你必须调用 enableCors() 方法。

  1. const app = await NestFactory.create(ApplicationModule);
  2. app.enableCors();
  3. await app.listen(3000);

而且,你可以传递一个配置对象作为这个函数的参数。可用的属性在官方的 cors 仓库中详细描述。另一种方法是使用Nest选项对象:

  1. const app = await NestFactory.create(ApplicationModule, { cors: true });
  2. await app.listen(3000);

您也可以使用cors配置对象,而不是传递布尔值。

Configuration

用于在不同的环境中运行的应用程序。根据环境的不同,应该使用各种配置变量。例如,很可能本地环境会针对特定数据库凭证进行中继,仅对本地数据库实例有效。为了解决这个问题,我们过去利用了 .env 包含键值对的文件,每个键代表一个特定的值,因为这种方法非常方便。

安装

为了解析我们的环境文件,我们将使用一个 dotenv 软件包。

  1. $ npm i --save dotenv

Service

首先,我们来创建一个 ConfigService 类。

  1. import * as dotenv from 'dotenv';
  2. import * as fs from 'fs';
  3. export class ConfigService {
  4. private readonly envConfig: { [prop: string]: string };
  5. constructor(filePath: string) {
  6. this.envConfig = dotenv.parse(fs.readFileSync(filePath))
  7. }
  8. get(key: string): string {
  9. return this.envConfig[key];
  10. }
  11. }

这个类只有一个参数,filePath 是你的 .env 文件的路径。提供 get() 方法以启用对私有 envConfig 对象的访问,该对象包含在环境文件中定义的每个属性。

最后一步是创建一个 ConfigModule

  1. import { Module } from '@nestjs/common';
  2. import { ConfigService } from './config.service';
  3. @Module({
  4. providers: [
  5. {
  6. provide: ConfigService,
  7. useValue: new ConfigService(`${process.env.NODE_ENV}.env`),
  8. },
  9. ],
  10. exports: [ConfigService],
  11. })
  12. export class ConfigModule {}

ConfigModule 会注册 ConfigService 并将其导出。此外,我们还传递了 .env 文件的路径。此路径将因实际执行环境而异。现在,您可以简单地在任何位置插入 ConfigService,并根据传递的密钥提取特定值Sample。.env文件可能如下所示:

development.env

  1. DATABASE_USER=test
  2. DATABASE_PASSWORD=test

使用ConfigService

要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。

app.module.ts

  1. @Module({
  2. imports: [ConfigModule],
  3. ...
  4. })

之后,您可以使用注入标记来注入它。默认情况下,标记等于类名(在我们的例子中 ConfigService)。

app.service.ts

  1. @Injectable()
  2. export class AppService {
  3. private isAuthEnabled: boolean;
  4. constructor(config: ConfigService) {
  5. // Please take note that this check is case sensitive!
  6. this.isAuthEnabled = config.get('IS_AUTH_ENABLED') === 'true' ? true : false;
  7. }
  8. }

您也可以将 ConfigModule 声明为全局模块,而不是在所有模块中重复导入 ConfigModule

高级配置(可选)

我们刚刚实现了一个基础 ConfigService。但是,这种方法有几个缺点,我们现在将解决这些缺点:

  • 缺少环境变量的名称和类型(无智能感知)
  • 缺少提供对 .env 文件的验证
  • env文件将布尔值作为string (‘true‘),提供,因此每次都必须将它们转换为 boolean

验证

我们将从验证提供的环境变量开始。如果未提供所需的环境变量或者它们不符合您的预定义要求,则可以抛出错误。为此,我们将使用npm包 Joi。通过Joi,您可以定义一个对象模式(schema)并根据它来验证JavaScript对象。

安装Joi和它的类型(用于TypeScript用户):

  1. $ npm install --save joi
  2. $ npm install --save-dev @types/joi

安装软件包后,我们就可以转到 ConfigService

config.service.ts

  1. import * as Joi from 'joi';
  2. import * as fs from 'fs';
  3. export interface EnvConfig {
  4. [prop: string]: string;
  5. }
  6. export class ConfigService {
  7. private readonly envConfig: EnvConfig;
  8. constructor(filePath: string) {
  9. const config = dotenv.parse(fs.readFileSync(filePath));
  10. this.envConfig = this.validateInput(config);
  11. }
  12. /**
  13. * Ensures all needed variables are set, and returns the validated JavaScript object
  14. * including the applied default values.
  15. */
  16. private validateInput(envConfig: EnvConfig): EnvConfig {
  17. const envVarsSchema: Joi.ObjectSchema = Joi.object({
  18. NODE_ENV: Joi.string()
  19. .valid(['development', 'production', 'test', 'provision'])
  20. .default('development'),
  21. PORT: Joi.number().default(3000),
  22. API_AUTH_ENABLED: Joi.boolean().required(),
  23. });
  24. const { error, value: validatedEnvConfig } = Joi.validate(
  25. envConfig,
  26. envVarsSchema,
  27. );
  28. if (error) {
  29. throw new Error(`Config validation error: ${error.message}`);
  30. }
  31. return validatedEnvConfig;
  32. }
  33. }

由于我们为 NODE_ENVPORT 设置了默认值,因此如果不在环境文件中提供这些变量,验证将不会失败。然而, 我们需要明确提供 API_AUTH_ENABLED。如果我们的.env文件中的变量不是模式(schema)的一部分, 则验证也会引发错误。此外,Joi 还会尝试将env字符串转换为正确的类型。

类属性

对于每个配置属性,我们必须添加一个getter方法。

config.service.ts

  1. get isApiAuthEnabled(): boolean {
  2. return Boolean(this.envConfig.API_AUTH_ENABLED);
  3. }

用法示例

现在我们可以直接访问类属性。

config.service.ts

  1. @Injectable()
  2. export class AppService {
  3. constructor(config: ConfigService) {
  4. if (config.isApiAuthEnabled) {
  5. // Authorization is enabled
  6. }
  7. }
  8. }

HTTP模块

Axios 是丰富功能的 HTTP 客户端, 广泛应用于许多应用程序中。这就是为什幺Nest包装这个包, 并公开它默认为内置 HttpModuleHttpModule 导出 HttpService, 它只是公开了基于 axios 的方法来执行 HTTP 请求, 而且还将返回类型转换为 Observables

为了使用 httppservice,我们需要导入 HttpModule

  1. @Module({
  2. imports: [HttpModule],
  3. providers: [CatsService],
  4. })
  5. export class CatsModule {}

?> HttpModule@nestjs/common 包提供的

然后,你可以注入 HttpService。这个类可以从@nestjs/common 包中获取。

  1. @Injectable()
  2. export class CatsService {
  3. constructor(private readonly httpService: HttpService) {}
  4. findAll(): Observable<AxiosResponse<Cat[]>> {
  5. return this.httpService.get('http://localhost:3000/cats');
  6. }
  7. }

MVC

Nest 默认使用 Express 库,因此有关 Express 中的 MVC(模型 - 视图 - 控制器)模式的每个教程都与 Nest 相关。首先,让我们使用CLI工具搭建一个简单的Nest应用程序:

  1. $ npm i -g @nestjs/cli
  2. $ nest new project

为了创建一个简单的MVC应用程序,我们必须安装一个模板引擎

  1. $ npm install --save hbs

我们决定使用 hbs 引擎,但您可以使用任何符合您要求的内容。安装过程完成后,我们需要使用以下代码配置快速实例:

main.ts

  1. import { NestFactory } from '@nestjs/core';
  2. import { ApplicationModule } from './app.module';
  3. async function bootstrap() {
  4. const app = await NestFactory.create(ApplicationModule);
  5. app.useStaticAssets(__dirname + '/public');
  6. app.setBaseViewsDir(__dirname + '/views');
  7. app.setViewEngine('hbs');
  8. await app.listen(3000);
  9. }
  10. bootstrap();

我们告诉express,该 public 目录将用于存储静态文件, views 将包含模板,并且 hbs 应使用模板引擎来呈现 HTML 输出。

现在,让我们在该文件夹内创建一个 views 目录和一个 index.hbs 模板。在模板内部,我们将打印从控制器传递的 message

index.hbs

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>App</title>
  6. </head>
  7. <body>
  8. {{ message }}
  9. </body>
  10. </html>

然后, 打开 app.controller 文件, 并用以下代码替换 root() 方法:

app.controller.ts

  1. import { Get, Controller, Render } from '@nestjs/common';
  2. @Controller()
  3. export class AppController {
  4. @Get()
  5. @Render('index')
  6. root() {
  7. return { message: 'Hello world!' };
  8. }
  9. }

?> 事实上,当 Nest 检测到 @Res() 装饰器时,它会注入 response 对象。在这里了解更多关于它的能力。

在应用程序运行时,打开浏览器访问 http://localhost:3000/ 你应该看到这个 Hello world! 消息。

这里有一个可用的例子

Fastify

本章所述,我们可以将任何兼容的HTTP提供程序与Nest一起使用。其中一个是fastify库。为了创建一个具有fastify的MVC应用程序,我们必须安装以下软件包:

  1. $ npm i --save fastify point-of-view handlebars

接下来的步骤几乎涵盖了与express库相同的内容(差别很小)。安装过程完成后,我们需要打开 main.ts 文件并更新其内容:

main.ts

  1. import { NestFactory } from '@nestjs/core';
  2. import { ApplicationModule } from './app.module';
  3. import { FastifyAdapter } from '@nestjs/core/adapters/fastify-adapter';
  4. import { join } from 'path';
  5. async function bootstrap() {
  6. const app = await NestFactory.create(ApplicationModule, new FastifyAdapter());
  7. app.useStaticAssets({
  8. root: join(__dirname, 'public'),
  9. prefix: '/public/',
  10. });
  11. app.setViewEngine({
  12. engine: {
  13. handlebars: require('handlebars'),
  14. },
  15. templates: join(__dirname, 'views'),
  16. });
  17. await app.listen(3000);
  18. }
  19. bootstrap();

API略有不同,但这些方法调用背后的想法保持不变。此外,我们还必须确保传递到 @Render() 装饰器中的模板名称包含文件扩展名。

app.controller.ts

  1. import { Get, Controller, Render } from '@nestjs/common';
  2. @Controller()
  3. export class AppController {
  4. @Get()
  5. @Render('index.hbs')
  6. root() {
  7. return { message: 'Hello world!' };
  8. }
  9. }

在应用程序运行时,打开浏览器并导航至http://localhost:3000/。你应该看到这个Hello world!消息。

这里有 一个可用的例子。

性能(Fastify)

在底层,Nest使用了Express,但如前所述,它提供了与各种其他库的兼容性,例如 Fastify。它是怎幺工作的?事实上,Nest需要使用您最喜欢的库,它是一个兼容的适配器,它主要将相应的处理程序代理到适当的库特定的方法。此外,您的库必须至少提供与express类似的请求-响应周期管理。

Fastify非常适合这里,因为它以与express类似的方式解决设计问题。然而,fastify的速度要快得多,达到了几乎两倍的基准测试结果。问题是,为什幺Nest仍然使用express作为默认的HTTP提供程序?因为express是应用广泛、广为人知的,而且拥有一套庞大的兼容中间件。

但我们并没有将人们锁定在单一的模式中。我们让他们使用任何他们需要的东西。如果您关心真正出色的性能,Fastify是一个更好的选择,这就是为什幺我们提供内置 FastifyAdapter 有助于将此库与Nest整合在一起的原因。

安装

首先,我们需要安装所需的软件包:

  1. $ npm i --save fastify fastify-formbody

适配器(Adapter)

安装fastify后,我们可以使用 FastifyAdapter

  1. import { NestFactory } from '@nestjs/core';
  2. import { FastifyAdapter } from '@nestjs/core/adapters';
  3. import { ApplicationModule } from './app.module';
  4. async function bootstrap() {
  5. const app = await NestFactory.create(ApplicationModule, new FastifyAdapter());
  6. await app.listen(3000);
  7. }
  8. bootstrap();

就这样。此外,您还可以通过 FastifyAdapter 构造函数将选项传递到fastify构造函数中。请记住,Nest现在使用fastify作为HTTP提供程序,这意味着在express上转发的每个配方都将不再起作用。您应该使用fastify等效软件包。

热重载(Webpack)

对应用程序的引导过程影响最大的是TypeScript编译。但问题是,每次发生变化时,我们是否必须重新编译整个项目?一点也不。这就是为什幺 webpack HMR(Hot-Module Replacement)大大减少了实例化您的应用程序所需的时间。

安装

首先,我们安装所需的软件包:

  1. $ npm i --save-dev webpack webpack-cli webpack-node-externals

配置(Configuration)

然后,我们需要创建一个webpack.config.js,它是webpack的一个配置文件,并将其放入根目录。

  1. const webpack = require('webpack');
  2. const path = require('path');
  3. const nodeExternals = require('webpack-node-externals');
  4. module.exports = {
  5. entry: ['webpack/hot/poll?100', './src/main.ts'],
  6. watch: true,
  7. target: 'node',
  8. externals: [
  9. nodeExternals({
  10. whitelist: ['webpack/hot/poll?100'],
  11. }),
  12. ],
  13. module: {
  14. rules: [
  15. {
  16. test: /.tsx?$/,
  17. use: 'ts-loader',
  18. exclude: /node_modules/,
  19. },
  20. ],
  21. },
  22. mode: 'development',
  23. resolve: {
  24. extensions: ['.tsx', '.ts', '.js'],
  25. },
  26. plugins: [new webpack.HotModuleReplacementPlugin()],
  27. output: {
  28. path: path.join(__dirname, 'dist'),
  29. filename: 'server.js',
  30. },
  31. };

此配置告诉webpack关于我们的应用程序的一些基本内容。其中有一个入口文件,应该使用哪个目录来保存编译后的文件,以及为了编译源文件我们要使用哪种加载程序。基本上,你不应该担心太多,你根本不需要理解这个文件的内容。

热模块更换

为了启用HMR,我们必须打开Nest应用程序入口文件(这是 main.ts)并添加一些关键的事情。

  1. declare const module: any;
  2. async function bootstrap() {
  3. const app = await NestFactory.create(ApplicationModule);
  4. await app.listen(3000);
  5. if (module.hot) {
  6. module.hot.accept();
  7. module.hot.dispose(() => app.close());
  8. }
  9. }
  10. bootstrap();

就这样。为了简化执行过程,请将这两行添加到 package.json 文件的脚本中。

  1. "start": "node dist/server",
  2. "webpack": "webpack --config webpack.config.js"

现在只需打开你的命令行并运行下面的命令:

  1. $ npm run webpack

webpack开始监视文件后,在另一个命令行窗口中运行另一个命令:

  1. $ npm run start

这里有一个可用的例子