写在前面

最近忙里偷闲,趁着学习​​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验证)
  • 其他...

其他

  • 源码地址 https://github.com/ahwgs/fast_nest_temp