Nest.js 最終版已經(jīng)放出,歡迎來試

原文來自卡米爾大神(Kamil My?liwiec):Nest FINAL release is here! Node.js framework built on top of TypeScript

Nest 是 Node.js圈子中又一強(qiáng)大的 Web 框架, 使用 Nest 可以輕松構(gòu)建高效可拓展應(yīng)用。 Nest 使用現(xiàn)代 JavaScript 技術(shù)構(gòu)建,編碼使用TypeScript,同時(shí)汲取了 OOP (面向?qū)ο缶幊? 以及 FP(函數(shù)式編程) 的精華所在。

Nest 不僅僅是一個 Web 框架,使用 Nest 時(shí),無需擔(dān)心有沒有豐富的社區(qū)支持, 因?yàn)镹est 本身就基于聞名遐邇的老牌框架而建 – Expresssocket.io! 因此,學(xué)習(xí) Nest 能夠讓你輕松上手,而不用學(xué)習(xí)一整套新的工具全家桶。

核心概念

Nest 的核心內(nèi)容是提供一個服務(wù)框架 (architecture), 幫助開發(fā)者實(shí)現(xiàn)多層分離,業(yè)務(wù)抽象。

安裝

$ npm install --save nest.js

配置應(yīng)用

Nest 支持 ES6 和 ES7 (decorators, async / await) 新特性。 所以最簡單方式就是使用 Babel 或者 TypeScript
本文中會使用 TypeScript (當(dāng)然不是唯一方式),我也推薦大家使用這種方式,下邊是一個簡單的 tsconfig.json 文件

{ 
    "compilerOptions": { 
        "module": "commonjs", 
        "declaration": false, 
        "noImplicitAny": false, 
        "noLib": false, 
        "emitDecoratorMetadata": true, 
        "experimentalDecorators": true, 
        "target": "es6" 
    }, 
    "exclude": [ "node_modules" ]
}

注意 emitDecoratorMetadata 以及 experimentalDecorators 需要設(shè)置為 true, 來支持項(xiàng)目中使用 Decorator 新特性。

現(xiàn)在可以寫一個小服務(wù)了。首先,創(chuàng)建一個應(yīng)用的入口模塊(entry module) (app.module.ts):

import { Module } from 'nest.js';
@Module({})
export class ApplicationModule {}

此時(shí)尚未為模塊添加修飾 metadata 是空對象 ({}), 我們現(xiàn)在只想讓服務(wù)跑起來 (現(xiàn)在也沒有控制器和組件能夠讓我們添加的).

第二步,編寫主文件:index.ts (這個文件名隨意,約定俗成,喜歡使用 Index) ,調(diào)用 Nest 提供的工廠函數(shù)創(chuàng)建 Nest應(yīng)用實(shí)例,并將這個實(shí)例掛載在剛創(chuàng)建的入口模塊上。

import { NestFactory } from 'nest.js';
import { ApplicationModule } from './app.module';
const app = NestFactory.create(ApplicationModule);
app.listen(3000, () => console.log('Application is listening on port 3000'));

完工,齊活。跑一下,看看。

Express 實(shí)例

如果你需要控制 express 實(shí)例的生命周期,可以將創(chuàng)建的實(shí)例傳遞給工廠函數(shù) NestFactory.create(),就成了下邊這樣

import express from 'express';
import { NestFactory } from 'nest.js';
import { ApplicationModule } from './modules/app.module';
const instance = express();
const app = NestFactory.create(ApplicationModule, instance);
app.listen(3000, () => console.log('Application is listening on port 3000'));

換句話說,你可以向工廠函數(shù)添加配置項(xiàng),來生成想要的服務(wù)實(shí)例。(例如,添加常用的插件 morgan 或者 body-parser).

第一個控制器

控制器層用來處理 HTTP 請求. Nest 中,控制器是用 @Controller() 修飾的類。

Controllers_1
Controllers_1

上一節(jié)中,我們已經(jīng)寫好了程序入口,基于此,我們來構(gòu)建一個應(yīng)用端口 /users

import { Controller, Get, Post } from 'nest.js';

@Controller()
export class UsersController { 
    @Get('users') 
    getAllUsers() {} 

    @Get('users/:id') 
    getUser() {} 

    @Post('users') 
    addUser() {}
}

正如所見,我們快速創(chuàng)建了同一個端口的3個應(yīng)用路徑:

GET: users
GET: users/:id 
POST: users

看看剛創(chuàng)建的這個類,有木有可優(yōu)化的地方呢? users 這個詞貌似有點(diǎn)重復(fù),能優(yōu)化么?當(dāng)然!只需要向 @Controller()添加元數(shù)據(jù)配置即可,這里添加一個路徑,重寫一下這個類。

@Controller('users')
export class UsersController { 
    @Get() 
    getAllUsers(req, res, next) {} 
    
    @Get('/:id') 
    getUser(req, res, next) {} 
    
    @Post() 
    addUser(req, res, next) {}
}

世界忽然清凈了。這里多寫了一點(diǎn),那就是所有的 Nest 控制器默認(rèn)包含 Express 自帶參數(shù)列表和參數(shù)方法。如果你想學(xué)習(xí)更多 req (request), res (response) 以及 next 的使用 你需要讀一下 Express 路由相關(guān)的文檔。在 Nest 中,這些參數(shù)能力依舊,不僅如此,Nest 還對其進(jìn)行了拓展。 Nest提供了一整套網(wǎng)絡(luò)請求裝飾器,你可以用這些裝飾器來增強(qiáng)請求參數(shù)處理。

Nest Express
@Request() req
@Response() res
@Next() next
@Session() req.session
@Param(param?: string) req.params[param]
@Body(param?: string) req.body[param]
@Query(param?: string) req.query[param]
@Headers(param?: string) req.headers[param]

簡單 demo 這些裝飾器的用法

@Get('/:id')
public async getUser(@Response() res, @Param('id') id) { 
    const user = await this.usersService.getUser(id); 
    res.status(HttpStatus.OK).json(user);
}

使用這些裝飾器的時(shí)候,記得從 Nest 引入 Param 模塊

import { Response, Param } from 'nest.js';

現(xiàn)在 UsersController 已經(jīng)可以投放使用了,但模塊并不知道有這個控制器的存在,所以進(jìn)入 ApplicationModule 給裝飾器添加些元數(shù)據(jù)。

import { Module } from 'nest.js';
import { UsersController } from "./users.controller";
@Module({ 
    controllers: [ UsersController ]
})
export class ApplicationModule {}

正如所見,引入控制器,并將其添加到元數(shù)據(jù)的 controllers 數(shù)組,就完成了注冊。

組件

Nest 中所有內(nèi)容都可以視為組件 Service, Repository, Provider 等都是組件。同時(shí)組件可以相互嵌套,也可以作為服務(wù)從構(gòu)造函數(shù)里注入控制器。

Components_1
Components_1

上一節(jié),我們創(chuàng)建了一個簡單的控制器,UsersController。這個控制器負(fù)責(zé)響應(yīng)網(wǎng)絡(luò)請求并將請求映射到數(shù)據(jù)(當(dāng)然現(xiàn)在還是假數(shù)據(jù),但并不影響)上。實(shí)際上,這并不是好的實(shí)踐,數(shù)據(jù)和請求分離才更便于分離依賴,抽象業(yè)務(wù)。所以應(yīng)該將控制器的職責(zé)單一化,只負(fù)責(zé)響應(yīng)網(wǎng)絡(luò)請求,而將更復(fù)雜的任務(wù)交付給 服務(wù) 來實(shí)現(xiàn)。我們稱這個抽離出來的依賴叫 UsersService 組件。實(shí)際開發(fā)中,UsersService應(yīng)從持久層調(diào)用方法實(shí)現(xiàn)數(shù)據(jù)整合,比如從 UsersRepository組件,為了說明簡單,我們暫時(shí)跳過這個階段,依然使用假數(shù)據(jù)來說明組件的概念。

import { Component, HttpException } from 'nest.js';
@Component()
export class UsersService { 
    private users = [ 
        { id: 1, name: "John Doe" }, 
        { id: 2, name: "Alice Caeiro" }, 
        { id: 3, name: "Who Knows" }, 
    ]; 
    
    getAllUsers() { 
        return Promise.resolve(this.users); 
    } 
    
    getUser(id: number) { 
        const user = this.users.find((user) => user.id === id); 
        if (!user) { 
            throw new HttpException("User not found", 404); 
        } 
        return Promise.resolve(user); 
    } 
    
    addUser(user) { 
        this.users.push(user); 
        return Promise.resolve(); 
    }
}

Nest 組件也是個類,他使用 @Component() 裝飾器來修飾。 正如所見,getUser() 方法中調(diào)用了 HttpException 模塊,這是 Nest 內(nèi)置的 Http 請求異常處理模塊,傳入兩個參數(shù),錯誤信息和返給客戶端的狀態(tài)碼。 優(yōu)先處理異常是一種優(yōu)良習(xí)慣,更多情況可以拓展 HttpException 來實(shí)現(xiàn) (更多細(xì)節(jié)查看錯誤處理小結(jié))。如此,我們的用戶服務(wù)已經(jīng)就緒,那就在用戶控制器中使用起來吧。

@Controller('users')
export class UsersController { 
    constructor(private usersService: UsersService) {} 
    
    @Get() 
    getAllUsers(@Response res) { 
        this.usersService.getAllUsers() 
          .then((users) => res.status(HttpStatus.OK).json(users)); 
    } 
    
    @Get('/:id') 
    getUser(@Response() res, @Param('id') id) { 
        this.usersService.getUser(+id) 
          .then((user) => res.status(HttpStatus.OK).json(user)); 
    } 
    @Post() 
    addUser(@Response() res, @Body('user') user) { 
        this.usersService.addUser(req.body.user) 
          .then((msg) => res.status(HttpStatus.CREATED).json(msg)); 
    }
}

注意,這里沒有寫 next參數(shù),因?yàn)檫@個場景中并未用到。

正如所見,UsersService 會自動注入控制器,這是 TypeScript 提供的語法糖,讓你輕而易舉的實(shí)現(xiàn)依賴注入,Nest 能夠從這種方式中識別出對應(yīng)的依賴。

constructor(private usersService: UsersService)

如上,即可。再次聲明,如果你在自己 demo 時(shí)如果報(bào)錯,請查看 tsconfig.json 文件中是否將 emitDecoratorMetadata 置為 true, 當(dāng)然,你從頭就沒用 ts,無可厚非,就像你非得滿足 IE6用戶需求一樣,Nest 也提供了相應(yīng)的寫法。

import { Dependencies, Controller, Post, Get, HttpStatus, Response, Param, Body } from 'nest.js';

@Controller('users')
@Dependencies(UsersService)
export class UsersController { 
    constructor(usersService) { 
        this.usersService = usersService; 
    } 
    
    @Get() 
    getAllUsers(@Response() res) { 
        this.usersService.getAllUsers() 
          .then((users) => res.status(HttpStatus.OK).json(users)); 
        } 
    
    @Get('/:id') 
    getUser(@Response() res, @Param('id') id) { 
        this.usersService.getUser(+id) 
          .then((user) => res.status(HttpStatus.OK).json(user)); 
    } 
    
    @Post() 
    addUser(@Response() res, @Body('user') user) { 
        this.usersService.addUser(user) 
          .then((msg) => res.status(HttpStatus.CREATED).json(msg)); 
    }
}

也不難是吧,眼熟不?當(dāng)然,沒有 ts 之前,我項(xiàng)目里絕大多數(shù)依賴注入都是如此實(shí)現(xiàn)的。不過,當(dāng)你運(yùn)行項(xiàng)目的時(shí)候,又要報(bào)錯了,因?yàn)?Nest 和 ApplicationModule 并不知道 UserService 是什么東西,需要像添加控制器一樣,將組件(服務(wù),提供商,持久層)也添加到模塊里,統(tǒng)一叫做組件。

import { Module } from 'nest.js';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService ]
})
export class ApplicationModule {}

齊活!現(xiàn)在應(yīng)用能夠正常跑了,不過路由 addUser 貌似不太管用, 為啥呢?因?yàn)槲覀儾⒉荒苷_讀取 req.body.user 啊,這時(shí)候咋整呢?這一步的思考至關(guān)重要,是自己造輪子解決這個問題,還是用別人的輪子?首先告訴你有 body-parser這個express中間件 解決這個問題,你可以在 express 實(shí)例中使用這些插件。當(dāng)然,你可以自己寫一個組件來處理這個問題(從用輪子到造輪子)。

讓我們來裝上這個神奇的插件

$ npm install --save body-parser

express實(shí)例中使用起來。

import express from 'express';
import * as bodyParser from 'body-parser';
import { NestFactory } from 'nest.js';
import { ApplicationModule } from './modules/app.module';

const instance = express();
instance.use(bodyParser.json());
const app = NestFactory.create(ApplicationModule, instance);
app.listen(3000, () => console.log('Application is listening on port 3000'));

異步回調(diào)方案 Async/await (ES7)

Nest 支持使用 ES7的 async / await 解決異步回調(diào)問題, 所以,我們可以順利的重構(gòu) userController

@Controller('users')
export class UsersController { 
    constructor(private usersService: UsersService) {} 
    
    @Get() 
    async getAllUsers(@Response() res) { 
        const users = await this.usersService.getAllUsers(); 
        res.status(HttpStatus.OK).json(users); 
    } 
        
    @Get('/:id') 
    async getUser(@Response() res, @Param('id') id) { 
        const user = await this.usersService.getUser(+id); 
        res.status(HttpStatus.OK).json(user); 
    } 
    
    @Post() 
    async addUser(@Response() res, @Body('user') user) { 
        const msg = await this.usersService.getUser(user); 
        res.status(HttpStatus.CREATED).json(msg); 
    }
}

這樣能看起來舒服一點(diǎn)是吧,至少 async/await 要比 promise/then 方案更酷炫一些。更多可以去看 async / await.

模塊

模塊是用 @Module({}) 裝飾器修飾的類。Nest 會使用這些元數(shù)據(jù)來組織模塊的結(jié)構(gòu)。

Modules_1
Modules_1

截止到現(xiàn)在,我們的 ApplicationModule 看上去是這樣的。

import { Module } from 'nest.js';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService ]
})
export class ApplicationModule {}

默認(rèn)情況下,模塊封裝了所有的依賴,換句話說,控制器和組件對于模塊外部是透明的。模塊之間可以相互引用,實(shí)際上,Nest 本身也是一個模塊樹。基于最佳實(shí)踐,我們將 UsersControllerUsersService 轉(zhuǎn)移到 UsersModule,新建一個 users.module.ts來存儲這個模塊。

import { Module } from 'nest.js';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService ]
})
export class UsersModule {}

然后在主模塊 ApplicationModule 引入 UsersModule,并在元數(shù)據(jù)中使用 modules 聲明。

import { Module } from 'nest.js';
import { UsersModule } from './users/users.module';
@Module({ 
    modules: [ UsersModule ]
})
export class ApplicationModule {}

齊活!正如所見,你可以根據(jù)業(yè)務(wù)需求,將部分模塊抽離出來做成可重用的模塊。

依賴注入

模塊可以輕松的注入她自己的組件,在元數(shù)據(jù)里說明一下就行了。

@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService, ChatGateway ]
})
export class UsersModule implements NestModule { 
    constructor(private usersService: UsersService) {}
}

與此同時(shí),組件也可以注入模塊

export class UsersController { constructor(private module: UsersModule) {}}

中間件

中間件本質(zhì)是一個函數(shù),這個函數(shù)在路由處理器之前調(diào)用。中間件能夠查看請求和回復(fù)對象,同時(shí)也能修改這些內(nèi)容。你可以把他們理解為 圍欄。如果中間件不放行,請求是不會抵達(dá)路由處理控制器的。

Middlewares_1
Middlewares_1

我們來構(gòu)建一個虛擬的授權(quán)中間件(簡單起見,只驗(yàn)證 username)。我們使用 HTTP 的頭部信息 X-Access-Token 來提供 username(這種實(shí)現(xiàn)很奇怪,不過例子嘛,不要太認(rèn)真)

import { UsersService } from './users.service';
import { HttpException, Middleware, NestMiddleware } from 'nest.js';

@Middleware()
export class AuthMiddleware implements NestMiddleware { 
    constructor(private usersService: UsersService) {} 
    
    resolve() { 
        return (req, res, next) => { 
            const userName = req.headers["x-access-token"]; 
            const users = this.usersService.getUsers(); 
            const user = users.find((user) => user.name === userName); 
            if (!user) { 
                throw new HttpException('User not found.', 401); 
            } 
            req.user = user; 
            next(); 
        } 
    }
}

關(guān)于中間件,你需要知道的只有四條

  • 你使用 @Middleware() 裝飾器告訴 Nest,下邊這個類是個中間件
  • 你可以使用 NestMiddleware 模塊提供的接口,這樣你就可以將注意力集中在輸入輸出上了,換句話說,你只需要實(shí)現(xiàn) resolve()函數(shù)
  • 像組件一樣,中間件可以通過構(gòu)造函數(shù)注入其他組件作為依賴,這里的組件應(yīng)該是模塊內(nèi)部的。
  • 中間件必須包含一個 resolve()函數(shù), 同時(shí)return 一個新的函數(shù)(高階函數(shù))為啥這么設(shè)計(jì)呢?因?yàn)榇罅康?express 第三方插件都是這么搞,Nest 也支持的話,就能夠讓大家輕松平移過來了。

注意: 上邊代碼中子在 UsersService 添加了一個 getUsers 方法,用以封裝獲取所有用戶。

現(xiàn)在我們有這個中間件了,嘗試用起來吧。

import { Module, MiddlewaresConsumer } from 'nest.js';

@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService ], 
    exports: [ UsersService ]
})
export class UsersModule { 
    configure(consumer: MiddlewaresConsumer) { 
        consumer.apply(AuthMiddleware)
          .forRoutes(UsersController); 
    }
}

如上所見,在類 UsersModule 內(nèi)部調(diào)用 configure() 方法。方法有一個參數(shù),通過參數(shù)配置中間件消費(fèi)者 MiddlewaresConsumer, 這是一個用于配置中間件的對象。這個對象有 apply() 方法,可以接收逗號分隔的多個中間件,apply() 方法返回一個對象,有兩個內(nèi)置方法

  • forRoutes() – 將中間件配置給哪個控制器使用,可以用逗號分隔多個
  • with – 向中間件反向傳遞參數(shù)

原理是啥呢?

當(dāng)把 UsersController作為參數(shù)傳遞給 forRoutes方法時(shí),Nest 會自動為控制器配置中間件。

GET: users
GET: users/:id 
POST: users

也可通過配置的方式進(jìn)行逐個操作,告知中間件作用在哪個路由上。一般用于指明某一個認(rèn)證作用于全部控制器的場景。

consumer.apply(AuthMiddleware)
  .forRoutes({ path: '*', method: RequestMethod.ALL });

鏈?zhǔn)讲僮?/h4>

Nest 支持鏈?zhǔn)讲僮鳎憧梢暂p松實(shí)現(xiàn)以下代碼

consumer.apply(AuthMiddleware, PassportMidleware) 
    .forRoutes(UsersController, OrdersController, ClientController)
    .apply(...) 
    .forRoutes(...);

調(diào)用順序

中間件按照其出現(xiàn)在數(shù)組中的順序依次調(diào)用,子模塊的中間件會在父模塊中間件調(diào)用之后調(diào)用。

基于網(wǎng)關(guān)的實(shí)時(shí)應(yīng)用

Nest 中有一種獨(dú)特的組件叫做網(wǎng)關(guān)。網(wǎng)關(guān)可以幫助開發(fā)者構(gòu)造實(shí)時(shí)應(yīng)用。Nest 在實(shí)現(xiàn)過程中封裝了 socket.io中的部分特性。

Gateways_1
Gateways_1

網(wǎng)關(guān)也是組件,一樣可以通過構(gòu)造函數(shù)注入依賴,當(dāng)然也可作為其他組件的依賴。

import { WebSocketGateway, OnGatewayInit } from 'nest.js/websockets';

@WebSocketGateway()
export class UsersGateway implements OnGatewayInit { 
    afterInit(server) {} 
    handleConnection(client) {} 
    handleDisconnect(client) {}
}

默認(rèn)情況下,Websocket 會開通80端口,并使用默認(rèn)命名空間,可以通過向裝飾器傳遞元數(shù)據(jù)的方式進(jìn)行配置。

@WebSocketGateway({ port: 81, namespace: 'users' })

當(dāng)然,想要網(wǎng)關(guān)生效,還需要將其加入主模塊,跟其他服務(wù)組件并列。

@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService, UsersGateway ], 
    exports: [ UsersService ]
})

另外,網(wǎng)關(guān)有三個重要的生命周期鉤子:

  • afterInit 有個本地服務(wù)器 socket.io 對象可以操作,一般寫為 server
  • handleConnection/handleDisconnect 有個本地客戶端 socket.io 對象可以操作,一般寫為 client
  • OnGatewayInit/OnGatewayConnection/OnGatewayDisconnect 網(wǎng)關(guān)狀態(tài)管理接口

如何傳遞數(shù)據(jù)

在網(wǎng)關(guān)中可以輕松訂閱廣播信息,使用 SubscribeMessage 裝飾器實(shí)現(xiàn)。

import { WebSocketGateway, OnGatewayInit, SubscribeMessage } from 'nest.js/websockets';

@WebSocketGateway({ port: 81, namespace: 'users' })
export class UsersGateway implements OnGatewayInit { 
    afterInit(server) {} 
    handleConnection(client) {} 
    handleDisconnect(client) {} 
    
    @SubscribeMessage('drop') 
    handleDropMessage(sender, data) { // sender 是本地客戶端 socket.io 對象}
}

客戶端可以輕松實(shí)現(xiàn)

import * as io from 'socket.io-client';
const socket = io('http://URL:PORT/');
socket.emit('drop', { msg: 'test' });

@WebSocketServer()

如果你想準(zhǔn)確的指明使用哪個 socket.io 本地服務(wù)實(shí)例,可以將這個實(shí)例通過 @WebSocketServer() 裝飾器進(jìn)行包裹。

import { WebSocketGateway, WebSocketServer, OnGatewayInit } from 'nest.js/websockets';

@WebSocketGateway({ port: 81, namespace: 'users' })
export class UsersGateway implements OnGatewayInit { 
    @WebSocketServer() server; 
    afterInit(server) {} 

    @SubscribeMessage('drop') 
    handleDropMessage(sender, data) { // sender是本地客戶端 socket.io 對象}
}

服務(wù)端初始化以后便完成指定。

網(wǎng)關(guān)中間件

網(wǎng)關(guān)中間件與路由中間件相似。中間件是一個函數(shù),在訂閱事件觸發(fā)之前調(diào)用。網(wǎng)關(guān)可以操作本地 socket 對象。你可以把他們理解為 圍欄。如果中間件不放行,信息是不會被廣播的。

@Middleware()
export class AuthMiddleware implements GatewayMiddleware { 
    public resolve(): (socket, next) => void { 
        return (socket, next) => { 
            console.log('Authorization...'); next(); 
        } 
    }
}

網(wǎng)關(guān)中間件須知須會:(跟中間件一節(jié)一毛一樣,我是拖戲的,騙稿費(fèi)的)

  • 你需要通過 @Middeware 告知 Nest 這是一個中間件
  • 你可以繼承自 GatewayMiddleware然后集中處理 resolve 中的內(nèi)容
  • 像組件一樣,中間件可以通過構(gòu)造函數(shù)注入其他組件作為依賴,這里的組件應(yīng)該是模塊內(nèi)部的。
  • 中間件必須包含一個 resolve()函數(shù), 同時(shí)return 一個新的函數(shù)(高階函數(shù))

中間件搞好了,就用起來。

@WebSocketGateway({ 
    port: 2000, 
    middlewares: [ ChatMiddleware ]
})
export class ChatGateway {}

如上所見,網(wǎng)關(guān)@WebSocketGateway()接收元數(shù)據(jù)來配置使用的中間件,這里可以配置一組中間件,他們有個名,叫做“事前諸葛亮”

交互流

如上所見,Nest 中的網(wǎng)關(guān)也是組價(jià),可以作為依賴注入其他組件,這就能讓我們針對不同的信息作出不同的操作。比如做信息過濾(fuck=>fu*k),當(dāng)然也可將組件注入網(wǎng)關(guān),將網(wǎng)關(guān)的功能再次封裝拆分。有個話題專門解決這個方向,網(wǎng)關(guān)交互流,你可以去這里查看詳情

微服務(wù)

將 Nest 應(yīng)用服務(wù)轉(zhuǎn)為微服務(wù)異常簡單。創(chuàng)建服務(wù)應(yīng)用的時(shí)候這么寫:

const app = NestFactory.create(ApplicationModule);
app.listen(3000, () => console.log('Application is listening on port 3000'));

轉(zhuǎn)成微服務(wù),只需要改一個方法即可,Java 工程師,PHP?看一看!

const app = NestFactory.createMicroservice(ApplicationModule, { port: 3000 });
app.listen() => console.log('Microservice is listening on port 3000'));

使用 TCP 進(jìn)行通信

Microservices_1
Microservices_1

默認(rèn)情況下,Nest 的微服務(wù)監(jiān)聽 TCP 協(xié)議信息,于是微服務(wù)中 @RequestMapping() (以及 @Post(), @Get()) 這些裝飾器就用不到了,因?yàn)樗麄兓?HTTP協(xié)議。微服務(wù)中如何識別信息呢?Nest 提供了 patterns,這可能是對象,字符串,甚至數(shù)字(當(dāng)然,數(shù)字不太好)

import { MessagePattern } from 'nest.js/microservices';

@Controller()
export class MathController { 
    @MessagePattern({ cmd: 'add' }) 
    add(data, respond) { 
        const numbers = data || []; 
        respond(null, numbers.reduce((a, b) => a + b)); 
    }
}

如上所見,如果你想處理信息,那么只需使用 @MessagePattern(pattern)裝飾器,裝飾器中的元數(shù)據(jù)cmd指明了處理數(shù)據(jù)的方式,默認(rèn)傳遞兩個參數(shù)

  • data, 其他微服務(wù)傳遞過來的數(shù)據(jù),或者網(wǎng)絡(luò)數(shù)據(jù)
  • respond 響應(yīng)方法,默認(rèn)傳遞兩個參數(shù)(錯誤,響應(yīng))

客戶端

能夠接收數(shù)據(jù)還沒完事,你得知道怎么發(fā)送數(shù)據(jù)。在發(fā)送之前,你需要告訴 Nest 你給誰發(fā)數(shù)據(jù)。實(shí)際上也很簡單,首先你腦子里需要構(gòu)建一個 server-client 鏈接,你數(shù)據(jù)的接收方就是你的 Client,你可以使用 @Client來裝飾

import { Controller } from 'nest.js';
import { Client, ClientProxy, Transport } from 'nest.js/microservices';

@Controller()
export class ClientController { 
    @Client({ 
        transport: Transport.TCP, 
        port: 5667 
    }) 
    client: ClientProxy;
}

@Client() 裝飾器接收三個元數(shù)據(jù)配置項(xiàng)

  • transport – 傳遞數(shù)據(jù)的通道和協(xié)議,可以是 Transport.TCP 或者 Transport.REDIS,默認(rèn)為 TCP
  • url – 只有傳遞通道為 Redis 的時(shí)候用到(默認(rèn) – redis://localhost:6379),
  • port - 端口號(默認(rèn) 3000).

創(chuàng)建客戶端

創(chuàng)建一個端口服務(wù)來驗(yàn)證一下剛剛創(chuàng)建的 Server-Client 微服務(wù)通道

import { Controller, Get } from 'nest.js';
import { Client, ClientProxy, Transport } from 'nest.js/microservices';

@Controller()
export class ClientController { 
    
    @Client({ 
        transport: Transport.TCP, 
        port: 5667 
    }) 
    client: ClientProxy; 
    
    @Get('client') 
    sendMessage(req, res) { 
        const pattern = { command: 'add' }; 
        const data = [ 1, 2, 3, 4, 5 ]; 
        this.client.send(pattern, data) 
          .catch((err) => Observable.empty()) 
          .subscribe((result) => res.status(200).json({ result })); 
    }
}

如上所見,如果你想發(fā)送信息,你需要調(diào)用 send 方法,發(fā)送數(shù)據(jù)被封裝為參數(shù),一個 pattern一個 data.這個方法返回了 Rxjs 式的觀察者對象。這可是不得了的一個特性,因?yàn)榻换ナ接^察者對象提供了一整套炫酷的方法。比如 combine, zip, retryWhen, timeout 等等,當(dāng)然你還是喜歡用 Promises-then 那套,你可以使用 toPromise() 方法。由此,當(dāng)請求發(fā)送到此控制器時(shí),就會得到下邊的結(jié)果(前提是微服務(wù)和 Web應(yīng)用都啟動起來)

{ "result": 15}

Redis

上文書 Nest 微服務(wù)還能鏈接 Redis,與 TCP 通信不同,與 Redis鏈接就能使用其自帶的各種特性,最贊的當(dāng)屬 – 發(fā)布/訂閱模式.

Redis_1
Redis_1

當(dāng)然,你得先安裝 Redis.

創(chuàng)建 Redis 微服務(wù)

創(chuàng)建一個 Redis 微服務(wù)也很簡單,在創(chuàng)建微服務(wù)時(shí),傳遞 Redis 服務(wù)配置即可。

const app = NestFactory.createMicroservice( 
    MicroserviceModule, { 
        transport: Transport.REDIS, 
        url: 'redis://localhost:6379'
    });
app.listen(() => console.log('Microservice listen on port:', 5667 ));

如此便鏈接了 Redis,其服務(wù)中心分發(fā)的信息就能夠被此微服務(wù)捕獲到。剩下的工作跟 TCP 微服務(wù)一樣。

Redis 客戶端

現(xiàn)在仿照微服務(wù)創(chuàng)建一個客戶端,在 TCP 微服務(wù)中,你的客戶端配置像下邊這樣

@Client({ 
    transport: Transport.TCP, 
    port: 5667 
})
client: ClientProxy;

Redis 微服務(wù)的客戶端修改一下即可,添加一個 url 配置項(xiàng)

@Client({ 
    transport: Transport.REDIS, 
    url: 'redis://localhost:6379'
})
client: ClientProxy;

是不是很簡單,數(shù)據(jù)操作和管道跟 TCP的微服務(wù)一致。

模塊共享

Nest 可以指明向外暴漏哪些組件,換句話說,我們能夠輕松地在模塊之間共享組件實(shí)例。更好的方式當(dāng)然是使用 Nest 內(nèi)置的 Shared 模塊.

Shared_Module_1
Shared_Module_1

以共享 ChatGateway 組件為例,添加一個 exports 關(guān)鍵字來指明導(dǎo)出的組件服務(wù)。

@Module({ 
    components: [ ChatGateway ], 
    exports: [ ChatGateway ]
})
export class SharedModule {}

完事兒作為依賴注入到其他模塊就行了。

@Module({ 
    modules: [ SharedModule ]
})
export class FeatureModule {}

完工齊活,你可以在 FeatureModule 中直接使用 ChatGateway,就跟自己模塊內(nèi)部一毛一樣。

依賴注入

依賴注入是一個強(qiáng)大的特性,能夠幫我們輕松管理類的依賴。在強(qiáng)類型語言,比如 Java 和 .Net中,依賴注入很常見。在 Nodejs 中,這種特性并不重要,因?yàn)槲覀円呀?jīng)擁有完善的模塊加載機(jī)制。例如,我可以不費(fèi)吹灰之力,從另外一個文件中引入一個模塊。模塊加載機(jī)制對于中小型應(yīng)用已經(jīng)足夠了。但隨著代碼量逐步增長,系統(tǒng)層級復(fù)雜以后,通過引用的方式管理他們之間的依賴會越來越麻煩。現(xiàn)在沒錯,不代表未來不會錯。所以自動化實(shí)現(xiàn)依賴管理呼之欲出。

giphy
giphy

這種方式尚不如通過構(gòu)造函數(shù)導(dǎo)入依賴更加清晰明了,這也是 Nest 為何內(nèi)置依賴注入機(jī)制的緣故。

自定義組件

依上文,你可以輕松將控制器綁定到模塊,這種注入方式異常簡單

@Module({ 
    controllers: [ UsersController ], 
    components: [ UsersService ]
})

實(shí)際開發(fā)中,注入到模塊的可能并非指明的這些關(guān)鍵字。

useValue

const value = {};
@Module({ 
    controllers: [ UsersController ], 
    components: [ { 
        provide: UsersService, 
        useValue: value } ]
    })

使用場景

  • 用于向服務(wù)插值,如此一來,UsersService的元數(shù)據(jù)中就會有 value這個值,
  • 用于單元測試,注入初始值

useClass

@Component()
class CustomUsersService {}

@Module({ 
    controllers: [ UsersController ], 
    components: [ { 
        provide: UsersService, 
        useClass: CustomUsersService } ]
    })

使用場景

  • 向指定的服務(wù)注入特定的類。

useFactory

@Module({ 
    controllers: [ UsersController ], 
    components: [ 
        ChatService, 
        { 
            provide: UsersService, 
            useFactory: (chatService) => { return Observable.of('value');}, 
            inject: [ ChatService ] 
        }]
    })

使用場景

  • 使用其他組件之前需要計(jì)算得到一個值,并注入這個組件
  • 服務(wù)的啟動依賴一個異步值,比如讀寫文件或者鏈接數(shù)據(jù)庫

注意

  • 如果想要調(diào)用其他模塊的內(nèi)部控制器和組件,需要注明引用

自定義提供者

@Module({ 
    controllers: [ UsersController ], 
    components: [ { provide: 'isProductionMode', useValue: false } ]
})

使用場景

  • you want to provide value with a chosen key.

注意

  • it is possible to use each types useValue, useClass and useFactory.

使用方法

To inject custom provided component / value with chosen key, you have to tell Nest about it, just like that:

import { Component, Inject } from 'nest.js';

@Component()
class SampleComponent { 
    constructor(@Inject('isProductionMode') private isProductionMode: boolean) { 
        console.log(isProductionMode); // false 
    }
}

ModuleRef

Sometimes you might want to directly get component instance from module reference. It not a big thing with Nest – just inject ModuleRef in your class:

export class UsersController { constructor( private usersService: UsersService, private moduleRef: ModuleRef) {}}

ModuleRef 提供了一個方法:

  • get<T>(key), which returns instance for equivalent key (mainly metatype). Example moduleRef.get<UsersService>(UsersService) returns instance of UsersService component from current module.

    moduleRef.get<UsersService>(UsersService)

It returns instance of UsersService component from current module.

測試

Nest gives you a set of test utilities, which boost application testing process. There are two different approaches to test your components and controllers – isolated tests or with dedicated Nest test utilities.

單元測試

Both Nest controllers and components are a simple JavaScript classes. Itmeans, that you could easily create them by yourself:

const instance = new SimpleComponent();

If your class has any dependency, you could use test doubles, for example from such libraries as Jasmine or Sinon:

const stub = sinon.createStubInstance(DependencyComponent);
const instance = new SimpleComponent(stub);

Nest 測試套件

The another way to test your applications building block is to use dedicated Nest Test Utilities. Those Test Utilities are placed in static Test class (nest.js/testing module).

import { Test } from 'nest.js/testing';

這個模塊類提供了兩個方法

  • createTestingModule(metadata: ModuleMetadata), which receives as an parameter simple module metadata (the same as Module() class). This method creates a Test Module (the same as in real Nest Application) and stores it in memory.
  • get<T>(metatype: Metatype<T>), which returns instance of chosen (metatype passed as parameter) controller / component (or null if it is not a part of module.

例如:

Test.createTestingModule({ controllers: [ UsersController ], components: [ UsersService ]});const usersService = Test.get<UsersService>(UsersService);

Mocks, spies, stubs

Sometimes you might not want to use a real instance of component / controller. Instead of this – you can use test doubles or custom values / objects.

const mock = {};
Test.createTestingModule({ 
    controllers: [ UsersController ], 
    components: [ { provide: UsersService, useValue: mock } ]
});
const usersService = Test.get<UsersService>(UsersService); // mock

異常過濾器

With Nest you can move exception handling logic to special classes called Exception Filters. Let’s take a look at following code:

@Get('/:id')
public async getUser(@Response() res, @Param('id') id) { 
    const user = await this.usersService.getUser(id); 
    res.status(HttpStatus.OK).json(user);
}

Imagine that usersService.getUser(id) method could throws UserNotFoundException. What’s now? We have to catch an exception in route handler:

@Get('/:id')
public async getUser(@Response() res, @Param('id') id) { 
    try { 
        const user = await this.usersService.getUser(id); 
        res.status(HttpStatus.OK).json(user); 
    } catch(exception) { 
        res.status(HttpStatus.NOT_FOUND).send(); 
    }
}

總的來說,我們在可能出現(xiàn)異常的地方添加 try...catch 語句塊,更好的方式當(dāng)然是使用內(nèi)置的異常過濾器。

import { Catch, ExceptionFilter, HttpStatus } from 'nest.js';
export class UserNotFoundException {}
export class OrderNotFoundException {}

@Catch(UserNotFoundException, OrderNotFoundException)
export class NotFoundExceptionFilter implements ExceptionFilter { 
    catch(exception, response) { 
        response.status(HttpStatus.NOT_FOUND).send(); 
    }
}

可以在控制器中加入這個異常過濾器了。

import { ExceptionFilters } from 'nest.js';

@ExceptionFilters(NotFoundExceptionFilter)
export class UsersController {}

此時(shí),如果找不到用戶就會調(diào)起這個異常。

其他異常過濾器

Each controller may has infinite count of exception filters (just separate them by comma).

@ExceptionFilters(NotFoundExceptionFilter, AnotherExceptionFilter)

異常捕獲的依賴注入

異常過濾器與組件一樣工作,所以可以通過構(gòu)造函數(shù)注入依賴

HttpException

注意,這個異常模塊主要用于 RESTful API 構(gòu)造

Nest 內(nèi)置了錯誤處理層,可以捕獲所有未處理的異常,如果拋出一個非 HttpException,Nest 會自動處理并返回下邊這個 Json 對象(狀態(tài)碼 500):

{ "message": "Unkown exception"}

異常層次結(jié)構(gòu)

在你的應(yīng)用中,你應(yīng)該創(chuàng)建你自己的錯誤層級。所有 HTTP異常相關(guān)都可以繼承自內(nèi)置 HttpException,例如,你可以創(chuàng)建 NotFoundException 和 UserNotFoundException。

import { HttpException } from 'nest.js';

export class NotFoundException extends HttpException { 
    constructor(msg: string | object) { 
        super(msg, 404); 
    }
}

export class UserNotFoundException extends NotFoundException { 
    constructor() { 
        super('User not found.'); 
    }
}

當(dāng)你項(xiàng)目中出現(xiàn)這兩個異常的時(shí)候,Nest會調(diào)起你自己配置的這個異常處理句柄。

{ "message": "User not found."}

如此一來,你可以將將精力集中在業(yè)務(wù)邏輯和代碼實(shí)現(xiàn)上了。

更多參考

GitHub
文檔
NPM

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

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,973評論 19 139
  • 最近再看阮一峰的一篇博客提到了一本書《Software Architecture Patterns》(PDF),寫...
    卓_然閱讀 7,878評論 0 22
  • 最近因業(yè)務(wù)需要,小拾君深入學(xué)習(xí)了一下微服務(wù)架構(gòu)相關(guān)的技術(shù),跟大家分享一下。本文并不會涉及太多晦澀難懂的技術(shù)術(shù)語以及...
    每日一拾閱讀 7,503評論 0 28
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,446評論 25 708
  • 今天早上把我家二寶寶送回梁村了,準(zhǔn)備給她斷奶了。剛送回家就舍不得走了,真的看著還小又有點(diǎn)后悔了,不過沒辦法...
    王嬡閱讀 233評論 0 0