一文帶你入門Nestjs
點選上方 前端進階之旅 ,關注公眾號
>>前端求職面試刷題神器<<
Nest (NestJS) 是一個用於構建高效、可擴充套件的 Node.js 伺服器端應用程式的開發框架。它利用 JavaScript 的漸進增強的能力,使用並完全支援 TypeScript (仍然允許開發者使用純 JavaScript 進行開發),並結合了 OOP (面向物件程式設計)、FP (函數語言程式設計)和 FRP (函式響應式程式設計)。
-
在底層,Nest 構建在強大的 HTTP 伺服器框架上,例如 Express (預設),並且還可以通過配置從而使用 Fastify !
-
Nest 在這些常見的 Node.js 框架 (Express/Fastify) 之上提高了一個抽象級別,但仍然向開發者直接暴露了底層框架的 API。這使得開發者可以自由地使用適用於底層平臺的無數的第三方模組。
建立專案
$ npm i -g @nestjs/cli
nest new project-name
建立一個專案
$ tree
.
├── README.md
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
2 directories, 12 files
以下是這些核心檔案的簡要概述:
-
app.controller.ts
帶有單個路由的基本控制器示例。 -
app.module.ts
應用程式的根模組。 -
main.ts
應用程式入口檔案。它使用 NestFactory 用來建立 Nest 應用例項。
main.ts
包含一個非同步函式,它負責引導我們的應用程式:
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
await app.listen(3000);
}
bootstrap();
-
NestFactory
暴露了一些靜態方法用於建立應用例項 -
create()
方法返回一個實現INestApplication
介面的物件, 並提供一組可用的方法
nest
有兩個支援開箱即用的 HTTP 平臺: express
和 fastify
。您可以選擇最適合您需求的產品
-
platform-express @nestjs/platform-express Express
-
platform-fastify
Fastify
是一個高效能,低開銷的框架,專注於提供最高的效率和速度。
Nest控制器
Nest中的控制器層負責處理傳入的請求, 並返回對客戶端的響應。
控制器的目的是接收應用的特定請求。路由機制控制哪個控制器接收哪些請求。通常,每個控制器有多個路由,不同的路由可以執行不同的操作
通過NestCLi建立控制器:
nest -h
可以看到 nest
支援的命令
常用命令:
-
建立控制器:
nest g co user module
-
建立服務:
nest g s user module
-
建立模組:
nest g mo user module
-
預設以src為根路徑生成
nest g controller posts
表示建立posts的控制器,這個時候會在src目錄下面生成一個posts的資料夾,這個裡面就是posts的控制器,程式碼如下
import { Controller } from '@nestjs/common';
@Controller('posts')
export class PostsController {
}
建立好控制器後, nestjs
會自動的在 app.module.ts
中引入 PostsController
,程式碼如下
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsController } from './posts/posts.controller'
@Module({
imports: [],
controllers: [AppController, PostsController],
providers: [AppService],
})
export class AppModule {}
nest配置路由請求資料
Nestjs提供了其他HTTP請求方法的裝飾器 @Get()
@Post()
@Put()
、 @Delete()
、 @Patch()
、 @Options()
、 @Head()
和 @All()
在Nestjs中獲取 Get
傳值或者 Post提
交的資料的話我們可以使用Nestjs中的裝飾器來獲取。
@Request() req
@Response() res
@Next() next
@Session() req.session
@Param(key?: string) req.params / req.params[key]
@Body(key?: string) req.body / req.body[key]
@Query(key?: string) req.query / req.query[key]
@Headers(name?: string) req.headers / req.headers[name]
示例
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post('create')
create(@Body() createPostDto: CreatePostDto) {
return this.postsService.create(createPostDto);
}
@Get('list')
findAll(@Query() query) {
return this.postsService.findAll(query);
}
@Get(':id')
findById(@Param('id') id: string) {
return this.postsService.findById(id);
}
@Put(':id')
update(
@Param('id') id: string,
@Body() updatePostDto: UpdatePostDto,
) {
return this.postsService.update(id, updatePostDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.postsService.remove(id);
}
}
注意
-
關於nest的return
:當請求處理程式返回 JavaScript 物件或陣列時,它將自動序列化為 JSON。但是,當它返回一個字串時,Nest 將只發送一個字串而不是序列化它
Nest服務
Nestjs中的服務可以是 service
也可以是 provider
。他們都可以 通過 constructor 注入依賴關係
。服務本質上就是通過 @Injectable()
裝飾器註解的類。在Nestjs中服務相當於 MVC
的 Model
建立服務
nest g service posts
建立好服務後就可以在服務中定義對應的方法
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, Between, Equal, Like, In } from 'typeorm';
import * as dayjs from 'dayjs';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostsEntity } from './entities/post.entity';
import { PostsRo } from './interfaces/posts.interface';
@Injectable()
export class PostsService {
constructor(
@InjectRepository(PostsEntity)
private readonly postsRepository: Repository<PostsEntity>,
) {}
async create(post: CreatePostDto) {
const { title } = post;
const doc = await this.postsRepository.findOne({ where: { title } });
console.log('doc', doc);
if (doc) {
throw new HttpException('文章標題已存在', HttpStatus.BAD_REQUEST);
}
return {
data: await this.postsRepository.save(post),
message: '建立成功',
};
}
// 分頁查詢列表
async findAll(query = {} as any) {
let { pageSize, pageNum, orderBy, sort, ...params } = query;
orderBy = query.orderBy || 'create_time';
sort = query.sort || 'DESC';
pageSize = Number(query.pageSize || 10);
pageNum = Number(query.pageNum || 1);
console.log('query', query);
const queryParams = {} as any;
Object.keys(params).forEach((key) => {
if (params[key]) {
queryParams[key] = Like(`%${params[key]}%`); // 所有欄位支援模糊查詢、%%之間不能有空格
}
});
const qb = await this.postsRepository.createQueryBuilder('post');
// qb.where({ status: In([2, 3]) });
qb.where(queryParams);
// qb.select(['post.title', 'post.content']); // 查詢部分欄位返回
qb.orderBy(`post.${orderBy}`, sort);
qb.skip(pageSize * (pageNum - 1));
qb.take(pageSize);
return {
list: await qb.getMany(),
totalNum: await qb.getCount(), // 按條件查詢的數量
total: await this.postsRepository.count(), // 總的數量
pageSize,
pageNum,
};
}
// 根據ID查詢詳情
async findById(id: string): Promise<PostsEntity> {
return await this.postsRepository.findOne({ where: { id } });
}
// 更新
async update(id: string, updatePostDto: UpdatePostDto) {
const existRecord = await this.postsRepository.findOne({ where: { id } });
if (!existRecord) {
throw new HttpException(`id為${id}的文章不存在`, HttpStatus.BAD_REQUEST);
}
// updatePostDto覆蓋existRecord 合併,可以更新單個欄位
const updatePost = this.postsRepository.merge(existRecord, {
...updatePostDto,
update_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
});
return {
data: await this.postsRepository.save(updatePost),
message: '更新成功',
};
}
// 刪除
async remove(id: string) {
const existPost = await this.postsRepository.findOne({ where: { id } });
if (!existPost) {
throw new HttpException(`文章ID ${id} 不存在`, HttpStatus.BAD_REQUEST);
}
await this.postsRepository.remove(existPost);
return {
data: { id },
message: '刪除成功',
};
}
}
Nest模組
模組是具有 @Module()
裝飾器的類。 @Module()
裝飾器提供了元資料,Nest 用它來組織應用程式結構
每個 Nest 應用程式至少有一個模組,即根模組。根模組是 Nest 開始安排應用程式樹的地方。事實上,根模組可能是應用程式中唯一的模組,特別是當應用程式很小時,但是對於大型程式來說這是沒有意義的。在大多數情況下,您將擁有多個模組,每個模組都有一組緊密相關的功能。
@module() 裝飾器接受一個描述模組屬性的物件:
-
providers
由 Nest 注入器例項化的提供者,並且可以至少在整個模組中共享 -
controllers
必須建立的一組控制器 -
imports
匯入模組的列表,這些模組匯出了此模組中所需提供者 -
exports
由本模組提供並應在其他模組中可用的提供者的子集
// 建立模組 posts
nest g module posts
Nestjs中的共享模組
每個模組都是一個共享模組。一旦建立就能被任意模組重複使用。假設我們將在幾個模組之間共享 PostsService 例項。我們需要把 PostsService 放到 exports 陣列中:
// posts.modules.ts
import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
@Module({
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService] // 共享模組匯出
})
export class PostsModule {}
可以使用 nest g res posts
一鍵建立以上需要的各個模組
配置靜態資源
NestJS中配置靜態資源目錄完整程式碼
npm i @nestjs/platform-express -S
import { NestExpressApplication } from '@nestjs/platform-express';
// main.ts
async function bootstrap() {
// 建立例項
const app = await NestFactory.create<NestExpressApplication>(AppModule);
//使用方式一
app.useStaticAssets('public') //配置靜態資源目錄
// 使用方式二:配置字首目錄 設定靜態資源目錄
app.useStaticAssets(join(__dirname, '../public'), {
// 配置虛擬目錄,比如我們想通過 http://localhost:3000/static/1.jpg 來訪問public目錄裡面的檔案
prefix: '/static/', // 設定虛擬路徑
});
// 啟動埠
const PORT = process.env.PORT || 9000;
await app.listen(PORT, () =>
Logger.log(`服務已經啟動 http://localhost:${PORT}`),
);
}
bootstrap();
配置模板引擎
npm i ejs --save
配置模板引擎
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {join} from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setBaseViewsDir(join(__dirname, '..', 'views')) // 放檢視的檔案
app.setViewEngine('ejs'); //模板渲染引擎
await app.listen(9000);
}
bootstrap();
專案根目錄新建 views
目錄然後新建 根目錄 -> views -> default -> index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h3>模板引擎</h3>
<%=message%>
</body>
</html>
渲染頁面
Nestjs中 Render
裝飾器可以渲染模板, 使用路由匹配渲染引擎
mport { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
@Get()
@Render('default/index') //使用render渲染模板引擎,引數就是檔案路徑:default資料夾下的index.ejs
getUser(): any {
return {message: "hello word"} //只有返回引數在模板才能獲取,如果不傳遞引數,必須返回一個空物件
}
}
Cookie的使用
cookie和session的使用 依賴 於當前使用的平臺,如:express和fastify 兩種的使用方式不同,這裡主要記錄基於 express 平臺的用法
cookie可以用來儲存使用者資訊,儲存購物車等資訊,在實際專案中用的非常多
npm instlal cookie-parser --save
npm i -D @types/cookie-parser --save
引入註冊
// main.ts
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as cookieParser from 'cookie-parser'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
//註冊cookie
app.use(cookieParser('dafgafa')); //加密密碼
await app.listen(3000);
}
bootstrap();
介面中設定cookie 使用response
請求該介面,響應一個cookie
@Get()
index(@Response() res){
//設定cookie, signed:true加密
//引數:1:key, 2:value, 3:配置
res.cookie('username', 'poetry', {maxAge: 1000 * 60 * 10, httpOnly: true, signed:true})
//注意:
//使用res後,返回資料必須使用res
//如果是用了render模板渲染,還是使用return
res.send({xxx})
}
cookie相關配置引數
-
domain
String 指定域名下有效 -
expires
Date 過期時間(秒),設定在某個時間點後會在該cookoe
後失效 -
httpOnly false true js cookie
-
maxAge
String 最大失效時間(毫秒),設定在多少時間後失效 -
path cookie path=/ cookie
-
secure secure true cookie HTTPS
-
signed cookie true cookie res.signedCookies() cookie res.cookies()
獲取cookie
@Get()
index(@Request() req){
console.log(req.cookies.username)
//加密的cookie獲取方式
console.log(req.signedCookies.username)
return req.cookies.username
}
Cookie加密
// 配置中介軟體的時候需要傳參
app.use(cookieParser('123456'));
// 設定cookie的時候配置signed屬性
res.cookie('userinfo','hahaha',{domain:'.ccc.com',maxAge:900000,httpOnly:true,signed:true});
// signedCookies呼叫設定的cookie
console.log(req.signedCookies);
Session的使用
-
session
是另一種記錄客戶狀態的機制,不同的是Cookie儲存在客戶端瀏覽器中,而session
儲存在伺服器上 -
當瀏覽器訪問伺服器併發送第一次請求時,伺服器端會建立一個session物件,生成一個類似於key,value的鍵值對, 然後將key(cookie)返回到瀏覽器(客戶)端,瀏覽器下次再訪問時,攜帶key(cookie),找到對應的session(value)。客戶的資訊都儲存在session中
安裝 express-session
npm i express-session --save
npm i -D @types/express-session --save
// main.ts
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as session from 'express-seesion'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
//配置session
app.use(session({
secret: 'dmyxs',
cookie: { maxAge: 10000, httpOnly: true }, //以cookie儲存到客戶端
rolling: true //每次重新請求時,重新設定cookie
}))
await app.listen(3000);
}
bootstrap();
session相關配置引數
-
secret
String 生成session
簽名的金鑰 -
name cookie connect.sid
-
resave session true false
-
saveUninitalized session session cookie true
-
cookie cookie { path: ‘/’, httpOnly: true, secure: false, maxAge: null }
-
rolling cookie cookie false
介面中設定session
@Get()
index(@Request() req){
//設定session
req.session.username = 'poetry'
}
獲取session
@Get('/session')
session(@Request() req, @Session() session ){
//獲取session:兩種方式
console.log(req.session.username)
console.log(session.username)
return 'hello session'
}
跨域,字首路徑、網站安全、請求限速
跨域,路徑字首,網路安全
yarn add helmet csurf
// main.ts
import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import * as helmet from 'helmet';
import * as csurf from 'csurf';
import { AppModule } from './app.module';
const PORT = process.env.PORT || 8000;
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 路徑字首:如:http://www.test.com/api/v1/user
app.setGlobalPrefix('api/v1');
//cors:跨域資源共享,方式一:允許跨站訪問
app.enableCors();
// 方式二:const app = await NestFactory.create(AppModule, { cors: true });
//防止跨站指令碼攻擊
app.use(helmet());
//CSRF保護:跨站點請求偽造
app.use(csurf());
await app.listen(PORT, () => {
Logger.log(
`服務已經啟動,介面請訪問:localhost:${PORT}${PREFIX}`,
)
});
}
bootstrap();
限速:限制客戶端在一定時間內的請求次數
yarn add @nestjs/throttler
在需要使用的模組引入使用,這裡是 全域性
使用,在 app.module.ts
中引入。這裡設定的是: 1分鐘內只能請求10次,超過則報status為429的錯誤
app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { Module } from '@nestjs/common';
import { UserModule } from './modules/user/user.module';
//引入
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
UserModule,
ThrottlerModule.forRoot({
ttl: 60, //1分鐘
limit: 10, //請求10次
}),
],
providers: [ //全域性使用
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule { }
管道、守衛、攔截器、過濾器、中介軟體
-
管道:資料處理與轉換,資料驗證
-
守衛:驗證使用者登陸,保護路由
-
攔截器:對請求響應進行攔截,統一響應內容
-
過濾器:異常捕獲
-
中介軟體:日誌列印
執行順序(時機)
從客戶端傳送一個post請求,路徑為: /user/login
,請求引數為: {userinfo: ‘xx’,password: ‘xx’}
,到伺服器接收請求內容,觸發繫結的函式並且執行相關邏輯完畢,然後返回內容給客戶端的整個過程大體上要經過如下幾個步驟:
全域性使用: 管道 - 守衛 - 攔截器 - 過濾器 - 中介軟體。統一在main.ts檔案中使用,全域性生效
import { NestFactory } from '@nestjs/core';
import { ParseIntPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { AuthGuard } from './common/guard/auth.guard';
import { AuthInterceptor } from './common/interceptors/auth.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//全域性使用管道:這裡使用的是內建,也可以使用自定義管道,在下文
app.useGlobalPipes(new ParseIntPipe());
//全域性使用中介軟體
app.use(LoggerMiddleware)
//全域性使用過濾器
//這裡使用的是自定義過濾器,先別管,先學會怎麼在全域性使用
app.useGlobalFilters(new HttpExceptionFilter());
//全域性使用守衛
app.useGlobalGuards(new AuthGuard());
//全域性使用攔截器
app.useGlobalInterceptors(new AuthInterceptor());
await app.listen(3000);
}
bootstrap();
管道
常用內建管道,從 @nestjs/common
匯出
-
ParseIntPipe
:將字串數字轉數字 -
ValidationPipe
:驗證管道
區域性使用管道
-
匹配整個路徑,使用UsePipes
-
只匹配某個介面,使用UsePipes
-
在獲取引數時匹配,一般使用內建管道
import {
Controller,
Get,
Put,
Body,
Param,
UsePipes,
ParseIntPipe
} from '@nestjs/common';
import { myPipe } from '../../common/pipes/user.pipe';
@Controller('user')
@UsePipes(new myPipe()) //區域性方式1:匹配整個/user, get請求和put請求都會命中
export class UserController {
@Get(':id')
getUserById(@Param('id', new ParseIntPipe()) id) { //區域性方式3:只匹配/user的get請求,使用的是內建管道
console.log('user', typeof id);
return id;
}
@Put(':id')
@UsePipes(new myPipe()) //區域性方式2:只匹配/user的put請求
updateUser(@Body() user, @Param('id') id) {
return {
user,
id,
};
}
}
自定義管道
使用快捷命令生成: nest g pi myPipe common/pipes
import {
ArgumentMetadata,
Injectable,
PipeTransform,
BadRequestException,
} from '@nestjs/common';
//自定義管道必須實現自PipeTransform,固定寫法,該介面有一個transform方法
//transform引數:
//value:使用myPipe時所傳遞的值,可以是param傳遞的的查詢路徑引數,可以是body的請求體
//metadata:元資料,可以用它判斷是來自param或body或query
@Injectable()
export class myPipe implements PipeTransform<string> {
transform(value: string, metadata: ArgumentMetadata) {
if (metadata.type === 'body') {
console.log('來自請求體', value);
}
if (metadata.type === 'param') {
console.log('來自查詢路徑', value);
const val = parseInt(value, 10);
//如果不是傳遞一個數字,丟擲錯誤
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
return value;
}
}
守衛
自定義守衛
使用快捷命令生成: nest g gu myGuard common/guards
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; //反射器,作用與自定義裝飾器橋接,獲取資料
//自定義守衛必須CanActivate,固定寫法,該介面只有一個canActivate方法
//canActivate引數:
//context:請求的(Response/Request)的引用
//通過守衛返回true,否則返回false,返回403狀態碼
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly reflector: Reflector) { }
// 白名單陣列
private whiteUrlList: string[] = ['/user'];
// 驗證該次請求是否為白名單內的路由
private isWhiteUrl(urlList: string[], url: string): boolean {
if (urlList.includes(url)) {
return true;
}
return false;
}
canActivate(context: ExecutionContext): boolean {
// 獲取請求物件
const request = context.switchToHttp().getRequest();
//console.log('request', request.headers);
//console.log('request', request.params);
//console.log('request', request.query);
//console.log('request', request.url);
// 用法一:驗證是否是白名單內的路由
if (this.isWhiteUrl(this.whiteUrlList, request.url)) {
return true;
} else {
return false;
}
// 用法二:使用反射器,配合裝飾器使用,獲取裝飾器傳遞過來的資料
const roles = this.reflector.get<string[]>('roles', context.getHandler());
//console.log(roles); // [ 'admin' ]
//http://localhost:3000/user/9?user=admin,如果與裝飾器傳遞過來的值匹配則通過,否則不通過
//真實開發中可能從cookie或token中獲取值
const { user } = request.query;
if (roles.includes(user)) {
return true;
} else {
return false;
}
// 其他用法
// 獲取請求頭中的token欄位
const token = context.switchToRpc().getData().headers.token;
// console.log('token', token);
// 獲取session
const userinfo = context.switchToHttp().getRequest().session;
// console.log('session', userinfo);
return true;
}
}
區域性使用守衛
import {
Controller,
Get,
Delete,
Param,
UsePipes,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { AuthGuard } from '../../common/guard/auth.guard';
import { Role } from '../../common/decorator/role.decorator'; //自定義裝飾器
@UseGuards(AuthGuard) //區域性使用守衛,守衛整個user路徑
@Controller('user')
export class UserController {
@Get(':id')
getUserById(@Param('id', new ParseIntPipe()) id) {
console.log('user', typeof id);
return id;
}
@Delete(':id')
@Role('admin') //使用自定義裝飾器,傳入角色,必須是admin才能刪除
removeUser(@Param('id') id) {
return id;
}
}
裝飾器
自定義守衛中使用到了自定義裝飾器
nest g d role common/decorator
//這是快捷生成的程式碼
import { SetMetadata } from '@nestjs/common';
//SetMetadata作用:將獲取到的值,設定到元資料中,然後守衛通過反射器才能獲取到值
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
攔截器
使用快捷命令生成: nest g in auth common/intercepters
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
//自定義攔截器必須實現自NestInterceptor,固定寫法,該介面只有一個intercept方法
//intercept引數:
//context:請求上下文,可以拿到的Response和Request
@Injectable()
export class AuthInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
console.log('攔截器', request.url);
return next.handle().pipe(
map((data) => {
console.log('全域性響應攔截器方法返回內容後...');
return {
status: 200,
timestamp: new Date().toISOString(),
path: request.url,
message: '請求成功',
data: data,
};
}),
);
}
}
過濾器
區域性使用過濾器
import {
Controller,
Get,
UseFilters,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpExceptionFilter } from '../../common/filters/http-exception.filter';
//區域性使用過濾器
@UseFilters(new HttpExceptionFilter())
@Controller('/user')
export class ExceptionController {
@Get()
getUserById(@Query() { id }): string {
if (!id) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
message: '請求引數id 必傳',
error: 'id is required',
},
HttpStatus.BAD_REQUEST,
);
}
return 'hello error';
}
}
自定義過濾器
使用快捷命令生成: nest g f myFilter common/filters
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
//必須實現至ExceptionFilter,固定寫法,該介面只有一個catch方法
//catch方法引數:
//exception:當前正在處理的異常物件
//host:傳遞給原始處理程式的引數的一個包裝(Response/Request)的引用
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus(); //獲取狀態碼
const exceptionRes: any = exception.getResponse(); //獲取響應物件
const { error, message } = exceptionRes;
//自定義的異常響應內容
const msgLog = {
status,
timestamp: new Date().toISOString(),
path: request.url,
error,
message,
};
response.status(status).json(msgLog);
}
}
中介軟體
區域性使用中介軟體
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middlerware';
import { UserModule } from './modules/user/user.module';
@Module({
imports:[ UserModule ]
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) //應用中介軟體
.exclude({ path: 'user', method: RequestMethod.POST }) //排除user的post方法
.forRoutes('user'); //監聽路徑 引數:路徑名或*,*是匹配所以的路由
// .forRoutes({ path: 'user', method: RequestMethod.POST }, { path: 'album', method: RequestMethod.ALL }); //多個
// .apply(UserMiddleware) //支援多箇中間件
// .forRoutes('user')
}
}
自定義中介軟體
nest g mi logger common/middleware
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
//req:請求引數
//res:響應引數
//next:執行下一個中件間
use(req: Request, res: Response, next: () => void) {
const { method, path } = req;
console.log(`${method} ${path}`);
next();
}
}
函式式中介軟體
// 函式式中介軟體-應用於全域性
export function logger(req, res, next) {
next();
}
// main.ts
async function bootstrap() {
// 建立例項
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 設定全域性日誌函式中介軟體
app.use(logger);
}
bootstrap();
資料驗證
如何 限制 和 驗證 前端傳遞過來的資料?
常用: dto
(data transfer object資料傳輸物件) + class-validator
,自定義提示內容,還能整合swagger
class-validator的驗證項裝飾器
http://github.com/typestack/class-validator#usage
@IsOptional() //可選的
@IsNotEmpty({ message: ‘不能為空’ })
@MinLength(6, {message: ‘密碼長度不能小於6位’})
@MaxLength(20, {message: ‘密碼長度不能超過20位’})
@IsEmail({}, { message: ‘郵箱格式錯誤’ }) //郵箱
@IsMobilePhone(‘zh-CN’, {}, { message: ‘手機號碼格式錯誤’ }) //手機號碼
@IsEnum([0, 1], {message: ‘只能傳入數字0或1’}) //列舉
@ValidateIf(o => o.username === ‘admin’) //條件判斷,條件滿足才驗證,如:這裡是傳入的username是admin才驗證
yarn add class-validator class-transformer
全域性使用內建管道 ValidationPipe
,不然會報錯,無法起作用
import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); //全域性內建管道
await app.listen(3000);
}
bootstrap();
編寫 dto
,使用 class-validator
的校驗項驗證
建立DTO:只需要使用者名稱,密碼即可,兩種都不能為空
可以使用 nest g res user
一鍵建立帶有dto的介面模組
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: '使用者名稱不能為空' })
username: string;
@IsNotEmpty({ message: '密碼不能為空' })
@MinLength(6, {
message: '密碼長度不能小於6位',
})
@MaxLength(20, {
message: '密碼長度不能超過20位',
})
password: string;
}
修改DTO:使用者名稱,密碼,手機號碼,郵箱,性別,狀態,都是 可選的
import {
IsEnum,
MinLength,
MaxLength,
IsOptional,
IsEmail,
IsMobilePhone,
} from 'class-validator';
import { Type } from 'class-transformer';
export class UpdateUserDto {
@IsOptional()
username: string;
@IsOptional()
@MinLength(6, {
message: '密碼長度不能小於6位',
})
@MaxLength(20, {
message: '密碼長度不能超過20位',
})
password: string;
@IsOptional()
@IsEmail({}, { message: '郵箱格式錯誤' })
email: string;
@IsOptional()
@IsMobilePhone('zh-CN', {}, { message: '手機號碼格式錯誤' })
mobile: string;
@IsOptional()
@IsEnum(['male', 'female'], {
message: 'gender只能傳入字串male或female',
})
gender: string;
@IsOptional()
@IsEnum({ 禁用: 0, 可用: 1 },{
message: 'status只能傳入數字0或1',
})
@Type(() => Number) //如果傳遞的是string型別,不報錯,自動轉成number型別
status: number;
}
controller
和 service
一起使用
// user.controller.ts
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) { }
@Post()
@HttpCode(HttpStatus.OK)
async create(@Body() user: CreateUserDto) { //使用建立dto
return await this.userService.create(user);
}
@Patch(':id')
async update(@Param('id') id: string, @Body() user: UpdateUserDto) { //使用更新dto
return await this.userService.update(id, user);
}
}
// user.service.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { UsersEntity } from './entities/user.entity';
import { ToolsService } from '../../utils/tools.service';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UserService {
constructor(
@InjectRepository(UsersEntity)
private readonly usersRepository: Repository<UsersEntity>,
) { }
async create(user: CreateUserDto) { //使用dto
do some thing....
}
}
進階
配置抽離
yarn add nestjs-config
app.module.ts
import * as path from 'path';
import { Module } from '@nestjs/common';
//資料庫
import { TypeOrmModule } from '@nestjs/typeorm';
//全域性配置
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
//1.配置config目錄
ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
//2.讀取配置,這裡讀取的是資料庫配置
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService], // 獲取服務注入
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
配置資料庫
src -> config -> database
import { join } from 'path';
export default {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'your password',
database: 'test',
entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')],
synchronize: true,
};
環境配置
yarn add cross-env
cross-env的作用是相容window系統和mac系統來設定環境變數
在package.json中配置
"scripts": {
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
},
dotenv的使用
yarn add dotenv
根目錄建立 env.parse.ts
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
const isProd = process.env.NODE_ENV === 'production';
const localEnv = path.resolve('.env.local');
const prodEnv = path.resolve('.env.prod');
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
// 配置 通過process.env.xx讀取變數
dotenv.config({ path: filePath });
匯入環境
// main.ts
import '../env.parse'; // 匯入環境變數
.env.local
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=123
MYSQL_DATABASE=test
.env.prod
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=1234
MYSQL_DATABASE=test
讀取環境變數 process.env.MYSQL_HOST
形式
檔案上傳與下載
// upload.controller.ts
import {
Controller,
Get,
Post,
UseInterceptors,
UploadedFile,
UploadedFiles,
Body,
Res,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { FileUploadDto } from './dto/upload-file.dto';
import { UploadService } from './upload.service';
import { Response } from 'express';
@Controller('common')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file) {
this.uploadService.uploadSingleFile(file);
return true;
}
// 多檔案上傳
@Post('uploads')
@UseInterceptors(FilesInterceptor('file'))
uploadMuliFile(@UploadedFiles() files, @Body() body) {
this.uploadService.UploadMuliFile(files, body);
return true;
}
@Get('export')
async downloadAll(@Res() res: Response) {
const { filename, tarStream } = await this.uploadService.downloadAll();
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
tarStream.pipe(res);
}
}
// upload.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { join } from 'path';
import { createWriteStream } from 'fs';
import { tar } from 'compressing';
import { ConfigService } from 'nestjs-config';
@Injectable()
export class UploadService {
constructor(private readonly configService: ConfigService) {}
uploadSingleFile(file: any) {
console.log('file', file);
}
UploadMuliFile(files: any, body: any) {
console.log('files', files);
}
async downloadAll() {
const uploadDir = this.configService.get('file').root;
const tarStream = new tar.Stream();
await tarStream.addEntry(uploadDir);
return { filename: 'download.tar', tarStream };
}
}
// upload.module.ts
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigService } from 'nestjs-config';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
@Module({
imports: [
MulterModule.registerAsync({
useFactory: (config: ConfigService) => config.get('file'),
inject: [ConfigService],
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
// src/config/file.ts 上傳檔案配置
import { join } from 'path';
import { diskStorage } from 'multer';
/**
* 上傳檔案配置
*/
export default {
root: join(__dirname, '../../assets/uploads'),
storage: diskStorage({
destination: join(
__dirname,
`../../assets/uploads/${new Date().toLocaleDateString()}`,
),
filename: (req, file, cb) => {
const filename = `${new Date().getTime()}.${file.mimetype.split('/')[1]}`;
return cb(null, filename);
},
}),
};
// app.module.ts
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
// 載入配置檔案目錄 src/config
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {}
郵件服務
郵件服務使用文件 http://nest-modules.github.io/mailer/docs/mailer
// 郵件服務配置
// app.module.ts
import { MailerModule } from '@nestjs-modules/mailer';
import { resolve, join } from 'path';
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
// 載入配置檔案目錄 src/config
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
// 郵件服務配置
MailerModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('email'),
inject: [ConfigService],
}),
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {}
// src/config/email.ts 郵件服務配置
import { join } from 'path';
// npm i ejs -S
import { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter';
export default {
transport: {
host: 'smtp.qq.com',
secureConnection: true, // use SSL
secure: true,
port: 465,
ignoreTLS: false,
auth: {
user: '[email protected]',
pass: 'dfafew1',
},
},
defaults: {
from: '"nestjs" <[email protected]>',
},
// preview: true, // 傳送郵件前預覽
template: {
dir: join(__dirname, '../templates/email'), // 郵件模板
adapter: new EjsAdapter(),
options: {
strict: true,
},
},
};
郵件服務使用
// email.services.ts
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class EmailService {
// 郵件服務注入
constructor(private mailerService: MailerService) {}
async sendEmail() {
console.log('傳送郵件');
await this.mailerService.sendMail({
to: '[email protected]', // 收件人
from: '[email protected]', // 發件人
// subject: '副標題',
text: 'welcome', // plaintext body
html: '<h1>hello</h1>', // HTML body content
// template: 'email', // 郵件模板
// context: { // 傳入郵件模板的data
// email: '[email protected]',
// },
});
return '傳送成功';
}
}
nest基於possport + jwt做登陸驗證
npm i @nestjs/passport passport @nestjs/jwt passport-local -S
auth.controller.ts
// src/modules/auth/auth.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
// 登入測試 無需token
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
// 在需要的地方使用守衛,需要帶token才可訪問
@UseGuards(AuthGuard('jwt'))
@Get('userInfo')
getUserInfo(@Request() req) {
return req.user;
}
}
auth.module.ts
// src/modules/auth/auth.module.ts
import { LocalStrategy } from './local.strategy';
import { jwtConstants } from './constants';
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
// import { usersModule } from '../users/user.module'
@Module({
imports: [
// usersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '10d' },
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
auth.service.ts
// src/modules/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
validateUser(username: string, password: string) {
// let user = await this.userService.find(username)
const user = { username: 'test', password: '11', userId: '121212143141' };
if (user && user.password) {
const { password, ...result } = user;
return result;
}
return null;
}
login(user: any) {
return {
token: this.jwtService.sign(user),
};
}
}
constants.ts
// src/modules/auth/constants.ts
export const jwtConstants = {
secret: 'secretKey',
};
jwt.strategy.ts
// src/modules/auth/jwt.strategy.ts
import { Strategy, ExtractJwt } from 'passport-jwt';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromHeader('token'),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.userId, username: payload.username };
}
}
local.strategy.ts
// src/modules/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
console.log('username', username);
console.log('password', password);
return { username, password };
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new HttpException('沒有登入', HttpStatus.BAD_REQUEST);
}
return user;
}
}
最後在app.module.ts中匯入即可測試
// app.modules.ts
import { AuthModule } from './modules/auth/auth.module';
@Module({
imports: [
...
AuthModule, // 匯入模組
],
controllers: [AppController],
providers: [],
})
export class AppModule implements NestModule {}
接入Swagger介面文件
-
優點:不用寫介面文件,線上生成,自動生成,可操作資料庫,完美配合
dto
-
缺點:多一些程式碼,顯得有點亂,習慣就好
yarn add @nestjs/swagger swagger-ui-express -D
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
// 建立例項
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 建立介面文件服務
const options = new DocumentBuilder()
.addBearerAuth() // token認證,輸入token才可以訪問文件
.setTitle('介面文件')
.setDescription('介面文件介紹') // 文件介紹
.addServer('http://localhost:9000', '開發環境')
.addServer('http://test.com/release', '正式環境')
.setVersion('1.0.0') // 文件版本
.setContact('poetry', '', '[email protected]')
.build();
// 為了建立完整的文件(具有定義的HTTP路由),我們使用類的createDocument()方法SwaggerModule。此方法帶有兩個引數,分別是應用程式例項和基本Swagger選項。
const document = SwaggerModule.createDocument(app, options, {
extraModels: [], // 這裡匯入模型
});
// 啟動swagger
SwaggerModule.setup('api-docs', app, document); // 訪問路徑 http://localhost:9000/api-docs
// 啟動埠
const PORT = process.env.PORT || 9000;
await app.listen(PORT, () =>
Logger.log(`服務已經啟動 http://localhost:${PORT}`),
);
}
bootstrap();
swagger裝飾器
http://swagger.io/
@ApiTags('user') // 設定模組介面的分類,不設定預設分配到default
@ApiOperation({ summary: '標題', description: '詳細描述'}) // 單個介面描述
// 傳參
@ApiQuery({ name: 'limit', required: true}) // query引數
@ApiQuery({ name: 'role', enum: UserRole }) // query引數
@ApiParam({ name: 'id' }) // parma引數
@ApiBody({ type: UserCreateDTO, description: '輸入使用者名稱和密碼' }) // 請求體
// 響應
@ApiResponse({
status: 200,
description: '成功返回200,失敗返回400',
type: UserCreateDTO,
})
// 驗證
@ApiProperty({ example: 'Kitty', description: 'The name of the Cat' })
name: string;
在 controller
引入 @nestjs/swagger
, 並配置 @ApiBody()
和 @ApiParam()
不寫也是可以的
user.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Query,
Param,
Delete,
HttpCode,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import {
ApiOperation,
ApiTags,
ApiQuery,
ApiBody,
ApiResponse,
} from '@nestjs/swagger';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
@ApiTags('user') // 設定分類
export class UserController {
constructor(private readonly userService: UserService) { }
@Post()
@ApiOperation({ summary: '建立使用者', description: '建立使用者' }) // 該介面
@HttpCode(HttpStatus.OK)
async create(@Body() user: CreateUserDto) {
return await this.userService.create(user);
}
@Get()
@ApiOperation({ summary: '查詢全部使用者', description: '建立使用者' })
@ApiQuery({ name: 'limit', required: true }) 請求引數
@ApiQuery({ name: 'offset', required: true }) 請求引數
async findAll(@Query() query) {
console.log(query);
const [data, count] = await this.userService.findAll(query);
return { count, data };
}
@Get(':id')
@ApiOperation({ summary: '根據ID查詢使用者' })
async findOne(@Param('id', new ParseIntPipe()) id: number) {
return await this.userService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: '更新使用者' })
@ApiBody({ type: UpdateUserDto, description: '引數可選' }) 請求體
@ApiResponse({ 響應示例
status: 200,
description: '成功返回200,失敗返回400',
type: UpdateUserDto,
})
async update(
@Param('id', new ParseIntPipe()) id: number,
@Body() user: UpdateUserDto,
) {
return await this.userService.update(id, user);
}
@Delete(':id')
@ApiOperation({ summary: '刪除使用者' })
async remove(@Param('id', new ParseIntPipe()) id: number) {
return await this.userService.remove(id);
}
}
編寫dto,引入@nestjs/swagger
建立
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'kitty', description: '使用者名稱' }) 新增這裡即可
@IsNotEmpty({ message: '使用者名稱不能為空' })
username: string;
@ApiProperty({ example: '12345678', description: '密碼' })
@IsNotEmpty({ message: '密碼不能為空' })
@MinLength(6, {
message: '密碼長度不能小於6位',
})
@MaxLength(20, {
message: '密碼長度不能超過20位',
})
password: string;
}
更新
import {
IsEnum,
MinLength,
MaxLength,
IsOptional,
ValidateIf,
IsEmail,
IsMobilePhone,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class UpdateUserDto {
@ApiProperty({ description: '使用者名稱', example: 'kitty', required: false }) 不是必選的
@IsOptional()
username: string;
@ApiProperty({ description: '密碼', example: '12345678', required: false })
@IsOptional()
@MinLength(6, {
message: '密碼長度不能小於6位',
})
@MaxLength(20, {
message: '密碼長度不能超過20位',
})
password: string;
@ApiProperty({
description: '郵箱',
example: '[email protected]',
required: false,
})
@IsOptional()
@IsEmail({}, { message: '郵箱格式錯誤' })
@ValidateIf((o) => o.username === 'admin')
email: string;
@ApiProperty({
description: '手機號碼',
example: '13866668888',
required: false,
})
@IsOptional()
@IsMobilePhone('zh-CN', {}, { message: '手機號碼格式錯誤' })
mobile: string;
@ApiProperty({
description: '性別',
example: 'female',
required: false,
enum: ['male', 'female'],
})
@IsOptional()
@IsEnum(['male', 'female'], {
message: 'gender只能傳入字串male或female',
})
gender: string;
@ApiProperty({
description: '狀態',
example: 1,
required: false,
enum: [0, 1],
})
@IsOptional()
@IsEnum(
{ 禁用: 0, 可用: 1 },
{
message: 'status只能傳入數字0或1',
},
)
@Type(() => Number)
status: number;
}
開啟:localhost:3000/api-docs,開始測試介面
資料庫
nest連線Mongodb
mac中,直接使用 brew install mongodb-community
安裝MongoDB,然後啟動服務 brew services start mongodb-community
檢視服務已經啟動 ps aux | grep mongo
Nestjs中操作Mongodb資料庫可以使用Nodejs封裝的DB庫,也可以使用Mongoose。
// http://docs.nestjs.com/techniques/mongodb
npm install --save @nestjs/mongoose mongoose
npm install --save-dev @types/mongoose
在app.module.ts中配置資料庫連線
// app.module.ts
import { ConfigModule, ConfigService } from 'nestjs-config';
import { MongooseModule } from '@nestjs/mongoose';
import { MongodbModule } from '../examples/mongodb/mongodb.module';
@Module({
imports: [
// 載入配置檔案目錄
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
// mongodb
MongooseModule.forRootAsync({
useFactory: async (configService: ConfigService) =>
configService.get('mongodb'),
inject: [ConfigService],
}),
MongodbModule,
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {}
// mongodb配置
// src/config/mongodb.ts
export default {
uri: 'mongodb://localhost:27017/nest', // 指定nest資料庫
};
配置Schema
// article.schema
import * as mongoose from 'mongoose';
export const ArticleSchema = new mongoose.Schema({
title: String,
content:String,
author: String,
status: Number,
});
在控制器對應的Module中配置Model
// mongodb.module.ts
import { Module } from '@nestjs/common';
import { MongodbService } from './mongodb.service';
import { MongodbController } from './mongodb.controller';
import { ArticleSchema } from './schemas/article.schema';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forFeature([
{
name: 'Article', // schema名稱對應
schema: ArticleSchema, // 引入的schema
collection: 'article', // 資料庫名稱
},
]),
],
controllers: [MongodbController],
providers: [MongodbService],
})
export class MongodbModule {}
在服務裡面使用@InjectModel 獲取資料庫Model實現操作資料庫
// mongodb.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
@Injectable()
export class MongodbService {
// 注入模型
constructor(@InjectModel('Article') private readonly articleModel) {}
async findAll() {
return await this.articleModel.find().exec();
}
async findById(id) {
return await this.articleModel.findById(id);
}
async create(body) {
return await this.articleModel.create(body);
}
async update(body) {
const { id, ...params } = body;
return await this.articleModel.findByIdAndUpdate(id, params);
}
async delete(id) {
return await this.articleModel.findByIdAndDelete(id);
}
}
瀏覽器測試 http://localhost:9000/api/mongodb/list
typeORM操作Mysql資料庫
mac中,直接使用 brew install mysql
安裝mysql,然後啟動服務 brew services start mysql
檢視服務已經啟動 ps aux | grep mysql
Nest 操作Mysql官方文件:http://docs.nestjs.com/techniques/database
npm install --save @nestjs/typeorm typeorm mysql
配置資料庫連線地址
// src/config/typeorm.ts
const { MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE } =
process.env;
const config = {
type: 'mysql',
host: MYSQL_HOST,
port: MYSQL_PORT,
username: MYSQL_USER,
password: MYSQL_PASSWORD,
database: MYSQL_DATABASE,
synchronize: process.env.NODE_ENV !== 'production', // 生產環境不要開啟
autoLoadEntities: true, // 如果為true,將自動載入實體(預設:false)
keepConnectionAlive: true, // 如果為true,在應用程式關閉後連線不會關閉(預設:false)
retryDelay: 3000, // 兩次重試連線的間隔(ms)(預設:3000)
retryAttempts: 10, // 重試連線資料庫的次數(預設:10)
dateStrings: 'DATETIME', // 轉化為時間
timezone: '+0800', // +HHMM -HHMM
// 自動需要匯入模型
entities: ['dist/**/*.entity{.ts,.js}'],
};
export default config;
// app.module.ts中配置
import { resolve, join } from 'path';
import { ConfigModule, ConfigService } from 'nestjs-config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
// 載入配置檔案目錄
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
// 連線mysql資料庫
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('typeorm'),
inject: [ConfigService],
}),
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {}
配置實體entity
// photo.entity.ts
import {
Column,
Entity,
ManyToMany,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { PostsEntity } from './post.entity';
@Entity('photo')
export class PhotoEntity {
// @PrimaryGeneratedColumn()
// id: number; // 標記為主列,值自動生成
@PrimaryGeneratedColumn('uuid')
id: string; // 該值將使用uuid自動生成
@Column({ length: 50 })
url: string;
// 多對一關係,多個圖片對應一篇文章
@ManyToMany(() => PostsEntity, (post) => post.photos)
posts: PostsEntity;
}
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { PhotoEntity } from './photo.entity';
export type UserRoleType = 'admin' | 'editor' | 'ghost';
export type postStatus = 1 | 2 | 3;
// mysql的列型別: type
/**
* int, tinyint, smallint, mediumint, bigint, float, double, dec, decimal,
* numeric, date, datetime, timestamp, time, year, char, varchar, nvarchar,
* text, tinytext, mediumtext, blob, longtext, tinyblob, mediumblob, longblob, enum,
* json, binary, geometry, point, linestring, polygon, multipoint, multilinestring,
* multipolygon, geometrycollection
*/
/**
* ColumnOptions中可用選項列表:
* length: number - 列型別的長度。 例如,如果要建立varchar(150)型別,請指定列型別和長度選項。
width: number - 列型別的顯示範圍。 僅用於MySQL integer types(opens new window)
onUpdate: string - ON UPDATE觸發器。 僅用於 MySQL (opens new window).
nullable: boolean - 在資料庫中使列NULL或NOT NULL。 預設情況下,列是nullable:false。
update: boolean - 指示"save"操作是否更新列值。如果為false,則只能在第一次插入物件時編寫該值。 預設值為"true"。
select: boolean - 定義在進行查詢時是否預設隱藏此列。 設定為false時,列資料不會顯示標準查詢。 預設情況下,列是select:true
default: string - 新增資料庫級列的DEFAULT值。
primary: boolean - 將列標記為主要列。 使用方式和@ PrimaryColumn相同。
unique: boolean - 將列標記為唯一列(建立唯一約束)。
comment: string - 資料庫列備註,並非所有資料庫型別都支援。
precision: number - 十進位制(精確數字)列的精度(僅適用於十進位制列),這是為值儲存的最大位數。僅用於某些列型別。
scale: number - 十進位制(精確數字)列的比例(僅適用於十進位制列),表示小數點右側的位數,且不得大於精度。 僅用於某些列型別。
zerofill: boolean - 將ZEROFILL屬性設定為數字列。 僅在 MySQL 中使用。 如果是true,MySQL 會自動將UNSIGNED屬性新增到此列。
unsigned: boolean - 將UNSIGNED屬性設定為數字列。 僅在 MySQL 中使用。
charset: string - 定義列字符集。 並非所有資料庫型別都支援。
collation: string - 定義列排序規則。
enum: string[]|AnyEnum - 在enum列型別中使用,以指定允許的列舉值列表。 你也可以指定陣列或指定列舉類。
asExpression: string - 生成的列表達式。 僅在MySQL (opens new window)中使用。
generatedType: "VIRTUAL"|"STORED" - 生成的列型別。 僅在MySQL (opens new window)中使用。
hstoreType: "object"|"string" -返回HSTORE列型別。 以字串或物件的形式返回值。 僅在Postgres中使用。
array: boolean - 用於可以是陣列的 postgres 列型別(例如 int [])
transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType } - 用於將任意型別EntityType的屬性編組為資料庫支援的型別DatabaseType。
注意:大多數列選項都是特定於 RDBMS 的,並且在MongoDB中不可用
*/
@Entity('posts')
export class PostsEntity {
// @PrimaryGeneratedColumn()
// id: number; // 標記為主列,值自動生成
@PrimaryGeneratedColumn('uuid')
id: string; // 該值將使用uuid自動生成
@Column({ length: 50 })
title: string;
@Column({ length: 18 })
author: string;
@Column({ type: 'longtext', default: null })
content: string;
@Column({ default: null })
cover_url: string;
@Column({ default: 0 })
type: number;
@Column({ type: 'text', default: null })
remark: string;
@Column({
type: 'enum',
enum: [1, 2, 3],
default: 1,
})
status: postStatus;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
create_time: Date;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
})
update_time: Date;
@Column({
type: 'enum',
enum: ['admin', 'editor', 'ghost'],
default: 'ghost',
select: false, // 定義在進行查詢時是否預設隱藏此列
})
role: UserRoleType;
// 一對多關係,一篇文章對應多個圖片
// 在service中查詢使用 .find({relations: ['photos]}) 查詢文章對應的圖片
@OneToMany(() => PhotoEntity, (photo) => photo.posts)
photos: [];
}
引數校驗
Nest 與 class-validator 配合得很好。這個優秀的庫允許您使用基於裝飾器的驗證。裝飾器的功能非常強大,尤其是與 Nest 的 Pipe 功能相結合使用時,因為我們可以通過訪問 metatype
資訊做很多事情,在開始之前需要安裝一些依賴。
npm i --save class-validator class-transformer
// posts.dto.ts
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreatePostDto {
@IsNotEmpty({ message: '文章標題必填' })
readonly title: string;
@IsNotEmpty({ message: '缺少作者資訊' })
readonly author: string;
readonly content: string;
readonly cover_url: string;
@IsNotEmpty({ message: '缺少文章型別' })
readonly type: number;
readonly remark: string;
}
在控制器對應的Module中配置Model
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { PostsEntity } from './entities/post.entity';
@Module({
imports: [TypeOrmModule.forFeature([PostsEntity])],
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}
在服務裡面使用@InjectRepository獲取資料庫Model實現操作資料庫
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, Between, Equal, Like, In } from 'typeorm';
import * as dayjs from 'dayjs';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostsEntity } from './entities/post.entity';
import { PostsRo } from './interfaces/posts.interface';
@Injectable()
export class PostsService {
constructor(
@InjectRepository(PostsEntity)
private readonly postsRepository: Repository<PostsEntity>,
) {}
async create(post: CreatePostDto) {
const { title } = post;
const doc = await this.postsRepository.findOne({ where: { title } });
console.log('doc', doc);
if (doc) {
throw new HttpException('文章標題已存在', HttpStatus.BAD_REQUEST);
}
return {
data: await this.postsRepository.save(post),
message: '建立成功',
};
}
// 分頁查詢列表
async findAll(query = {} as any) {
// eslint-disable-next-line prefer-const
let { pageSize, pageNum, orderBy, sort, ...params } = query;
orderBy = query.orderBy || 'create_time';
sort = query.sort || 'DESC';
pageSize = Number(query.pageSize || 10);
pageNum = Number(query.pageNum || 1);
console.log('query', query);
const queryParams = {} as any;
Object.keys(params).forEach((key) => {
if (params[key]) {
queryParams[key] = Like(`%${params[key]}%`); // 所有欄位支援模糊查詢、%%之間不能有空格
}
});
const qb = await this.postsRepository.createQueryBuilder('post');
// qb.where({ status: In([2, 3]) });
qb.where(queryParams);
// qb.select(['post.title', 'post.content']); // 查詢部分欄位返回
qb.orderBy(`post.${orderBy}`, sort);
qb.skip(pageSize * (pageNum - 1));
qb.take(pageSize);
return {
list: await qb.getMany(),
totalNum: await qb.getCount(), // 按條件查詢的數量
total: await this.postsRepository.count(), // 總的數量
pageSize,
pageNum,
};
}
// 根據ID查詢詳情
async findById(id: string): Promise<PostsEntity> {
return await this.postsRepository.findOne({ where: { id } });
}
// 更新
async update(id: string, updatePostDto: UpdatePostDto) {
const existRecord = await this.postsRepository.findOne({ where: { id } });
if (!existRecord) {
throw new HttpException(`id為${id}的文章不存在`, HttpStatus.BAD_REQUEST);
}
// updatePostDto覆蓋existRecord 合併,可以更新單個欄位
const updatePost = this.postsRepository.merge(existRecord, {
...updatePostDto,
update_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
});
return {
data: await this.postsRepository.save(updatePost),
message: '更新成功',
};
}
// 刪除
async remove(id: string) {
const existPost = await this.postsRepository.findOne({ where: { id } });
if (!existPost) {
throw new HttpException(`文章ID ${id} 不存在`, HttpStatus.BAD_REQUEST);
}
await this.postsRepository.remove(existPost);
return {
data: { id },
message: '刪除成功',
};
}
}
nest連線Redis
Redis 字串資料型別的相關命令用於管理 redis 字串值
-
檢視所有的key:
keys *
-
普通設定:
set key value
-
設定並加過期時間:
set key value EX 30
表示30秒後過期 -
獲取資料:
get key
-
刪除指定資料:
del key
-
刪除全部資料:
flushall
-
檢視型別:
type key
-
設定過期時間:
expire key 20
表示指定的key5
秒後過期
Redis列表是簡單的字串列表,按照插入順序排序。你可以新增一個元素到列表的頭部(左邊)或者尾部(右邊)
-
列表右側增加值:
rpush key value
-
列表左側增加值:
lpush key value
-
右側刪除值:
rpop key
-
左側刪除值:
lpop key
-
獲取資料:
lrange key
-
刪除指定資料:
del key
-
刪除全部資料:
flushall
-
檢視型別:
type key
Redis 的 Set 是 String 型別的無序集合。集合成員是唯一的,這就意味著集合中不能出現重複的資料。它和列表的最主要區別就是沒法增加重複值
-
給集合增資料:
sadd key value
-
刪除集合中的一個值:
srem key value
-
獲取資料:
smembers key
-
刪除指定資料:
del key
-
刪除全部資料:
flushall
Redis hash 是一個string型別的field和value的對映表,hash特別適合用於儲存物件。
-
設定值hmset :
hmset zhangsan name "張三" age 20 sex “男”
-
設定值hset :
hset zhangsan name "張三"
-
獲取資料:
hgetall key
-
刪除指定資料:
del key
-
刪除全部資料:
flushall
Redis 釋出訂閱(pub/sub)是一種訊息通訊模式:傳送者(pub)傳送訊息,訂閱者(sub)接收訊息
// 釋出
client.publish('publish', 'message from publish.js');
// 訂閱
client.subscribe('publish');
client.on('message', function(channel, msg){
console.log('client.on message, channel:', channel, ' message:', msg);
});
Nestjs中使用redis
Nestjs Redis 官方文件:http://github.com/kyknow/nestjs-redis
npm install nestjs-redis --save
如果是nest8需要注意該問題:http://github.com/skunight/nestjs-redis/issues/82
// app.modules.ts
import { RedisModule } from 'nestjs-redis';
import { RedisTestModule } from '../examples/redis-test/redis-test.module';
@Module({
imports: [
// 載入配置檔案目錄
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
// redis連線
RedisModule.forRootAsync({
useFactory: (configService: ConfigService) => configService.get('redis'),
inject: [ConfigService],
}),
RedisTestModule,
],
controllers: [],
providers: [ ],
})
export class AppModule implements NestModule {}
// src/config/redis.ts 配置
export default {
host: '127.0.0.1',
port: 6379,
db: 0,
password: '',
keyPrefix: '',
onClientReady: (client) => {
client.on('error', (err) => {
console.log('-----redis error-----', err);
});
},
};
建立一個cache.service.ts 服務 封裝操作redis的方法
// src/common/cache.service.ts
import { Injectable } from '@nestjs/common';
import { RedisService } from 'nestjs-redis';
@Injectable()
export class CacheService {
public client;
constructor(private redisService: RedisService) {
this.getClient();
}
async getClient() {
this.client = await this.redisService.getClient();
}
//設定值的方法
async set(key: string, value: any, seconds?: number) {
value = JSON.stringify(value);
if (!this.client) {
await this.getClient();
}
if (!seconds) {
await this.client.set(key, value);
} else {
await this.client.set(key, value, 'EX', seconds);
}
}
//獲取值的方法
async get(key: string) {
if (!this.client) {
await this.getClient();
}
const data = await this.client.get(key);
if (!data) return;
return JSON.parse(data);
}
// 根據key刪除redis快取資料
async del(key: string): Promise<any> {
if (!this.client) {
await this.getClient();
}
await this.client.del(key);
}
// 清空redis的快取
async flushall(): Promise<any> {
if (!this.client) {
await this.getClient();
}
await this.client.flushall();
}
}
使用redis服務
redis-test.controller
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { CacheService } from 'src/common/cache/redis.service';
@Controller('redis-test')
export class RedisTestController {
// 注入redis服務
constructor(private readonly cacheService: CacheService) {}
@Get('get')
async get(@Query() query) {
return await this.cacheService.get(query.key);
}
@Post('set')
async set(@Body() body) {
const { key, ...params } = body as any;
return await this.cacheService.set(key, params);
}
@Get('del')
async del(@Query() query) {
return await this.cacheService.del(query.key);
}
@Get('delAll')
async delAll() {
return await this.cacheService.flushall();
}
}
redis-test.module.ts
import { Module } from '@nestjs/common';
import { RedisTestService } from './redis-test.service';
import { RedisTestController } from './redis-test.controller';
import { CacheService } from 'src/common/cache/redis.service';
@Module({
controllers: [RedisTestController],
providers: [RedisTestService, CacheService], // 注入redis服務
})
export class RedisTestModule {}
redis-test.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class RedisTestService {}
寫在最後
歡迎新增poetry 微信poetries4070 , 我會第一時間和你分享前端行業趨勢,學習途徑,成長內容等,2022年陪你一起度過!
如果你覺得這篇內容對你有幫助,我想請你幫我3個小忙:
1. 訂閱前端面試寶典 interview2.poetries.top 讓我們成為長期關係,共同成長。
2. 訂閱程式設計導航 nav.poetries.top 整理了大量免費的程式設計學習資源。
3. 領取精心為您整理的前端面試資料 。
>>前端面試求職刷題神器<<
- Vue3 生命週期Hooks函式與排程器Scheduler的原理
- 一文帶你入門Nestjs
- Web Worker 現狀
- 寫給前端的 K8S 上手指南
- 從 0 到 1 落地前端程式碼檢測工具
- React Hooks 使用誤區,駁官方文件
- 迄今為止最全的前端監控體系搭建篇
- 你知道如何提升JSON.stringify()的效能嗎?
- 你知道的前端優化手段
- 用 Three.js 擼一個 3D 中國地圖
- 肝了兩週,做了一個前端面試刷題小程式
- 44道JS難題,做對一半就是高手
- 臥槽!用程式碼實現冰墩墩,太浪漫了吧
- 我寫 CSS 的常用套路
- 位元組面試被虐後,是時候搞懂 DNS 了
- 2022年你還不會serverless?看看這篇保姆級教程(上)
- Vue 元件通訊方式居然有這麼多?你瞭解幾種
- 38道關於this的面試題,萬字解析,讓你徹底理解 this 的指向問題
- vite vue3 ts 搭建通用後臺管理系統
- Vue 前端程式碼風格指南總結