nest mysql 实战

写在前面

Nest(NestJS)是用于构建高效,可扩展的Node.js服务器端应用程序的框架。它使用渐进式JavaScript,并已构建并完全支持TypeScript(但仍使开发人员能够使用纯JavaScript进行编码),并结合了OOP(面向对象编程),FP(功能编程)和FRP(功能反应性编程)的元素。

这次我就帮助童鞋快速入门,尽快应用起 nest,在实战中学习 nest,对工作和学习都有更好的帮助。

1、利用基于 nest 快速搭建项目。
2、学习如何连接 mysql 数据库,利用 typeorm 操作 mysql 数据库。
3、创建用户模块,并进行 token 验证
4、返回数据进行处理
5、文件上传

1、利用基于 nest 快速搭建项目

使用 nest cli 快速创建项目

$ npm i -g @nestjs/cli
$ nest new project-name

将创建 project 目录, 安装node模块和一些其他样板文件,并将创建一个 src 目录,目录中包含几个核心文件。

src
├── app.controller.ts
├── app.module.ts
└── main.ts

以下是这些核心文件的简要概述:
app.controller.ts 带有单个路由的基本控制器示例。
app.module.ts 应用程序的根模块。
main.ts 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
运行程序

npm run start

在应用程序运行时, 打开浏览器并访问 http://localhost:3000/。 应该看到 Hello world! 信息。

2、学习如何连接 mysql 数据库,利用 typeorm 操作 mysql 数据库。

typeorm 对 mysql 数据库版本有要求,需要5.6以上,在这个地方遇到过问题,找了很长时间,才发现这个问题。最好是使用最新的 mysql 数据库。
运行命令安装 mysql、typeorm

npm install --save @nestjs/typeorm typeorm mysql

安装过程完成后,我们可以将 TypeOrmModule 导入AppModule 。
app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

我们可以也创建 ormconfig.json ,而不是将配置对象传递给 forRoot()。

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "root",
  "database": "test",
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": true
}

然后,我们可以不带任何选项地调用 forRoot() :
app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}

创建实体,这里创建 admin 实体为例。

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  BeforeInsert,
  JoinTable,
  ManyToMany,
  OneToMany,
  BeforeUpdate
} from 'typeorm';
import { IsEmail } from 'class-validator';
import * as argon2 from 'argon2';

@Entity('admin')
export class AdminEntity {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;


  @Column()
  password: string;

  @BeforeInsert()
  async hashPassword() {
    this.password = await argon2.hash(this.password);
  }


}

admin.module.ts

import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/common';
import { AdminController } from './admin.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminEntity } from './admin.entity';
import { AdminService } from './admin.service';
import { AuthMiddleware } from './auth.middleware';

@Module({
  imports: [TypeOrmModule.forFeature([AdminEntity])],
  providers: [AdminService],
  controllers: [
    AdminController
  ],
  exports: [AdminService]
})

此模块使用 forFeature() 方法定义在当前范围中注册哪些存储库。这样,我们就可以使用 @InjectRepository()装饰器将 adminRepository 注入到 AdminService 中:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository, DeleteResult } from 'typeorm';
import { AdminEntity } from './admin.entity';
import {UserDto, UserPassDto} from './dto';
const jwt = require('jsonwebtoken');
import { SECRET } from '../config';
import { validate } from 'class-validator';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { HttpStatus } from '@nestjs/common';
import * as argon2 from 'argon2';
import {MsgService} from "../msg/msg.service";


@Injectable()
export class AdminService {
  constructor(
    @InjectRepository(AdminEntity)
    private readonly adminRepository: Repository<AdminEntity>,
    private readonly MSG: MsgService
  ) {}

  async findAll(): Promise<AdminEntity[]> {
    return await this.adminRepository.find();
  }

  async findOne({username, password}: UserDto): Promise<AdminEntity> {
    const user = await this.adminRepository.findOne({username});
    if (!user) {
      return null;
    }

    if (await argon2.verify(user.password, password)) {
      return user;
    }

    return null;
  }
}

3、创建用户模块,并进行 token 验证

使用 nest 命令创建用户模块

 nest g module admin
 nest g service admin
 nest g controller admin

这里我们使用中间件middleware来做用户的 token 验证

中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next() 中间件函数。 next() 中间件函数通常由名为 next 的变量表示。

图1

Nest 中间件实际上等价于 express 中间件。 下面是Express官方文档中所述的中间件功能:

中间件函数可以执行以下任务:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

您可以在函数中或在具有 @Injectable() 装饰器的类中实现自定义 Nest中间件。 这个类应该实现 NestMiddleware 接口, 而函数没有任何特殊的要求。

import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { NestMiddleware, HttpStatus, Injectable } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
import { SECRET } from '../config';
import { AdminService } from './admin.service';
import {MsgService} from "../msg/msg.service";

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly userService: AdminService,
              private readonly MSG: MsgService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const authHeaders = req.headers.authorization;
    if (authHeaders) {
      const token = authHeaders;

      const decoded: any = this.verify(token);
      const user = await this.userService.findById(decoded.id);

      if (!user) {
        throw new HttpException('User not found.', HttpStatus.UNAUTHORIZED);
      }

      req.user = user;
      next();

    } else {
      throw new HttpException('Not authorized.', HttpStatus.UNAUTHORIZED);
    }
  }
  private verify (token:string){
    let decoded: any
    try {
      decoded = jwt.verify(token, SECRET);
    }catch (e) {
      this.MSG.fail('token error')
    }
   return decoded
  }
}

在admin.module.ts中应用AuthMiddleware,forRoutes里面配置的路径是 controller 里需要进行 token 验证的接口路径

import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/common';
import { AdminController } from './admin.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminEntity } from './admin.entity';
import { AdminService } from './admin.service';
import { AuthMiddleware } from './auth.middleware';

@Module({
  imports: [TypeOrmModule.forFeature([AdminEntity])],
  providers: [AdminService],
  controllers: [
    AdminController
  ],
  exports: [AdminService]
})
export class AdminModule implements NestModule {
  public configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes({path: 'admin/users', method: RequestMethod.GET}, {path: 'user', method: RequestMethod.PUT});
  }
}

admin.controller.ts

import { Get, Post, Body, Put, Delete, Param, Controller, UsePipes } from '@nestjs/common';
import { Request } from 'express';
import { AdminService } from './admin.service';
import {UserDto, UserPassDto} from './dto';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { ValidationPipe } from '../shared/pipes/validation.pipe';

import {
  ApiBearerAuth, ApiTags
} from '@nestjs/swagger';
import {MsgService} from "../msg/msg.service";

@ApiBearerAuth()
@ApiTags('admin')
@Controller('admin')
export class AdminController {

  constructor(private readonly userService: AdminService,
              private readonly MSG: MsgService) {}

  @Get('users')
  async getall(){
    return this.userService.findAll();
  }

  @UsePipes(new ValidationPipe())
  @Put('users')
  async create(@Body() userData: UserDto) {
    return this.userService.create(userData);
  }

  @UsePipes(new ValidationPipe())
  @Post('users/login')
  async login(@Body() loginUserDto: UserDto) {
    console.log(loginUserDto)
    const _user = await this.userService.findOne(loginUserDto);

    if (!_user) this.MSG.fail('no user')

    const token = await this.userService.generateJWT(_user);
    const { username} = _user;
    const user = {token, username};
    return {user}
  }
  @Post('change/pass')
  async changePass(@Body() changePassBody: UserPassDto){
    return this.userService.changePass(changePassBody)
  }
}

完整代码:https://github.com/baiqingchun/nest/tree/master/src/admin

4、返回数据进行处理

这里的返回数据我们并不满意,希望可以反回类似

 {
                    data:{},
                    code: 200,
                    message: '请求成功',
}

那我们要在每个请求后面都进行这样的数据结构的整理,这样太麻烦了,可以使用interceptor拦截器来处理返回数据
transform.interceptor.ts

import {
    Injectable,
    NestInterceptor,
    CallHandler,
    ExecutionContext,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
interface Response<T> {
    data: T;
}
@Injectable()
export class TransformInterceptor<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: '请求成功',
                };
            }),
        );
    }
}

对异常返回处理
使用Exception filters异常过滤器。
http-exception.filter.ts

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import {QueryFailedError} from "typeorm";

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const message = exception.message;
    Logger.log('错误提示', message);
    const errorResponse = {
      message: message,
      code: exception.getStatus(), // 自定义code
      url: request.originalUrl, // 错误的url地址
    };
    const status =
        exception instanceof HttpException
            ? exception.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;
    // 设置返回的状态码、请求头、发送错误信息
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

如果觉得平常写抛出异常太长太麻烦,可以对抛出异常再进行封装。

   throw new HttpException({message: 'reason'},HttpStatus.BAD_REQUEST);

改为

   this.MSG.fail('Username must be unique',400)

this.MSG是对异常的封装

import {HttpException, HttpStatus, Injectable} from '@nestjs/common';

@Injectable()
export class MsgService {
 public fail(reason:string, code?:number){
     if (code) {
         throw new HttpException({message: reason},code);
     } else {
         throw new HttpException({message: reason},HttpStatus.BAD_REQUEST);
     }
 }


完整代码:https://github.com/baiqingchun/nest/tree/master/src/msg

5、文件上传

一个项目离不开文件上传,nest 对文件上传也做了相应处理
创建文件模块。
如果需要详细理解文件上传,可以看另外一篇文章https://www.jianshu.com/p/28f8dd9a732e

还是使用命令来做

 nest g module file
 nest g service file
 nest g controller file

file.module.ts

import { Module } from '@nestjs/common';
import { FileController } from './file.controller';
import { FileService } from './file.service';
import { MulterModule } from '@nestjs/platform-express';
import dayjs = require('dayjs');
import { diskStorage } from 'multer';
import * as nuid from 'nuid';
@Module({
  imports:[
    MulterModule.register({
      storage: diskStorage({
        // 配置文件上传后的文件夹路径
        destination: `./public/uploads/${dayjs().format('YYYY-MM-DD')}`,
        filename: (req, file, cb) => {
          // 在此处自定义保存后的文件名称
          // const filename = `${nuid.next()}.${file.mimetype.split('/')[1]}`;
          // return cb(null, filename);
          return  cb(null, file.originalname);
        },
      }),
    }),

  ],
  controllers: [FileController],
  providers: [FileService]
})
export class FileModule {}

file.controller.ts

import { Controller, Post, UseInterceptors, UploadedFile, UploadedFiles, Body } from '@nestjs/common';
import {
  AnyFilesInterceptor,
  FileFieldsInterceptor,
  FileInterceptor,
  FilesInterceptor,
} from '@nestjs/platform-express';
import multer = require('multer');
// import {StorageService} from 'src/common/storage/storage.service'
import {FileService} from './file.service'
@Controller('file')
export class FileController {
  constructor(
    private readonly fileService:FileService,
  ) {
  }
  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  async UploadedFile(@UploadedFile() file) {
    // 这里的 file 已经是保存后的文件信息了,在此处做数据库处理,或者直接返回保存后的文件信息
    // const buckets =await this.storageService.getBuckets()
    console.log(file)
    return file;
  }

  @Post('upload3')
  @UseInterceptors(FilesInterceptor('file'))
  uploadFile(@UploadedFiles() file) {
    console.log(file);
  }

  @Post('uploads')
  @UseInterceptors(FileFieldsInterceptor([
    { name: 'avatar', maxCount: 1 },
    { name: 'background', maxCount: 1 },
    { name: 'avatar_name' },
    { name: 'background_name'}
  ]))
  async uploads(@UploadedFiles() files,@Body() body) {
    console.log(files,body)
  }

总结

至此关于 nest 实战结束,希望对你有所帮助。全部代码https://github.com/baiqingchun/nest

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343