从零到一搭建nest版若依框架(一)
从零到一搭建nest版若依框架(一)
项目概述
项目 前端一共有两套 一套是我自己模仿若依项目原型搭建的并且优化了一下页面使用的vite+ts+vue3+pinia 另外一套就是vue3版本的若依框架 后端服务的话就是nestjs+typeorm,可以直接对接若依框架。
自言自语
一不小心入了nestjs这个坑,中途反反复复的放弃又继续捡起来。学习成本个人觉得不亚于直接上手java,主要是国内关于这方面的教学资料太少了,只能对着文档硬着头皮啃。这个项目也是刚完成不久,看了许多大佬的发现还有很多地方不足,现在从头到尾在重构一遍,记录一下整个过程。不太会表达 所以直接上图上代码。完整源码的话。等这一系列更新完就会发出来,现在就先不发出来丢人了。。。
创建项目
- 这里使用 Cli 创建,执行下面命令
nest new nest-server
- 选择 yarn
- 目录结构
- 运行项目
npm run start:dev //监测代码变动
配置环境
- 根目录新建
.en
v 和.env.prod
两个文件,对应测试和线上的环境配置,提前把后面用到的配置都加好
.env 文件
# 运行环境
NODE_ENV = development
# =========typeorm配置============
# 数据库类型
MYSQL_TYPE= mysql
# 数据库地址
MYSQL_HOST= localhost
# 数据库端口
MYSQL_PORT= 3306
# 数据库账号
MYSQL_USERNAME= root
# 数据库密码
MYSQL_PASSWORD= 123456
# 数据库名
MYSQL_DATABASE= nest-server
# =========jwt配置==============
# jwt 秘钥
JWT_SECRET= xiaoqi666
# =============================
# =========redis配置============
# redis url地址
REDIS_HOST= localhost
REDIS_PROT= 6379
REDIS_PASSWORD=123456
REDIS_DB= 0
# ===============================
# =========队列redis 配置===========
# redis地址
BULL_REDIS_HOST= localhost
# redis端口
BULL_REDIS_PROT= 6379
# redis密码
BULL_REDIS_PASSWORD=123456
# ==================================
# 文件上传地址 例如: E:/upload/test
UPLOAD_PATH = ''
# 运行环境
NODE_ENV = development
# =========typeorm配置============
# 数据库类型
MYSQL_TYPE= mysql
# 数据库地址
MYSQL_HOST= localhost
# 数据库端口
MYSQL_PORT= 3306
# 数据库账号
MYSQL_USERNAME= root
# 数据库密码
MYSQL_PASSWORD= 123456
# 数据库名
MYSQL_DATABASE= nest-server
# =========jwt配置==============
# jwt 秘钥
JWT_SECRET= xiaoqi666
# =============================
# =========redis配置============
# redis url地址
REDIS_HOST= localhost
REDIS_PROT= 6379
REDIS_PASSWORD=123456
REDIS_DB= 0
# ===============================
# =========队列redis 配置===========
# redis地址
BULL_REDIS_HOST= localhost
# redis端口
BULL_REDIS_PROT= 6379
# redis密码
BULL_REDIS_PASSWORD=123456
# ==================================
# 文件上传地址 例如: E:/upload/test
UPLOAD_PATH = ''
.env.prod 文件 这里暂时和.env都一样
# 运行环境
NODE_ENV = production
# =========typeorm配置============
# 数据库类型
MYSQL_TYPE= mysql
# 数据库地址
MYSQL_HOST= localhost
# 数据库端口
MYSQL_PORT= 3306
# 数据库账号
MYSQL_USERNAME= root
# 数据库密码
MYSQL_PASSWORD= 123456
# 数据库名
MYSQL_DATABASE= nest-server
# =========jwt配置==============
# jwt 秘钥
JWT_SECRET= xiaoqi666
# =============================
# =========redis配置============
# redis url地址
REDIS_HOST= localhost
REDIS_PROT= 6379
REDIS_PASSWORD=123456
REDIS_DB= 0
# ===============================
# =========队列redis 配置===========
# redis地址
BULL_REDIS_HOST= localhost
# redis端口
BULL_REDIS_PROT= 6379
# redis密码
BULL_REDIS_PASSWORD=123456
# ==================================
# 文件上传地址 例如: E:/upload/test
UPLOAD_PATH = ''
- 在根目录新建config目录 新建以下几个文件
- 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', //数据库名称
autoLoadModels: true, //模型自动加载,无需在在配置处重复写实体。
synchronize: true, //如果为true 自动加载的模型将被同步进数据库,生产环境要关闭,否则可能因为字段的删除而造成数据的丢失。
logging: false, //是否启动日志记录
},
// 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 || '',
// 是否演示环境
isDemoEnvironment: false,
});
- 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', //数据库名称
autoLoadModels: true, //模型自动加载,无需在在配置处重复写实体。
synchronize: true, //如果为true 自动加载的模型将被同步进数据库,生产环境要关闭,否则可能因为字段的删除而造成数据的丢失。
logging: false, //是否启动日志记录
},
// 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 || '',
// 是否演示环境
isDemoEnvironment: false,
});
- 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?: {
secret: string;
};
/**
* 文件上传路径, 绝对路径 例如: E:/upload/test
*/
uploadPath?: string;
/**
* 数据库配置
*/
database?: {
type?: string;
host?: string;
port?: number | string;
username?: string;
password?: string;
database?: string;
autoLoadModels: boolean; // 如果为true,模型将自动载入(默认:false)
synchronize?: boolean; //如果为true,自动载入的模型将同步
logging?: any;
};
/**
* redis 配置
*/
redis?: {
host: string;
port: string;
password: string;
db: string
};
/* 队列配置 */
bullRedis?: {
host: string;
port: string;
password: string;
};
/* 是否演示环境 */
isDemoEnvironment?: boolean;
}
- defineConfig.ts 文件
import { IConfig } from './configuration';
/* 用于智能提示 */
export function defineConfig(config: IConfig): IConfig {
return config;
}
接入 mysql redis
- 安装mysql和redis 我们在根目录新建 /shared目录 该目录用作全局公共模块 依赖存储公共方法和模块 这里我们使用 typeorm 这个orm工具来操作
- shared.module.ts
import { SharedService } from './shared.service';
import { Global, Module, ValidationPipe } 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({
isGlobal: true,
load: [configuration],
}),
/* 连接mysql数据库 */
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
autoLoadEntities: true,
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(req: Request): string {
return (
// 判断是否有反向代理 IP
(
(req.headers['x-forwarded-for'] as string) ||
// 判断后端的 socket 的 IP
req.socket.remoteAddress ||
''
).replace('::ffff:', '')
);
}
/* 判断IP是不是内网 */
IsLAN(ip: string) {
ip.toLowerCase();
if (ip == 'localhost') return true;
let a_ip = 0;
if (ip == '') return false;
const aNum = ip.split('.');
if (aNum.length != 4) return 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 {string} msg
* @param {string} secret
* @return {*}
*/
aesEncrypt(msg: string, secret: string): string {
return CryptoJS.AES.encrypt(msg, secret).toString();
}
/**
* @description: AES解密
* @param {string} encrypted
* @param {string} secret
* @return {*}
*/
aesDecrypt(encrypted: string, secret: string): string {
return CryptoJS.AES.decrypt(encrypted, secret).toString(CryptoJS.enc.Utf8);
}
/**
* @description: md5加密
* @param {string} msg
* @return {*}
*/
md5(msg: string): string {
return CryptoJS.MD5(msg).toString();
}
/**
* @description: 生成一个UUID
* @param {*}
* @return {*}
*/
generateUUID(): string {
return nanoid();
}
/**
* @description: 生成随机数
* @param {number} length
* @param {*} placeholder
* @return {*}
*/
generateRandomValue(
length: number,
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: [SharedModule, DemoModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
安装 MySql
因为后面要做数据入库的操作,所以先装好
- Mac 用户下载完之后,启动位置在
系统偏好设置
面板,点进去,start server
就可以了
可以手动创建库表,或者代码中去用实体映射创建。
安装 Redis
后面需要用 Redis 做 token 存储等的缓存方案
- 现在我们 运行 npm run start:dev 没有报错信息就表示成功了
日志收集、DTO校验、全局异常拦截器、统一返回请求体
日志收集是最为常见的后端服务的基础功能里,我将使用 Nestjs中的两个技术点 中间价 +拦截器 ,以及Nodejs中流行的log处理器log4js 来实现。最后的实现出来的效果是 ,错误日志和请求日志都会被写入到本地日志文件和控制台中。后续我们还会写一个job定时的把日志清理 以及转存
- 我们需要新建如下图的目录结构
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
alwaysIncludePattern: true,
pattern: 'yyyyMMdd',
daysToKeep: 60,
numBackups: 3,
category: 'http',
keepFileExt: true, // 是否保留文件后缀
},
app: {
type: 'dateFile',
filename: `${baseLogPath}/app-out/app.log`,
alwaysIncludePattern: true,
layout: {
type: 'pattern',
pattern:
'{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":'%m'}',
},
// 日志文件按日期(天)切割
pattern: 'yyyyMMdd',
daysToKeep: 60,
// maxLogSize: 10485760,
numBackups: 3,
keepFileExt: true,
},
errorFile: {
type: 'dateFile',
filename: `${baseLogPath}/errors/error.log`,
alwaysIncludePattern: true,
layout: {
type: 'pattern',
pattern:
'{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":'%m'}',
},
// 日志文件按日期(天)切割
pattern: 'yyyyMMdd',
daysToKeep: 60,
// maxLogSize: 10485760,
numBackups: 3,
keepFileExt: true,
},
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' },
},
pm2: true, // 使用 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 (logEvent: Log4js.LoggingEvent): string => {
let moduleName = '';
let position = '';
// 日志组装
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 = `[${logConfig.type}] ${logEvent.pid.toString()} - `;
const dateOutput = `${Moment(logEvent.startTime).format(
'YYYY-MM-DD HH:mm:ss',
)}`;
const moduleOutput: string = 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 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 basename: string = 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(context: ExecutionContext, next: CallHandler): 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 { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } 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 code: number;
readonly msg: string;
readonly data: any;
// [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 errCode: number;
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: unknown, host: ArgumentsHost) {
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 message: string;
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,
result: AjaxResult.error(message, code),
};
}
}
全局注册使用
在 /shared/shared.module.ts 公共模块中 全局注册使用
中间价 -> 守卫 —> req拦截器 -> 管道 -> 返回res -> res拦截器
TypeOrm的使用与接口测试
- 现在我们编写一个测试接口 来测试一下 新建/modules 文件夹 这里我们原来存放 crud 接口代码 在其目录下新建一个全局crud模块 这里我们使用nest 自带的命令 nest g res 目录
$nest g res /modules/demo
- 选择 生成restful 风格的api
- 选择是否自动生成一个crud的模板 这里我们选是
- 现在我们的目录结构是这样的
- spec.ts文件是原来集成测试接口的 可以去掉可以不去掉 强迫症看着有点难受 可以在 nest-cli.json 这个文件里添加这个配置 这样我们使用 nest g res 目录 生成的文件就不包含.spec.ts的测试文件了 看着简单舒服多了
接下来编写个demo接口 测试是否成功
demo.entity.ts
- 编写数据库实体文件 demo.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Demo {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
age: number;
}
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 { IsNotEmpty, IsString, IsNumber } from 'class-validator';
export class CreateDemoDto {
@IsString()
@IsNotEmpty({ message: '名字不能为空' })
name: string;
@IsNumber()
@IsNotEmpty({ message: '年龄不能为空' })
age: number;
}
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 { HttpException, Injectable } 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 demoRepository: Repository<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校验生效
- 新增成功
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