从头开始学习nestjs-第七章-技术-日志/Cookies/订阅监听/压缩

222 阅读10分钟

日志

Nest 带有一个内置的基于文本的日志器,它在应用引导和其他几种情况下使用,例如显示捕获的异常(即系统日志记录)。此功能是通过 @nestjs/common 包中的 Logger 类提供的。用户可以完全控制日志系统的行为,包括以下任何一项:

  • 完全禁用日志记录
  • 指定日志的详细级别(例如,显示错误、警告、调试信息等)
  • 覆盖默认日志器中的时间戳(例如,使用 ISO8601 标准作为日期格式)
  • 完全覆盖默认日志器
  • 通过扩展它来自定义默认日志器
  • 使用依赖注入来简化应用的编写和测试

还可以使用内置日志器或创建自己的自定义实现来记录自己的应用级事件和消息

对于更高级的日志记录功能,可以使用任何 Node.js 日志记录包(例如 Winston)来实现完全自定义的生产级日志记录系统

基本定制

禁用日志记录,请在作为第二个参数传递给 NestFactory.create() 方法的(可选)Nest 应用选项对象中将 logger 属性设置为 false

const app = await NestFactory.create(AppModule, {
  logger: false,
});
await app.listen(3000);

启用特定的日志记录级别,请将 logger 属性设置为指定要显示的日志级别的字符串数组,如下所示:

const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn'],
});
await app.listen(3000);

数组中的值可以是 logfatalerrorwarndebugverbose 的任意组合

提示:要禁用默认日志器消息中的颜色,请将 NO_COLOR 环境变量设置为某个非空字符串

自定义实现

可以通过将 logger 属性的值设置为满足 LoggerService 接口的对象来提供 Nest 用于系统日志记录的自定义日志器实现。例如,可以告诉 Nest 使用内置的全局 JavaScript console 对象(它实现了 LoggerService 接口),如下所示:

const app = await NestFactory.create(AppModule, {
  logger: console,
});
await app.listen(3000);

实现自己的自定义日志器非常简单。只需按如下所示实现 LoggerService 接口的每个方法。

import { LoggerService } from '@nestjs/common';

@Injectable()
export class MyLogger implements LoggerService {
  /**

   * Write a 'log' level log.
   */
  log(message: any, ...optionalParams: any[]) {}

  /**

   * Write a 'fatal' level log.
   */
  fatal(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[]) {}
}

然后,可以通过 Nest 应用选项对象的 logger 属性提供 MyLogger 的实例

const app = await NestFactory.create(AppModule, {
  logger: new MyLogger(),
});
await app.listen(3000);

这种技术虽然简单,但并未对 MyLogger 类使用依赖注入。这可能会带来一些挑战,特别是对于测试,并限制 MyLogger 的可重用性

扩展内置日志器

无需从头编写日志器,可以通过扩展内置 ConsoleLogger 类并覆盖默认实现的选定行为来满足需求

import { ConsoleLogger } from '@nestjs/common';

export class MyLogger extends ConsoleLogger {
  error(message: any, stack?: string, context?: string) {
    // add your tailored logic here
    super.error(...arguments);
  }
}

可以在功能模块中使用此类扩展日志器

可以通过应用选项对象的 logger 属性或 依赖注入 部分所示的技术来告诉 Nest 使用自己的扩展日志器进行系统日志记录。如果这样做,应该注意调用 super,如上面的示例代码所示,将特定的日志方法调用委托给父(内置)类,以便 Nest 可以依赖它期望的内置功能

依赖注入

对于更高级的日志记录功能,需要利用依赖注入。例如,可能希望将 ConfigService 注入到自己的日志器中以对其进行自定义,然后将自定义日志器注入到其他 controllerprovider 中。要为自定义日志器启用依赖注入,请创建一个实现 LoggerService 的类并将该类注册为某个 module 中的 provider 。例如,可以:

  1. 定义一个扩展内置 ConsoleLogger 或完全覆盖它的 MyLogger 类,如前面部分所示。务必实现 LoggerService 接口
  2. 如下所示创建 LoggerModule,并从该模块提供 MyLogger
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Module({
  providers: [MyLogger],
  exports: [MyLogger],
})
export class LoggerModule {}

使用此构造,现在可以提供自定义日志器以供任何其他 module 使用。因为 MyLogger 类module 的一部分,所以它可以使用依赖注入(例如,注入 ConfigService)。还需要一种技术来提供此自定义日志器,供 Nest 用于系统记录(例如,用于引导和错误处理)

因为应用实例化(NestFactory.create())发生在任何 module 的上下文之外,所以它不参与初始化的正常依赖注入阶段。所以必须保证至少有一个应用模块导入了 LoggerModule 来触发 Nest 实例化 MyLogger 类的单例实例

然后可以指示 Nest 使用具有以下构造的 MyLogger 的相同单例实例:

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(app.get(MyLogger));
await app.listen(3000);

注意:上面的示例将 bufferLogs 设置为 true,以确保所有日志都将被缓冲,直到附加自定义日志器(本例中为 MyLogger)并且应用初始化过程完成或失败。如果初始化过程失败,Nest 将回退到原始 ConsoleLogger 以打印出任何报告的错误消息。此外,可以将 autoFlushLogs 设置为 false(默认 true)以手动刷新日志(使用 Logger#flush() 方法)

这里在 NestApplication 实例上使用 get() 方法来检索 MyLogger 对象的单例实例。这种技术本质上是一种 inject 日志器实例供 Nest 使用的方法。app.get() 调用检索 MyLogger 的单例实例,并取决于该实例首先注入另一个 module,如上所述

还可以在要素类中注入此 MyLogger provider,从而确保跨 Nest 系统日志记录和应用日志记录的日志记录行为一致

使用日志器记录应用

可以结合上述几种技术,在 Nest 系统日志记录和自己的应用事件/消息日志记录中提供一致的行为和格式

一个好的做法是在每个服务中从 @nestjs/common 实例化 Logger 类。可以在 Logger 构造函数中提供自己的服务名称作为 context 参数,如下所示:

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)),则从 MyServicethis.logger.log() 的后续调用将导致从 MyLogger 实例调用方法 log

注入自定义日志器

首先,使用如下代码扩展内置日志器。提供 scope 选项作为 ConsoleLogger 类的配置元数据,指定 transient 作用域,以确保在每个功能模块中都有一个唯一的 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 方法。所以可以开始使用上下文感知的自定义日志器,如下所示:

import { Injectable } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  constructor(private myLogger: MyLogger) {
    // Due to transient scope, CatsService has its own unique instance of MyLogger,
    // so setting context here will not affect other instances in other services
    this.myLogger.setContext('CatsService');
  }

  findAll(): Cat[] {
    // You can call all the default methods
    this.myLogger.warn('About to return cats!');
    // And your custom methods
    this.myLogger.customLog();
    return this.cats;
  }
}

最后,指示 Nest 在 main.ts 文件中使用自定义日志器的实例,如下所示。当然在这个例子中,并没有真正定制日志器的行为(通过扩展 Logger 方法,如 log()、warn() 等),所以实际上不需要这一步。但如果向这些方法添加自定义逻辑并希望 Nest 使用相同的实现,则需要这样做

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(new MyLogger());
await app.listen(3000);

提示:或者,你可以使用 logger: false 指令暂时禁用日志器,而不是将 bufferLogs 设置为 true。请注意,如果你将 logger: false 提供给 NestFactory.create,则在你调用 useLogger 之前不会记录任何内容,因此可能会遗漏一些重要的初始化错误。如果你不介意某些初始消息将使用默认日志器记录,则可以省略 logger: false 选项

使用外部日志

生产应用通常有特定的日志记录要求,包括高级过滤、格式化和集中日志记录。Nest 的内置日志器用于监视 Nest 系统行为,也可用于在开发过程中在功能模块中记录基本格式的文本,但生产应用通常会利用专用的日志记录模块,如 Winston。与任何标准 Node.js 应用一样,可以充分利用 Nest 中的此类模块

Cookies

HTTP cookie 是用户浏览器存储的一小段数据。Cookie 旨在成为网站记住状态信息的可靠机制。当用户再次访问该网站时,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 file
app.use(cookieParser());

可以将几个选项传递给 cookieParser 中间件:

  • secret:用于加密 cookie 的字符串或数组的秘钥。这是可选的,如果未指定,将不会解析已加密的 cookie。如果提供了字符串,则将其用作密钥。如果提供了一个数组,将尝试按顺序解析对每个加密的 cookie 的签名
  • options:作为第二个选项传递给 cookie.parse 的对象

中间件将解析请求中的 Cookie 标头并将 cookie 数据设置为属性 req.cookies,如果提供了秘钥,则设置为属性 req.signedCookies。这些属性是 cookie 名称到 cookie 值的名称值对

当提供秘钥时,该 module 将取消加密并验证任何已加密的 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 包导入的

创建自定义装饰器(跨平台)

为了提供一种方便的、声明性的方式来访问传入的 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;
});

@Cookies() 装饰器将从 req.cookies 对象中提取所有 cookie 或命名的 cookie,并使用该值填充装饰参数

现在可以在路由处理程序签名中使用装饰器,如下所示:

@Get()
findAll(@Cookies('name') name: string) {}

订阅监听

事件触发器包(@nestjs/event-emitter)提供了一个简单的观察者模式的实现,允许订阅和监听应用中发生的各种事件。事件是一个分离应用各个方面的好方法,因为单个事件可以有多个互不依赖的监听器

EventEmitterModule 内部使用 eventemitter2

入门

安装需要的包:

$ npm i --save @nestjs/event-emitter

安装完成后,将 EventEmitterModule 导入到根 AppModule 中,运行 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({
  // 是否使用通配符
  wildcard: false,
  // 用于分割命名空间的分隔符
  delimiter: '.',
  // 是否触发 newListener 监听事件
  newListener: false,
  // 是否触发 removeListener 事件
  removeListener: false,
  // 可以分配给事件的监听器的最大数量
  maxListeners: 10,
  // 当分配的监听器数量超过最大数量时,是否在内存泄漏消息中显示事件名称
  verboseMemoryLeak: false,
  // 如果error事件被触发并且没有监听器,是否禁用并抛出异常uncaughtException
  ignoreErrors: false,
});

调度事件

要分派(即触发)事件,首先使用标准构造函数注入来注入 EventEmitter2:

constructor(private eventEmitter: EventEmitter2) {}

提示:从 @nestjs/event-emitter 包导入 EventEmitter2

然后在一个类中使用它,如下所示:

this.eventEmitter.emit(
  'order.created',
  new OrderCreatedEvent({
    orderId: 1,
    payload: {},
  }),
);

监听事件

要声明事件监听器,请在包含要执行的代码的方法定义之前使用 @OnEvent() 装饰器装饰方法,如下所示:

@OnEvent('order.created')
handleOrderCreatedEvent(payload: OrderCreatedEvent) {
  // handle and process "OrderCreatedEvent" event
}

警告:事件订阅者不能处于请求范围

对于简单事件触发器,第一个参数可以是 string ****或 symbol,对于通配符触发器,可以是 string | symbol | Array<string | symbol>

第二个参数(可选)是监听器选项对象,如下所示:

export type OnEventOptions = OnOptions & {
  /**

   * If "true", prepends (instead of append) the given listener to the array of listeners.

   *    * @see https://github.com/EventEmitter2/EventEmitter2#emitterprependlistenerevent-listener-options

   *    * @default false
   */
  prependListener?: boolean;

  /**

   * If "true", the onEvent callback will not throw an error while handling the event. Otherwise, if "false" it will throw an error.

   * 

   * @default true
   */
  suppressErrors?: boolean;
};

提示:这里 eventemitter2 中有关 OnOptions 选项对象的更多信息

@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.createdorder.shipped,但不匹配 order.delayed.out_of_stock。为了监听此类事件,请使用 EventEmitter2documentation 中描述的 multilevel wildcard 模式(即 **

使用此模式,例如,可以创建一个事件监听器来捕获所有事件

@OnEvent('**')
handleEverything(payload: any) {
  // handle and process an event
}

示例

此处 提供了一个工作示例

压缩

压缩可以大大减小响应主体的大小,从而提高 Web 应用的速度

对于生产中的高流量网站,强烈建议从应用服务器卸载压缩 - 通常在反向代理中(例如 Nginx)。在那种情况下,不应该使用压缩中间件

与 Express 一起使用(默认)

使用 compression 中间件包启用 gzip 压缩

安装包:

$ npm i --save compression

安装完成后,将压缩中间件应用为全局中间件

import * as compression from 'compression';
// somewhere in your initialization file
app.use(compression());