likes
comments
collection
share

从零到一搭建nest版若依框架(一)

作者站长头像
站长
· 阅读数 23

从零到一搭建nest版若依框架(一)

项目概述

项目 前端一共有两套 一套是我自己模仿若依项目原型搭建的并且优化了一下页面使用的vite+ts+vue3+pinia 另外一套就是vue3版本的若依框架 后端服务的话就是nestjs+typeorm,可以直接对接若依框架。

自言自语

一不小心入了nestjs这个坑,中途反反复复的放弃又继续捡起来。学习成本个人觉得不亚于直接上手java,主要是国内关于这方面的教学资料太少了,只能对着文档硬着头皮啃。这个项目也是刚完成不久,看了许多大佬的发现还有很多地方不足,现在从头到尾在重构一遍,记录一下整个过程。不太会表达 所以直接上图上代码。完整源码的话。等这一系列更新完就会发出来,现在就先不发出来丢人了。。。

创建项目

  • 这里使用 Cli 创建,执行下面命令
nest new nest-server

从零到一搭建nest版若依框架(一)

  • 选择 yarn

从零到一搭建nest版若依框架(一)

  • 目录结构

从零到一搭建nest版若依框架(一)

  • 运行项目
npm run start:dev //监测代码变动

从零到一搭建nest版若依框架(一)

配置环境

  • 根目录新建 .env 和 .env.prod 两个文件,对应测试和线上的环境配置,提前把后面用到的配置都加好

.env 文件

# 运行环境
NODE_ENV = development
# =========typeorm配置============
# 数据库类型
MYSQL_TYPE= mysql
# 数据库地址
MYSQL_HOST= localhost 
# 数据库端口
MYSQL_PORT3306      
# 数据库账号
MYSQL_USERNAME= root
# 数据库密码
MYSQL_PASSWORD123456
# 数据库名
MYSQL_DATABASE= nest-server


# =========jwt配置==============
# jwt 秘钥
JWT_SECRET= xiaoqi666
# =============================

# =========redis配置============
# redis url地址
REDIS_HOST= localhost
REDIS_PROT6379
REDIS_PASSWORD=123456
REDIS_DB0
# ===============================

# =========队列redis 配置===========
# redis地址
BULL_REDIS_HOST= localhost
# redis端口
BULL_REDIS_PROT6379
# redis密码
BULL_REDIS_PASSWORD=123456
# ==================================

# 文件上传地址 例如: E:/upload/test
UPLOAD_PATH = ''




# 运行环境
NODE_ENV = development
# =========typeorm配置============
# 数据库类型
MYSQL_TYPE= mysql
# 数据库地址
MYSQL_HOST= localhost 
# 数据库端口
MYSQL_PORT3306      
# 数据库账号
MYSQL_USERNAME= root
# 数据库密码
MYSQL_PASSWORD123456
# 数据库名
MYSQL_DATABASE= nest-server


# =========jwt配置==============
# jwt 秘钥
JWT_SECRET= xiaoqi666
# =============================

# =========redis配置============
# redis url地址
REDIS_HOST= localhost
REDIS_PROT6379
REDIS_PASSWORD=123456
REDIS_DB0
# ===============================

# =========队列redis 配置===========
# redis地址
BULL_REDIS_HOST= localhost
# redis端口
BULL_REDIS_PROT6379
# redis密码
BULL_REDIS_PASSWORD=123456
# ==================================

# 文件上传地址 例如: E:/upload/test
UPLOAD_PATH = ''

.env.prod 文件 这里暂时和.env都一样

# 运行环境
NODE_ENV = production
# =========typeorm配置============
# 数据库类型
MYSQL_TYPE= mysql
# 数据库地址
MYSQL_HOST= localhost 
# 数据库端口
MYSQL_PORT3306      
# 数据库账号
MYSQL_USERNAME= root
# 数据库密码
MYSQL_PASSWORD123456
# 数据库名
MYSQL_DATABASE= nest-server


# =========jwt配置==============
# jwt 秘钥
JWT_SECRET= xiaoqi666
# =============================

# =========redis配置============
# redis url地址
REDIS_HOST= localhost
REDIS_PROT6379
REDIS_PASSWORD=123456
REDIS_DB0
# ===============================

# =========队列redis 配置===========
# redis地址
BULL_REDIS_HOST= localhost
# redis端口
BULL_REDIS_PROT6379
# redis密码
BULL_REDIS_PASSWORD=123456
# ==================================

# 文件上传地址 例如: E:/upload/test
UPLOAD_PATH = ''
  • 在根目录新建config目录 新建以下几个文件

从零到一搭建nest版若依框架(一)

  • config.development.ts 文件
import { defineConfig } from './defineConfig';

export default defineConfig({
  jwt: {
    secret: process.env.JWT_SECRET || 'xiaoqi666',
  },
  // typeorm 配置
  database: {
    type: process.env.MYSQL_TYPE || 'mysql'//数据库类型
    host: process.env.MYSQL_HOST || 'localhost'//数据库地址
    port: process.env.MYSQL_PORT || 3306//数据库端口
    username: process.env.MYSQL_USERNAME || 'root'//数据库账号
    password: process.env.MYSQL_PASSWORD || '123456'//数据库密码
    database: process.env.MYSQL_DATABASE || 'nest-server'//数据库名称
    autoLoadModelstrue//模型自动加载,无需在在配置处重复写实体。
    synchronizetrue//如果为true 自动加载的模型将被同步进数据库,生产环境要关闭,否则可能因为字段的删除而造成数据的丢失。
    loggingfalse//是否启动日志记录
  },
  // redis 配置
  redis: {
    host: process.env.REDIS_HOST || 'localhost3',
    port: process.env.REDIS_PORT || '6379w',
    password: process.env.REDIS_PASSWORD || '123456',
    db: process.env.REDIS_DB || '0',
  },

  // 队列reids 配置
  bullRedis: {
    host: process.env.BULL_REDIS_HOST || 'localhost',
    port: process.env.BULL_REDIS_PROT || '6379',
    password: process.env.BULL_REDIS_PASSWORD || '123456',
  },

  //文件上传地址 例如: E:/upload/test
  uploadPath: process.env.UPLOAD_PATH || '',

  // 是否演示环境
  isDemoEnvironmentfalse,
});
  • config.production.ts 文件
import { defineConfig } from './defineConfig';

export default defineConfig({
  jwt: {
    secret: process.env.JWT_SECRET || 'xiaoqi666',
  },
  // typeorm 配置
  database: {
    type: process.env.MYSQL_TYPE || 'mysql'//数据库类型
    host: process.env.MYSQL_HOST || 'localhost'//数据库地址
    port: process.env.MYSQL_PORT || 3306//数据库端口
    username: process.env.MYSQL_USERNAME || 'root'//数据库账号
    password: process.env.MYSQL_PASSWORD || '123456'//数据库密码
    database: process.env.MYSQL_DATABASE || 'nest-server'//数据库名称
    autoLoadModelstrue//模型自动加载,无需在在配置处重复写实体。
    synchronizetrue//如果为true 自动加载的模型将被同步进数据库,生产环境要关闭,否则可能因为字段的删除而造成数据的丢失。
    loggingfalse//是否启动日志记录
  },
  // redis 配置
  redis: {
    host: process.env.REDIS_HOST || 'localhost3',
    port: process.env.REDIS_PORT || '6379w',
    password: process.env.REDIS_PASSWORD || '123456',
    db: process.env.REDIS_DB || '0',
  },

  // 队列reids 配置
  bullRedis: {
    host: process.env.BULL_REDIS_HOST || 'localhost',
    port: process.env.BULL_REDIS_PROT || '6379',
    password: process.env.BULL_REDIS_PASSWORD || '123456',
  },

  //文件上传地址 例如: E:/upload/test
  uploadPath: process.env.UPLOAD_PATH || '',

  // 是否演示环境
  isDemoEnvironmentfalse,
});
  • configuration,ts
import { Logger } from '@nestjs/common';

// 判断系统是否是开发环境
export function isDev(): boolean {
  return process.env.NODE_ENV === 'development';
}

// 根据环境变量判断使用配置
export default () => {
  let envConfig: IConfig = {};
  try {
    envConfig = require(`./config.${process.env.NODE_ENV}`).default;
    //将文件上传路径绑定到环境变量上
    process.env.uploadPath = envConfig.uploadPath ?? '/upload';
  } catch (e) {
    const logger = new Logger('ConfigModule');
    logger.error(e);
  }

  // 返回环境配置
  return envConfig;
};

// 配置文件接口
export interface IConfig {
  /**
   * 后台管理jwt token密钥
   */
  jwt?: {
    secretstring;
  };

  /**
   * 文件上传路径, 绝对路径 例如: E:/upload/test
   */
  uploadPath?: string;

  /**
   * 数据库配置
   */
  database?: {
    type?: string;
    host?: string;
    port?: number | string;
    username?: string;
    password?: string;
    database?: string;
    autoLoadModelsboolean// 如果为true,模型将自动载入(默认:false)
    synchronize?: boolean//如果为true,自动载入的模型将同步
    logging?: any;
  };

  /**
   * redis 配置
   */
  redis?: {
    hoststring;
    portstring;
    passwordstring;
    dbstring
  };

  /* 队列配置 */

  bullRedis?: {
    hoststring;
    portstring;
    passwordstring;
  };

  /* 是否演示环境 */
  isDemoEnvironment?: boolean;
}
  • defineConfig.ts 文件
import { IConfigfrom './configuration';

/* 用于智能提示 */
export function defineConfig(config: IConfig): IConfig {
  return config;
}

接入 mysql redis

  • 安装mysql和redis 我们在根目录新建 /shared目录 该目录用作全局公共模块 依赖存储公共方法和模块 这里我们使用 typeorm 这个orm工具来操作

从零到一搭建nest版若依框架(一)

  • shared.module.ts
import { SharedService } from './shared.service';
import { GlobalModuleValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RedisModule } from '@nestjs-modules/ioredis';
import configuration from '../config/configuration';
import { ConfigModule } from '@nestjs/config';
@Global()
@Module({
  imports: [
    /* 配置文件模块 */
    ConfigModule.forRoot({
      isGlobaltrue,
      load: [configuration],
    }),
    /* 连接mysql数据库 */
    TypeOrmModule.forRootAsync({
      useFactory(configService: ConfigService) => ({
        autoLoadEntitiestrue,
        type: configService.get<any>('database.type'),
        host: configService.get<string>('database.host'),
        port: configService.get<number>('database.port'),
        username: configService.get<string>('database.username'),
        password: configService.get<string>('database.password'),
        database: configService.get<string>('database.database'),
        autoLoadModels: configService.get<boolean>('database.autoLoadModels'),
        synchronize: configService.get<boolean>('database.synchronize'),
        logging: configService.get('database.logging'),
      }),
      inject: [ConfigService],
    }),

    /* 连接redis */
    RedisModule.forRootAsync({
      useFactory(configService: ConfigService) => {
        return {
          config: {
            host: configService.get<string>('redis.host'),
            port: configService.get<number>('redis.port'),
            password: configService.get<string>('redis.password'),
            db: configService.get<number>('redis.db'),
          },
        };
      },

      inject: [ConfigService]
    }),
  ],
  controllers: [],
  providers: [SharedService],
  exports: [SharedService],
})
export class SharedModule { }
  • shared.service,ts
import { Injectable } from '@nestjs/common';
import * as CryptoJS from 'crypto-js';
import { customAlphabet, nanoid } from 'nanoid';
import { Request } from 'express';
import axios from 'axios';
import * as iconv from 'iconv-lite';

@Injectable()
export class SharedService {
  /**
   * 构造树型结构数据
   */
  public handleTree(
    data: any[],
    id?: string,
    parentId?: string,
    children?: string,
  ) {
    const config = {
      id: id || 'id',
      parentId: parentId || 'parentId',
      childrenList: children || 'children',
    };

    const childrenListMap = {};
    const nodeIds = {};
    const tree = [];

    for (const d of data) {
      const parentId = d[config.parentId];
      if (childrenListMap[parentId] == null) {
        childrenListMap[parentId] = [];
      }
      nodeIds[d[config.id]] = d;
      childrenListMap[parentId].push(d);
    }

    for (const d of data) {
      const parentId = d[config.parentId];
      if (nodeIds[parentId] == null) {
        tree.push(d);
      }
    }

    for (const t of tree) {
      adaptToChildrenList(t);
    }

    function adaptToChildrenList(o) {
      if (childrenListMap[o[config.id]] !== null) {
        o[config.childrenList] = childrenListMap[o[config.id]];
      }
      if (o[config.childrenList]) {
        for (const c of o[config.childrenList]) {
          adaptToChildrenList(c);
        }
      }
    }
    return tree;
  }

  /* 获取请求IP */
  getReqIP(reqRequest): string {
    return (
      // 判断是否有反向代理 IP
      (
        (req.headers['x-forwarded-for'as string) ||
        // 判断后端的 socket 的 IP
        req.socket.remoteAddress ||
        ''
      ).replace('::ffff:''')
    );
  }

  /* 判断IP是不是内网 */
  IsLAN(ipstring) {
    ip.toLowerCase();
    if (ip == 'localhost'return true;
    let a_ip = 0;
    if (ip == ''return false;
    const aNum = ip.split('.');
    if (aNum.length != 4return false;
    a_ip += parseInt(aNum[0]) << 24;
    a_ip += parseInt(aNum[1]) << 16;
    a_ip += parseInt(aNum[2]) << 8;
    a_ip += parseInt(aNum[3]) << 0;
    a_ip = (a_ip >> 16) & 0xffff;
    return (
      a_ip >> 8 == 0x7f ||
      a_ip >> 8 == 0xa ||
      a_ip == 0xc0a8 ||
      (a_ip >= 0xac10 && a_ip <= 0xac1f)
    );
  }

  /* 通过ip获取地理位置 */
  async getLocation(ip: string) {
    if (this.IsLAN(ip)) return '内网IP';
    try {
      let { data } = await axios.get(
        `http://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`,
        { responseType'arraybuffer' },
      );
      data = JSON.parse(iconv.decode(data, 'gbk'));
      return data.pro + ' ' + data.city;
    } catch (error) {
      return '未知';
    }
  }

  /**
   * @description: AES加密
   * @param {stringmsg
   * @param {stringsecret
   * @return {*}
   */
  aesEncrypt(msgstringsecretstring): string {
    return CryptoJS.AES.encrypt(msg, secret).toString();
  }

  /**
   * @description: AES解密
   * @param {stringencrypted
   * @param {stringsecret
   * @return {*}
   */
  aesDecrypt(encryptedstringsecretstring): string {
    return CryptoJS.AES.decrypt(encrypted, secret).toString(CryptoJS.enc.Utf8);
  }

  /**
   * @description: md5加密
   * @param {stringmsg
   * @return {*}
   */
  md5(msgstring): string {
    return CryptoJS.MD5(msg).toString();
  }

  /**
   * @description: 生成一个UUID
   * @param {*}
   * @return {*}
   */
  generateUUID(): string {
    return nanoid();
  }

  /**
   * @description: 生成随机数
   * @param {numberlength
   * @param {*placeholder
   * @return {*}
   */
  generateRandomValue(
    lengthnumber,
    placeholder = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM',
  ): string {
    const customNanoid = customAlphabet(placeholder, length);
    return customNanoid();
  }
}
  • 在app.module.ts 中引入SharedModule模块
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SharedModule } from './shared/shared.module';
import { DemoModule } from './modules/demo/demo.module';
@Module({
  imports: [SharedModuleDemoModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

安装 MySql

因为后面要做数据入库的操作,所以先装好

从零到一搭建nest版若依框架(一)

  • Mac 用户下载完之后,启动位置在 系统偏好设置 面板,点进去,start server就可以了

从零到一搭建nest版若依框架(一)

从零到一搭建nest版若依框架(一)

从零到一搭建nest版若依框架(一)

可以手动创建库表,或者代码中去用实体映射创建。

从零到一搭建nest版若依框架(一)

安装 Redis

后面需要用 Redis 做 token 存储等的缓存方案

从零到一搭建nest版若依框架(一)

  • 现在我们 运行 npm run start:dev 没有报错信息就表示成功了

从零到一搭建nest版若依框架(一)

日志收集、DTO校验、全局异常拦截器、统一返回请求体

日志收集是最为常见的后端服务的基础功能里,我将使用 Nestjs中的两个技术点 中间价 +拦截器 ,以及Nodejs中流行的log处理器log4js 来实现。最后的实现出来的效果是 ,错误日志和请求日志都会被写入到本地日志文件和控制台中。后续我们还会写一个job定时的把日志清理 以及转存

  • 我们需要新建如下图的目录结构

从零到一搭建nest版若依框架(一)

log4js.ts

  • /config/log4js.ts 配置文件
import * as path from 'path';
const baseLogPath = path.resolve(__dirname, '../../logs'); // 日志要写入哪个目录

const log4jsConfig = {
  appenders: {
    console: {
      type'console'// 会打印到控制台
    },
    access: {
      type'dateFile'// 会写入文件,并按照日期分类
      filename`${baseLogPath}/access/access.log`// 日志文件名,会命名为:access.20200320.log
      alwaysIncludePatterntrue,
      pattern'yyyyMMdd',
      daysToKeep60,
      numBackups3,
      category'http',
      keepFileExttrue// 是否保留文件后缀
    },
    app: {
      type'dateFile',
      filename`${baseLogPath}/app-out/app.log`,
      alwaysIncludePatterntrue,
      layout: {
        type'pattern',
        pattern:
          '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":'%m'}',
      },
      // 日志文件按日期(天)切割
      pattern'yyyyMMdd',
      daysToKeep60,
      // maxLogSize: 10485760,
      numBackups3,
      keepFileExttrue,
    },
    errorFile: {
      type'dateFile',
      filename`${baseLogPath}/errors/error.log`,
      alwaysIncludePatterntrue,
      layout: {
        type'pattern',
        pattern:
          '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":'%m'}',
      },
      // 日志文件按日期(天)切割
      pattern'yyyyMMdd',
      daysToKeep60,
      // maxLogSize: 10485760,
      numBackups3,
      keepFileExttrue,
    },
    errors: {
      type'logLevelFilter',
      level'ERROR',
      appender'errorFile',
    },
  },
  categories: {
    default: {
      appenders: ['console''app''errors'],
      level'DEBUG',
    },
    info: { appenders: ['console''app''errors'], level'info' },
    access: { appenders: ['console''app''errors'], level'info' },
    http: { appenders: ['access'], level'DEBUG' },
  },
  pm2true// 使用 pm2 来管理项目时,打开
  pm2InstanceVar'INSTANCE_ID'// 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突
};

export default log4jsConfig;
  • /shared/log4js.ts
// src/utils/log4js.ts
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';
import config from 'src/config/log4js';

// 日志级别
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 (logEventLog4js.LoggingEvent): string => {
    let moduleName = '';
    let position = '';

    // 日志组装
    const messageListstring[] = [];
    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, false3true);
      }

      messageList.push(value);
    });

    // 日志组成部分
    const messageOutputstring = messageList.join(' ');
    const positionOutputstring = position ? ` [${position}]` : '';
    const typeOutput = `[${logConfig.type}${logEvent.pid.toString()}   - `;
    const dateOutput = `${Moment(logEvent.startTime).format(
      'YYYY-MM-DD HH:mm:ss',
    )}`;
    const moduleOutputstring = moduleName
      ? `[${moduleName}] `
      : '[LoggerService] ';
    let levelOutput = `[${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(config);

// 实例化
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 access(...args) {
    const loggerCustom = Log4js.getLogger('http');
    loggerCustom.info(Logger.getStackTrace(), ...args);
  }

  // 日志追踪,可以追溯到哪个文件、第几行第几列
  static getStackTrace(deep = 2): string {
    const stackListStackTrace.StackFrame[] = StackTrace.getSync();
    const stackInfoStackTrace.StackFrame = stackList[deep];

    const lineNumbernumber = stackInfo.lineNumber;
    const columnNumbernumber = stackInfo.columnNumber;
    const fileNamestring = stackInfo.fileName;
    const basenamestring = Path.basename(fileName);
    return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;
  }
}

// 这个文件,不但可以单独调用,也可以做成中间件使用。

收集日志(全局拦截器)

  • /common/interceptor/transform.interceptor.ts 全局拦截器用来收集日志
// 全局 拦截器 用来收集日志
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Logger } from 'src/shared/log4js';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(contextExecutionContextnextCallHandler): Observable<any> {
    const req = context.getArgByIndex(1).req;
    return next.handle().pipe(
      map((data) => {
        const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    Request original url: ${req.originalUrl}
    Method: ${req.method}
    IP: ${req.ip}
    User: ${JSON.stringify(req.user)}
    Response data:\n ${JSON.stringify(data)}
    <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`;
        Logger.info(logFormat);
        Logger.access(logFormat);
        return data;
      }),
    );
  }
}

日志中间件

  • 新建 /common/middleware/loger.middleware.ts 文件
import { InjectableNestMiddleware } from '@nestjs/common';
import { RequestResponse } from 'express';
import { Logger } from 'src/shared/log4js';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const code = res.statusCode// 响应状态码
    next();
    // 组装日志信息
    const logFormat = `Method: ${req.method} \n Request original url: ${req.originalUrl} \n IP: ${req.ip} \n Status code: ${code} \n`;
    // 根据状态码,进行日志类型区分
    if (code >= 500) {
      Logger.error(logFormat);
    } else if (code >= 400) {
      Logger.warn(logFormat);
    } else {
      Logger.access(logFormat);
      Logger.log(logFormat);
    }
  }
}

// 函数式中间件
export function logger(req: Request, res: Response, next: () => any) {
  const code = res.statusCode// 响应状态码
  next();
  // 组装日志信息
  const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    Request original url: ${req.originalUrl}
    Method: ${req.method}
    IP: ${req.ip}
    Status code: ${code}
    Parmas: ${JSON.stringify(req.params)}
    Query: ${JSON.stringify(req.query)}
    Body: ${JSON.stringify(
      req.body,
    )} \n  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  `;
  // 根据状态码,进行日志类型区分
  if (code >= 500) {
    Logger.error(logFormat);
  } else if (code >= 400) {
    Logger.warn(logFormat);
  } else {
    Logger.access(logFormat);
    Logger.log(logFormat);
  }
}

统一响应体

  • 新建 /common/class/ajax-result.class.ts 文件
 /*
 * @Description: 返回值封装对象
 */
export class AjaxResult {
  readonly codenumber;
  readonly msgstring;
  readonly dataany;
  // [key: string]: any;

  constructor(code, msg, data) {
    this.code = code;
    this.msg = msg;
    this.data = data;
    // Object.assign(this, data);
  }

  static success(msg = '操作成功', code = 200, data?: any) {
    return new AjaxResult(code, msg, data);
  }

  static error(msg = '操作失败', code = 500, data?: any) {
    return new AjaxResult(code, msg, data);
  }
}

异常过滤器

  • 自定义异常 如果是使用自定义异常类抛出的异常 权限问题返回401 其他问题统一返回500 报错
/*
 * @Description: 自定义异常
 */
import { HttpException } from '@nestjs/common';

export class ApiException extends HttpException {
  private errCodenumber;
  constructor(msg: string, errCode?: number) {
    //权限问题一律使用401错误码
    if (errCode && errCode == 401) {
      super(msg, 200);
      this.errCode = 401;
    } else {
      //其他异常一律使用500错误码
      super(msg, errCode ?? 200);
      this.errCode = errCode ?? 500;
    }
  }
  getErrCode(): number {
    return this.errCode;
  }
}
  • /common/filter/all-exception.filter 全局错误过滤器
/*
 * @Description: 全局错误过滤器
 */

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
from '@nestjs/common';
import { Logger } from 'src/shared/log4js';
import { AjaxResult } from '../class/ajax-result.class';
import { ApiException } from '../exceptions/api.exception';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknownhostArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const { status, result } = this.errorResult(exception);
    response.header('Content-Type''application/json; charset=utf-8');
    response.status(status).json(result);
    const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    Request original url: ${request.originalUrl}
    Method: ${request.method}
    IP: ${request.ip}
    Status code: ${status}
    Response: ${exception} \n  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    `;
    Logger.error(logFormat);
  }

  /* 解析错误类型,获取状态码和返回值 */
  errorResult(exception: unknown) {
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const code =
      exception instanceof ApiException
        ? (exception as ApiException).getErrCode()
        : status;

    let messagestring;
    if (exception instanceof HttpException) {
      const response = exception.getResponse();
      if ((response as any).message && (response as any).message.length > 0) {
        message = `${(response as any).message[0]}`;
      } else {
        message = (response as any).message ?? response;
      }

    } else {
      message = `${exception}`;
    }
    return {
      status,
      resultAjaxResult.error(message, code),
    };
  }
}

全局注册使用

在 /shared/shared.module.ts 公共模块中 全局注册使用

从零到一搭建nest版若依框架(一)

中间价 -> 守卫 —> req拦截器 -> 管道 -> 返回res -> res拦截器

TypeOrm的使用与接口测试

  • 现在我们编写一个测试接口 来测试一下 新建/modules 文件夹 这里我们原来存放 crud 接口代码 在其目录下新建一个全局crud模块 这里我们使用nest 自带的命令 nest g res 目录
$nest g res /modules/demo
  • 选择 生成restful 风格的api

从零到一搭建nest版若依框架(一)

  • 选择是否自动生成一个crud的模板 这里我们选是

从零到一搭建nest版若依框架(一)

  • 现在我们的目录结构是这样的

从零到一搭建nest版若依框架(一)

  • spec.ts文件是原来集成测试接口的 可以去掉可以不去掉 强迫症看着有点难受 可以在 nest-cli.json 这个文件里添加这个配置 这样我们使用 nest g res 目录 生成的文件就不包含.spec.ts的测试文件了 看着简单舒服多了

从零到一搭建nest版若依框架(一)

从零到一搭建nest版若依框架(一)

接下来编写个demo接口 测试是否成功

demo.entity.ts

  • 编写数据库实体文件 demo.entity.ts
import { EntityColumnPrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Demo {
  @PrimaryGeneratedColumn()
  idnumber;
  @Column()
  namestring;
  @Column()
  agenumber;
}

demo.module.ts

  • demo.module.ts 使用 typeorm.forFeature()  将实体注册到模块当中
import { Module } from '@nestjs/common';
import { DemoService } from './demo.service';
import { DemoController } from './demo.controller';
import { Demo } from 'src/modules/demo/entities/demo.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Demo])],
  controllers: [DemoController],
  providers: [DemoService],
})
export class DemoModule {}

create-demo.dto.ts

  • DTO 校验 文件 create-demo.dto.ts 这里使用到了 class-validator 这个校验工具库
import { IsNotEmptyIsStringIsNumber } from 'class-validator';
export class CreateDemoDto {
  @IsString()
  @IsNotEmpty({ message'名字不能为空' })
  namestring;

  @IsNumber()
  @IsNotEmpty({ message'年龄不能为空' })
  agenumber;
}

demo.controller.ts

  • demo.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  HttpException,
  HttpStatus,
from '@nestjs/common';
import { DemoService } from './demo.service';
import { CreateDemoDto } from './dto/create-demo.dto';
import { UpdateDemoDto } from './dto/update-demo.dto';

@Controller('demo')
export class DemoController {
  constructor(private readonly demoService: DemoService) { }

  @Post()
  async create(@Body() createDemoDto: CreateDemoDto) {
    console.log(createDemoDto, 'createDemoDto');
    return await this.demoService.create(createDemoDto);
  }

  @Get()
  findAll() {
    return this.demoService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.demoService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string@Body() updateDemoDto: UpdateDemoDto) {
    return this.demoService.update(+id, updateDemoDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.demoService.remove(+id);
  }
}

demo.service.ts

  • demo.service.ts
import { HttpExceptionInjectable } from '@nestjs/common';
import { CreateDemoDto } from './dto/create-demo.dto';
import { UpdateDemoDto } from './dto/update-demo.dto';
import { Demo } from './entities/demo.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AjaxResult } from 'src/common/class/ajax-result.class';
@Injectable()
export class DemoService {
  @InjectRepository(Demo)
  private readonly demoRepositoryRepository<Demo>;

  async create(createDemoDto: CreateDemoDto) {
    const data = await this.demoRepository.save(createDemoDto);
    return AjaxResult.success('新增成功'200, data);
  }

  async findAll() {
    return await this.demoRepository.find();
  }

  findOne(id: number) {
    return `This action returns a #${id} demo`;
  }

  update(id: number, updateDemoDto: UpdateDemoDto) {
    return `This action updates a #${id} demo`;
  }

  remove(id: number) {
    return `This action removes a #${id} demo`;
  }
}
  • 使用 apipox 测试 调用 /demo 接口

    dto校验生效

从零到一搭建nest版若依框架(一)

  • 新增成功

从零到一搭建nest版若依框架(一)

package.json文件

{
  "name": "xiaoqi",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "cross-env NODE_ENV=production node dist/main",
    "start:dev": "cross-env NODE_ENV=development nest start --watch",
    "start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
    "start:prod": "cross-env NODE_ENV=production node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs-modules/ioredis": "^1.0.1",
    "@nestjs/bull": "^0.6.1",
    "@nestjs/common": "^10.3.9",
    "@nestjs/config": "^2.2.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/jwt": "^9.0.0",
    "@nestjs/passport": "^9.0.0",
    "@nestjs/platform-express": "^9.0.0",
    "@nestjs/schedule": "^2.1.0",
    "@nestjs/swagger": "^6.1.2",
    "@nestjs/throttler": "^3.0.0",
    "@nestjs/typeorm": "^9.0.1",
    "@types/bull": "^3.15.9",
    "@types/cron": "^2.0.0",
    "@types/crypto-js": "^4.1.1",
    "@types/iconv-lite": "^0.0.1",
    "@types/ioredis": "^4.28.10",
    "@types/moment": "^2.13.0",
    "@types/multer": "^1.4.7",
    "@types/node-xlsx": "^0.15.3",
    "@types/systeminformation": "^3.54.1",
    "@types/ua-parser-js": "^0.7.36",
    "axios": "0.24.0",
    "bull": "^4.10.0",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "connect-history-api-fallback": "^2.0.0",
    "cron": "^3.1.7",
    "cross-env": "^7.0.3",
    "crypto-js": "^4.1.1",
    "express": "^4.19.2",
    "helmet": "^6.0.0",
    "iconv-lite": "^0.6.3",
    "install": "^0.13.0",
    "ioredis": "^5.2.3",
    "log4js": "^6.9.1",
    "moment": "^2.29.4",
    "mysql2": "^2.3.3",
    "nanoid": "3.3.4",
    "node-xlsx": "^0.21.0",
    "passport": "^0.6.0",
    "passport-jwt": "^4.0.0",
    "passport-local": "^1.0.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.5.7",
    "stacktrace-js": "^2.0.2",
    "svg-captcha": "^1.4.0",
    "systeminformation": "^5.12.6",
    "typeorm": "^0.3.10",
    "ua-parser-js": "^1.0.2",
    "whatwg-mimetype": "^3.0.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.0.0",
    "@nestjs/schematics": "^9.0.0",
    "@nestjs/testing": "^9.0.0",
    "@types/express": "^4.17.13",
    "@types/jest": "28.1.8",
    "@types/node": "^16.0.0",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "28.1.3",
    "prettier": "^2.3.2",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "28.0.8",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "4.1.0",
    "typescript": "^4.7.4"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}


转载自:https://juejin.cn/post/7382408161385496586
评论
请登录