关于nest:Nest-快速通关攻略

45次阅读

共计 17196 个字符,预计需要花费 43 分钟才能阅读完成。

写在结尾

每一篇文章都是作者用 写出,还须要破费大量工夫去校对调整,旨在给您带来最好的浏览体验。

您的 点赞、珍藏、转发 是对作者的最大激励,也能够让更多人看到本篇文章,万分感激!

如果感觉本文对您有帮忙,还请帮忙在 github 上点亮 star 激励一下吧!

注释

Nest 是一个用于构建高效,可扩大的 Node.js 服务器端应用程序的框架。它应用渐进式 JavaScript,内置并齐全反对 TypeScript 并联合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。

在底层,Nest 应用弱小的 HTTP Server 框架,如 Express(默认)和 FastifyNest 在这些框架之上提供了肯定水平的形象,同时也将其 API 间接裸露给开发人员。这样能够轻松应用每个平台的有数第三方模块。

从上图也能够看出,Nest 目前是热度仅次于老牌 Express,目前排名第二的 Nodejs 框架。

明天,咱们通过本篇 Nest 疾速通关攻略,应用 Nest 来打造一个游览攻略,将应用到包含但不限于 Nest 的下列性能

  • 中间件
  • 管道
  • 类验证器
  • 守卫
  • 拦截器
  • 自定义装璜器
  • 数据库
  • 文件上传
  • MVC
  • 权限

本我的项目有一个指标,针对 Nest 文档中的简略案例,放到理论场景中,从而找到最佳实际。

好了,话不多说,咱们筹备开始吧!

初始化我的项目

本案例的源码仓库在 源码地址 可下载。

首先,应用 npm i -g @nestjs/cli 命令装置 nest-cli,而后应用脚手架命令创立一个新的 nest 我的项目即可。(如下)

nest new webapi-travel

我的项目初始化实现后,咱们进入我的项目,运行 npm run start:dev 命令启动我的项目吧!(我的项目启动后关上 (http://localhost:3000/)[http://localhost:3000/] 可查看成果)

如上图所示,进入页面看到 Hello World 后,阐明我的项目启动胜利啦!

配置数据库

数据表设计

接下来,咱们须要设计一下咱们的数据表构造,咱们当初筹备先做一个 吃喝玩乐 店铺集锦,店铺须要展现这些信息:

  • 店铺名称

    • 店铺简介(slogan)
    • 店铺类型(吃喝玩乐)
  • 封面(单张图片)

    • 轮播图(多张图片)
    • 标签(多个)
    • 人均生产
    • 评分(0 – 5)
    • 具体地址
    • 经度
    • 纬度

从下面能够看出,咱们至多应该要有两张表:店铺表、店铺轮播图表。

那么接下来,咱们把两张表的 DDL 定义一下。(如下)

CREATE TABLE IF NOT EXISTS `shop` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(16) NOT NULL DEFAULT '',
  `description` varchar(64) NOT NULL DEFAULT '',
  `type` tinyint unsigned NOT NULL DEFAULT 0,
  `poster` varchar(200) NOT NULL DEFAULT '',
  `average_cost` smallint NOT NULL DEFAULT 0 COMMENT '人均生产',
  `score` float NOT NULL DEFAULT 0,
  `tags` varchar(200) NOT NULL DEFAULT '',
  `evaluation` varchar(500) NOT NULL DEFAULT '',
  `address` varchar(200) NOT NULL DEFAULT '',
  `longitude` float NOT NULL DEFAULT 0,
  `latitude` float NOT NULL DEFAULT 0,
  index `type`(`type`)
) engine=InnoDB charset=utf8;

CREATE TABLE IF NOT EXISTS `shop_banner` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `shop_id` int NOT NULL DEFAULT 0,
  `url` varchar(255) NOT NULL DEFAULT '',
  `sort` smallint NOT NULL DEFAULT 0 COMMENT '排序',
  index `shop_id`(`shop_id`, `sort`, `url`)
) engine=InnoDB charset=utf8;

其中 shop_banner 应用了联结索引,可能无效缩小回表次数,并且可能将图片进行排序。创立实现后,能够检查一下。(如下图)

配置数据库连贯

在数据表初始化实现后,咱们须要在 nest 中配置咱们的数据库连贯。在本教程中,咱们应用 typeorm 库来进行数据库操作,咱们先在我的项目中装置一下相干依赖。

npm install --save @nestjs/typeorm typeorm mysql2

而后,咱们来配置一下数据库连贯配置,在我的项目根目录下创立 ormconfig.json 配置文件。

{
  "type": "mysql",
  "host": "localhost", // 数据库主机地址
  "port": 3306, // 数据库连贯端口
  "username": "root", // 数据库用户名
  "password": "root", // 数据库明码
  "database": "test", // 数据库名
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": false // 同步设置,这个倡议设置为 false,数据库构造对立用 sql 来调整
}

数据库配置依据每个人的服务器设置而不一样,这个文件我并没有传到仓库中,大家想体验 Demo 的话,须要本人创立该文件。

配置实现后,咱们在 app.module.ts 文件中,实现数据库连贯配置。

// app.module.ts
import {Module} from '@nestjs/common';
import {TypeOrmModule} from '@nestjs/typeorm';
import {AppController} from './app.controller';
import {AppService} from './app.service';

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

typeorm 会主动载入我的项目中的 ormconfig.json 配置文件,所以不须要显示引入。

我的项目初始化

本篇文章将作为实战指南,会将局部内容提前,因为我认为这些内容才是 nest 中比拟外围的局部。

验证器

nest 中,应用管道进行函数验证,咱们先定义一个 ValidationPipe 用于校验,该文件内容如下:

// src/pipes/validate.pipe.ts

import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform,
} from '@nestjs/common';
import {plainToClass} from 'class-transformer';
import {validate} from 'class-validator';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {async transform(value: any, { metatype}: ArgumentMetadata) {if (!metatype || this.toValidate(metatype)) {return value;}

    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {const error = errors[0];
      const firstKey = Object.keys(error.constraints)[0];
      throw new BadRequestException(error.constraints[firstKey]);
    }

    return value;
  }

  // eslint-disable-next-line @typescript-eslint/ban-types
  private toValidate(metatype: Function): boolean {
    // eslint-disable-next-line @typescript-eslint/ban-types
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return types.includes(metatype);
  }
}

而后,咱们在 main.ts 中,注册该管道为全局管道即可,代码实现如下:

// src/main.ts

async function bootstrap() {const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(7788);
}
bootstrap();

这样,咱们就能够在实体中定义校验类,疾速实现对单个字段的校验。

响应后果格式化

我想要所有的响应后果能够依照对立的格局返回,就是 code(状态码)+ message(响应信息)+ data(数据)的格局返回,这样的话,咱们能够定义一个拦截器 ResponseFormatInterceptor,用于对所有的响应后果进行序列格式化。

代码实现如下:

// src/interceptors/responseFormat.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

@Injectable()
export class ResponseFormatInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {return next.handle().pipe(
      // 将原有的 `data` 转化为对立的格局后返回
      map((data) => ({
        code: 1,
        message: 'ok',
        data,
      })),
    );
  }
}

而后,同样在 main.ts 中的 bootstrap 中注册该拦截器(如下)

app.useGlobalInterceptors(new ResponseFormatInterceptor());

谬误对立解决

在这里,我心愿咱们的谬误不返回谬误的状态码(因为这可能会导致前端引发跨域谬误)。所以,我心愿将所有的谬误都返回状态码 200,而后在响应体中的 code 中,再返回理论的错误码,咱们须要写一个拦截器来实现该性能 —— ResponseErrorInterceptor

// src/interceptors/responseError.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import {Observable} from 'rxjs';
import {catchError} from 'rxjs/operators';

@Injectable()
export class ResponseErrorInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {return next.handle().pipe(catchError(async (err) => ({
        code: err.status || -1,
        message: err.message,
        data: null,
      })),
    );
  }
}

而后,同样在 main.ts 中的 bootstrap 中注册该拦截器(如下)

app.useGlobalInterceptors(new ResponseErrorInterceptor());

解析头部 token

在咱们后续的鉴权操作中,咱们筹备应用头部传入的 token 参数。我心愿在每个接口理论申请产生时,对 token 进行解析,解析出该 token 对应的用户信息,而后将该用户信息持续向下传递。

这里,咱们须要实现一个中间件,该中间件能够解析 token 信息,代码实现如下:

// src/middlewares/auth.middleware.ts
import {Injectable, NestMiddleware} from '@nestjs/common';
import {NextFunction} from 'express';
import {UserService} from '../user/user/user.service';

@Injectable()
export class AuthMiddleware implements NestMiddleware {constructor(private readonly userService: UserService) {}

  async use(req: Request, res: Response, next: NextFunction) {const token = req.headers['token'];
    req['context'] = req['context'] || {};
    if (!token) return next();

    try {
      // 应用 token 查问相干的用户信息,如果该函数抛出谬误,阐明 token 有效,则用户信息不会被写入 req.context 中
      const user = await this.userService.queryUserByToken(token);
      req['context']['token'] = token;
      req['context']['user_id'] = user.id;
      req['context']['user_role'] = user.role;
    } finally {next();
    }
  }
}

而后,咱们须要在 src/app.module.ts 中全局注册该中间件(如下)

export class AppModule {configure(consumer: MiddlewareConsumer) {
    // * 代表该中间件在所有路由均失效
    consumer.apply(AuthMiddleware).forRoutes('*');
  }
}

路由守卫

咱们在局部路由中须要设置守卫,只有指定权限的用户能力拜访,这里须要实现一个路由守卫 AuthGuard 用于守卫路由,和一个自定义装璜器 Roles 用于设置路由权限。

代码实现如下:

// src/guards/auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
} from '@nestjs/common';
import {Reflector} from '@nestjs/core';
import {Observable} from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {const req = context.switchToHttp().getRequest();
    const token = req.headers['token'];
    const user_id = req['context']['user_id'];
    const user_role = req['context']['user_role'];
    // 没有 token,或者 token 不蕴含用户信息时,认为 token 生效
    if (!token || !user_id) {throw new ForbiddenException('token 已生效');
    }

    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    // 没有角色权限限度时,间接放行
    if (!roles) {return true;}

    // 角色权限为 `admin` 时,须要用户 role 为 99 能力拜访
    if (roles[0] === 'admin' && user_role !== 99) {throw new ForbiddenException('角色权限有余');
    }

    return true;
  }
}

上面是自定义装璜器的实现:

// src/decorators/roles.decorator.ts
import {SetMetadata} from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

因为守卫只针对局部路由失效,所以咱们只须要在指定的路由应用即可。

店铺操作

接下来,咱们回到我的项目中,筹备开始实现咱们的店铺操作,咱们须要实现上面几个性能:

  • 查问所有店铺信息
  • 查问单个店铺信息
  • 减少店铺
  • 删除店铺
  • 批改店铺信息

注册 ShopModule

咱们依照程序创立 shop/shop.controller.tsshop/shop.service.tsshop/shop.module.ts。(如下)

// shop/shop.controller.ts
import {Controller, Get} from '@nestjs/common';

@Controller('shop')
export class ShopController {@Get('list')
  async findAll() {return 'Test Shops List';}
}
// shop/shop.service.ts
import {Injectable} from '@nestjs/common';

@Injectable()
export class ShopService {}
import {Module} from '@nestjs/common';
import {ShopController} from './shop.controller';
import {ShopService} from './shop.service';

@Module({controllers: [ShopController],
  providers: [ShopService],
})
export class ShopModule {}

在初始化实现后,别忘了在 app.module.ts 中注册 ShopModule

// app.module.ts
// ...
@Module({imports: [TypeOrmModule.forRoot(), ShopModule], // 注册 ShopModule
  controllers: [AppController],
  providers: [AppService],
})

注册实现后,咱们能够应用 postman 验证一下咱们的服务。(如下图)

从上图能够看出,咱们的路由注册胜利了,接下来咱们来定义一下咱们的数据实体。

定义数据实体

数据实体在 typeorm 应用 @Entity 装璜器装璜的模型,能够用来创立数据库表(开启 synchronize 时),还能够用于 typeorm 数据表 CURD 操作。

咱们在后面新建了两个数据表,当初咱们来创立对应的数据实体吧。

// src/shop/models/shop.entity.ts
import {Column, Entity, OneToMany, PrimaryGeneratedColumn} from 'typeorm';
import {ShopBanner} from './shop_banner.entity';

export enum ShopType {
  EAT = 1,
  DRINK,
  PLAY,
  HAPPY,
}

// 数据表 —— shops
@Entity()
export class Shop {
  // 自增主键
  @PrimaryGeneratedColumn()
  id: number;

  @Column({default: ''})
  name: string;

  @Column({default: ''})
  description: string;

  @Column({default: 0})
  type: ShopType;

  @Column({default: ''})
  poster: string;

  // 一对多关系,单个店铺对应多张店铺图片
  @OneToMany(() => ShopBanner, (banner) => banner.shop)
  banners: ShopBanner[];

  @Column({default: ''})
  tags: string;

  @Column({default: 0})
  score: number;

  @Column({default: ''})
  evaluation: string;

  @Column({default: ''})
  address: string;

  @Column({default: 0})
  longitude: number;

  @Column({default: 0})
  latitude: number;

  @Column({default: 0})
  average_cost: number;

  @Column({default: ''})
  geo_code: string;
}
// src/shop/models/shop_banner.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import {Shop} from './shop.entity';

@Entity()
export class ShopBanner {@PrimaryGeneratedColumn()
  id: number;

  // 多对一关系,多张店铺图片对应一家店铺
  // 在应用 left join 时,应用 shop_id 字段查问驱动表
  @ManyToOne(() => Shop, (shop) => shop.banners)
  @JoinColumn({name: 'shop_id'})
  shop: Shop;

  @Column()
  url: string;

  @Column()
  sort: number;
}

从下面能够看出,咱们的三个数据实体都有对应的装璜器形容,装璜器的用途大家能够参考 TypeORM 文档

新增店铺接口

在实体定义好当前,咱们来写 新增店铺 接口。

该接口须要接管一个 店铺 对象入参,后续咱们也用该对象来进行参数校验,咱们先定义一下这个类。(如下)

// src/shop/dto/create-shop.dto.ts
import {IsNotEmpty} from 'class-validator';
import {ShopType} from '../models/shop.entity';

export class CreateShopDto {
  // 应用了 ValidationPipe 进行校验
  @IsNotEmpty({message: '店铺名称不能为空'})
  name: string;

  description: string;

  @IsNotEmpty({message: '店铺类型不能为空'})
  type: ShopType;

  poster: string;

  banners: string[];

  tags: string[];

  @IsNotEmpty({message: '店铺评分不能为空'})
  score: number;

  evaluation: string;

  @IsNotEmpty({message: '店铺地址不能为空'})
  address: string;

  @IsNotEmpty({message: '店铺经度不能为空'})
  longitude: number;

  @IsNotEmpty({message: '店铺纬度不能为空'})
  latitude: number;

  average_cost: number;
}

而后,咱们在 ShopController 中增加一个办法,注册 新增店铺 接口。(如下)

// src/shop/shop.controller.ts
@Controller('shop')
export class ShopController {constructor(private readonly shopService: ShopService) {}

  // add 接口
  @Post('add')
  // 返回状态码 200
  @HttpCode(200)
  // 应用鉴权路由守卫
  @UseGuards(AuthGuard)
  // 定义只有 admin 身份可拜访
  @Roles('admin')
  // 接支出参,类型为 CreateShopDto
  async addShop(@Body() createShopDto: CreateShopDto) {
    // 调用 service 的 addShop 办法,新增店铺
    await this.shopService.addShop(createShopDto);
    // 胜利后返回 null
    return null;
  }
}

咱们在接口申请发动后,调用了 service 的新增店铺办法,而后返回了胜利提醒。

接下来,咱们来编辑 ShopService,来定义一个 新增店铺 的办法 —— addShop(如下)

export class ShopService {constructor(private readonly connection: Connection) {}

  async addShop(createShopDto: CreateShopDto) {const shop = this.getShop(new Shop(), createShopDto);

    // 解决 banner
    if (createShopDto.banners?.length) {shop.banners = this.getBanners(createShopDto);
      await this.connection.manager.save(shop.banners);
    }

    // 存储店铺信息
    return this.connection.manager.save(shop);
  }

  getShop(shop: Shop, createShopDto: CreateShopDto) {
    shop.name = createShopDto.name;
    shop.description = createShopDto.description;
    shop.poster = createShopDto.poster;
    shop.score = createShopDto.score;
    shop.type = createShopDto.type;
    shop.tags = createShopDto.tags.join(',');
    shop.evaluation = createShopDto.evaluation;
    shop.address = createShopDto.address;
    shop.longitude = createShopDto.longitude;
    shop.latitude = createShopDto.latitude;
    shop.average_cost = createShopDto.average_cost;
    shop.geo_code = geohash.encode(
      createShopDto.longitude,
      createShopDto.latitude,
    );
    return shop;
  }

  getBanners(createShopDto: CreateShopDto) {return createShopDto.banners.map((item, index) => {const banner = new ShopBanner();
      banner.url = item;
      banner.sort = index;
      return banner;
    });
  }
}

能够看到,ShopService 是负责与数据库交互的,这里先做了店铺信息的存储,而后再存储 店铺 banner 店铺标签

在接口实现后,咱们用 postman 来验证一下咱们新增的接口吧,上面是咱们筹备的测试数据。

{
  "name": "蚝满园",
  "description": "固戍的宝藏店铺!生蚝馆!还有超大蟹钳!",
  "type": 1,
  "poster": "https://img1.baidu.com/it/u=2401989050,2062596849&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
  "banners": [
    "https://img1.baidu.com/it/u=2401989050,2062596849&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
    "https://img1.baidu.com/it/u=2043954707,1889077177&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",
    "https://img1.baidu.com/it/u=1340805476,3006236737&fm=253&fmt=auto&app=120&f=JPEG?w=360&h=360"
  ],
  "tags": [
    "绝绝子好吃",
    "宝藏店铺",
    "价格实惠"
  ],
  "score": 4.5,
  "evaluation": "吃过两次了,他们家的高压锅生蚝、蒜蓉生蚝、粥、蟹钳、虾都是首推,风味十足,特地好吃!",
  "address": "宝安区上围园新村十二巷 5-6 号 101",
  "longitude": 113.151415,
  "latitude": 22.622297,
  "average_cost": 80
}

查问店铺列表 / 更新店铺信息 / 删除店铺

在新增店铺实现后,咱们来把查问店铺列表、更新店铺信息接口和删除店铺接口都欠缺一下。

首先,还是定义好对应的 controller 入口。

// src/shop/shop.controller.ts
@Controller('shop')
export class ShopController {constructor(private readonly shopService: ShopService) {}

  // 获取店铺列表接口
  @Get('list')
  async getShopList(@Query() queryShopListDto: QueryShopListDto) {const list = await this.shopService.getShopList(queryShopListDto);
    return {
      pageIndex: queryShopListDto.pageIndex,
      pageSize: queryShopListDto.pageSize,
      list,
    };
  }

  // update 接口
  @Post('update')
  @HttpCode(200)
  @UseGuards(AuthGuard)
  @Roles('admin')
  // 接支出参,类型为 UpdateShopDto
  async updateShop(@Body() updateShopDto: UpdateShopDto) {
    // 调用 service 的 addShop 办法,新增店铺
    await this.shopService.updateShop(updateShopDto);
    // 返回胜利提醒
    return null;
  }

  // delete 接口
  @Post('delete')
  @HttpCode(200)
  @UseGuards(AuthGuard)
  @Roles('admin')
  async deleteShop(@Body() deleteShopDto: QueryShopDto) {await this.shopService.deleteShop(deleteShopDto);
    return null;
  }
}

而后,咱们将对应的 service 补全就好。

在更新店铺信息时,还须要解决一种状况,那就是店铺更新胜利,然而店铺图片更新失败的状况。在这种状况下,该更新只有局部失效。

所以,店铺信息、店铺图片信息应该是要么一起存储胜利,要么一起存储失败(通过事务回退),依据这个个性,咱们这里将须要启用事务。

应用 TypeORM 中的 getManager().transaction() 办法可显式启动事务,代码实现如下:


export class ShopService {constructor(private readonly connection: Connection) {}

  async getShopList(queryShopListDto: QueryShopListDto) {const shopRepository = this.connection.getRepository(Shop);
    const {pageIndex = 1, pageSize = 10} = queryShopListDto;
    const data = await shopRepository
      .createQueryBuilder('shop')
      .leftJoinAndSelect('shop.banners', 'shop_banner')
      .take(pageSize)
      .skip((pageIndex - 1) * pageSize)
      .getMany();

    return data
      .map((item) => {
        // 计算用户传入的地位信息与以后店铺的间隔信息
        const distance = computeInstance(
          +queryShopListDto.longitude,
          +queryShopListDto.latitude,
          item.longitude,
          item.latitude,
        );
        return {
          ...item,
          tags: item.tags.split(','),
          distanceKm: distance,
          distance: convertKMToKmStr(distance),
        };
      })
      .sort((a, b) => a.distanceKm - b.distanceKm);
  }

  async updateShop(updateShopDto: UpdateShopDto) {return getManager().transaction(async (transactionalEntityManager) => {
      await transactionalEntityManager
        .createQueryBuilder()
        .delete()
        .from(ShopBanner)
        .where('shop_id = :shop_id', { shop_id: updateShopDto.id})
        .execute();

      const originalShop: Shop = await transactionalEntityManager.findOne(
        Shop,
        updateShopDto.id,
      );
      const shop = this.getShop(originalShop, updateShopDto);

      if (updateShopDto.banners?.length) {shop.banners = this.getBanners(updateShopDto);
        await transactionalEntityManager.save(shop.banners);
      }

      await transactionalEntityManager.save(shop);
    });
  }

  async deleteShop(deleteShopDto: QueryShopDto) {return getManager().transaction(async (transactionalEntityManager) => {
      await transactionalEntityManager
        .createQueryBuilder()
        .delete()
        .from(Shop)
        .where('id = :id', { id: deleteShopDto.id})
        .execute();

      await transactionalEntityManager
        .createQueryBuilder()
        .delete()
        .from(ShopBanner)
        .where('shop_id = :shop_id', { shop_id: deleteShopDto.id})
        .execute();});
  }
}

在查问列表接口时,还波及到一个间隔计算的点,因为该局部并不是 nest 的核心内容,所以这里就不做开展介绍了,感兴趣的童鞋能够找到 源码地址 进行浏览。

咱们来看看成果吧。(如下图)

至此,列表查问、更新店铺信息、删除店铺,都曾经实现了。

文件上传

最初,再对一些衍生知识点进行介绍,比方应用 nest 如何进行文件上传。

这里能够应用 nest 自带提供的 FileInterceptor 拦截器和 UploadedFile 文件接收器,对文件流进行接管,而后再应用本人的图床工具,例如 oss 传输到本人的服务器上,上面是一段代码示例,可供参考。

// common.controller.ts
import {
  Controller,
  HttpCode,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import {FileInterceptor} from '@nestjs/platform-express';
import '../../utils/oss';
import {CommonService} from './common.service';

@Controller('common')
export class CommonController {constructor(private readonly commonService: CommonService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  @HttpCode(200)
  uploadFile(@UploadedFile() file: Express.Multer.File) {return upload(file);
  }
}
// oss.ts
const {createHmac} = require('crypto');
const OSS = require('ali-oss');
const Duplex = require('stream').Duplex;
const path = require('path');

export const hash = (str: string): string => {return createHmac('sha256', 'jacklove' + new Date().getTime())
    .update(str)
    .digest('hex');
};

const ossConfig = {
  region: process.env.OSS_REGION,
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  bucket: process.env.OSS_BUCKET,
};
const client = new OSS(ossConfig);

export const upload = async (file: Express.Multer.File): Promise<string> => {const stream = new Duplex();
  stream.push(file.buffer);
  stream.push(null);
  // 文件名 hash 化解决
  const fileName = hash(file.originalname) + path.extname(file.originalname);
  await client.putStream(fileName, stream);
  const url = `http://${ossConfig.bucket}.${ossConfig.region}.aliyuncs.com/${fileName}`;
  return url;
};

除了图床上传局部,其余代码基本上是大同小异的,重点在于文件信息的接管解决。

部署利用

咱们能够应用 pm2 来进行利用的部署,首先要应用 npm run build 构建生产产物。

在生产产物构建实现后,在以后我的项目目录下执行上面这个命令即可运行我的项目。

pm2 start npm --name "webapi-travel" -- run start:prod

而后,就能够看到咱们的我的项目胜利启动了(如下图)。

大家能够通过 https://webapi-travel.jt-gmall.com 进行拜访,这是我部署后的站点地址。

小结

本篇文章没有过于认真的探讨每一行代码的实现,只是很简略粗犷的将 nest 文档中的简略案例,放到理论场景中去看对应的解决形式。

当然还会有更好的解决形式,比方鉴权那块就能够有更好的解决形式。

这里就不做开展介绍了,感兴趣的童鞋能够本人去钻研一下。

本篇文章旨在提供 nest 场景实战案例疾速参考。

大家有什么感兴趣的场景也能够列出来,我会选一些典型的场景在文章中持续补充。

正文完
 0