【譯】基於 TypeScript 的 Node.js 框架 Nest 正式版發佈!(下)

本文爲譯文,原文地址:https://kamilmysliwiec.com/nest-final-release-is-here-node-js-framework-built-top-of-typescript,做者,@Kamil Myśliwiechtml

上篇文章地址爲: http://www.javashuo.com/article/p-xbnqlvsm-md.htmlnode

中間件(Middlewares)

中間件是一個在路由處理程序前被調用的函數。中間件函數能夠訪問請求和響應對象,所以能夠修改它們。它們也能夠向一個屏障,若是中間件函數不調用 next(),請求將永遠不會被路由處理程序處理。git

middlewares

讓咱們來構建一個虛擬受權中間件。咱們將使用 X-Access-Token HTTP 頭來提供用戶名(這是個奇怪的想法,可是不重要)。github

import { UsersService } from './users.service';
import { HttpException } from '@nestjs/core';
import { Middleware, NestMiddleware } from '@nestjs/common';

@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();
        }
    }
}

一些事實以下:web

  • 你應該使用 @Middleware() 註解來告訴 Nest,這個類是一箇中間件,redis

  • 你可使用 NestMiddleware 界面,這強制你使用 resolve() 方法,typescript

  • 中間件(與組件相同)能夠經過其構造函數的注入依賴項(依賴關係必須是模塊的一部分),數據庫

  • 中間件必須有 resolve() 方法,它必須返回另外一個函數(高階函數)。爲何?由於有不少第三方插件準備使用 express 中間件,你能夠簡單地在 Nest 中使用,還要感謝這個方案。express

好了,咱們已經準備好了中間件,可是咱們並無在任何地方使用它。咱們來設置它:npm

import { Module, MiddlewaresConsumer } from '@nestjs/common';

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

如上所示,模塊能夠有其餘方法,configure()。此方法接收 MiddlewaresConsumer  對象做爲參數,它能夠幫助咱們配置中間件。

這個對象有 apply() 方法,它接收到無數的中間件(這個方法使用擴展運算符,因此能夠傳遞多個由逗號分隔的類)。 apply() 方法返回對象,它有兩種方法:

  • forRoutes():咱們使用這種方法經過逗號分隔控制器或對象(具備 pathmethod 屬性),不限個數,

  • with():咱們使用這種方法將自定義參數傳遞給 resolve() 中間件方法。

它如何工做?

當你在 forRoutes 方法中傳遞 UsersController 時,Nest 將爲控制器中的每一個路由設置中間件:

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

可是也能夠直接定義應該使用哪一個路徑的中間件,就像這樣:

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

鏈(Chaining)

你能夠簡單的調用 apply() 鏈。

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

順序

中間件按照與數組相同的順序調用。在子模塊中配置的中間件將在父模塊配置以後被調用。

網關(Gateways)實時應用程序

Nest 中有特殊組件稱爲網關。網關幫助咱們建立實時的網絡應用程序。他們是一些封裝的 socket.io 功能調整到框架架構。

gateways

網關是一個組件,所以它能夠經過構造函數注入依賴關係。網關也能夠注入到另外一個組件。

import { WebSocketGateway } from '@nestjs/websockets';

@WebSocketGateway()
export class UsersGateway {}

默認狀況下,服務器在 80 端口上運行,並使用默認的命名空間。咱們能夠輕鬆地改變這些設置:

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

固然,只有當 UsersGatewaycomponents 模塊組件數組中時,服務器纔會運行,因此咱們必須把它放在那裏:

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

默認狀況下,網關有三個有用的事件:

  • afterInit,做爲本機服務器 socket.io 對象參數,

  • handleConnectionhandleDisconnect,做爲本機客戶端 socket.io 對象參數。

有特殊的接口OnGatewayInit, OnGatewayConnectionOnGatewayDisconnect 來幫助咱們管理生命週期。

什麼是消息

在網關中,咱們能夠簡單地訂閱發出的消息:

import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';

@WebSocketGateway({ port: 81, namespace: 'users' })
export class UsersGateway {
    @SubscribeMessage('drop')
    handleDropMessage(sender, data) {
        // sender is a native socket.io client object
    }
}

而在客戶端接收以下:

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

@WebSocketServer()

若是要分配選定的 socket.io 本地服務器實例屬性,你可使用 @WebSocketServer() 裝飾器來簡單地進行裝飾。

import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';

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

    @SubscribeMessage('drop')
    handleDropMessage(sender, data) {
        // sender is a native socket.io client object
    }
}

服務器初始化後將分配值。

網關中間件

網關中間件與路由器中間件幾乎相同。中間件是一個函數,它在網關消息用戶以前被調用。網關中間件函數能夠訪問本地 socket 對象。他們就像屏障同樣,若是中間件函數不調用 next(),消息將永遠不會由用戶處理。

例如:

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

關於網關中間件的一些事實

  • 你應該使用 @Middleware() 註解來告訴 Nest,這個類是一箇中間件,

  • 你可使用 GatewayMiddleware 界面,這迫使你實現 resolve() 方法,

  • 中間件(和組件同樣)能夠經過其構造函數注入依賴項(依賴關係必須是模塊的一部分),

  • 中間件必須是 resolve() 方法,它必須返回另外一個函數(高階函數)

好了,咱們已經準備好中間件,可是咱們並無在任何地方使用它。咱們來設定一下:

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

如上所示,@WebSocketGateway() 接受額外的元數組屬性 - middlewares,它是一箇中間件數組。這些中間件將在消息處理程序前調用。

反應流(Reactive Streams)

Nest 網關是一個簡單的組件,能夠注入到另外一個組件中。這個功能使得咱們有可能選擇將如何對消息作出反應。

固然,只有有必要,咱們能夠向網關注入組件並調用其適當的方法。

可是還有另外一個解決方案,網關反應流(Gateway Reactive Streams)。你能夠在這裏閱讀更多關於他們的信息。

微服務(Microservices)支持

將 Nest 應用程序轉換爲 Nest 微服務是很是簡單的。

讓咱們來看看如何建立 Web 應用程序:

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

如今,切換到微服務:

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

就是這樣!

經過 TCP 進行通訊

tcp

默認狀況下, Nest 微服務經過 TCP 協議監聽消息。這意味着如今 @RequestMapping() (以及 @Post()@Get() 等等)也不會有用,由於它是映射 HTTP 請求。那麼微服務如何識別消息?只是經過模式

什麼是模式?沒什麼特別的,它是一個對象,字符串或者數字(但這不是一個好注意)。

讓咱們建立 MathController :

import { MessagePattern } from '@nestjs/microservices';

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

你可能會看到,若是你想建立消息處理程序,你必須使用@MessagePattern(pattern) 進行裝飾。在這個例子中,我選擇 { cmd: 'add' } 做爲模式。

該處理程序方法接收兩個參數:

  • data,它是從另外一個微服務器(或者只是 Web 應用程序)發送的數據變量,

  • respond,接收兩個參數(errorresponse)的函數。

客戶端

你已經知道了如何接收消息。如今,咱們來看看如何從另外一個微服務器或者 Web 應用程序發送它們。

在你開始以前,Nest 必須知道你要發送的郵件的位置。很簡單,你只須要建立一個 @Client 對象。

import { Controller } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';

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

@Client() 裝飾器接收對象做爲參數。此對象能夠有 3 個屬性:

  • transport,經過這種方式,你能夠決定使用哪一種方法,TCP 或者 Redis(默認爲 TCP)

  • url,僅用於 Redis 參數(默認爲 redis://localhost:6379),

  • port,端口,默認爲 3000。

使用客戶端

讓咱們來建立自定義路徑來測試咱們的通訊。

import { Controller, Get } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/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 }));
    }
}

正如你看到的,爲了發送消息,你必須使用 send 方法,它接收消息模式和數據做爲參數。此方法從 Rxjs 包返回一個 Observable

這是很是重要的特性,由於 Observables 提供了一組使人驚奇的操做來處理,例如combine, zip, retryWhen, timeout 等等。

固然,若是你想使用 Promise 而不是 Observables,你能夠直接使用 toPromise()  方法。

就這樣。

如今,當有人發送 /test 請求(GET)時,應該如何應用(若是微服務和 web 應用均可用):

{
    "result": 15
}

Redis

還有另外一種方式與 Nest 微服務合做。咱們可使用 Redis發佈/訂閱功能,而不是直接 TCP 通訊。

redis

在使用以前,你必須安裝 Redis。

建立 Redis 微服務

要建立 Redis 微服務,你必須在 NestFactory.createMicroservice() 方法中傳遞其餘配置。

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

就這樣。如今,你的微服務將訂閱經過 Redis 發佈的消息。其它方式依舊相同,如 模式和錯誤處理等等。

Redis 客戶端

如今讓咱們來看看如何建立客戶端。之前,你的客戶端配置以下:

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

咱們想使用 Redis 而不是 TCP, 因此咱們必須改變這些配置:

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

很容易,對麼? 就是這樣。其餘功能與 TCP 通訊中的功能相同。

共享模塊

Nest 模塊能夠導出其它組件。這意味着,咱們能夠輕鬆地在它們之間共享組件實例。

在兩個或者更多模塊之間共享實例的最佳方式是建立共享模塊

share_module

例如,若是咱們要在整個應用程序中共享 ChatGateway 組件,咱們能夠這樣作:

import { Module, Shared } from '@nestjs/common';

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

而後,咱們只須要將這個模塊導入到另外一個模塊中,這個模塊應該共享組件實例:

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

就這樣。

注意,也能夠直接定義共享模塊的範圍,瞭解更多細節

依賴注入

依賴注入是一個強大的機制,能夠幫助咱們輕鬆地管理咱們類的依賴。它是很是受歡迎的語言,如 C# 和 Java。

在 Node.js 中,這些功能並不重要,由於咱們已經有了神奇的模塊加載系統,如在文件之間共享實例的很容易的。

模塊加載系統對於中小應用足夠用了。當代碼增加時,順利組織層之間的依賴就變得更加困難。總有一天會變得爆炸。

它比 DI 構造函數更直觀。這就是爲何 Nest 有本身的 DI 系統。

自定義組件

你已經瞭解到了,將組件添加到所選的模塊是很是容易的。

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

是還有一些其餘狀況, Nest 容許你利用。

使用 value :

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

當:

  • 你想要使用具體的值,如今,在這個模式中, Nest 將把 valueUsersService 元類型相關聯,

  • 你想要使用單元測試。

使用 class :

@Component()
class CustomUsersService {}

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

當:

  • 你只想在此模塊中使用所選的更具體的類。

使用工廠

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

當:

  • 你想提供一個值,它必須使用其餘組件(或定製包特性)來計算,

  • 你要提供異步值(只返回 ObservablePromise),例如數據庫鏈接。

請記住:

  • 若是要使用模塊中的組件,則必須將它們傳遞給注入數組。 Nest 將按照相同的順序傳遞做爲參數的實例。

定製 providers

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

當:

  • 你想提供一個選擇的鍵值。

請注意:

  • 可使用各類類型的 useValue, useClassuseFactory

怎麼使用?

使用選擇的鍵注入自定義提供組件 / 值,你必須告訴 Nest,就像這樣:

import { Component, Inject } from '@nestjs/common';

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

`

ModuleRef

有時候你可能但願直接從模塊引用獲取組件實例。對於 Nest 並非一件大事,你只須要在類中注入 ModuleRef

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

ModuleRef 提供一個方法:

  • get<T>(key),它返回等效鍵值實例(主要是元類型)。如 moduleRef.get<UsersService>(UsersService) 從當前模塊返回 UsersService 組件的實例

例如:

moduleRef.get<UsersService>(UsersService)

它將從當前模塊返回 UsersService 組件的實例。

測試

Nest 爲你提供了一套測試工具,能夠提供應用測試過程。能夠有兩種方法來測試你的組件和控制器,隔離測試和使用專用的 Nest 測試工具

隔離測試

Nest 的控制器和組件都是一個簡單的 JavaScript 類。這意味着,你能夠很容易的本身建立它們:

const instance = new SimpleComponent();

若是你的類還有其它依賴,你可使用 test doubles,例如 JasmineSinon 庫:

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

專用的 Nest 測試工具

測試應用程序構建塊的另外一種方法是使用專用的 Nest 測試工具。

這些測試工具放在靜態 Test 類中(@nestjs/testing 模塊)。

import { Test } from '@nestjs/testing';

該類提供兩種方法:

  • createTestingModule(metadata: ModuleMetadata),它做爲參數接收簡單的模塊元數據(和 Module() class 相同)。此方法建立一個測試模塊(與實際 Nest 應用程序相同)並將其存儲在內存中。

  • get<T>(metatype: Metatype<T>),它返回選擇的實例(metatype 做爲參數傳遞),控制器/組件(若是它不是模塊的一部分,則返回 null)。

例如:

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

`

Mocks, spies, stubs

有時候你可能不想使用組件/控制器的實例。你能夠選擇這樣,使用 test doubles 或者 自定義 values / objects

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

異常過濾器(Exception Filters)

使用 Nest 能夠將異常處理邏輯移動到稱爲 Exception Filters 的特殊類。

如何工做?

讓咱們來看下下面的代碼:

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

想象一下,usersService.getUser(id) 方法會拋出一個 UserNotFoundException 異常。咱們須要在路由處理程序中捕獲異常:

@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();
    }
}

總而言之,咱們必須向每一個可能發生異常的路由處理添加 try ... catch 塊。還有其它方式麼? 是的,Exception Filters

讓咱們建立 NotFoundExceptionFilter

import { Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';

export class UserNotFoundException {}
export class OrderNotFoundException {}

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

`
如今,咱們只須要告訴咱們的 Controller 使用這個過濾器:

import { ExceptionFilters } from '@nestjs/common';

@ExceptionFilters(NotFoundExceptionFilter)
export class UsersController {}

因此若是 usersService.getUser(id) 會拋出 UserNotFoundException,那麼, NotFoundExceptionFilter 將會捕獲它。

更多異常過濾器

每一個控制器可能具備無限次的異常過濾器(僅用逗號分隔)。

@ExceptionFilters(NotFoundExceptionFilter, AnotherExceptionFilter)

依賴注入

Exception filters 與組件相同,所以能夠經過構造函數注入依賴關係。

HttpException

注意:它主要用於 REST 應用程序。

Nest 具備錯誤處理層,捕獲全部未處理的異常。

若是在某處,在你的應用程序中,你將拋出一個異常,這不是 HttpException(或繼承的),Nest 會處理它並返回到下面用戶的 json 對象(500 狀態碼):

{
    "message": "Unkown exception"
}

異常層次結構

在應用程序中,你應該建立本身的異常層次結構(Exceptions Hierarchy)。全部的 HTTP 異常都應該繼承自內置的 HttpException

例如,您能夠建立 NotFoundExceptionUserNotFoundException 類:

import { HttpException } from '@nestjs/core';

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

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

而後,若是你的應用程序中的某個地方拋出 UserNotFoundException,Nest 將響應用戶狀態代碼 404 及如下 json 對象:

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

它容許你專一於邏輯,並使你的代碼更容易閱讀。

有用的參考

相關文章
相關標籤/搜索