- 教程地址: v.pincman.com/courses/64.html
- 影片地址: www.bilibili.com/video/BV1mT411u76...
- qq: 1849600177
- qq 群: 455820533
另,本人在找工作中,希望能有遠端工作匹配(無法去外地),有需要的老闆可以看一下我的個人介紹:pincman.com/about
學習目標
- 簡單地整合nestjs框架與typeorm
- 實現基本的CRUD資料操作
- 使用class-validator驗證請求資料
- 更換更加快速的fastify介面卡
- 使用Thunder Client對測試介面
安裝Mysql
實際生產環境中建議使用PostgreSQL,因為教程以學習為主,所以直接使用相對來說比較通用和簡單的Mysql
使用以下命令安裝Mysql
如果本機不是使用linux(比如使用wsl2),請到mysql官網點選download按鈕下載安裝包後在chrome檢視下載地址,然後在開發機用
wget
下載
如果本機使用MacOS,使用
brew install mysql
,如果本機使用Arch系列,使用sudo pacman -Syy mysql
# 下載映象包
cd /usr/local/src
sudo wget sudo wget https://repo.mysql.com/mysql-apt-config_0.8.22-1_all.deb
# 新增映象(其它選項不用管,直接OK就可以)
sudo apt-get install ./mysql-apt-config_0.8.22-1_all.deb
# 升級包列表
sudo apt-get update
# 開始安裝,輸入密碼後,有一個密碼驗證方式,因為是開發用,所以選擇第二個弱驗證即可
sudo apt-get install mysql-server
# 初始化,在是否載入驗證元件時選擇No,在是否禁用遠端登入時也選擇No
sudo mysql_secure_installation
# 因為是遠端SSH連線開發所以需要開啟遠端資料庫連結,如果是本地或者wsl2則不需要開啟
mysql -u root -p
CREATE USER 'root'@'%' IDENTIFIED BY '密碼';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
接著使用Navicat等客戶端就可以連線了
預裝依賴
- lodash是常用的工具庫
- cross-env用於跨平臺設定環境變數
- class-transformer用於對請求和返回等資料進行序列化
- class-validator用於驗證請求
dto
等 - typeorm一個TS編寫的[node.js][]ORM
- [@nestjs/typeorm][]Nestjs的TypeOrm整合模組
- @nestjs/platform-fastifyFastify介面卡,用於替代express
- nestjs-swagger生成open api文件,目前我們使用其
PartialType
函式是UpdateDto
中的屬性可選 - fastify-swagger生成Fastify的Open API
~ pnpm add class-transformer \
@nestjs/platform-fastify \
class-validator \
lodash \
@nestjs/swagger \
fastify-swagger \
mysql2 \
typeorm \
@nestjs/typeorm
~ pnpm add @types/lodash cross-env @types/node typescript -D
生命週期
要合理的編寫應用必須事先了解清楚整個程式的訪問流程,本教程會講解如何一步步演進每一次訪問流,作為第一步課時,我們的訪問流非常簡單,可以參考下圖
檔案結構
我們透過整合typeorm來連線mysql實現一個基本的CRUD應用,首先我們需要建立一下檔案結構
建議初學者手動建立,沒必要使用CLI去建立,這樣目錄和檔案更加清晰
- 建立模組
- 編寫模型
- 編寫Repository(如果有需要的話)
- 編寫資料驗證的DTO
- 編寫服務
- 編寫控制器
- 在每個以上程式碼各自的目錄下建立一個
index.ts
並匯出它們 - 在各自的
Module
裡進行註冊提供者,匯出等 - 在
AppModule
中匯入這兩個模組
編寫好之後的目錄結構如下
.
├── app.module.ts # 引導模組
├── config # 配置檔案目錄
│ ├── database.config.ts # 資料庫配置
│ └── index.ts
├── main.ts # 應用啟動器
├── modules
├── content # 內容模組目錄
│ ├── content.module.ts # 內容模組
│ ├── controllers # 控制器
│ ├── dtos # DTO訪問資料驗證
│ ├── entities # 資料實體模型
| ├── index.ts
│ ├── repositories # 自定義Repository
│ ├── services # 服務
└── core
├── constants.ts # 常量
├── core.module.ts # 核心模組
├── decorators # 裝飾器
└── types.ts # 公共型別
應用編碼
在開始編碼之前需要先更改一下package.json
和nestjs-cli.json
兩個檔案
在package.json
中修改一下啟動命令,以便每次啟動可以自動配置執行環境併相容windows
環境
"prebuild": "cross-env rimraf dist",
"start": "cross-env NODE_ENV=development nest start",
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
為了在每次重新編譯前自動刪除上次的產出,在nestjs-cli.json
中配置"deleteOutDir": true
main.ts
把介面卡由express換成更快的fastify,並把監聽的IP改成0.0.0.0
方便外部訪問.為了在使用class-validator的DTO
類中也可以注入nestjs容器的依賴,需要新增useContainer
// main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
useContainer(app.select(AppModule), { fallbackOnErrors: true });
await app.listen(3000,'0.0.0.0');
}
bootstrap();
連線配置
建立一個src/config/database.config.ts
檔案
export const database: () => TypeOrmModuleOptions = () => ({
// ...
// 此處entites設定為空即可,我們直接透過在模組內部使用`forFeature`來註冊模型
// 後續魔改框架的時候,我們會透過自定義的模組建立函式來重置entities,以便給自己編寫的CLI使用
// 所以這個配置後面會刪除
entities: [],
// 自動載入模組中註冊的entity
autoLoadEntities: true,
// 可以在開發環境下同步entity的資料結構到資料庫
// 後面教程會使用自定義的遷移命令來代替,以便在生產環境中使用,所以以後這個選項會永久false
synchronize: process.env.NODE_ENV !== 'production',
});
CoreModule
核心模組用於掛載一些全域性類服務,比如整合typeorm的``TypeormModule`
注意: 這裡不要使用@Global()
裝飾器來構建全域性模組,因為後面在CoreModule
類中新增一些其它方法
返回值中新增global: true
來註冊全域性模組,並匯出metadata
.
// src/core/core.module.ts
export class CoreModule {
public static forRoot(options?: TypeOrmModuleOptions) {
const imports: ModuleMetadata['imports'] = [TypeOrmModule.forRoot(options)];
return {
global: true,
imports,
module: CoreModule,
};
}
}
在AppModule
匯入該模組,並註冊資料庫連線
// src/app.module.ts
@Module({
imports: [CoreModule.forRoot(database())],
...
})
export class AppModule {}
自定義儲存類
由於原來用於自定義Repository的@EntityRepository
在typeorm0.3版本後已經不可用,特別不方便,所以根據這裡的示例來自定義一個CustomRepository
裝飾器
// src/modules/core/constants.ts
// 傳入裝飾器的metadata資料標識
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
// src/modules/core/decorators/repository.decorator.ts
// 定義裝飾器
import { CUSTOM_REPOSITORY_METADATA } from '../constants';
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator =>
SetMetadata(CUSTOM_REPOSITORY_METADATA, entity);
// src/modules/core/decorators/index.ts
export * from './repository.decorator';
定義靜態方法用於註冊自定義Repository
public static forRepository<T extends Type<any>>(
repositories: T[],
dataSourceName?: string,
): DynamicModule {
const providers: Provider[] = [];
for (const Repo of repositories) {
const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo);
if (!entity) {
continue;
}
providers.push({
inject: [getDataSourceToken(dataSourceName)],
provide: Repo,
useFactory: (dataSource: DataSource): typeof Repo => {
const base = dataSource.getRepository<ObjectType<any>>(entity);
return new Repo(base.target, base.manager, base.queryRunner);
},
});
}
return {
exports: providers,
module: CoreModule,
providers,
};
}
ContentModule
內容模組用於存放CRUD操作的邏輯程式碼
// src/modules/content/content.module.ts
@Module({})
export class ContentModule {}
在AppModule
中註冊
// src/app.module.ts
@Module({
imports: [CoreModule.forRoot(database()),ContentModule],
...
})
export class AppModule {}
實體模型
建立一個PostEntity
用於文章資料表
PostEntity
繼承``BaseEntity,這樣做是為了我們可以進行
ActiveRecord操作,例如
PostEntity.save(post),因為純
DataMapper`的方式有時候程式碼會顯得囉嗦,具體請檢視此處
@CreateDateColumn
和@UpdateDateColumn
是自動欄位,會根據建立和更新資料的時間自動產生,寫入後不必關注
// src/modules/content/entities/post.entity.ts
// 'content_posts'是表名稱
@Entity('content_posts')
export class PostEntity extends BaseEntity {
...
@CreateDateColumn({
comment: '建立時間',
})
createdAt!: Date;
@UpdateDateColumn({
comment: '更新時間',
})
updatedAt!: Date;
}
儲存類
本節儲存類是一個空類,後面會新增各種操作方法
這裡用到我們前面定義的自定義CustomRepository裝飾器
// src/modules/content/repositories/post.repository.ts
@CustomRepository(PostEntity)
export class PostRepository extends Repository<PostEntity> {}
註冊模型和儲存類
在編寫好entity
和repository
之後我們還需要透過Typeorm.forFeature
這個靜態方法進行註冊,並把儲存類匯出為提供者以便在其它模組注入
// src/modules/content/content.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([PostEntity]),
// 註冊自定義Repository
CoreModule.forRepository([PostRepository]),
],
exports: [
// 匯出自定義Repository,以供其它模組使用
CoreModule.forRepository([PostRepository]),
],
})
export class ContentModule {}
DTO驗證
DTO
配合管道(PIPE)用於控制器的資料驗證,驗證器則使用class-validator
class-validator是基於validator.js的封裝,所以一些規則可以透過validator.js的文件查詢,後面教程中我們會編寫大量的自定義的驗證規則,這節先嚐試基本的用法
其基本的使用方法就是給DTO
類的屬性新增一個驗證裝飾器,如下
groups
選項用於配置驗證組
// src/modules/content/dtos/create-post.dto.ts
@Injectable()
export class CreatePostDto {
@MaxLength(255, {
always: true,
message: '文章標題長度最大為$constraint1',
})
@IsNotEmpty({ groups: ['create'], message: '文章標題必須填寫' })
@IsOptional({ groups: ['update'] })
title!: string;
...
}
更新驗證類UpdatePostDto
繼承自CreatePostDto
,為了使CreatePostDto
中的屬性變成可選,需要使用[@nestjs/swagger][]包中的PartialType
方法,請查閱此處文件
// src/modules/content/dtos/update-post.dto.ts
@Injectable()
export class UpdatePostDto extends PartialType(CreatePostDto) {
@IsUUID(undefined, { groups: ['update'], message: '文章ID格式錯誤' })
@IsDefined({ groups: ['update'], message: '文章ID必須指定' })
id!: string;
}
服務類
服務一共包括5個簡單的方法,透過呼叫PostRepository
來運算元據
// src/modules/content/services/post.service.ts
@Injectable()
export class PostService {
// 此處需要注入`PostRepository`的依賴
constructor(private postRepository: PostRepository) {}
// 查詢文章列表
async findList()
// 查詢一篇文章的詳細資訊
async findOne(id: string)
// 新增文章
async create(data: CreatePostDto)
// 更新文章
async update(data: UpdatePostDto)
// 刪除文章
async delete(id: string)
}
控制器
控制器的方法透過@GET
,@POST
,@PUT
,@PATCH
,@Delete
等裝飾器對外提供介面,並且透過注入PostService
服務來運算元據.在控制器的方法上使用框架自帶的ValidationPipe
管道來驗證請求中的body
資料,ParseUUIDPipe
來驗證params
資料
// 控制器URL的字首
@Controller('posts')
export class PostController {
constructor(protected postService: PostService) {}
...
// 其它方法請自行檢視原始碼
@Get(':post')
async show(@Param('post', new ParseUUIDPipe()) post: string) {
return this.postService.findOne(post);
}
@Post()
async store(
@Body(
new ValidationPipe({
transform: true,
forbidUnknownValues: true,
// 不在錯誤中暴露target
validationError: { target: false },
groups: ['create'],
}),
)
data: CreatePostDto,
) {
return this.postService.create(data);
}
}
註冊控制器等
- 為了後面``DTO
中可能會匯入服務,需要把
DTO,同樣註冊為提供者並且改造一下
main.ts,把容器加入到
class-containter`中 PostService
服務可能後續會被UserModule
等其它模組使用,所以此處我們也直接匯出
// src/modules/content/content.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([PostEntity]),
// 註冊自定義Repository
CoreModule.forRepository([PostRepository]),
],
providers: [PostService, CreatePostDto, UpdatePostDto],
controllers: [PostController],
exports: [
PostService,
// 匯出自定義Repository,以供其它模組使用
CoreModule.forRepository([PostRepository]),
],
})
export class ContentModule {}
// src/main.ts
...
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
useContainer(app.select(AppModule), { fallbackOnErrors: true });
await app.listen(3000, '0.0.0.0');
}
最後啟動應用在Thunder Client
中測試介面
本作品採用《CC 協議》,轉載必須註明作者和本文連結