本文旨在记录使用 Node.js 的企业级应用开发框架 NestJs 的快速构建流程,以便日后能够快速进行开发。

1.使用脚手架构建项目

1.npm i -g @nestjs/clipnpm 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文件中的启动命令。