写在开头
每一篇文章都是作者用 心
写出,还需要花费大量时间去校对调整,旨在给您带来最好的阅读体验。
正文
Nest
是一个用于构建高效,可扩展的 Node.js
服务器端应用程序的框架。它使用渐进式 JavaScript
,内置并完全支持 TypeScript
并结合了 OOP
(面向对象编程),FP
(函数式编程)和 FRP
(函数式响应编程)的元素。
在底层,Nest
使用强大的 HTTP Server
框架,如 Express
(默认)和 Fastify
。Nest
在这些框架之上提供了一定程度的抽象,同时也将其 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.ts
、shop/shop.service.ts
、shop/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
文档中的简单案例,放到实际场景中去看对应的处理方式。
当然还会有更好的处理方式,比如鉴权那块就可以有更好的处理方式。
这里就不做展开介绍了,感兴趣的童鞋可以自己去研究一下。