001.初始化nestjs后台管理项目

71 阅读2分钟

本章的目的

学习使用以下工具

  1. 学会配置swagger
  2. 读取配置文件
  3. redis的使用
  4. Filter, guard,interceptor的使用(后面章节有详细介绍)

源码地址:github.com/shenbo1/nes…

新建项目

nest new project-name

我选择ppm

安装并配置 Swagger

pnpm install --save @nestjs/swagger

修改 main.ts(初始化 Swagger 文档)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('backend')
    .setDescription('接口文档')
    .setVersion('1.0')
    .build();
  const documentFactory = () => SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, documentFactory);

  await app.listen(process.env.PORT ?? 1110);
}

bootstrap();

配置环境变量与全局配置

包安装

 pnpm i --save @nestjs/config

1.修改 app.module.ts

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

// 根据环境确定配置文件路径
const envFilePath = process.env.NODE_ENV
  ? `.env.${process.env.NODE_ENV}`
  : '.env';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath,
      isGlobal: true,
      load: [],
    }),
  ],
  controllers: [AppController],
  providers: [AppService, ApiConfigService],
  exports: [],
})
export class AppModule {}

2.创建环境变量文件

新建 .env.development



NODE_ENV = development
PORT = 1110

SWAGGER_ENABLED = false
SWAGGER_TITLE = Swagger
SWAGGER_DESCRIPTION = Swagger
SWAGGER_PREFIX = /api
SWAGGER_VERSION = 1.0

3.配置启动脚本(修改 package.json

{
  "scripts": {
    "start:dev": "NODE_ENV=development nest start --watch",
    "start:test": "NODE_ENV=test nest start --watch"
  }
}

4.创建配置服务 config/index.ts

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

export interface ApiConfig {
  port: number;
  swagger: {
    enabled: boolean;
    title: string;
    description: string;
    version: string;
    prefix: string;
  };
}

@Injectable()
export class ApiConfigService {
  constructor(private configService: ConfigService) {}

  get port(): number {
    return this.configService.get<number>('port', 1110);
  }

  get swagger(): ApiConfig['swagger'] {
    return {
      enabled: this.getBoolean('SWAGGER_ENABLED', false),
      title: this.configService.get<string>('SWAGGER_TITLE', ''),
      prefix: this.configService.get<string>('SWAGGER_PREFIX', ''),
      description: this.configService.get<string>('SWAGGER_DESCRIPTION', ''),
      version: this.configService.get<string>('SWAGGER_VERSION', ''),
    };
  }

  private getBoolean(key: string, defaultValue: boolean): boolean {
    const value = this.configService.get<string>(key, String(defaultValue));
    if (typeof value === 'boolean') {
      return value;
    }
    if (typeof value === 'string') {
      return value.toLowerCase() === 'true' || value === '1';
    }
    return Boolean(value);
  }
}

5.修改app.module.ts

 providers: [AppService, ApiConfigService],

6.优化 main.ts(通过配置服务动态初始化)

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { swagger, port } = app.get(ApiConfigService);

  if (swagger.enabled) {
    const config = new DocumentBuilder()
      .setTitle(swagger.title)
      .setDescription(swagger.description)
      .setVersion(swagger.version)
      .build();
    const documentFactory = () => SwaggerModule.createDocument(app, config);
    SwaggerModule.setup(swagger.prefix, app, documentFactory);
  }

  await app.listen(port);
}

集成redis

安装依赖

 pnpm i ioredis --save-dev

1.创建 Redis 模块(redis.module.ts

import { Module } from '@nestjs/common';
import { ApiConfigService } from '@/config/config.service';
import { REDIS_DB } from '@/constants';
import Redis from 'ioredis';

@Module({
  providers: [
    ApiConfigService,
    {
      provide: REDIS_DB,
      inject: [ApiConfigService],
      useFactory: (config: ApiConfigService) => {
        const redis = config.redis;
        return new Redis({
          port: redis.port,
          host: redis.host,
          password: redis.password,
          db: redis.db,
        });
      },
    },
  ],
  exports: [REDIS_DB],
})
export class RedisModule {}

2.注册 Redis 模块(修改 app.module.ts

imports: [
  // ...其他模块
  RedisModule, // 添加 Redis 模块
]

3.创建常量定义(constants/index.ts

export const REDIS_DB = Symbol('REDIS_DB');

4.配置路径别名(修改 tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"] // 支持 @/ 路径别名
    }
  }
}

5. 添加 Redis 环境变量(.env

.env

REDIS_HOST = 
REDIS_PORT = 
REDIS_PASSWORD = 
REDIS_DB = 

扩展配置服务(ApiConfigService 添加 Redis 配置)

// 扩展 ApiConfig 接口
export interface ApiConfig {
  // ...其他配置
  redis: {
    host: string;
    port: number;
    password: string;
    db: number;
  };
}

// 添加 Redis 配置读取方法
@Injectable()
export class ApiConfigService {
  // ...其他方法
  get redis(): ApiConfig['redis'] {
    return {
      host: this.configService.get<string>('REDIS_HOST', ''),
      port: this.configService.get<number>('REDIS_PORT', 0),
      password: this.configService.get<string>('REDIS_PASSWORD', ''),
      db: this.configService.get<number>('REDIS_DB', 0),
    };
  }
}

6.使用事例(app.service 添加)

import { HttpException, Inject, Injectable } from '@nestjs/common';
import { REDIS_DB } from './constants';
import Redis from 'ioredis';

@Injectable()
export class AppService {
  constructor(@Inject(REDIS_DB) private readonly redis: Redis) {}
  async getHello(): Promise<any> {
    await this.redis.set('mykey', 'hello world');
    return {
      redisKey: await this.redis.get('mykey'),
    };
  }
}

添加全局拦截

1.http的异常 filter

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

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';
    let code = 500;
    let errors: any = undefined;

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const res = exception.getResponse();
      if (typeof res === 'string') {
        message = res;
      } else if (typeof res === 'object' && res !== null) {
        // Nest's HttpException often returns object with message and error
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        message = res.message || res.error || message;
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        errors = res.errors || res;
      }
      code = status;
    } else if (exception instanceof Error) {
      message = exception.message;
    }

    const responseBody = {
      success: false,
      code,
      message,
      path: request.url,
      timestamp: new Date().toISOString(),
      errors,
    };

    this.logger.error(
      `${request.method} ${request.url} -> ${message}`,
      (exception as any)?.stack,
    );

    response.status(status).json(responseBody);
  }
}

2.jwt 令牌 Guard

pnpm i nestjs-cls jsonwebtoken @nestjs/passport
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
  Logger,
} from '@nestjs/common';
import { Request } from 'express';
import * as jwt from 'jsonwebtoken';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';
import { ClsService } from 'nestjs-cls';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  private readonly logger = new Logger(JwtAuthGuard.name);
  constructor(
    private readonly reflector: Reflector,
    private readonly clsService: ClsService,
  ) {}

  canActivate(context: ExecutionContext): boolean {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }

    const req = context.switchToHttp().getRequest<Request>();
    const auth = req.headers['authorization'];
    if (!auth) {
      throw new UnauthorizedException('未提供认证信息');
    }
    const parts = auth.split(' ');
    if (parts.length !== 2 || parts[0] !== 'Bearer')
      throw new UnauthorizedException('无效的认证格式');
    const token = parts[1];

    try {
      const secret = process.env.JWT_SECRET || 'jwt-secret';
      const payload = jwt.verify(token, secret);
      (req as any).user = payload;

      this.clsService.set('userCode', payload.userCode ?? payload?.id);
      this.clsService.set('user', payload);
      return true;
    } catch (err) {
      throw new UnauthorizedException('无效或过期的 token');
    }
  }
}

3.reponse拦截器 interceptors

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

export interface ResponseFormat<T = any> {
  success: boolean;
  code: number;
  message: string;
  data?: T;
}

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        if (
          data &&
          typeof data === 'object' &&
          'success' in data &&
          'code' in data &&
          'message' in data
        ) {
          return data;
        }

        return {
          success: true,
          code: 200,
          message: 'OK',
          data,
        } as ResponseFormat<any>;
      }),
    );
  }
}

4.main.ts修改

app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseInterceptor());

5.app.module.ts 修改

{
  import:[
		//... 其他配置
     ClsModule.forRoot({
      global: true,
      middleware: {
        mount: true,
      },
    }),
  ],
  providers: [
    AppService,
    ApiConfigService,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ]
}

本章结束