Nestjs入門(二)

源碼:awsome-nestnode

nestjs入門(一) 中,對 Nestjs 一些重要的概念有了一些瞭解,如今咱們開始建立一個基於 Nestjs 的應用吧。mysql

Nestjs 和 Angular 同樣,提供了 CLI 工具幫助咱們初始化和開發應用程序。git

$ npm install -g @nestjs/cli
$ nest new my-awesome-app
複製代碼

這時候你會獲得這樣的一個目錄結構:github

運行npm start後,在瀏覽器訪問http://localhost:3000/就能夠看到Hello World!sql

Controller 和 Service

在 Nestjs 中,全部的 controller 和 service 都要在對應的 module 中註冊,就像這樣:typescript

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

複製代碼

在 MVC 模式中,controller 經過 model 獲取數據。對應的,在 Nestjs 中,controller 負責處理傳入的請求, 並調用對應的 service 完成業務處理,返回對客戶端的響應。數據庫

一般能夠經過 CLI 命令來建立一個 controller:npm

$ nest g co cats
複製代碼

這時候,CLI 會自動生成 controller 文件,而且把 controller 註冊到對應的 module 中。json

和其餘一些 node 框架不同,Nestjs 路由不是集中式管理,而是分散在 controller 中,經過@controller()中聲明的(可選)前綴和請求裝飾器中指定的任何路由來肯定的。bootstrap

import { Controller, Get } from '@nestjs/common';

import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {
  }

  @Get(':id')
  findOne(@Param('id') id: string): string {
    return this.catsService.getCat();
  }
}
複製代碼

上面這段代碼中,經過 Get 請求去請求http://localhost:3000/cats/1就會調用findOne方法。

若是須要在全部請求以前加上 prefix,能夠在main.ts中直接設置 GlobalPrefix:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api/v1');
  await app.listen(3000);
}
bootstrap();
複製代碼

在 Nestjs 中,controller 就像是調用 service 的指揮者,把對應的請求分發到相應的 service 中去處理。

在 controller 中,咱們注意到,在構造函數中注入了CatsService實例,來調用對應 service 中的方法。這就是 Nestjs 中依賴注入的注入方式 — 構造函數注入。

service 能夠看作夾在 controller 和 model 之間的一層,在 service 調用 DAO (在 Nestjs 中是各類 ORM 工具或者本身封裝的 DAO 層)實現數據庫的訪問,進行數據的處理整合。

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

@Injectable()
export class CatsService {
  getCat(id: string): string {
    return `This action returns ${id} cats`;
  }
}
複製代碼

上面代碼中經過@Injectable()定義了一個 service,這樣你就能夠在其餘 controller 或者 service 中注入這個 service。

DTO 和 Pipe

經過nestjs入門(一)已經介紹了 DTO 的概念,在Nestjs 中,DTO 主要定義如何經過網絡發送數據的對象,一般會配合class-validatorclass-transformer作校驗。

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  readonly name: string;

  @IsInt()
  readonly age: number;

  @IsString()
  readonly breed: string;
}
複製代碼
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }
}
複製代碼

上面對請求body 定義了一個 DTO,而且在 DTO 中對參數類型進行了限制,若是body中傳過來的類型不符合要求,會直接報錯。

DTO 中的class-validator 還須要配合 pipe 才能完成校驗功能:

import {
  PipeTransform,
  ArgumentMetadata,
  BadRequestException,
  Injectable,
} from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
import * as _ from 'lodash'

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value, metadata: ArgumentMetadata) {
    const { metatype } = metadata
    if (!metatype || !this.toValidate(metatype)) {
      return value
    }
    const object = plainToClass(metatype, value)
    const errors = await validate(object)
    if (errors.length > 0) {
      const errorMessage = _.values(errors[0].constraints)[0]
      throw new BadRequestException(errorMessage)
    }
    return value
  }

  private toValidate(metatype): boolean {
    const types = [String, Boolean, Number, Array, Object]
    return !types.find(type => metatype === type)
  }
}
複製代碼

這個 pipe 會根據元數據和對象實例,去構建原有類型,而後經過validate去校驗。

這個 pipe 通常會做爲全局的 pipe 去使用:

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.setGlobalPrefix('api/v1');
  
  app.useGlobalPipes(new ValidationPipe());
  
  await app.listen(3000);
}
bootstrap();
複製代碼

假設咱們沒有這層 pipe,那在 controller 中就會進行參數校驗,這樣就會打破單一職責的原則。有了這一層 pipe 幫助咱們校驗參數,有效地下降了類的複雜度,提升了可讀性和可維護性。

Interceptor 和 Exception Filter

代碼寫到這裏,咱們發現直接返回了字符串,這樣有點太粗暴,須要把正確和錯誤的響應包裝一下。假設我但願返回的格式是這樣的:

# 請求成功
{
    status: 0,
    message: '請求成功',
    data: any
}

# 請求失敗
{
    status: 1,
    message: string,
}
複製代碼

此時,能夠利用 AOP 的思想去作這件事。首先,咱們須要全局捕獲錯誤的切片層去處理全部的 exception;其次,若是是一個成功的請求,須要把這個返回結果經過一個切片層包裝一下。

在 Nestjs 中,返回請求結果時,Interceptor 會在 Exception Filter 以前觸發,因此 Exception Filter 會是最後捕獲 exception的機會。咱們把它做爲處理全局錯誤的切片層。

import {
  Catch,
  ArgumentsHost,
  HttpException,
  ExceptionFilter,
  HttpStatus,
} from '@nestjs/common'

@Catch()
export class ExceptionsFilter implements ExceptionFilter {
  async catch(exception, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse()
    const request = ctx.getRequest()

    let message = exception.message
    let isDeepestMessage = false
    while (!isDeepestMessage) {
      isDeepestMessage = !message.message
      message = isDeepestMessage ? message : message.message
    }

    const errorResponse = {
      message: message || '請求失敗',
      status: 1,
    }

    const status = exception instanceof HttpException ? 
          exception.getStatus() :
    			HttpStatus.INTERNAL_SERVER_ERROR
    
    response.status(status)
    response.header('Content-Type', 'application/json; charset=utf-8')
    response.send(errorResponse)
  }
}
複製代碼

而 Interceptor 則負責對成功請求結果進行包裝:

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

interface Response<T> {
  data: T
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    return next.handle().pipe(
      map(rawData => {
          return {
            data: rawData,
            status: 0,
            message: '請求成功',
          }
        }
      )
    )
  }
}
複製代碼

一樣 Interceptor 和 Exception Filter 須要把它定義在全局範圍內:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api/v1');

  app.useGlobalFilters(new ExceptionsFilter());
  app.useGlobalInterceptors(new TransformInterceptor());
  app.useGlobalPipes(new ValidationPipe());

  await app.listen(3000);
}
複製代碼

TypeORM

TypeORM 至關於 Nestjs 中的 DAO 層,它支持多種數據庫,如 PostgreSQL,SQLite 甚至MongoDB(NoSQL)。這裏咱們以 MySQL 爲例,首先在 MySQL 中手動建立一個數據庫:

> CREATE DATABASE test
複製代碼

而後安裝 typeorm:

$ npm install --save @nestjs/typeorm typeorm mysql
複製代碼

一般咱們開發的時候,會有多套環境,這些環境中會有不一樣的數據庫配置,因此先建一個config文件夾,放置不一樣的數據庫配置:

// index.ts
import * as _ from 'lodash'
import { resolve } from 'path'

import productionConfig from './prod.config'

const isProd = process.env.NODE_ENV === 'production'

let config = {
  port: 3000,
  hostName: 'localhost',

  orm: {
    type: 'mysql',
    host: 'localhost',
    port: 3310,
    username: 'root',
    password: '123456',
    database: 'test',
    entities: [resolve(`./**/*.entity.ts`)],
    migrations: ['migration/*.ts'],
    timezone: 'UTC',
    charset: 'utf8mb4',
    multipleStatements: true,
    dropSchema: false,
    synchronize: true,
    logging: true,
  },
}

if (isProd) {
  config = _.merge(config, productionConfig)
}

export { config }
export default config
複製代碼
// prod.config.ts
import { resolve } from 'path'

export default {
  port: 3210,

  orm: {
    type: 'mysql',
    host: 'localhost',
    port: 3312,
    username: 'root',
    password: '123456',
    database: 'test',
    entities: [resolve('./**/*.entity.js')],
    migrations: ['migration/*.ts'],
    dropSchema: false,
    synchronize: false,
    logging: false,
  },
}
複製代碼

在線上環境強烈不建議開啓 orm 的 synchronize功能。本地若是要開啓,要注意一點,若是 entity 中定義的字段類型和數據庫原有類型不同,在開啓synchronize 後 orm 會執行 drop而後再add的操做,這會致使本地測試的時候數據丟失(這裏爲了方便,本地測試就把synchronize功能打開,這樣寫完 entity 就會自動同步到數據庫)。

app.module.ts中導入TypeOrmModule

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { CatsController } from './cats/cats.controller'
import { CatsService } from './cats/cats.service'
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'
import config from './config'

@Module({
  imports: [
    TypeOrmModule.forRoot(config.orm as TypeOrmModuleOptions),
  ],
  controllers: [AppController, CatsController],
  providers: [AppService, CatsService],
})
export class AppModule {}
複製代碼

接下來就是寫 entity,下面咱們定義了一個叫cat的表,id爲自增主鍵:

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity('cat')
export class CatEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ length: 50 })
  name: string

  @Column()
  age: number

  @Column({ length: 100, nullable: true })
  breed: string
}
複製代碼

這時候,entity 就會同步到數據庫,在test數據庫中,就能看到cat這張表了。

在某個模塊使用這個 entity 的時候,須要在對應的模塊中註冊,使用 forFeature() 方法定義定義哪些存儲庫應在當前範圍內註冊:

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { CatsController } from './cats/cats.controller'
import { CatsService } from './cats/cats.service'
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'
import config from './config'
import { CatEntity } from './cats/cat.entity'

const ENTITIES = [
  CatEntity,
]

@Module({
  imports: [
    TypeOrmModule.forRoot(config.orm as TypeOrmModuleOptions),
    TypeOrmModule.forFeature([...ENTITIES]),
  ],
  controllers: [AppController, CatsController],
  providers: [AppService, CatsService],
})
export class AppModule {}
複製代碼

這時候就能夠用@InjectRepository() 修飾器向 CatService 注入 CatRepository

import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { CatEntity } from './cat.entity'
import { Repository } from 'typeorm'

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(CatEntity) 
    private readonly catRepository: Repository<CatEntity>,
  ) {
  }

  async getCat(id: number): Promise<CatEntity[]> {
    return await this.catRepository.find({ id })
  }
}
複製代碼

這時候去請求http://localhost:3000/api/v1/cats/1這個 API,就會返回下面結果:

{
    "data": [],
    "status": 0,
    "message": "請求成功"
}
複製代碼

在 typeorm 中,若是須要用到比較複雜的 sql 語句,可使用 createQueryBuilder幫助你構建:

this.catRepository
  .createQueryBuilder('cat')
  .Where('name != ""')
  .andWhere('age > 2')
	.getMany()
複製代碼

若是 createQueryBuilder不能知足你的要求,能夠直接使用query寫 sql 語句:

this.catRepository.query(
  'select * from cat where name != ? and age > ?',
  [age],
)
複製代碼

Migration

在持續交付項目中,項目會不斷迭代上線,這時候就會出現數據庫改動的問題,對一個投入使用的系統,一般會使用 migration 幫咱們同步數據庫。TypeORM 也自帶了一個 CLI 工具幫助咱們進行數據庫的同步。

首先在本地建立一個ormconfig.json文件:

{
  "type": "mysql",
  "host": "localhost",
  "port": 3310,
  "username": "root",
  "password": "123456",
  "database": "test",
  "entities": ["./**/*.entity.ts"],
  "migrations": ["migrations/*.ts"],
  "cli": {
    "migrationsDir": "migrations"
  },
  "timezone": "UTC",
  "charset": "utf8mb4",
  "multipleStatements": true,
  "dropSchema": false,
  "synchronize": false,
  "logging": true
}
複製代碼

這個 json 文件中指定了 entity 和 migration 文件的匹配規則,而且在 CLI 中配置了 migration 文件放置的位置。

這時候運行下面命令就會在 migrations 文件夾下面自動生成1563725408398-update-cat.ts文件

$ ts-node node_modules/.bin/typeorm migration:create -n update-cat
複製代碼

文件名中1563725408398是生成文件的時間戳。這個文件中會有updown這兩個方法:

import {MigrationInterface, QueryRunner} from "typeorm";

export class updateCat1563725408398 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<any> {
    }

    public async down(queryRunner: QueryRunner): Promise<any> {
    }

}
複製代碼

up必須包含執行 migration 所需的代碼。 down必須恢復任何up改變。在updown裏面有一個QueryRunner對象。 使用此對象執行全部數據庫操做。好比咱們在 cat 這張表中寫入一個假數據:

import {MigrationInterface, QueryRunner} from "typeorm";

export class updateCat1563725408398 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.query(`insert into cat (id, name, age, breed) values (2, 'test', 3, 'cat') `)
    }

    public async down(queryRunner: QueryRunner): Promise<any> {
    }

}
複製代碼

這時候,在 package.json中寫入下面 script 並運行npm run migration:run,這時候 cat 表裏面就會有一個id2的假數據。

{
  "scripts": {
    "migration:run": "ts-node node_modules/.bin/typeorm migration:run",
  }
}
複製代碼

注意,這個ormconfig.json文件的配置是本地環境的配置,若是須要在生成環境使用,能夠從新寫一份ormconfig-prod.json,而後運行migration命名的時候加上--config ormconfig-prod.json

用 typeorm 生成的 migration 有一個缺點,sql 和代碼都耦合在一塊兒,最好仍是 sql 是單獨一個文件,migration 腳本是一個文件,這樣若是特殊狀況下,方便直接在 MySQL 中運行這些 sql 文件。這時候,能夠用db-migrate來代替 typeorm 來管理 migration 腳本,db-migrate 會在 migration 目錄下面生成一個 js 腳本和兩個 sql 文件,這兩個 sql 文件一個是up的 sql,一個是down的 sql。

對於已有項目,若是根據數據庫從頭開始建立對應的 entity 是一件很麻煩的事情,這時候,可使用typeorm-model-generator來自動生成這些 entity 。好比運行下面命令:

$ typeorm-model-generator -h 127.0.0.1 -d arya -p 3310 -u root -x 123456 -e mysql -d test -o 'src/entities/' --noConfig true --cf param --ce pascal
複製代碼

這時候就會在src/entities/下面生成cat.ts的 entity 文件:

import {BaseEntity,Column,Entity,Index,JoinColumn,JoinTable,ManyToMany,ManyToOne,OneToMany,OneToOne,PrimaryColumn,PrimaryGeneratedColumn,RelationId} from "typeorm";


@Entity("cat",{schema:"test", database:"test" } )
export class Cat {

    @PrimaryGeneratedColumn({
        type:"int", 
        name:"id"
        })
    id:number;
        

    @Column("varchar",{ 
        nullable:false,
        length:50,
        name:"name"
        })
    name:string;
        

    @Column("int",{ 
        nullable:false,
        name:"age"
        })
    age:number;
        

    @Column("varchar",{ 
        nullable:true,
        length:100,
        name:"breed"
        })
    breed:string | null;
        
}
複製代碼

日誌

官方給出了日誌的解決方案,不過這裏咱們參照nestify,使用log4js作日誌處理。主要緣由是 log4js 對日誌進行了分級、分盤和落盤,方便咱們更好地管理日誌。

在 log4js 中日誌分爲九個等級:

export enum LoggerLevel {
  ALL = 'ALL',
  MARK = 'MARK',
  TRACE = 'TRACE',
  DEBUG = 'DEBUG',
  INFO = 'INFO',
  WARN = 'WARN',
  ERROR = 'ERROR',
  FATAL = 'FATAL',
  OFF = 'OFF',
}
複製代碼

ALLOFF 這兩個等級通常不會直接在業務代碼中使用。剩下的七個即分別對應 Logger 實例的七個方法,也就是說,在調用這些方法的時候,就至關於爲這些日誌定了級。

對於不一樣的日誌級別,在 log4js 中經過不一樣顏色輸出,而且輸出時候帶上日誌輸出時間和對應的 module name:

Log4js.addLayout('Awesome-nest', (logConfig: any) => {
  return (logEvent: Log4js.LoggingEvent): string => {
    let moduleName: string = ''
    let position: string = ''

    const messageList: string[] = []
    logEvent.data.forEach((value: any) => {
      if (value instanceof ContextTrace) {
        moduleName = value.context
        if (value.lineNumber && value.columnNumber) {
          position = `${value.lineNumber}, ${value.columnNumber}`
        }
        return
      }

      if (typeof value !== 'string') {
        value = Util.inspect(value, false, 3, true)
      }

      messageList.push(value)
    })

    const messageOutput: string = messageList.join(' ')
    const positionOutput: string = position ? ` [${position}]` : ''
    const typeOutput: string = `[${ logConfig.type }] ${logEvent.pid.toString()} - `
    const dateOutput: string = `${Moment(logEvent.startTime).format( 'YYYY-MM-DD HH:mm:ss', )}`
    const moduleOutput: string = moduleName
      ? `[${moduleName}] `
      : '[LoggerService] '
    let levelOutput: string = `[${logEvent.level}] ${messageOutput}`

    switch (logEvent.level.toString()) {
      case LoggerLevel.DEBUG:
        levelOutput = Chalk.green(levelOutput)
        break
      case LoggerLevel.INFO:
        levelOutput = Chalk.cyan(levelOutput)
        break
      case LoggerLevel.WARN:
        levelOutput = Chalk.yellow(levelOutput)
        break
      case LoggerLevel.ERROR:
        levelOutput = Chalk.red(levelOutput)
        break
      case LoggerLevel.FATAL:
        levelOutput = Chalk.hex('#DD4C35')(levelOutput)
        break
      default:
        levelOutput = Chalk.grey(levelOutput)
        break
    }

    return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow( moduleOutput, )}${levelOutput}${positionOutput}`
  }
})
複製代碼

在 log4js 中,日誌的出口問題(即日誌輸出到哪裏)由 Appender 來解決:

Log4js.configure({
  appenders: {
    console: {
      type: 'stdout',
      layout: { type: 'Awesome-nest' },
    },
  },
  categories: {
    default: {
      appenders: ['console'],
      level: 'debug',
    },
  },
})
複製代碼

config 中配置了debug級別以上的日誌會經過console輸出。

接下來就是export一個 log class,對外暴露出 log4js 中不一樣等級的 log 方法以供調用,完整代碼以下:

import * as _ from 'lodash'
import * as Path from 'path'
import * as Log4js from 'log4js'
import * as Util from 'util'
import * as Moment from 'moment'
import * as StackTrace from 'stacktrace-js'
import Chalk from 'chalk'

export enum LoggerLevel {
  ALL = 'ALL',
  MARK = 'MARK',
  TRACE = 'TRACE',
  DEBUG = 'DEBUG',
  INFO = 'INFO',
  WARN = 'WARN',
  ERROR = 'ERROR',
  FATAL = 'FATAL',
  OFF = 'OFF',
}

export class ContextTrace {
  constructor( public readonly context: string, public readonly path?: string, public readonly lineNumber?: number, public readonly columnNumber?: number, ) {}
}

Log4js.addLayout('Awesome-nest', (logConfig: any) => {
  return (logEvent: Log4js.LoggingEvent): string => {
    let moduleName: string = ''
    let position: string = ''

    const messageList: string[] = []
    logEvent.data.forEach((value: any) => {
      if (value instanceof ContextTrace) {
        moduleName = value.context
        if (value.lineNumber && value.columnNumber) {
          position = `${value.lineNumber}, ${value.columnNumber}`
        }
        return
      }

      if (typeof value !== 'string') {
        value = Util.inspect(value, false, 3, true)
      }

      messageList.push(value)
    })

    const messageOutput: string = messageList.join(' ')
    const positionOutput: string = position ? ` [${position}]` : ''
    const typeOutput: string = `[${ logConfig.type }] ${logEvent.pid.toString()} - `
    const dateOutput: string = `${Moment(logEvent.startTime).format( 'YYYY-MM-DD HH:mm:ss', )}`
    const moduleOutput: string = moduleName
      ? `[${moduleName}] `
      : '[LoggerService] '
    let levelOutput: string = `[${logEvent.level}] ${messageOutput}`

    switch (logEvent.level.toString()) {
      case LoggerLevel.DEBUG:
        levelOutput = Chalk.green(levelOutput)
        break
      case LoggerLevel.INFO:
        levelOutput = Chalk.cyan(levelOutput)
        break
      case LoggerLevel.WARN:
        levelOutput = Chalk.yellow(levelOutput)
        break
      case LoggerLevel.ERROR:
        levelOutput = Chalk.red(levelOutput)
        break
      case LoggerLevel.FATAL:
        levelOutput = Chalk.hex('#DD4C35')(levelOutput)
        break
      default:
        levelOutput = Chalk.grey(levelOutput)
        break
    }

    return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow( moduleOutput, )}${levelOutput}${positionOutput}`
  }
})

Log4js.configure({
  appenders: {
    console: {
      type: 'stdout',
      layout: { type: 'Awesome-nest' },
    },
  },
  categories: {
    default: {
      appenders: ['console'],
      level: 'debug',
    },
  },
})

const logger = Log4js.getLogger()
logger.level = LoggerLevel.TRACE

export class Logger {
  static trace(...args) {
    logger.trace(Logger.getStackTrace(), ...args)
  }

  static debug(...args) {
    logger.debug(Logger.getStackTrace(), ...args)
  }

  static log(...args) {
    logger.info(Logger.getStackTrace(), ...args)
  }

  static info(...args) {
    logger.info(Logger.getStackTrace(), ...args)
  }

  static warn(...args) {
    logger.warn(Logger.getStackTrace(), ...args)
  }

  static warning(...args) {
    logger.warn(Logger.getStackTrace(), ...args)
  }

  static error(...args) {
    logger.error(Logger.getStackTrace(), ...args)
  }

  static fatal(...args) {
    logger.fatal(Logger.getStackTrace(), ...args)
  }

  static getStackTrace(deep: number = 2): ContextTrace {
    const stackList: StackTrace.StackFrame[] = StackTrace.getSync()
    const stackInfo: StackTrace.StackFrame = stackList[deep]

    const lineNumber: number = stackInfo.lineNumber
    const columnNumber: number = stackInfo.columnNumber
    const fileName: string = stackInfo.fileName

    const extnameLength: number = Path.extname(fileName).length
    let basename: string = Path.basename(fileName)
    basename = basename.substr(0, basename.length - extnameLength)
    const context: string = _.upperFirst(_.camelCase(basename))

    return new ContextTrace(context, fileName, lineNumber, columnNumber)
  }
}
複製代碼

這樣在須要輸出日誌的地方只要這樣調用就行:

Logger.info(id)
複製代碼

但是咱們並不但願每一個請求都本身打 log,這時候能夠把這個 log 做爲中間件:

import { Logger } from '../../shared/utils/logger'

export function logger(req, res, next) {
  const statusCode = res.statusCode
  const logFormat = `${req.method} ${req.originalUrl} ip: ${req.ip} statusCode: ${statusCode}`

  next()

  if (statusCode >= 500) {
    Logger.error(logFormat)
  } else if (statusCode >= 400) {
    Logger.warn(logFormat)
  } else {
    Logger.log(logFormat)
  }
}
複製代碼

main.ts中註冊:

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.setGlobalPrefix('api/v1')

  app.use(logger)
  app.useGlobalFilters(new ExceptionsFilter())
  app.useGlobalInterceptors(new TransformInterceptor())
  app.useGlobalPipes(new ValidationPipe())

  await app.listen(config.port, config.hostName)
}
複製代碼

而且在ExceptionsFilter中也對捕捉到的 Exception 進行日誌輸出:

export class ExceptionsFilter implements ExceptionFilter {
  async catch(exception, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse()
    const request = ctx.getRequest()

    Logger.error('exception', JSON.stringify(exception))

    let message = exception.message
    let isDeepestMessage = false
    while (!isDeepestMessage) {
      isDeepestMessage = !message.message
      message = isDeepestMessage ? message : message.message
    }

    const errorResponse = {
      message: message || '請求失敗',
      status: 1,
    }

    const status = exception instanceof HttpException ?
      exception.getStatus() :
      HttpStatus.INTERNAL_SERVER_ERROR

    Logger.error(
      `Catch http exception at ${request.method} ${request.url} ${status}`,
    )

    response.status(status)
    response.header('Content-Type', 'application/json; charset=utf-8')
    response.send(errorResponse)
  }
}
複製代碼

這樣一個基礎的日誌輸出系統差很少就完成了。固然,log4js 的appender還支持下面幾種:

  • DateFile:日誌輸出到文件,日誌文件能夠安特定的日期模式滾動,例現在天輸出到 default-2016-08-21.log,明天輸出到 default-2016-08-22.log

  • SMTP:輸出日誌到郵件;

  • Mailgun:經過 Mailgun API 輸出日誌到 Mailgun;

  • levelFilter 能夠經過 level 過濾;

  • 等等其餘一些 appender,到這裏能夠看到所有的列表。

好比,下面配置就會把日誌輸出到加上日期後綴的文件中,而且保留 60 天:

Log4js.configure({
    appenders: {
      fileAppender: {
        type: 'DateFile',
        filename: './logs/prod.log',
        pattern: '-yyyy-MM-dd.log',
        alwaysIncludePattern: true,
        layout: { type: 'Flash' },
        daysToKeep: 60
      }
    },
    categories: {
      default: {
        appenders: ['fileAppender'],
        level: 'info'
      }
    },
  })
複製代碼

CRUD

對於通常的 CRUD 的操做,在 Nestjs 中可使用@nestjsx/crud這個庫來幫咱們減小開發量。

首先安裝相關依賴:

npm i @nestjsx/crud @nestjsx/crud-typeorm class-transformer class-validator --save
複製代碼

而後新建dog.entity.ts

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity('dog')
export class DogEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ length: 50 })
  name: string

  @Column()
  age: number

  @Column({ length: 100, nullable: true })
  breed: string
}
複製代碼

dog.service.ts中只需寫下面幾行代碼:

import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm'

import { DogEntity } from './dog.entity'

@Injectable()
export class DogsService extends TypeOrmCrudService<DogEntity> {
  constructor(@InjectRepository(DogEntity) repo) {
    super(repo)
  }
}
複製代碼

dog.controller.ts中,使用@crud幫助自動生成API:

import { Controller } from '@nestjs/common'
import { Crud, CrudController } from '@nestjsx/crud'

import { DogEntity } from './dog.entity'
import { DogsService } from './dogs.service'

@Crud({
  model: {
    type: DogEntity,
  },
})
@Controller('dogs')
export class DogsController implements CrudController<DogEntity> {
  constructor(public service: DogsService) {}
}
複製代碼

這時候,就能夠按照@nestjsx/crud的文檔中 API 規則去請求對應的 CRUD 的操做。好比,請求GET api/v1/dogs,就會返回全部 dog 的數組;請求GET api/v1/dogs/1,就會返回 id1dog

參考

使用 CLI

遷移

Node.js 之 log4js 徹底講解

nestify

相關文章
相關標籤/搜索