【译】基于 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."
}

它容许你专一于逻辑,并使你的代码更容易阅读。

有用的参考

相关文章
相关标签/搜索