Nest.js 开启静态 Web 服务和打造日志系统

淮城一只猫 编程技术 阅读量 0 评论量 0

0x0 Web 服务

因为开发前端的页面,需要展示单应用程序服务器,搜索 Nest.js 可以提供服务的地方,使用起来非常简单。安装依赖包:

yarn add @nestjs/serve-static

然后在 app.module.ts 配置启用:

import { join } from 'path'
import { ServeStaticModule } from '@nestjs/serve-static'

ServeStaticModule.forRoot({
  rootPath: join(__dirname, '..', 'public'),
  exclude: ['/api*']
})

对于 ServeStaticModule 配置选项非常简单参考:API Spec

  • rootPath 指定映射物理目录,
  • exclude 排除的路径,fastify 服务不支持改选项,具体查阅文档。

0x1 守护进程

如果需要部署服务应用守护进程,安装全局依赖包:

yarn global add pm2

pm2 start PATH_TO_YOUR_PROJECT/dist/main.js --name=YOUR_APP_NAME

pm2 start YOUR_APP_NAME
pm2 restart YOUR_APP_NAME
pm2 stop YOUR_APP_NAME

0x3 日志系统

如果服务器系统出现错误情况,查找原因依然靠着日志文件,所以这样就需要打造一个完整的日志系统。在输出文件之前先需要把系统记录器完善。目前自带无法满足基本的需求,需要新建一个 LoggerService 业务来处理:

import { LoggerService as AppLoggerService, Injectable } from '@nestjs/common'

@Injectable()
export class LoggerService implements AppLoggerService {

  log(message: string): void {
  }

  error(message: string, trace: string): void {
  }

  warn(message: string): void {
  }

  debug(message: string): void {
  }

  verbose(message: string): void {
  }
}

然后在 LoggerModuel 导入:

import { Module } from '@nestjs/common'
import { LoggerService } from './logger.service'

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

因为程序实例化上下文之外,所以不参与正常依赖注入阶段,所以需要在下面进行实例:

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

不过上述方案有个缺点就是无法打印程序初始化的任何信息。所以就需要自定义日志系统,然后利用 log4js 存储日志记录。

yarn add log4js

改造 logger.service.ts

import { Logger as AppLogger } from '@nestjs/common'
import { Logger as log4jsLogger, configure, getLogger } from 'log4js'

export class LoggerService extends AppLogger {
  log4js: log4jsLogger

  constructor(logsDir: string) {
    super()

    configure({
      appenders: {
        all: {
          filename: `${logsDir}/nestjs.services.log`,
          type: 'dateFile',
          // 配置 layout,此处使用自定义模式 pattern
          layout: { type: 'pattern', pattern: '%d [%p] %m' },
          // 日志文件按日期(天)切割
          pattern: 'yyyy-MM-dd',
          // 回滚旧的日志文件时,保证以 .log 结尾 (只有在 alwaysIncludePattern 为 false 生效)
          keepFileExt: true,
          // 输出的日志文件名是都始终包含 pattern 日期结尾
          alwaysIncludePattern: true,
          // 指定日志保留的天数
          daysToKeep: 10
        }
      },
      categories: { default: { appenders: ['all'], level: 'all' } }
    })

    this.log4js = getLogger()
  }

  log(message: any, trace: string) {
    super.log(message, trace)
    this.log4js.info(trace, message)
  }

  error(message: any, trace: string) {
    super.error(message, trace)
    this.log4js.error(trace, message)
  }

  warn(message: any, trace: string) {
    super.warn(message, trace)
    this.log4js.warn(trace, message)
  }

  debug(message: any, trace: string) {
    super.debug(message, trace)
    this.log4js.debug(trace, message)
  }

  verbose(message: any, trace: string) {
    super.verbose(message, trace)
    this.log4js.info(trace, message)
  }
}

然后在 main.ts 替代默认的日志系统:

/*
 * Copyright (c) 2021 Jaxson
 * 项目名称:Vue-Admin-Plus-Nestjs-Api
 * 文件名称:main.ts
 * 创建日期:2021年03月27日
 * 创建作者:Jaxson
 */
import { NestFactory } from '@nestjs/core'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { ConfigService } from '@nestjs/config'

import { AppModule } from '@/app.module'
import { LoggerService } from '@/logger/logger.service'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  // 获取全局配置
  const configService = app.get<ConfigService>(ConfigService)

  const logsDir = configService.get<string>('logsDir')
  const logger = new LoggerService(logsDir)
  app.useLogger(logger)

  app.setGlobalPrefix('api')
  app.enableCors() // 启用允许跨域

  const config = new DocumentBuilder()
    .setTitle('Vue Admin Plus 管理系统接口文档')
    .setDescription('这是一份关于 Vue Admin Plus 管理系统的接口文档')
    .setVersion('1.0.0')
    .addBearerAuth()
    .build()
  const document = SwaggerModule.createDocument(app, config)
  SwaggerModule.setup('docs', app, document)

  await app.listen(configService.get<number>('port'))

  logger.log(`设置应用程序端口号:${configService.get<number>('port')}`, 'bootstrap')
  logger.log(`应用程序接口地址: http://localhost:${configService.get<number>('port')}/api`, 'bootstrap')
  logger.log(`应用程序文档地址: http://localhost:${configService.get<number>('port')}/docs`, 'bootstrap')
  logger.log('🚀 服务应用已经成功启动!', 'bootstrap')
}

bootstrap()

同样在请求接口和异常过滤器替代自带日志记录:

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'
import { Response, Request } from 'express'

import { LoggerService } from '@/logger/logger.service'

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {}
  catch(exception: HttpException, host: ArgumentsHost) {
    const context = host.switchToHttp()
    const response = context.getResponse<Response>()
    const request = context.getRequest<Request>()
    const status = exception.getStatus()
    const message = exception.getResponse()['message']

    this.logger.log(`${request.url} - ${message}`, '非正常接口请求')

    response.status(status).json({
      statusCode: status,
      message: message,
      path: request.url,
      timestamp: new Date().toISOString()
    })
  }
}
/*
 * Copyright (c) 2021 Jaxson
 * 项目名称:Vue-Admin-Plus-Nestjs-Api
 * 文件名称:transform.interceptor.ts
 * 创建日期:2021年03月27日
 * 创建作者:Jaxson
 */

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { Request } from 'express'

import { LoggerService } from '@/logger/logger.service'

interface Response<T> {
  data: T
}

interface HasTokenUserEntity extends Express.User {
  token?: string
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  constructor(private logger: LoggerService) {}
  intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
    const request = context.switchToHttp().getRequest<Request>()
    const user: HasTokenUserEntity = request.user

    this.logger.log(request.url, '正常接口请求')

    return next.handle().pipe(
      map(data => {
        const result = data
        // 判断接口是否更新 Token
        if (user.token) result['token'] = user.token
        return {
          data: result,
          statusCode: 200,
          message: '请求成功'
        }
      })
    )
  }
}

大概就这样,不过目前还是有点粗糙,日志类别分的不是很清楚,不过目前对 log4js 不是太熟悉,后期熟悉再慢慢改造。

喵~