Nest.js 最终版已经放出,欢迎来试

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

Nest 是 Node.js圈子中又一强大的 Web 框架, 使用 Nest 可以轻松构建高效可拓展应用。 Nest 使用现代 JavaScript 技术构建,编码使用TypeScript,同时汲取了 OOP (面向对象编程) 以及 FP(函数式编程) 的精华所在。

Nest 不仅仅是一个 Web 框架,使用 Nest 时,无需担心有没有丰富的社区支持, 因为Nest 本身就基于闻名遐迩的老牌框架而建 – Expresssocket.io! 因此,学习 Nest 能够让你轻松上手,而不用学习一整套新的工具全家桶。

核心概念

Nest 的核心内容是提供一个服务框架 (architecture), 帮助开发者实现多层分离,业务抽象。

安装

$ npm install --save nest.js

配置应用

Nest 支持 ES6 和 ES7 (decorators, async / await) 新特性。 所以最简单方式就是使用 Babel 或者 TypeScript
本文中会使用 TypeScript (当然不是唯一方式),我也推荐大家使用这种方式,下边是一个简单的 tsconfig.json 文件

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

注意 emitDecoratorMetadata 以及 experimentalDecorators 需要设置为 true, 来支持项目中使用 Decorator 新特性。

现在可以写一个小服务了。首先,创建一个应用的入口模块(entry module) (app.module.ts):

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

此时尚未为模块添加修饰 metadata 是空对象 ({}), 我们现在只想让服务跑起来 (现在也没有控制器和组件能够让我们添加的).

第二步,编写主文件:index.ts (这个文件名随意,约定俗成,喜欢使用 Index) ,调用 Nest 提供的工厂函数创建 Nest应用实例,并将这个实例挂载在刚创建的入口模块上。

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 实例

如果你需要控制 express 实例的生命周期,可以将创建的实例传递给工厂函数 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'));

换句话说,你可以向工厂函数添加配置项,来生成想要的服务实例。(例如,添加常用的插件 morgan 或者 body-parser).

第一个控制器

控制器层用来处理 HTTP 请求. Nest 中,控制器是用 @Controller() 修饰的类。

Controllers_1
Controllers_1

上一节中,我们已经写好了程序入口,基于此,我们来构建一个应用端口 /users

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

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

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

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

正如所见,我们快速创建了同一个端口的3个应用路径:

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

看看刚创建的这个类,有木有可优化的地方呢? users 这个词貌似有点重复,能优化么?当然!只需要向 @Controller()添加元数据配置即可,这里添加一个路径,重写一下这个类。

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

世界忽然清净了。这里多写了一点,那就是所有的 Nest 控制器默认包含 Express 自带参数列表和参数方法。如果你想学习更多 req (request), res (response) 以及 next 的使用 你需要读一下 Express 路由相关的文档。在 Nest 中,这些参数能力依旧,不仅如此,Nest 还对其进行了拓展。 Nest提供了一整套网络请求装饰器,你可以用这些装饰器来增强请求参数处理。

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

使用这些装饰器的时候,记得从 Nest 引入 Param 模块

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

现在 UsersController 已经可以投放使用了,但模块并不知道有这个控制器的存在,所以进入 ApplicationModule 给装饰器添加些元数据。

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

正如所见,引入控制器,并将其添加到元数据的 controllers 数组,就完成了注册。

组件

Nest 中所有内容都可以视为组件 Service, Repository, Provider 等都是组件。同时组件可以相互嵌套,也可以作为服务从构造函数里注入控制器。

Components_1
Components_1

上一节,我们创建了一个简单的控制器,UsersController。这个控制器负责响应网络请求并将请求映射到数据(当然现在还是假数据,但并不影响)上。实际上,这并不是好的实践,数据和请求分离才更便于分离依赖,抽象业务。所以应该将控制器的职责单一化,只负责响应网络请求,而将更复杂的任务交付给 服务 来实现。我们称这个抽离出来的依赖叫 UsersService 组件。实际开发中,UsersService应从持久层调用方法实现数据整合,比如从 UsersRepository组件,为了说明简单,我们暂时跳过这个阶段,依然使用假数据来说明组件的概念。

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() 方法中调用了 HttpException 模块,这是 Nest 内置的 Http 请求异常处理模块,传入两个参数,错误信息和返给客户端的状态码。 优先处理异常是一种优良习惯,更多情况可以拓展 HttpException 来实现 (更多细节查看错误处理小结)。如此,我们的用户服务已经就绪,那就在用户控制器中使用起来吧。

@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参数,因为这个场景中并未用到。

正如所见,UsersService 会自动注入控制器,这是 TypeScript 提供的语法糖,让你轻而易举的实现依赖注入,Nest 能够从这种方式中识别出对应的依赖。

constructor(private usersService: UsersService)

如上,即可。再次声明,如果你在自己 demo 时如果报错,请查看 tsconfig.json 文件中是否将 emitDecoratorMetadata 置为 true, 当然,你从头就没用 ts,无可厚非,就像你非得满足 IE6用户需求一样,Nest 也提供了相应的写法。

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

也不难是吧,眼熟不?当然,没有 ts 之前,我项目里绝大多数依赖注入都是如此实现的。不过,当你运行项目的时候,又要报错了,因为 Nest 和 ApplicationModule 并不知道 UserService 是什么东西,需要像添加控制器一样,将组件(服务,提供商,持久层)也添加到模块里,统一叫做组件。

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

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

齐活!现在应用能够正常跑了,不过路由 addUser 貌似不太管用, 为啥呢?因为我们并不能正确读取 req.body.user 啊,这时候咋整呢?这一步的思考至关重要,是自己造轮子解决这个问题,还是用别人的轮子?首先告诉你有 body-parser这个express中间件 解决这个问题,你可以在 express 实例中使用这些插件。当然,你可以自己写一个组件来处理这个问题(从用轮子到造轮子)。

让我们来装上这个神奇的插件

$ npm install --save body-parser

express实例中使用起来。

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'));

异步回调方案 Async/await (ES7)

Nest 支持使用 ES7的 async / await 解决异步回调问题, 所以,我们可以顺利的重构 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); 
    }
}

这样能看起来舒服一点是吧,至少 async/await 要比 promise/then 方案更酷炫一些。更多可以去看 async / await.

模块

模块是用 @Module({}) 装饰器修饰的类。Nest 会使用这些元数据来组织模块的结构。

Modules_1
Modules_1

截止到现在,我们的 ApplicationModule 看上去是这样的。

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

默认情况下,模块封装了所有的依赖,换句话说,控制器和组件对于模块外部是透明的。模块之间可以相互引用,实际上,Nest 本身也是一个模块树。基于最佳实践,我们将 UsersControllerUsersService 转移到 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,并在元数据中使用 modules 声明。

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

齐活!正如所见,你可以根据业务需求,将部分模块抽离出来做成可重用的模块。

依赖注入

模块可以轻松的注入她自己的组件,在元数据里说明一下就行了。

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

与此同时,组件也可以注入模块

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

中间件

中间件本质是一个函数,这个函数在路由处理器之前调用。中间件能够查看请求和回复对象,同时也能修改这些内容。你可以把他们理解为 围栏。如果中间件不放行,请求是不会抵达路由处理控制器的。

Middlewares_1
Middlewares_1

我们来构建一个虚拟的授权中间件(简单起见,只验证 username)。我们使用 HTTP 的头部信息 X-Access-Token 来提供 username(这种实现很奇怪,不过例子嘛,不要太认真)

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

关于中间件,你需要知道的只有四条

  • 你使用 @Middleware() 装饰器告诉 Nest,下边这个类是个中间件
  • 你可以使用 NestMiddleware 模块提供的接口,这样你就可以将注意力集中在输入输出上了,换句话说,你只需要实现 resolve()函数
  • 像组件一样,中间件可以通过构造函数注入其他组件作为依赖,这里的组件应该是模块内部的。
  • 中间件必须包含一个 resolve()函数, 同时return 一个新的函数(高阶函数)为啥这么设计呢?因为大量的 express 第三方插件都是这么搞,Nest 也支持的话,就能够让大家轻松平移过来了。

注意: 上边代码中子在 UsersService 添加了一个 getUsers 方法,用以封装获取所有用户。

现在我们有这个中间件了,尝试用起来吧。

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 内部调用 configure() 方法。方法有一个参数,通过参数配置中间件消费者 MiddlewaresConsumer, 这是一个用于配置中间件的对象。这个对象有 apply() 方法,可以接收逗号分隔的多个中间件,apply() 方法返回一个对象,有两个内置方法

  • forRoutes() – 将中间件配置给哪个控制器使用,可以用逗号分隔多个
  • with – 向中间件反向传递参数

原理是啥呢?

当把 UsersController作为参数传递给 forRoutes方法时,Nest 会自动为控制器配置中间件。

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

也可通过配置的方式进行逐个操作,告知中间件作用在哪个路由上。一般用于指明某一个认证作用于全部控制器的场景。

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

链式操作

Nest 支持链式操作,你可以轻松实现以下代码

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

调用顺序

中间件按照其出现在数组中的顺序依次调用,子模块的中间件会在父模块中间件调用之后调用。

基于网关的实时应用

Nest 中有一种独特的组件叫做网关。网关可以帮助开发者构造实时应用。Nest 在实现过程中封装了 socket.io中的部分特性。

Gateways_1
Gateways_1

网关也是组件,一样可以通过构造函数注入依赖,当然也可作为其他组件的依赖。

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

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

默认情况下,Websocket 会开通80端口,并使用默认命名空间,可以通过向装饰器传递元数据的方式进行配置。

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

当然,想要网关生效,还需要将其加入主模块,跟其他服务组件并列。

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

另外,网关有三个重要的生命周期钩子:

  • afterInit 有个本地服务器 socket.io 对象可以操作,一般写为 server
  • handleConnection/handleDisconnect 有个本地客户端 socket.io 对象可以操作,一般写为 client
  • OnGatewayInit/OnGatewayConnection/OnGatewayDisconnect 网关状态管理接口

如何传递数据

在网关中可以轻松订阅广播信息,使用 SubscribeMessage 装饰器实现。

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 对象}
}

客户端可以轻松实现

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, 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 对象}
}

服务端初始化以后便完成指定。

网关中间件

网关中间件与路由中间件相似。中间件是一个函数,在订阅事件触发之前调用。网关可以操作本地 socket 对象。你可以把他们理解为 围栏。如果中间件不放行,信息是不会被广播的。

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

网关中间件须知须会:(跟中间件一节一毛一样,我是拖戏的,骗稿费的)

  • 你需要通过 @Middeware 告知 Nest 这是一个中间件
  • 你可以继承自 GatewayMiddleware然后集中处理 resolve 中的内容
  • 像组件一样,中间件可以通过构造函数注入其他组件作为依赖,这里的组件应该是模块内部的。
  • 中间件必须包含一个 resolve()函数, 同时return 一个新的函数(高阶函数)

中间件搞好了,就用起来。

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

如上所见,网关@WebSocketGateway()接收元数据来配置使用的中间件,这里可以配置一组中间件,他们有个名,叫做“事前诸葛亮”

交互流

如上所见,Nest 中的网关也是组价,可以作为依赖注入其他组件,这就能让我们针对不同的信息作出不同的操作。比如做信息过滤(fuck=>fu*k),当然也可将组件注入网关,将网关的功能再次封装拆分。有个话题专门解决这个方向,网关交互流,你可以去这里查看详情

微服务

将 Nest 应用服务转为微服务异常简单。创建服务应用的时候这么写:

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

转成微服务,只需要改一个方法即可,Java 工程师,PHP?看一看!

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

使用 TCP 进行通信

Microservices_1
Microservices_1

默认情况下,Nest 的微服务监听 TCP 协议信息,于是微服务中 @RequestMapping() (以及 @Post(), @Get()) 这些装饰器就用不到了,因为他们基于 HTTP协议。微服务中如何识别信息呢?Nest 提供了 patterns,这可能是对象,字符串,甚至数字(当然,数字不太好)

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)装饰器,装饰器中的元数据cmd指明了处理数据的方式,默认传递两个参数

  • data, 其他微服务传递过来的数据,或者网络数据
  • respond 响应方法,默认传递两个参数(错误,响应)

客户端

能够接收数据还没完事,你得知道怎么发送数据。在发送之前,你需要告诉 Nest 你给谁发数据。实际上也很简单,首先你脑子里需要构建一个 server-client 链接,你数据的接收方就是你的 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() 装饰器接收三个元数据配置项

  • transport – 传递数据的通道和协议,可以是 Transport.TCP 或者 Transport.REDIS,默认为 TCP
  • url – 只有传递通道为 Redis 的时候用到(默认 – redis://localhost:6379),
  • port - 端口号(默认 3000).

创建客户端

创建一个端口服务来验证一下刚刚创建的 Server-Client 微服务通道

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

如上所见,如果你想发送信息,你需要调用 send 方法,发送数据被封装为参数,一个 pattern一个 data.这个方法返回了 Rxjs 式的观察者对象。这可是不得了的一个特性,因为交互式观察者对象提供了一整套炫酷的方法。比如 combine, zip, retryWhen, timeout 等等,当然你还是喜欢用 Promises-then 那套,你可以使用 toPromise() 方法。由此,当请求发送到此控制器时,就会得到下边的结果(前提是微服务和 Web应用都启动起来)

{ "result": 15}

Redis

上文书 Nest 微服务还能链接 Redis,与 TCP 通信不同,与 Redis链接就能使用其自带的各种特性,最赞的当属 – 发布/订阅模式.

Redis_1
Redis_1

当然,你得先安装 Redis.

创建 Redis 微服务

创建一个 Redis 微服务也很简单,在创建微服务时,传递 Redis 服务配置即可。

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

如此便链接了 Redis,其服务中心分发的信息就能够被此微服务捕获到。剩下的工作跟 TCP 微服务一样。

Redis 客户端

现在仿照微服务创建一个客户端,在 TCP 微服务中,你的客户端配置像下边这样

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

Redis 微服务的客户端修改一下即可,添加一个 url 配置项

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

是不是很简单,数据操作和管道跟 TCP的微服务一致。

模块共享

Nest 可以指明向外暴漏哪些组件,换句话说,我们能够轻松地在模块之间共享组件实例。更好的方式当然是使用 Nest 内置的 Shared 模块.

Shared_Module_1
Shared_Module_1

以共享 ChatGateway 组件为例,添加一个 exports 关键字来指明导出的组件服务。

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

完事儿作为依赖注入到其他模块就行了。

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

完工齐活,你可以在 FeatureModule 中直接使用 ChatGateway,就跟自己模块内部一毛一样。

依赖注入

依赖注入是一个强大的特性,能够帮我们轻松管理类的依赖。在强类型语言,比如 Java 和 .Net中,依赖注入很常见。在 Nodejs 中,这种特性并不重要,因为我们已经拥有完善的模块加载机制。例如,我可以不费吹灰之力,从另外一个文件中引入一个模块。模块加载机制对于中小型应用已经足够了。但随着代码量逐步增长,系统层级复杂以后,通过引用的方式管理他们之间的依赖会越来越麻烦。现在没错,不代表未来不会错。所以自动化实现依赖管理呼之欲出。

giphy
giphy

这种方式尚不如通过构造函数导入依赖更加清晰明了,这也是 Nest 为何内置依赖注入机制的缘故。

自定义组件

依上文,你可以轻松将控制器绑定到模块,这种注入方式异常简单

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

实际开发中,注入到模块的可能并非指明的这些关键字。

useValue

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

使用场景

  • 用于向服务插值,如此一来,UsersService的元数据中就会有 value这个值,
  • 用于单元测试,注入初始值

useClass

@Component()
class CustomUsersService {}

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

使用场景

  • 向指定的服务注入特定的类。

useFactory

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

使用场景

  • 使用其他组件之前需要计算得到一个值,并注入这个组件
  • 服务的启动依赖一个异步值,比如读写文件或者链接数据库

注意

  • 如果想要调用其他模块的内部控制器和组件,需要注明引用

自定义提供者

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

总的来说,我们在可能出现异常的地方添加 try...catch 语句块,更好的方式当然是使用内置的异常过滤器。

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 {}

此时,如果找不到用户就会调起这个异常。

其他异常过滤器

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

@ExceptionFilters(NotFoundExceptionFilter, AnotherExceptionFilter)

异常捕获的依赖注入

异常过滤器与组件一样工作,所以可以通过构造函数注入依赖

HttpException

注意,这个异常模块主要用于 RESTful API 构造

Nest 内置了错误处理层,可以捕获所有未处理的异常,如果抛出一个非 HttpException,Nest 会自动处理并返回下边这个 Json 对象(状态码 500):

{ "message": "Unkown exception"}

异常层次结构

在你的应用中,你应该创建你自己的错误层级。所有 HTTP异常相关都可以继承自内置 HttpException,例如,你可以创建 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.'); 
    }
}

当你项目中出现这两个异常的时候,Nest会调起你自己配置的这个异常处理句柄。

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

如此一来,你可以将将精力集中在业务逻辑和代码实现上了。

更多参考

GitHub
文档
NPM

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 最近再看阮一峰的一篇博客提到了一本书《Software Architecture Patterns》(PDF),写...
    卓_然阅读 7,463评论 0 22
  • 最近因业务需要,小拾君深入学习了一下微服务架构相关的技术,跟大家分享一下。本文并不会涉及太多晦涩难懂的技术术语以及...
    每日一拾阅读 7,365评论 0 28
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 今天早上把我家二宝宝送回梁村了,准备给她断奶了。刚送回家就舍不得走了,真的看着还小又有点后悔了,不过没办法...
    王嫒阅读 209评论 0 0