本文旨在记录使用 Node.js 的企业级应用开发框架 NestJs 的快速构建流程,以便日后能够快速进行开发。
1.使用脚手架构建项目
1.npm i -g @nestjs/cli
或 pnpm add -g @nestjs/cli
安装脚手架。
2.nest new project-name --strict
使用脚手架创建一个名为“project-name”的项目。
2.项目结构
src/common目录下有decorator、fliter、guard、interceptor、utils等目录。用于存放装饰器、过滤器、守卫、拦截器和一些常用方法等。
src/modules目录下存放项目所用到的业务模块。
.env存放一些配置常量。
3.统一返回响应结果
使用拦截器将返回给前端的数据格式化为标准统一的数据,方便前端对接口数据类型的统一编写。
// src/common/interceptor/response.ts import {NestInterceptor, CallHandler, ExecutionContext, Injectable } from '@nestjs/common' import { map } from 'rxjs/operators' import {Observable} from 'rxjs' interface Response<T> { data: T } @Injectable() export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler<T>):Observable<Response<T>> { return next.handle().pipe(map(data=> { return { data, code:200, message:"success", ok:true } })) } }
在main.js中注册为全局的响应拦截器。
// main.js import { ResponseInterceptor } from 'src/common/interceptor/response'; app.useGlobalInterceptors(new ResponseInterceptor()) // 注册响应拦截器
4.捕获项目异常
定义异常过滤器对项目中的异常进行捕获,并返回统一的响应结果。
// src/common/fliter/exception.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Response } from 'express'; @Catch() // 捕获所有类型的异常 export class HttpExceptionFilter implements ExceptionFilter { catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const res = ctx.getResponse<Response>(); // 如果是http异常,则返回对应状态和错误提示。其它错误则返回500服务器错误 const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; res.status(httpStatus).json({ // 统一响应结果 data: exception.message || '服务器错误', code: httpStatus, message: "error", ok: false }); } }
在main.js中注册为全局的异常过滤器。
// main.js import { HttpExceptionFilter } from 'src/common/fliter/exception'; app.useGlobalFilters(new HttpExceptionFilter()); // 注册异常过滤器
5.全局模块配置
在应用根模块中配置常用的模块。
安装所需依赖:pnpm i mysql2 ioredis typeorm @nestjs/config @nestjs/jwt @nestjs/typeorm @nestjs-modules/ioredis
// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule, ConfigService } from "@nestjs/config"; import { JwtModule } from "@nestjs/jwt"; import { TypeOrmModule } from "@nestjs/typeorm"; import { RedisModule } from '@nestjs-modules/ioredis'; // 环境变量 const envFilePath = ['production.env']; if (process.env.NODE_ENV === 'development') { // 开发环境使用development.env变量 envFilePath.unshift('development.env') } @Module({ imports: [ // .env 模块配置 ConfigModule.forRoot({ isGlobal: true, envFilePath }), // 数据库模块配置 TypeOrmModule.forRootAsync({ useFactory: (configService: ConfigService) => ({ type: 'mysql', host: configService.get<string>('DATABASE_HOST', '127.0.0.1'), port: +configService.get<string>('DATABASE_PORT','3306'), username: configService.get<string>('DATABASE_USERNAME', 'root'), password: configService.get<string>('DATABASE_PASSWORD', '123456'), database: configService.get<string>('DATABASE_NAME', 'root'), // synchronize: true, // 生产环境下不要打开 否则会造成数据的丢失 autoLoadEntities: true, }), inject: [ConfigService] }), // Redis 模块配置 RedisModule.forRootAsync({ useFactory: (configService: ConfigService) => ({ type: 'single', // 单例 options:{ host: configService.get<string>('REDIS_HOST','127.0.0.1'), port: +configService.get<string>('REDIS_PORT','6379'), db:+configService.get<string>('REDIS_DB','0') } }), inject:[ConfigService] }), // jwt 模块配置 JwtModule.registerAsync({ global: true, useFactory: (configService: ConfigService) => ({ secret: configService.get<string>('JWT_SECRET', 'luoaoxuan'), signOptions: { expiresIn: configService.get<string>('JWT_EXPIRES_IN', '2h'), }, }), inject: [ConfigService] }), // 其他模块 ], controllers: [AppController], providers: [AppService], exports: [JwtModule] }) export class AppModule { }
在.env文件中设置环境常量样例。
// .env # 数据库配置 DATABASE_HOST=127.0.0.1 DATABASE_PORT=3306 DATABASE_USERNAME=root DATABASE_PASSWORD=root DATABASE_NAME=mx_db # Redis 配置 REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_DB=1 # jwt配置 JWT_SECRET= JWT_EXPIRES_IN= # 其他配置
6.验证身份
定义身份验证的守卫,检查token的合法性。
// src/guard/AuthGuard.ts import { CanActivate, ExecutionContext, HttpException, Injectable } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; import { Request } from "express"; @Injectable() export class AuthGuard implements CanActivate { constructor(private jwtService: JwtService) { } getToken(req: Request) { // 获取token的方法 这里不唯一 return req.headers.authorization } async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.getToken(request); if (!token) { throw new HttpException('token无效', 401); } try { const payload = await this.jwtService.verifyAsync(token) request.userInfo = payload; // 将token中解析出来的用户信息存入request对象中 } catch { throw new HttpException('token无效', 401); } return true; } }
由于我们在解析token后将用户信息存入request对象中,因此可以自定义一个装饰器用于获取用户信息。
// src/common/decorator/UserInfo.ts import { ExecutionContext, createParamDecorator } from "@nestjs/common" export const UserInfo = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return data?request.userInfo[data]:request.userInfo // 如果没有传参则返回整个对象,如果传参则返回对应的字段值 } )
在controller中简单的使用:
@UseGuards(AuthGuard) // 使用守卫验证token的合法性 @Delete('/userUpdate') remove(@UserInfo('id') id: string) { // 使用自定义装饰器获取token解析出来的用户信息 return this.userService.remove(id) }
其次,还可以利用守卫完成一些如验签、限流等操作。
7.常用的工具函数封装
安装所需依赖:pnpm i moment
// src/common/utils/utils.ts import * as moment from "moment"; import * as crypto from "crypto"; // 获取当前时间|格式化时间 export const getTime = (time:number|Date = new Date().getTime(), rule : string = "YYYY-MM-DD HH:mm:ss") : string => { return moment(time).format(rule); } // 普通不可逆加密 export const hashEncode = (data: string, type: 'md5' | 'sha1') => { let hash = crypto.createHash(type); hash.update(data); return hash.digest('hex'); } // 不可逆加密 需要key export const hmacEncode = (data: string, key: string, type: 'md5' | 'sha1' | 'sha256') => { let hmac = crypto.createHmac(type, key); hmac.update(data); return hmac.digest('hex'); } // 可逆加密 需要key和iv export const encode = (data: string, key: string, iv: string) => { let cipher = crypto.createCipheriv('aes-128-cbc', key, iv); return cipher.update(data, 'binary', 'hex') + cipher.final('hex'); } // 可逆加密的解密 需要key和iv export const decode = (data: string, key: string, iv: string) => { let encipher = crypto.createDecipheriv('aes-128-cbc', key, iv); data = Buffer.from(data, 'hex').toString('binary'); return encipher.update(data, 'binary', 'utf-8') + encipher.final('utf8'); } // 生成指定长度的随机串 export const generateRandomString = (length: number) => { return crypto.randomBytes(Math.ceil(length / 2)) .toString('hex') // 将随机字节转换为十六进制字符串 .slice(0, length); // 保证字符串长度为指定的长度 }
8.云服务器中运行
pm2 start npm --name 'name' -- run start:prod
–name后面跟的是项目命名。
— 后面跟的是package.json文件中的启动命令。