NestJS從入門到跑路

什么是NestJS

Nest 是一個漸進的 Node.js 框架,可以在 TypeScript 和 JavaScript (ES6、ES7、ES8)之上構 建高效、可伸縮的企業級服務器端應用程序。

Nest 基于 TypeScript 編寫并且結合了 OOP(面向對象編程),FP(函數式編程)和 FRP (函數式響應編程)的相關理念。在設計上的很多靈感來自于 Angular,Angular 的很多模 式又來自于 Java 中的 Spring 框架,依賴注入、面向切面編程等,所以我們也可以認為: Nest 是 Node.js 版的 Spring 框架。

Nest 框架底層 HTTP 平臺默認是基于 Express 實現的,所以無需擔心第三方庫的缺失。 Nest 旨在成為一個與平臺無關的框架。 通過平臺,可以創建可重用的邏輯部件,開發人員可以利用這些部件來跨越多種不同類型的應用程序。 從技術上講,Nest 可以在創建適配器 后使用任何 Node HTTP 框架。 有兩個支持開箱即用的 HTTP 平臺:express 和 fastify。 您 可以選擇最適合您需求的產品。

NestJs 的核心思想:就是提供了一個層與層直接的耦合度極小,抽象化極高的一個架構 體系。

官網:https://nestjs.com/

中文網站:https://docs.nestjs.cn/

GitHub: https://github.com/nestjs/nest

1.png

圖一只是說明規范的模塊方式,實際上,可以只有根模塊,也可以劃分多個模塊,互相依賴,只要不是循環引入就行。

Nestjs 的特性

  • 依賴注入容器
  • 模塊化封裝
  • 可測試性
  • 內置支持 TypeScript
  • 可基于 Express 或者 fastify

腳手架nest-cli

安裝

npm i -g @nestjs/cli 或者 cnpm i -g @nestjs/cli 或者 yarn global add @nestjs/cli

創建

nest new nestdemo

相關指令

  • nest new 名稱 創建項目
  • nest -h/--help 幫助
  • nest g co 名稱 創建控制器
  • nest g s 名稱 創建服務
  • nest g mi 名稱 創建中間件
  • nest g pi 名稱 創建管道
  • nest g mo 名稱 創建模塊
  • nest g gu 名稱 創建守衛

創建類型指令都可以指定文件路徑,而且路徑全部是再src目錄下面,例如:
nest g co /aaa/bbb/user 則在src下面就會存在一個三級目錄,user的目錄下
有一個以user命名大寫的控制器 UserController.ts文件

注意:凡是以腳手架創建的模塊,控制器等等都會自動添加到對應配置位置,不需要手動配置

控制器

Nest 中的控制器層負責處理傳入的請求, 并返回對客戶端的響應。

import { Controller, Get } from '@nestjs/common';
@Controller('article')
export class ArticleController { 
    @Get() 
    index(): string { 
        return '這是 article 里面的 index'; 
    } 
    @Get('add') 
    add(): string { 
        return '這是 article 里面的 index'; 
    } 
}

關于 nest 的 return: 當請求處理程序返回 JavaScript 對象或數組時,它將自動序列化為 JSON。但是,當它返回一個字符串時,Nest 將只發送一個字符串而不是序列化它。這使響應處理變得簡單:只需要返回值,Nest 負責其余部分。

Get Post通過方法參數裝飾器獲取傳值

  1. 基本栗子

nestjs 內置裝飾器的時候必須得在@nestjs/common 模塊下面引入對應的裝飾器

import { Controller, Get, Post } from '@nestjs/common'; 
@Controller('cats') 
export class CatsController { 
    @Post() 
    create(): string { 
        return 'This action adds a new cat'; 
    } 
    @Get() 
    findAll(): string { 
        return 'This action returns all cats'; 
    } 
}

Nestjs 也提供了其他 HTTP 請求方法的裝飾器 @Put() 、@Delete()、@Patch()、 @Options()、 @Head()和 @All()

  1. Nest中獲取請求參數

在 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]
import { Controller, Get, Post,Query,Body } from '@nestjs/common'; 
@Controller('news') 
export class NewsController { 
    @Get() 
    getAbout(@Query() query): string { 
        console.log(query); 
        //這里獲取的就是所有的 Get 傳值 
        return '這是 about'
    }

    //針對參數是 localhost:3000/news/list?id=zq&age=12
    @Get('list') 
    getNews(@Query('id') id):string { 
        console.log(id); 
        //這里獲取的就是 Get 傳值里面的 Id 的值 
        //如果@Query()則是整個id=zq&age=12的對象
        return '這是新聞' 
    }
    @Post('doAdd') 
    async addNews(@Body() newsData){ 
        console.log(newsData); 
        return '增加新聞’'
    } 
}
  1. 動態路由
// 針對的參數是 /name/id這種類型,例如/name/1
@Get(':id') 
findOne(@Param() params): string { 
    console.log(params.id); 
    return `This action returns a #${params.id} cat`; 
}

補充: @Param() 裝飾器訪問以這種方式聲明的路由參數,該裝飾器應添 加到函數簽名中。@Param可以用在get或者post,但是都是針對 localhost:3000/news/list?id=zq&age=12這種才可以獲取,而針對body內部都是獲取不到的,可以使用@Body

  1. 綜合案例
@Controller('news')
export class NewsController {
    //依賴注入
    constructor(private readonly newsService:NewsService){}
    @Get('pip')
    @UsePipes(new NewsPipe(useSchema))
    indexPip(@Query() info){
        // console.log(info);
        return info;
    }
    
    @Get()
    @Render('default/news')
    index(){
      return  {
          newsList:this.newsService.findAll()
      }
    }

    /**
     * 路由順序:如果此時訪問http://localhost:3000/news/add
     * 則會正確執行,如果把add移動到:id下面,則只會執行:id的
     */
    @Get('add')
    addData(@Query('id') id){
        return id+'------';
    }

    //同理,這個模糊匹配如果移動到:id下面,訪問http://localhost:3000/news/aaa
    //也會只匹配:id的路由
    @Get('a*a')
    indexA(){
        return '模糊匹配';
    }

    //動態路由  /add/1
    @Get(':id')
    indexB(@Param('id') id){
        return id;
    }
}

Swagger集成

中文文檔地址:https://docs.nestjs.cn/6/recipes?id=openapi-swagger

  • 安裝

npm install --save @nestjs/swagger swagger-ui-express

如果你正在使用fastify,你必須安裝 fastify-swagger 而不是 swagger-ui-express

npm install --save @nestjs/swagger fastify-swagger

  • 引導
    • main.ts
    • 引入:import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    • 編碼
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    
    async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        const options = new DocumentBuilder()
            .setTitle('Cats example')
            .setDescription('The cats API description')
            .setVersion('1.0')
            //下面兩者結合組成請求基本路徑
            .setHost('http://www.baidu.com')
            .setBasePath('/api')
            // .addTag('cats') 分類
            .build();
    const document = SwaggerModule.createDocument(app, options);
    //指定文檔路徑
    SwaggerModule.setup('api-docs', app, document);
    await app.listen(3000);
    }
    bootstrap();
    
  • 打開http://localhost:3000/api-docs/#/,如下圖二
    圖二.png

swagger基本使用

  • 創建Dto
import { ApiModelProperty } from '@nestjs/swagger';
export class CreatePostDto{
  @ApiModelProperty({description:"應用名稱",example:'示例值'})
  title:string
  @ApiModelProperty({description:"應用內容"})
  content:string
}
  • @ApiModelProperty() 裝飾器接受選項對象
export const ApiModelProperty: (metadata?: {
  description?: string;
  required?: boolean;  //代表是否必須存在該參數
  type?: any;
  isArray?: boolean;
  collectionFormat?: string;
  default?: any;
  enum?: SwaggerEnumType;
  format?: string;
  multipleOf?: number;
  maximum?: number;
  exclusiveMaximum?: number;
  minimum?: number;
  exclusiveMinimum?: number;
  maxLength?: number;
  minLength?: number;
  pattern?: string;
  maxItems?: number;
  minItems?: number;
  uniqueItems?: boolean;
  maxProperties?: number;
  minProperties?: number;
  readOnly?: boolean;
  xml?: any;
  example?: any;
}) => PropertyDecorator;
  • 完整例子

只是入門,更多例子是使用規則查看文檔

import { Controller, Get, Post, Body, Query, Param } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiUseTags, ApiOperation, ApiModelProperty } from '@nestjs/swagger';

class CreatePostDto{
  //默認required都是true,Model上面會有個紅色星號,代表必須填寫,
  //但是實際上swagger本身不會限制,只是告知作用,swagger本身請求正常
  @ApiModelProperty({description:"應用名稱",example:'示例值',maxLength:1,required:false})
  title:string
  @ApiModelProperty({description:"應用內容"})
  content:string
}

@Controller('app')
@ApiUseTags('默認標簽')//其實是大分類
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('hello')
  @ApiOperation({title:"顯示hello"}) //api的title描述/注釋
  getHello(@Query() query,@Param() params): any[] {
    return this.appService.getHello();
  }

  @Post('create')
  @ApiOperation({title:"創建應用"})
  createApp(@Body() body:CreatePostDto): CreatePostDto{
    return body;
  }

  @Get(':id')
  @ApiOperation({title:'應用詳情'})
  detail(@Param('id') id:number){
      return{
        id
      }
  }
}

總結:swagger本身注解只是告知作用,不同于graphql,例如required本身是沒什么作用只是告知使用者需要填寫,但是實際上需要與否還是程序控制;同理,不論填寫不填寫swagger都會進行請求,最終結果以邏輯控制為準。

參數驗證

  1. 安裝

npm i class-validator class-transformer --save

  1. 啟用全局管道
    • main.ts中

app.useGlobalPipes(new ValidationPipe())

  1. 導包
    • 在需要使用參數校驗的文件內導入

import { IsNotEmpty } from 'class-validator'

class CreatePostDto{
  @ApiModelProperty({description:"應用名稱",example:'示例值'})
  //**此處就是**
  @IsNotEmpty({message:'我是沒填寫title屬性的時候,返回的給前端的錯誤信息'})
  title:string
  @ApiModelProperty({description:"應用內容"})
  content:string
}

說明: Nest 自帶兩個開箱即用的管道,即 ==ValidationPipe====ParseIntPipe==

補充:ParseIntPipe簡單使用

可把id自動轉換成Int類型

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

總結: 內置管道都支持全局,方法,參數三種級別的使用
文檔連接

靜態資源

官方文檔:https://docs.nestjs.com/techniques/mvc

app.useStaticAssets('public');

  • 栗子
async function bootstrap() {
  //指定平臺
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  //配置靜態資源
  //app.useStaticAssets(join(__dirname,'..','public'))
  
  //上面是直接訪問http://localhost:3000/a.png
  //下面可以設置虛擬目錄http://localhost:3000/public/a.png
  // app.useStaticAssets(join(__dirname,'..','public'),{
  //   prefix:'/public/'
  // })

  //如下方式也可以,因為默認nest會去尋找根目錄下面的參數一文件夾
  app.useStaticAssets('public',{
    prefix:'/public/'  
  })
  await app.listen(3000);
}

注意:NestFactory.create<NestExpressApplication>(AppModule);指定了范型其實就是Nest的平臺,默認使用的是express的平臺,因為靜態資源涉及平臺的選擇所以必須指定了。

模板引擎

官方文檔:https://docs.nestjs.com/techniques/mvc

  • 安裝

cnpm i ejs --save

  • 配置
app.setBaseViewsDir(join(__dirname, '..', 'views')) // 放視圖的文件
app.setViewEngine('ejs');
  • 完整代碼
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
//靜態資源中間件依賴于具體平臺,所以可以先引入express
import { NestExpressApplication } from '@nestjs/platform-express';
// import { join } from 'path';

//此時下面如果使用path則就是path.join
// import * as  path from 'path';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useStaticAssets('public',{
    prefix:'/public/'
  })

  //配置模板引擎,需要先安裝模板引擎
  // app.setBaseViewsDir(join(__dirname,'..','views'))
  app.setBaseViewsDir('views');
  app.setViewEngine('ejs');
  await app.listen(3000);
}
bootstrap();

==注意此處引入path的方式==

  • 渲染頁面
@Controller('user')
export class UserController {
    @Get()
    @Render('default/user')
    index(){
        //注意一般有render的路由則return值都是給模板引擎使用的
        //所以nest會判斷,一般都是對象,返回字符串會報錯
        // return '用戶中心';
        //此處是字符串key還是直接命名都可以被ejs搜索到
        // return {"name":"zs",age:12}
    }
}

說明:default指的是views下面的default文件夾內部的user模板

重定向

import { Controller, Get, Post, Body,Response, Render} from '@nestjs/common';
 @Controller('user') 
 export class UserController { 
     @Get() 
     @Render('default/user') 
     index(){ 
         return {"name":"張三"}; 
    }
    @Post('doAdd') 
    doAdd(@Body() body,@Response() res){
         console.log(body); 
         res.redirect('/user'); //路由跳轉 
    } 
}

提供者

幾乎所有的東西都可以被認為是提供者 - service, repository, factory, helper 等等。他們都可以通過 constructor注入依賴關系,也就是說,他們可以創建各種關系。但事實上,提供者不過是一個用@Injectable() 裝飾器注解的類。

export interface Cat {
  name: string;
  age: number;
  breed: string;
}
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
    //依賴注入
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

cookie

  • 安裝

cnpm instlal cookie-parser --save

  • 在 main.ts 中引入 cookie-parser

import * as cookieParser from 'cookie-parser'

  • 在 main.ts 配置中間件

app.use(cookieParser());

  • 設置 cookie

res.cookie("name",'zhangsan',{maxAge: 900000, httpOnly: true});

  • 獲取 Cookies
@Get('getCookies') 
getCookies(@Request() req){ 
    return req.cookies.name; 
}

cookie參數說明

屬性 說明
domain 域名
expires 過 期 時 間 ( 秒 ) , 在 設 置 的 某 個 時 間 點 后 該 Cookie 就 會 失 效 , 如 expires=Wednesday, 09-Nov-99 23:12:40 GMT
maxAge 最大失效時間(毫秒),設置在多少后失效
secure 當 secure 值為 true 時,cookie 在 HTTP 中是無效,在 HTTPS 中才有效
path 表示 cookie 影響到的路,如 path=/。如果路徑不能匹配時,瀏覽器則不發送這 個 Cookie
httpOnly 是微軟對 COOKIE 做的擴展。如果在 COOKIE 中設置了“httpOnly”屬性,則通 過程序(JS 腳本、applet 等)將無法讀取到 COOKIE 信息,防止 XSS 攻擊產生
signed 表 示 是 否 簽 名 cookie, 設 為 true 會 對 這 個 cookie 簽 名 , 這 樣 就 需 要 用 res.signedCookies 而不是 res.cookies 訪問它。被篡改的簽名 cookie 會被服務器拒絕,并且 cookie 值會重置為它的原始值,說白了==加密==

相關代碼

  • 設置 cookie
res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
res.cookie('name', 'tobi', { domain: '.example.com', path: '/admin', secure: true }); 
res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
  • 獲取cookie

req.cookies.name

  • 刪除 cookie
res.cookie('rememberme', '', { expires: new Date(0)});
res.cookie('username','zhangsan',{domain:'.ccc.com',maxAge:0,httpOnly:true});

加密cookie

  1. 配置中間件的時候需要傳參

app.use(cookieParser('123456'));

  1. 設置 cookie 的時候配置 signed 屬性

res.cookie('userinfo','hahaha',{domain:'.ccc.com',maxAge:900000,httpOnly:true,signed:true});

  1. signedCookies 調用設置的 cookie

console.log(req.signedCookies);
說明:加密的cookie使用3這種方式獲取

session

  • 安裝

cnpm install express-session --save

  • 導入

import * as session from 'express-session';

  • 設置中間價

app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))

  • 使用
設置值 req.session.username = "張三"; 
獲取值 req.session.username
  • 常用參數
app.use(session({ 
secret: '12345', 
name: 'name',
cookie: {maxAge: 60000}, 
resave: false, 
saveUninitialized: true 
}));
  • 參數說明
屬性 說明
secret 一個 String 類型的字符串,作為服務器端生成 session 的簽名
name 返回客戶端的 key 的名稱,默認為 connect.sid,也可以自己設置
resave 強制保存 session 即使它并沒有變化,。默認為 true。建議設置成
saveUninitialized 強制將未初始化的 session 存儲。當新建了一個 session 且未設定屬性或值時,它就處于 未初始化狀態。在設定一個 cookie 前,這對于登陸驗證,減輕服務端存儲壓力,權限控制是有幫助的。(默 認:true)。建議手動添加。
cookie 設置返回到前端 key 的屬性,默認值為{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }。
rolling 在每次請求時強行設置 cookie,這將重置 cookie 過期時間(默認:false)
  • 常用方法
req.session.destroy(function(err) { /*銷毀 session*/ })
req.session.username='張三'; //設置 
session req.session.username //獲取 
session req.session.cookie.maxAge=0; //重新設置 cookie 的過期時間

上傳

官方文檔:https://docs.nestjs.com/techniques/file-upload

單文件上傳

import { Controller, Get, Render, Post, Body, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { createWriteStream } from 'fs';
import { join } from 'path';

@Controller('upload')
export class UploadController {
    @Post('doAdd')
    @UseInterceptors(FileInterceptor('pic')) //pic對應 <input type="file" name="pic" id="">
    doAdd(@Body() body,@UploadedFile() file){
        // console.log(body);
        // console.log(file);
        let cws=createWriteStream(join(__dirname,'../../public/upload/',`${Date.now()}---${file.originalname}`))
        cws.write(file.buffer);
        return '上傳圖片成功';
    }
}
 <form action="upload/doAdd" method="post" enctype="multipart/form-data">
        <input type="text" name="title" placeholder="新聞標題">
        <br>
        <br>
        <input type="file" name="pic" id="">
        <br>
        <input type="submit" value="提交">
</form>

說明:注意enctype="multipart/form-data"屬性必須添加

多文件上傳

import { Controller, UseInterceptors, Get, Post, Render, Body, UploadedFiles } from '@nestjs/common';
import { createWriteStream } from 'fs';
import { FilesInterceptor } from '@nestjs/platform-express';
import { join } from 'path';
@Controller('uploadmany')
export class UploadmanyController {
    @Post('doAdd')
    //注意此處是FileFieldsInterceptor代表多文件的name不同的攔截器
    // @UseInterceptors(FileFieldsInterceptor([
    //     { name: 'pic1', maxCount: 1 },
    //     { name: 'pic2', maxCount: 1 }
    // ]))

    //注意此處是FilesInterceptor而上面是FileFieldsInterceptor
    @UseInterceptors(FilesInterceptor('pic')) //多個文件name屬性相同的情況下
    doAdd(@Body() body, @UploadedFiles() files) {
        for (const file of files) {
            let cws = createWriteStream(join(__dirname, '../../public/upload/', `${Date.now()}---${file.originalname}`))
            cws.write(file.buffer);
        }
        return '上傳多個文件成功';
    }
}

注意:此處html中的name相同和不同使用的處理方式不同

    <!-- 注意此處上傳文件的enctype -->
    <form action="uploadmany/doAdd" method="post" enctype="multipart/form-data">
        <input type="text" name="title" placeholder="新聞標題">
        <br>
        <br>
        <input type="file" name="pic" id="">
        <input type="file" name="pic" id="">
        <br>
        <input type="submit" value="提交">
    </form>

中間價

中間件就是匹配路由之前或者匹配路由完成做的一系列的操作。中間件中如果想往下 匹配的話,需要寫 next()

中間件任務

  • 執行任何代碼。
  • 對請求和響應對象進行更改。
  • 結束請求-響應周期。
  • 調用堆棧中的下一個中間件函數。
  • 如果當前的中間件函數沒有結束請求-響應周期, 它必須調用 next() 將控制傳遞給下一個中間 件函數。否則, 請求將被掛起。

Nest 中間件可以是一個函數,也可以是一個帶有@Injectable()裝飾器的類

使用中間件

  1. 創建
nest g middleware init

import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class InitMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    console.log(Date());
    next();
  }
}
  1. 配置中間件

在 app.module.ts 中繼承 NestModule 然后配置中間件

export class AppModule implements NestModule{
  configure(consumer:MiddlewareConsumer) {

    /**
     * 中間件相關
     */

    //寫*表示匹配所有路由
    // consumer.apply(InitMiddleware).forRoutes('*');
    //匹配指定路由
    // consumer.apply(InitMiddleware).forRoutes('news');
    //直接傳入控制器:不推薦
    // consumer.apply(InitMiddleware).forRoutes(NewsController);
    // consumer.apply(InitMiddleware).forRoutes({path:'ab*cd',method:RequestMethod.ALL});
    
    //所有路由都匹配InitMiddleware但是UserMiddleware不光匹配InitMiddleware
    //還匹配UserMiddleware
    // consumer.apply(InitMiddleware,logger).forRoutes('*')
    // .apply(UserMiddleware).forRoutes('user')
    
    //都可以添加多個:代表user/news都可以匹配InitMiddleware,UserMiddleware
    // consumer.apply(InitMiddleware,UserMiddleware).
    // forRoutes({path:'user',method:RequestMethod.ALL},{path:'news',method:RequestMethod.ALL})
  }
}
  1. 多個中間件

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

  1. 函數式中間件
export function logger(req, res, next) { 
    console.log(`Request...`); 
    next();
};
  1. 全局中間件
//全局中間件只能引入函數式中間件,引入類中間件會報錯
import { logger} from './middleware/logger.middleware'
const app = await NestFactory.create(ApplicationModule); 
app.use(logger); 
await app.listen(3000);

全局中間件只能使用函數式中間件

管道

Nestjs 中的管道可以將輸入數據轉換為所需的輸出。此外,它也可以處理驗證, 當數據不正確時可能會拋出異常。

  1. 創建
nest g pipe news

import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException, HttpStatus } from '@nestjs/common';
import * as Joi from '@hapi/joi';
@Injectable()
export class NewsPipe implements PipeTransform {
  // constructor(private readonly schema:Joi.Schema){
  // }
  constructor(private readonly schema:Joi.Schema){}

  transform(value: any, metadata: ArgumentMetadata) {
    // console.log(value); //value一般都是get/post等請求傳遞過來的值
    // value.age=30;  如果這樣修改之后,控制器里面的數據就變了


    const {error}=this.schema.validate(value);
    if (error) {
      // throw new BadRequestException('Validate failed')
      return error;
    }
    return value;
  }
}
  1. 使用
import { Controller,Get, Param, Query, Render, UsePipes } from '@nestjs/common';
import { NewsService } from './news.service';
import { NewsPipe } from '../pipe/news.pipe';
import * as Joi from '@hapi/joi';

let useSchema:Joi.Schema=Joi.object().keys({
    name:Joi.string().required(),
    age:Joi.number().integer().min(6).max(66).required()
});

@Controller('news')
export class NewsController {
    //依賴注入
    constructor(private readonly newsService:NewsService){}
    @Get('pip')
    @UsePipes(new NewsPipe(useSchema))
    indexPip(@Query() info){
        // console.log(info);
        return info;
    }
    
    
    @Get()
    @Render('default/news')
    index(){
      return  {
          newsList:this.newsService.findAll()
      }
    }

    /**
     * 路由順序:如果此時訪問http://localhost:3000/news/add
     * 則會正確執行,如果把add移動到:id下面,則只會執行:id的
     */
    @Get('add')
    addData(@Query('id') id){
        return id+'------';
    }

    //同理,這個模糊匹配如果移動到:id下面,訪問http://localhost:3000/news/aaa
    //也會只匹配:id的路由
    @Get('a*a')
    indexA(){
        return '模糊匹配';
    }

    //動態路由  /add/1
    @Get(':id')
    indexB(@Param('id') id){
        return id;
    }
}

安裝joi: cnpm i @hapi/joi --save

模塊

模塊是具有 @Module() 裝飾器的類。 @Module() 裝飾器提供了元數據,Nest 用它來組織應用 程序結構。


1.png

每個 Nest 應用程序至少有一個模塊,即根模塊。根模塊是 Nest 開始安排應用程序樹的地方。事實上,根模塊可能是應用程序中唯一的模塊,特別是當應用程序很小時,但是對于大型程序來說這是沒有意義的。在大多數情況下,您將擁有多個模塊,每個模塊都有一組緊密 相關的功能。

@module() 裝飾器接受一個描述模塊屬性的對象

屬性 描述
providers 由 Nest 注入器實例化的提供者,并且可以至少在整個模塊中共享
controllers 必須創建的一組控制器
imports 導入模塊的列表,這些模塊導出了此模塊中 所需提供者,注意導入的是模塊
exports 由本模塊提供并應在其他模塊中可用的提供 者的子集

模塊共享

  • 共享模塊:只是一個普通模塊
import { Module } from '@nestjs/common';
import { BaseService } from './service/base/base.service';

@Module({
  providers: [BaseService],
  exports:[BaseService]
})
export class ShareModule {}

總結:其實就是module內部exports的東西,此時是一個基礎服務類(可以是很多),只要導出了,其他模塊只要導入了該模塊,則該模塊的所有exports的都可以通過依賴注入方式直接使用,而不需要一個個導入service或者provider,然后放到想使用的模塊的provider中。

  • 使用共享模塊
import { Module } from '@nestjs/common';
import { UserController } from './controller/user/user.controller';
import { NewsService } from './service/news/news.service';
import { ShareModule } from '../share/share.module';

@Module({
  imports:[ShareModule],
  controllers: [UserController],
  providers: [NewsService],
})
export class AdminModule {
    
}

此時新板塊導入了module,沒有導入具體外部service例如上面的BaseService,但是可以依賴注入在現在的模塊中直接使用。

  • 使用共享模塊導出的service
import { Controller, Get } from '@nestjs/common';
import {BaseService} from '../../../share/service/base/base.service'

@Controller('admin/user')
export class UserController {
    constructor(private readonly baseService:BaseService){}
    @Get()
    index(){
        console.log(this.baseService.getData());
        return "哈哈"
    }   
}

==模塊總結:==由上面可知,其實所有模塊都可以導入,只要不出現循環導入就都可以,而模塊的概念,加強了約束,即使import導入了對應service,只要沒有在對應module導入module,則運行就報錯。

守衛

文檔: https://docs.nestjs.com/pipes

守衛是一個使用 @Injectable() 裝飾器的類。守衛應該實現 CanActivate 接口。

守衛有一個單獨的責任。它們確定請求是否應該由路由處理程序處理。到目前為止,訪問限 制邏輯大多在中間件內。這樣很好,因為諸如 token 驗證或將 request 對象附加屬性與 特定路由沒有強關聯。但中間件是非常笨的。它不知道調用 next() 函數后會執行哪個處 理程序。另一方面,守衛可以訪問 ExecutionContext 對象,所以我們確切知道將要執行 什么。

==簡單說:==
在 Nextjs 中如果我們想做權限判斷的話可以在守衛中完成,也可以在中間件中完成,但是一般通過守衛最好。

  • 創建
nest g gu auth

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // let n=Math.random();
    // if (n>0.3) {
    //   return false;
    // }

    //守衛返回false則無法訪問路由,true可以

    //判斷cookie或者session
    //其實context.switchToHttp().getRequest();就相當于express的req,所以什么path屬性等等都有
    // const {cookies,session}=context.switchToHttp().getRequest();
    // console.log(cookies,session);
    
    return true;
  }
}
  • 使用守衛
@Get('guard') 
@UseGuards(AuthGuard) 
guard(@Query() info){ 
    console.log(info); 
    return `this is guard`; 
}

### 也可以直接加在控制器上面

@Controller('cats') 
@UseGuards(RolesGuard) 
export class CatsController {}

### 全局使用守衛
app.useGlobalGuards(new AuthGuard());
  • 模塊上使用守衛
import {Module} from '@nestjs/common';
import {APP_GUARD} from '@nestjs/core';

@Module({
    providers:[
        {
            provide:APP_GUARD,
            useClass:RolesGuard
        }
    ]
})
export class AppModule{}

攔截器

  • 作用

    • 在函數執行之前/之后綁定額外的邏輯
    • 轉換從函數返回的結果
    • 轉換從函數拋出的異常
    • 擴展基本函數行為
    • 根據所選條件完全重寫函數 (例如, 緩存目的)
  • 創建

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

由于 handle() 返回一個RxJS Observable,我們有很多種操作符可以用來操作流。在上面的例子中,我們使用了 tap() 運算符,該運算符在可觀察序列的正常或異常終止時調用函數。

  • 使用
@UseInterceptors(LoggingInterceptor)
export class CatsController {}

此處使用LoggingInterceptor,初始化控制反轉交給框架本身,雖然可以傳入new的實例,但是不建議

  • 全局綁定
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());
  • 模塊綁定
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class ApplicationModule {}

更多rxjs響應功能

執行順序

過濾器詳情其實就是異常過濾器,整個流程不見得一定會指定,但是出現異常時候會進入,詳情查看官方文檔

graph LR
客戶端請求-->中間件
中間件-->守衛 
守衛 -->攔截器之前 
攔截器之前 -->管道
管道--> 控制器處理并響應
控制器處理并響應 -->攔截器之后
攔截器之后 -->過濾器
  • 接收客戶端發起請求
  • 中間件去做請求處理,比如helmet,csrf,rate limiting,compression等等常用的處理請求的中間件。
  • 守衛就驗證該用戶的身份,如果沒有權限或者沒有登錄,就直接拋出異常,最適合做權限管理。
  • 攔截器根據作者解釋,攔截器之前不能修改請求信息。只能獲取請求信息。
  • 管道做請求的數據驗證和轉化,如果驗證失敗拋出異常。
  • 這里處理響應請求的業務,俗稱controller,處理請求和服務橋梁,直接響應服務處理結果。
  • 攔截器之后只能修改響應body數據。
  • 最后走過濾器:如果前面任何位置發生拋出異常操作,都會直接走它。

總結

模塊是按業務邏輯劃分基本單元,包含控制器和服務。控制器是處理請求和響應數據的部件,服務處理實際業務邏輯的部件。

中間件是路由處理Handler前的數據處理層,只能在模塊或者全局注冊,可以做日志處理中間件、用戶認證中間件等處理,中間件和express的中間件一樣,所以可以訪問整個request、response的上下文,模塊作用域可以依賴注入服務。全局注冊只能是一個純函數或者一個高階函數。

管道是數據流處理,在中間件后路由處理前做數據處理,可以控制器中的類、方法、方法參數、全局注冊使用,只能是一個純函數。可以做數據驗證,數據轉換等數據處理。

守衛是決定請求是否可以到達對應的路由處理器,能夠知道當前路由的執行上下文,可以控制器中的類、方法、全局注冊使用,可以做角色守衛。

攔截器是進入控制器之前和之后處理相關邏輯,能夠知道當前路由的執行上下文,可以控制器中的類、方法、全局注冊使用,可以做日志、事務處理、異常處理、響應數據格式等。

過濾器是捕獲錯誤信息,返回響應給客戶端。可以控制器中的類、方法、全局注冊使用,可以做自定義響應異常格式。

中間件、過濾器、管道、守衛、攔截器,這是幾個比較容易混淆的東西。他們有個共同點都是和控制器掛鉤的中間抽象處理層,但是他們的職責卻不一樣。

NestJS很多提供的內部功能,看似功能重復,但是實際上有明確的職責劃分,按照約定來,能使結構清晰,代碼更好維護

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容