寫在開頭
每一篇文章都是作者用 心
寫出,還需要花費大量時間去校對調整,旨在給您帶來最好的閱讀體驗。
您的 點贊、收藏、轉發
是對作者的最大鼓勵,也可以讓更多人看到本篇文章,萬分感謝!
如果覺得本文對您有幫助,還請幫忙在 github 上點亮 star
鼓勵一下吧!
正文
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
文件中的簡單案例,放到實際場景中去看對應的處理方式。
當然還會有更好的處理方式,比如鑑權那塊就可以有更好的處理方式。
這裡就不做展開介紹了,感興趣的童鞋可以自己去研究一下。
本篇文章旨在提供 nest
場景實戰案例快速參考。
大家有什麼感興趣的場景也可以列出來,我會選一些典型的場景在文章中繼續補充。