基于Nest快速构建Web应用

发布于 / 学习

写在前面

最近忙里偷闲,趁着学习Nest的功夫,抽离写了一个Nest模块。这里简单介绍一下什么是Nestjs

Nestjs是一个用于构建高效且可伸缩的服务端应用程序的渐进式 Node.js 框架。

他主要有以下几个特点

  • 完美支持 Typescript
  • 面向 AOP 编程
  • 支持 Typeorm
  • 高并发,异步非阻塞 IO
  • Node.js 版的 spring
  • 构建微服务应用

依赖

  • @nestjs/core 7.5.1 核心包
  • @nestjs/config 环境变量治理
  • @nestjs/swagger 生成接口文档
  • swagger-ui-express 装@nestjs/swagger 必装的包 处理接口文档样式
  • joi 校验参数
  • log4js 日志处理
  • helmet 处理基础 web 漏洞
  • compression 服务端压缩中间件
  • express-rate-limit 请求次数限制
  • typeorm 数据库 orm 框架
  • @nestjs/typeorm nest typeorm 集成
  • ejs 模版引擎
  • class-validator 校验参数
  • ioredis redis 客户端
  • nestjs-redis nest redis 配置模块
  • uuid uuid 生成器
  • @nestjs-modules/mailer 邮箱发送

目录结构

├─.vscode
├─public
│  ├─assets # 静态资源
│  └─views # ejs模板
└─src
    ├─assets
    │  └─email-template # 邮箱模板
    ├─config
    │  ├─env # 配置相关
    │  └─module # 配置模块相关
    ├─controllers # 控制器层
    │  ├─account
    │  └─user
    ├─decorators # 装饰器
    ├─dtos
    │  └─user
    ├─entities # 实体
    ├─enum # 枚举
    ├─exception # 异常分类
    ├─filters # 过滤器
    ├─guard # 守卫
    ├─interceptor # 转换器
    ├─interfaces # 所有类型接口文件
    ├─modules # 所有模块
    │  ├─account # 业务账号模块
    │  ├─base # 基础模块
    │  ├─common # 公共模块
    │  └─user # 业务用户模
    ├─pipes # 管道
    ├─services # 服务层
    │  ├─account
    │  ├─common
    │  │  ├─code
    │  │  ├─jwt
    │  │  └─redis
    │  └─user
    └─utils # 工具类

使用

开始开发

  • 复制根目录下default.env文件,重命名为.env文件,修改其配置
  • yarn start:dev 开始开发
  • 本地新建数据库,Redis,修改.env中相关配置

  • 主要配置项

# ------- 环境变量模版 ---------

# 服务启动端口
SERVE_LISTENER_PORT=3000

# Swagger 文档相关
SWAGGER_UI_TITLE = Fast-nest-temp 接口文档
SWAGGER_UI_TITLE_DESC = 接口文档
SWAGGER_API_VERSION = 0.0.1
SWAGGER_SETUP_PATH = api-docs
SWAGGER_ENDPOINT_PREFIX = nest_api


# 开发模式相关
NODE_ENV=development

# 应用配置

# 数据库相关
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_PORT = 3306
DB_DATABASE = fast_nest
DB_USERNAME = root
DB_PASSWORD = 123456
DB_SYNCHRONIZE = 1
DB_LOGGING = 1
DB_TABLE_PREFIX = t_

# Redis相关
REDIS_HOST = localhost
REDIS_PORT = 6379
REDIS_PASSWORD = 

# Token相关
TOKEN_SECRET = secret
TOKEN_EXPIRES = 7d

# Email相关
EMAIL_HOST = smtp.126.com
EMAIL_PORT = 465
EAMIL_AUTH_USER = xxxxx
EMAIL_AUTH_PASSWORD = xxxxx
EMAIL_FROM = "FAST_NEST_TEMP ROBOT" <xxxx@126.com>

主要功能

  • 基于守卫封装授权守卫,用于校验是否需要登录才可访问资源
# /guard/auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const requestToken =
      request.headers['authtoken'] || request.headers['AuthToken'];
    if (requestToken) {
      try {
        const ret = await this.jwtService.verifyToken(requestToken);
        const { sub, account } = ret as IToken;
        const currentUser: ICurrentUser = {
          userId: sub,
          account,
        };
        request.currentUser = currentUser;
      } catch (e) {
        throw new ApiException('token格式不正确', ApiCodeEnum.ERROR);
      }
    } else {
      throw new ApiException('你还没登录,请先登录', ApiCodeEnum.SHOULD_LOGIN);
    }
    return true;
  }
}

校验成功之后会在全局request中注入curentUser对象

使用守卫 accounnt 下接口都需要登录才可访问

@Controller('account')
@UseGuards(AuthGuard)
export class AccountController {
  constructor(private readonly accountService: AccountService) {}
}
  • 基于装饰器封装获取当前登录用户信息
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (key: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    if (key && request.currentUser) {
      return request.currentUser[key] || '';
    } else {
      return request.currentUser;
    }
  },
);

使用@currentUser装饰器获取参数

async getInfo(@CurrentUser('userId') userId: number): Promise<IAccountInfo> {
    return this.accountService.getUserInfo(userId);
}
  • 基于邮箱模块封装邮箱服务

具体可查看 src/services/common/code/email-code.service.ts

@Injectable()
export class EmailCodeService {
  constructor(private readonly mailerService: MailerService) {}
  /**
   * 邮箱发送
   * @param params IEmailParams
   */
  public async sendEmail(params: IEmailParams) {
    const { to, title, content, template, context } = params;
    return await this.mailerService.sendMail({
      to: to,
      subject: title,
      text: content,
      template,
      context,
    });
  }
}
  • 图形验证码获取工具
    具体可查看 src/services/common/code/img-captcha.service.ts
@Injectable()
export class ImageCaptchaService {
  /**
   * 生成图形验证码
   */
  public createSvgCaptcha(length?: number) {
    const defaultLen = 4;
    const captcha: { data: any; text: string } = svgCaptcha.create({
      size: length || defaultLen,
      fontSize: 50,
      width: 100,
      height: 34,
      ignoreChars: '0o1i',
      background: '#01458E',
      inverse: false,
    });
    return captcha;
  }
}
  • 封装Redis 工具类
    具体可查看 src/services/common/redis/redis-cache.service.ts
@Injectable()
export class RedisClientService {
  public client: Redis;
  constructor(private redisService: RedisService) {}

  onModuleInit() {
    this.getClient();
  }

  public getClient() {
    this.client = this.redisService.getClient();
  }

  public async set(
    key: string,
    value: Record<string, unknown> | string,
    second?: number,
  ) {
    value = JSON.stringify(value);
    // 如果没有传递时间就默认时间
    if (!second) {
      await this.client.setex(key, 24 * 60 * 60, value); // 秒为单位
    } else {
      await this.client.set(key, value, 'EX', second);
    }
  }

  public async get(key: string): Promise<any> {
    const data = await this.client.get(key);
    if (data) {
      return JSON.parse(data);
    } else {
      return null;
    }
  }

  public async del(key: string): Promise<any> {
    await this.client.del(key);
  }

  public async flushall(): Promise<any> {
    await this.client.flushall();
  }
}
  • 封装全局异常过滤器

用于统一处理异常返回信息,更友好的提示用户

文件位于 src/filters/http-exception.filter.ts

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const timestamp = Date.now();
    let errorResponse: IHttpResponse = null;
    const message = exception.message;
    const path = request.url;
    const method = request.method;
    const result = null;
    if (exception instanceof ApiException) {
      const message = exception.getErrorMessage();
      errorResponse = {
        result,
        code: exception.getErrorCode(),
        message,
        path,
        method,
        timestamp,
      };
    } else {
      errorResponse = {
        result,
        message:
          typeof message === 'string'
            ? message || CommonText.REQUEST_ERROR
            : JSON.stringify(message),
        path,
        method,
        timestamp,
        code: ApiCodeEnum.ERROR,
      };
    }

    response.status(HttpStatus.OK);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

  • 封装全局日志打点转换
    文件位于 src/interceptor/logger.interceptor.filter.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private genAccessLog(request, time, res, status, context): any {
    const log = {
      statusCode: status,
      responseTime: `${Date.now() - time}ms`,
      ip: request.ip,
      header: request.headers,
      query: request.query,
      params: request.params,
      body: request.body,
      response: res,
    };
    Logger.access(JSON.stringify(log), `${context.getClass().name}`);
  }
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();
    const status = response.statusCode;
    const now = Date.now();

    return next.handle().pipe(
      tap((res) => {
        // 其他的都进access
        this.genAccessLog(
          request,
          `${Date.now() - now}ms`,
          res,
          status,
          context,
        );
      }),
      catchError((err) => {
        if (err instanceof ApiException) {
          // 其他的都进access
          this.genAccessLog(
            request,
            `${Date.now() - now}ms`,
            err.getErrorMessage(),
            status,
            context,
          );
          Logger.error(err);
        } else {
          Logger.error(err);
        }
        // 返回原异常
        throw err;
      }),
    );
  }
}
  • 更多功能可自行查看源码

接口

模板自带接口如下

  • 登录注册
  • 邮箱验证码
  • 图形验证码
  • 获取个人信息(token验证)
  • 其他…

其他

本文采用 CC BY-NC-SA 3.0 Unported 协议进行许可
本文链接: https://www.ahwgs.cn/jiyunestkuaisugoujianwebyingyong.html