likes
comments
collection
share

Nest实战 - 基础资源整合

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

缘起

前言

本章会基于Nest9.x搭建一个基于Nest通用的模板

基础篇

创建项目

nest new nest-study

Nest实战 - 基础资源整合

  • 选择一个喜欢的源,安装依赖,我选择yarn

删除测试文件

Nest实战 - 基础资源整合

  • 左侧是项目初始化的目录结构
  • 红框圈起来的属于测试用例文件,一般不用。可通过添加
"generateOptions": {
    "spec": false
  }

进行删除

  • 我们通过 nest g res user 创建一个 user模块看下配置是否生效

Nest实战 - 基础资源整合 很明显,配置已经生效了

启动项目

  • 执行npm run start启动项目

Nest实战 - 基础资源整合

拦截器 - 返回参数格式统一

  • 问题
    • 现在返回的数据是一段文本数据,一般前端需要的是JSON格式的数据,不慌,我们把AppService中的返回值改成对象即可

    • Nest实战 - 基础资源整合

    • 重启项目后,刷新页面就会发现数据以json的格式返回了

    • 正常的后端返回的结构是这样的

      {
          data, // 数据 
          status: 0, // 接口状态值 
          extra: {}, // 拓展信息 
          message: 'success', // 异常信息 
          success:true // 接口业务返回状态 
      }
      
  • 介绍
    • 拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

      • 在函数执行之前/之后绑定额外的逻辑
      • 转换从函数返回的结果
      • 转换从函数抛出的异常
      • 扩展基本函数行为
      • 根据所选条件完全重写函数 (例如, 缓存目的)
  • 使用
    • 新建文件 src/common/interceptors/transform.interceptor.ts
    • 写入
        import {
           Injectable,
           NestInterceptor,
           ExecutionContext,
           CallHandler,
         } from '@nestjs/common';
         import { Observable } from 'rxjs';
         import { map } from 'rxjs/operators';
      
         interface Response<T> {
           data: T;
         }
      
         @Injectable()
         export class TransformInterceptor<T>
           implements NestInterceptor<T, Response<T>>
         {
           intercept(
             context: ExecutionContext,
             next: CallHandler,
           ): Observable<Response<T>> {
             return next.handle().pipe(
               map((data) => ({
                 data,
                 status: 0,
                 extra: {},
                 message: 'success',
                 success: true,
               })),
             );
           }
         }
      
    • 修改app.module.ts添加TransformInterceptor到全局
        import { Module } from '@nestjs/common';
        import { APP_INTERCEPTOR } from '@nestjs/core';
        import { AppController } from './app.controller';
        import { AppService } from './app.service';
        import { TransformInterceptor } from './common/interceptors/transform.interceptor';
        import { UserModule } from './user/user.module';
    
        @Module({
          imports: [UserModule],
          controllers: [AppController],
          providers: [
            AppService,
            { 
              // 全局拦截器
              provide: APP_INTERCEPTOR,
              useClass: TransformInterceptor,
            },
          ],
        })
        export class AppModule {}
    
    • 启动项目npm run start:dev 可以实现热重启,同时发现数据已经被拦截器拦截了
    • Nest实战 - 基础资源整合

异常过滤器 - 统一处理程序异常以及Http异常

  • 问题
    • Nest实战 - 基础资源整合
    • 当我们程序抛出异常和访问路径404的时候为了规范格式,需要自定义处理异常

全部异常拦截

  • 介绍
    • 内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。
  • 使用
    • 新建文件 src/common/exceptions/base.exception.filter.ts

    • 写入

        import {
        ArgumentsHost,
        Catch,
        ExceptionFilter,
        HttpStatus,
      } from '@nestjs/common';
      import { Request, Response } from 'express';
      
      @Catch()
      export class BaseExceptionFilter implements ExceptionFilter {
        catch(exception: Error, host: ArgumentsHost) {
          const request = host.switchToHttp().getRequest<Request>();
          const response = host.switchToHttp().getResponse<Response>();
      
          // 程序异常
          response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
            statusCode: HttpStatus.SERVICE_UNAVAILABLE,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: exception.message,
          });
        }
      }
      
    • 修改app.module.ts添加BaseExceptionFilter到全局

      import { BaseExceptionFilter } from './common/exceptions/base.exception.filter';
      {
       provide: APP_FILTER,
       useClass: BaseExceptionFilter,
      },
      
      
    • 启动项目npm run start:dev,同时发现数据已经被异常过滤器处理掉了

    • Nest实战 - 基础资源整合

    • 问题

      • 访问 err1的时候statusCode应该是404才对,我们接着添加Http异常过滤器

HTTP异常拦截

自定义异常

  • 新建src/common/exceptions/custom.business.ts
  • 写入
        import { HttpException, HttpStatus } from '@nestjs/common';
        import { BUSINESS_ERROR_CODE } from './business.error.codes';
    
        export type TBusinessError = {
          code: number;
          message: string;
        };
    
        /**
         * 自定义异常 - 用于主动抛出
         */
        export class CustomException extends HttpException {
          constructor(error: TBusinessError | string) {
            if (typeof error === 'string') {
              error = {
                code: BUSINESS_ERROR_CODE.COMMON,
                message: error,
              };
            }
            super(error, HttpStatus.OK);
          }
          static throwForbidden() {
            throw new CustomException({
              code: BUSINESS_ERROR_CODE.ACCESS_FORBIDDEN,
              message: '抱歉哦,您无此权限!',
            });
          }
        }
    
  • 新建src/common/exceptions/custom.business.error.codes.ts
  • 写入
    export const BUSINESS_ERROR_CODE = {
      // 公共错误码
      COMMON: 10001,
      // 无访问权限
      ACCESS_FORBIDDEN: 10003,
    };
    
  • 打开src/common/exceptions/http.exception.filter.ts
  • 修改
    import {
      ArgumentsHost,
      Catch,
      ExceptionFilter,
      HttpException,
      HttpStatus,
    } from '@nestjs/common';
    import { Request, Response } from 'express';
    import { CustomException, TBusinessError } from './custom.exception';
    
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
      catch(exception: HttpException, host: ArgumentsHost) {
        const request = host.switchToHttp().getRequest<Request>();
        const response = host.switchToHttp().getResponse<Response>();
    
        // 自定义异常走这个
        if (exception instanceof CustomException) {
          const { code, message } = exception.getResponse() as TBusinessError;
          response.status(HttpStatus.OK).send({
            data: null,
            status: code,
            extra: {},
            message,
            success: false,
          });
        }
        // http异常
        response.status(HttpStatus.NOT_FOUND).send({
          statusCode: HttpStatus.NOT_FOUND,
          timestamp: new Date().toISOString(),
          path: request.url,
          message: exception.getResponse(),
        });
      }
    }
    
    
  • app.controller.ts中更改以下代码
      @Get('err')
      getErr() {
        throw new CustomException('自定义异常抛出');
        return this.appService.getHello();
      }
    
  • 刷新浏览器效果如下 -Nest实战 - 基础资源整合

API多版本控制

  • 介绍
    • 接口版本可以在同一个应用程序中的控制器或者路由层面支持 不同的版本。 应用程序经常更改,在仍然需要支持以前版本的应用程序的同时,需要进行重大更改的情况并不少见。
  • main.ts中写入
     // api多版本控制
      app.enableVersioning({
        type: VersioningType.URI,
      });
    
  • 请求上添加 Version('1') 即可
  • Nest实战 - 基础资源整合
  • 如果所有接口想要增加v1版本前缀,添加app.enableVersioningdefaultVersion属性即可
      // api多版本控制
      app.enableVersioning({
        type: VersioningType.URI,
        defaultVersion: ['1'],
      });
    
  • 这个时候直接访问3000端口就会出现404的问题
  • Nest实战 - 基础资源整合

自定义配置文件

  • 介绍
    • 为了方便维护配置项,故需要集中管理
  • 安装
    yarn add cross-env @nestjs/config js-yaml
    
    yarn add @types/js-yaml -D
    
  • 新建根目录/.config/.dev.yml
  • 写入
    HTTP:
      host: 'localhost'
      port: 3000
    
    
  • package.json更改 "start:dev": "cross-env RUNNING_ENV=dev nest start --watch"
  • 新建src/utils/ymlConfig.ts
  • 写入
    import { readFileSync } from 'fs';
    import * as yaml from 'js-yaml';
    import { join } from 'path';
    
    const getEnv = () => process.env.RUNNING_ENV;
    
    export const getConfig = (key?: string) => {
     const ymlInfo = yaml.load(
       readFileSync(join(process.cwd(), `.config/.${getEnv()}.yml`), 'utf-8'),
     ) as Record<string, any>;
     if (key) {
       return ymlInfo[key];
     }
     return ymlInfo;
    };
    
    
  • 为了解决process.env.没提示的问题,新建src/types/index.d.ts,写入
        declare namespace NodeJS {
         interface ProcessEnv {
           RUNNING: string;
         }
       }
    

通过TS的类型合并使其生效

  • app.module.tsimports中添加
    import { getConfig } from './common/utils/ymlConfig';
    
    ConfigModule.forRoot({
         isGlobal: true,
         ignoreEnvFile: true,
         load: [getConfig],
       }),
    
  • app.controller.ts中添加
     constructor(
        private readonly appService: AppService,
        private readonly configService: ConfigService,
      ) {}
      Get()
      @Version('1')
      getHello1() {
        return this.configService.get('HTTP');
      }
    
  • 启动项目
  • Nest实战 - 基础资源整合

swagger文档

  • 介绍
    • 提供文档支持
  • 安装
        yarn add @nestjs/swagger
    
  • 新建 src/doc.ts
  • 写入
        import { INestApplication } from '@nestjs/common';
        import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
        import { knife4jSetup } from 'nest-knife4j';
        // 文档
        export const generateDocmment = (app: INestApplication) => {
          const config = new DocumentBuilder()
            .setTitle('nest-study')
            .setDescription('The nest-study API description')
            .setVersion('1.0')
            .build();
          const document = SwaggerModule.createDocument(app, config);
          SwaggerModule.setup('api', app, document);
        };
    
    
  • main.ts新增
      // 文档支持
      generateDocmment(app);
    
  • 启动项目访问localhost:3000/api
  • Nest实战 - 基础资源整合

swagger新皮 - knife4j

  • 安装
        yarn add nestjs-knife4j
    
  • doc.ts中新增
    import { knife4jSetup } from 'nestjs-knife4j';
        // doc.html
    knife4jSetup(app, {
    urls: [
      {
        name: '1.X版本',
        url: `/api-json`,
        swaggerVersion: '3.0',
        location: `/api-json`,
      },
    ],
    });
    
  • 启动项目访问localhost:3000/doc.html

Nest实战 - 基础资源整合

热重载

  • 介绍
    • 对应用程序的引导过程影响最大的是 TypeScript 编译。但问题是,每次发生变化时,我们是否必须重新编译整个项目?一点也不。这就是为什么 webpack HMR(Hot-Module Replacement)大大减少了实例化您的应用程序所需的时间。
  • 安装
    yarn add  webpack-node-externals run-script-webpack-plugin webpack -D
    
  • 新建 更目录/webpack-hmr.config.js
  • 写入
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const nodeExternals = require('webpack-node-externals');
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
    
    module.exports = function (options, webpack) {
      return {
        ...options,
        entry: ['webpack/hot/poll?100', options.entry],
        externals: [
          nodeExternals({
            allowlist: ['webpack/hot/poll?100'],
          }),
        ],
        plugins: [
          ...options.plugins,
          new webpack.HotModuleReplacementPlugin(),
          new webpack.WatchIgnorePlugin({
            paths: [/\.js$/, /\.d\.ts$/],
          }),
          new RunScriptWebpackPlugin({
            name: options.output.filename,
            autoRestart: false,
          }),
        ],
      };
    };
    
  • 修改main.ts注意热更代码要放到启动项目之后,否则项目无法启动
    declare const module: any;
    
    async function bootstrap() {
      // 创建实例
      const app = await NestFactory.create(AppModule);
    
      // api多版本控制
      app.enableVersioning({
        type: VersioningType.URI,
      });
    
      // 日志
      app.use(logger);
    
      // 文档支持
      generateDocmment(app);
    
      // 启动项目
      await app.listen(getConfig('HTTP').port);
    
      if (module.hot) {
        module.hot.accept();
        module.hot.dispose(() => app.close());
      }
    }
    bootstrap();
    
    
  • package.json中添加热更命令
        "start:dev:hot": "cross-env RUNNING_ENV=dev nest build --webpack --webpackPath webpack-hmr.config.js --watch",
    
  • npm run start:dev:hot启动项目,然后随便更改一个文件保存,效果如下
  • Nest实战 - 基础资源整合
  • 可以看到热更已经成功启动

日志篇

log4j

  • 介绍
    • 我们将配置自定义日志信息,写入本地文件,方便排查问题
  • 安装
    yarn add log4js stacktrace-js moment chalk@4.1.2
    
  • 新建根目录/.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.当前时间.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,
                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,
                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:false,//使用pm2来管理项目时打开
        pm2InstanceVar:'INSTANCE_ID',// 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突
    }
    export default log4jsConfig;
    
    
  • 新建src/common/logger/log4js.ts
  • 写入
    // 日志
    // src/common/logger/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 '../../../.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,
        )}`;
      };
    });
    
    // 注入配置
    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`;
      }
    }
    
  • 新建src/common/middleware/logger.middleware.ts
  • 写入
    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { NextFunction, Request, Response } from 'express';
    import { Logger } from '../logger/log4js';
    
    @Injectable()
    export class LoggerMiddleware implements NestMiddleware {
      use(req: Request, res: Response, next: NextFunction) {
        const code = res.statusCode; //响应状态吗
        next();
        //  组装日志信息
        const logFormat = `Method:${req.method}
          Request original url: ${req.originalUrl}
          IP:${req.ip}
          Status code:${code} `;
        // 根据状态码进行日志类型区分
        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, null, 2)}
      Query: ${JSON.stringify(req.query, null, 2)}
      Body: ${JSON.stringify(
        req.body,
        null,
        2,
      )} \n  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    `;
      //根据状态码,进行日志类型区分
      if (code >= 500) {
        Logger.error(logFormat);
      } else if (code >= 400) {
        Logger.warn(logFormat);
      } else {
        Logger.access(logFormat);
        // Logger.log(logFormat);
      }
    }
    
    
  • 修改main.ts
    import { logger } from './common/middleware/logger.middleware';
      // 日志
    app.use(logger);
    
  • 启动项目输入locaohost:3000?name=张三&age=18,看效果如下
  • Nest实战 - 基础资源整合

数据库篇

TypeOrm-Mysql

  • 介绍
    • Nestjs本身就是基于注解进行开发,搭配同样基于注解的ORM框架TypeORM开发,效果杠杠滴,废话不多说,开始吧
基础
  • 新建名为nest-study的数据库
  • Nest实战 - 基础资源整合
  • Nest实战 - 基础资源整合
  • 点击OK即可
  • 安装依赖
    yarn add @nestjs/typeorm typeorm mysql2
    
  • 根目录/.config/.dev.yml添加mysql配置
          # mysql
        MYSQL_CONFIG:
          type: mysql # 数据库链接类型
          host: localhost
          port: 3306
          username: "root" # 数据库链接用户名
          password: "你的数据库密码" # 数据库链接密码
          database: "nest-study" # 数据库名
          logging: true # 数据库打印日志
          synchronize: true # 是否开启同步数据表功能 , 生产环境一定要关闭,开启会删除所有数据
          autoLoadEntities: true # 是否自动加载实体
    
  • app.module.ts添加以下代码
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { DataSource } from 'typeorm';
        TypeOrmModule.forRootAsync({
          useFactory() {
            return {
              ...getConfig('MYSQL_CONFIG'),
            };
          },
          async dataSourceFactory(options) {
            if (!options) {
              throw new Error('Invalid options passed');
            }
            return new DataSource(options);
          },
        }),
    
  • 如图所示
  • Nest实战 - 基础资源整合
  • 修改user.module.ts
    import { Module } from '@nestjs/common';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { UserService } from './user.service';
    import { UserController } from './user.controller';
    import { User } from './entities/user.entity';
    @Module({
      imports: [TypeOrmModule.forFeature([User])],
      controllers: [UserController],
      providers: [UserService],
    })
    export class UserModule {}
    
  • 修改 user.entity.ts,注册实体
    import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
    import { ApiProperty } from '@nestjs/swagger';
    @Entity()
    export class User {
      @ApiProperty({ example: '1' })
      @PrimaryGeneratedColumn('increment', {
        comment: '主键ID',
      })
      id?: number;
    
      @ApiProperty({ example: 'admin' })
      @Column({ comment: '用户名' })
      name: string;
    
      @ApiProperty({ example: '123456' })
      @Column({ comment: '密码' })
      passowrd: string;
    
      @ApiProperty({ example: 'tyf' })
      @Column({ comment: '昵称' })
      nickName: string;
    }
    
    
  • 修改user.service.ts
    import { Injectable } from '@nestjs/common';
    import { Repository } from 'typeorm';
    import { InjectRepository } from '@nestjs/typeorm';
    import { User } from './entities/user.entity';
    @Injectable()
    export class UserService {
      // 俩种注入方式
      // 通过构造函数注入
      constructor(
        @InjectRepository(User) private readonly userRepositroy: Repository<User>,
      ) {}
      // 直接注入
      // @InjectRepository(User)
      // private userRepositroy: Repository<User>;
    
      /**
       *
       * @returns 列表查询
       */
      list() {
        return this.userRepositroy.find();
      }
    
      /**
       *
       * @param user
       * @returns 新增用户
       */
      save(user: User) {
        return this.userRepositroy.save(user);
      }
    
      /**
       *
       * @param user
       * @returns 更新
       */
      update(user: User) {
        return this.userRepositroy.update({ id: user.id }, user);
      }
    
      /**
       *
       * @param user
       * @returns 删除
       */
      delete(id: User['id']) {
        return this.userRepositroy.delete({ id });
      }
    }
    
    
  • 俩种注入方式都可以,看个人习惯
  • 接着修改user.controller.ts
    import {
      Body,
      Controller,
      Delete,
      Get,
      Param,
      Post,
      Put,
    } from '@nestjs/common';
    import { UserService } from './user.service';
    import { ApiTags, ApiOperation } from '@nestjs/swagger';
    import { User } from './entities/user.entity';
    @ApiTags('用户管理')
    @Controller('user')
    export class UserController {
      constructor(private readonly userService: UserService) {}
    
      @ApiOperation({
        summary: '用户列表',
      })
      @Get()
      list() {
        return this.userService.list();
      }
    
      @ApiOperation({
        summary: '新增用户',
      })
      @Post()
      save(@Body() user: User) {
        return this.userService.save(user);
      }
    
      @ApiOperation({
        summary: '修改用户',
      })
      @Put()
      update(@Body() user: User) {
        return this.userService.update(user);
      }
    
      @ApiOperation({
        summary: '删除',
      })
      @Delete(':id')
      delete(@Param('id') id: number) {
        return this.userService.delete(id);
      }
    }
    
  • 启动项目会发现数据库中user表已经被创建成功了
  • Nest实战 - 基础资源整合
  • 访问http://localhost:3000/api
  • Nest实战 - 基础资源整合
  • swagger文档已经被生成了,接下来就可以利用swagger文档进行接口测试了
  • 当然你使用knife4j也是可以的,访问http://localhost:3000/doc.html即可
  • Nest实战 - 基础资源整合

Nest实战 - 基础资源整合

Nest实战 - 基础资源整合

  • data返回空数组正常,因为表中还没数据
  • 新增的时候id可以手动去掉,因为我们设置的id是自增的,(不去也没关系)

  • Nest实战 - 基础资源整合

  • Nest实战 - 基础资源整合

  • 这个时候数据已经新增成功了,调用用户列表接口看能否查出来

  • Nest实战 - 基础资源整合

  • 修改用户的时候把 name的值改成admin1试试看
  • Nest实战 - 基础资源整合
  • 调用列表接口发现也没有问题
  • Nest实战 - 基础资源整合
  • 我们把刚才新增的id1的数据删掉,发现没问题
  • Nest实战 - 基础资源整合
  • 继续调用列表接口验证
  • Nest实战 - 基础资源整合
  • 到目前为止,基本的 增删改查就全部实现了
驼峰命名转下划线
  • 介绍
    • 在工作中,数据库表头采用的是下横线方式,如图
    • Nest实战 - 基础资源整合
    • 工作中一般使用小驼峰的形式命名代码,由于typeORM会把属性和数据库的表头做自动映射关系,所以生成的表头就是这样的
    • Nest实战 - 基础资源整合
解法一 设置注解Columnname字段即可
  • Nest实战 - 基础资源整合
  • 可以通过打印的sql或者刷新数据库可以看到变化
  • 问题
    • 表结构多了,这样写会累死人的
解法二 全局处理
  • 安装 yarn add typeorm-naming-strategies
  • 修改app.module.ts
            import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
            TypeOrmModule.forRootAsync({
              useFactory() {
                return {
                  ...getConfig('MYSQL_CONFIG'),
                  namingStrategy: new SnakeNamingStrategy(),
                };
              },
              async dataSourceFactory(options) {
                if (!options) {
                  throw new Error('Invalid options passed');
                }
                return new DataSource(options);
              },
            })
    
  • 启动项目,发现已生效 Nest实战 - 基础资源整合
事务
  • 介绍

    • 为了解决同一个方法中执行多条sql语句,只成功写入部分sql的问题
  • 安装

     yarn add typeorm-transactional
    
  • main.ts添加以下代码

    import { initializeTransactionalContext } from 'typeorm-transactional';
    initializeTransactionalContext();
    

    Nest实战 - 基础资源整合

  • app.module.ts中加入以下代码

  • Nest实战 - 基础资源整合

  • 这样注解就添加成功了,如果想要使用的话只需要在方法上添加注解Transactional即可,具体实践后期项目会有

  • Nest实战 - 基础资源整合

写在最后