inversify-express-utils

原文链接

使用InversifyJS开发Express应用的一些实用工具

安装

您可以使用npm来安装inversify-express-utils

npm install inversify inversify-express-utils reflect-metadata --save
1

inversify-express-utils的类型定义已经包含至npm模块中,需要TypeScript版本 > 2.0

基础

第一步:装饰你的控制器

想要将类作为Express的“控制器(controller)”,只需要将@controller装饰器添加到类中即可。同理我们可以将类的方法作为请求句柄进行修饰。

下面的示例展示了如何声明一个控制器来处理GET /foo请求:

import * as express from "express";
import { interfaces, controller, httpGet, httpPost, httpDelete, request, queryParam, response, requestParam } from "inversify-express-utils";
import { injectable, inject } from "inversify";

@controller("/foo")
export class FooController implements interfaces.Controller {

    constructor( @inject("FooService") private fooService: FooService ) {}

    @httpGet("/")
    private index(req: express.Request, res: express.Response, next: express.NextFunction): string {
        return this.fooService.get(req.query.id);
    }

    @httpGet("/")
    private list(@queryParam("start") start: number, @queryParam("count") count: number): string {
        return this.fooService.get(start, count);
    }

    @httpPost("/")
    private async create(@request() req: express.Request, @response() res: express.Response) {
        try {
            await this.fooService.create(req.body);
            res.sendStatus(201);
        } catch (err) {
            res.status(400).json({ error: err.message });
        }
    }

    @httpDelete("/:id")
    private delete(@requestParam("id") id: string, @response() res: express.Response): Promise<void> {
        return this.fooService.delete(id)
            .then(() => res.sendStatus(204))
            .catch((err: Error) => {
                res.status(400).json({ error: err.message });
            });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

第二步:配置您的容器和服务

像往常一样配置您的inversify容器。

接下来,将容器传入InversifyExpressServer构造器。这样就把容器中所有的控制器和依赖都进行了注册,并附加到express应用中。然后调用server.build()来准备应用程序。

为了让InversifyExpressServer能够找到您的控制器,您必须将其绑定到TYPE.Controller服务标识符并用控制器的名称作为绑定的标签。inversify-express-utils导出的Controller接口是空的,只是为了方便,所以如果需要,可以随意实现自己的控制器接口。

import * as bodyParser from 'body-parser';

import { Container } from 'inversify';
import { interfaces, InversifyExpressServer, TYPE } from 'inversify-express-utils';

// declare metadata by @controller annotation
import "./controllers/foo_controller";

// set up container
let container = new Container();

// set up bindings
container.bind<FooService>('FooService').to(FooService);

// create server
let server = new InversifyExpressServer(container);
server.setConfig((app) => {
  // add body parser
  app.use(bodyParser.urlencoded({
    extended: true
  }));
  app.use(bodyParser.json());
});

let app = server.build();
app.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

关于@controller装饰器的一些重要信息

我们已经发布了inversify-express-util@5.0.0版本。在这个版本中,在使用了@controller注释的类中不必再使用@injectable注释了。同样地,在使用@controller注释的类中,也不再需要声明控制器的类型绑定。

⚠️ 声明绑定对控制器来说不是必需的,但是需要一次性导入控制器。当控制器文件被导入时(例如,import "./controllers/some_controller"),类被声明,元数据被生成。如果您不导入它,就无法生成元数据,因此也就找不到控制器。可以看这个示例

如果您在一个共享的运行时程序中(比如,单元测试)多次运行应用,您可能需要在每次测试之前清理现有的元数据。

import { cleanUpMetadata } from "inversify-express-utils";

describe("Some Component", () => {

    beforeEach(() => {
        cleanUpMetadata();
    });

    it("Some test case", () => {
        // ...
    });

});
1
2
3
4
5
6
7
8
9
10
11
12
13

这里有单元测试示例。

如果应用程序没有控制器,那么inversify-express-utils将抛出异常。您可以通过设置forceControllers选项来禁用该行为。这里有一些关于forceControllers的单元测试示例。

InversifyExpressServer

对express应用的封装。

.setConfig(configFn)

配置项 —— 暴露express应用对象,以方便加载服务器级别的中间件。

import * as morgan from 'morgan';
// ...
let server = new InversifyExpressServer(container);

server.setConfig((app) => {
    var logger = morgan('combined')
    app.use(logger);
});
1
2
3
4
5
6
7
8

setErrorConfig(errorConfigFn)

配置项 —— 与.setConfig()相类似,只是这个函数是在注册所有应用中间件和控制器路由之后进行应用的。

let server = new InversifyExpressServer(container);
server.setErrorConfig((app) => {
    app.use((err, req, res, next) => {
        console.error(err.stack);
        res.status(500).send('Something broke!');
    });
});
1
2
3
4
5
6
7

.build()

将所有注册的控制器和中间件连接到express应用。返回应用的实例。

// ...
let server = new InversifyExpressServer(container);
server
    .setConfig(configFn)
    .setErrorConfig(errorConfigFn)
    .build()
    .listen(3000, 'localhost', callback);
1
2
3
4
5
6
7

使用自定义路由

可以将自定义Router实例传递给InversifyExpressServer

let container = new Container();

let router = express.Router({
    caseSensitive: false,
    mergeParams: false,
    strict: false
});

let server = new InversifyExpressServer(container, router);
1
2
3
4
5
6
7
8
9

默认情况下,服务器以/path路径提供API,不过有时候可能需要使用不同的根命名空间,举例来说如果规定所有的路由都应该以/api/v1开头。我们可以通过路由配置将该设置传递给InversifyExpressServer

let container = new Container();

let server = new InversifyExpressServer(container, null, { rootPath: "/api/v1" });
1
2
3

使用自定义express应用

可以向InversifyExpressServer传递自定义的express.Application实例:

let container = new Container();

let app = express();
//Do stuff with app

let server = new InversifyExpressServer(container, null, null, app);
1
2
3
4
5
6

内置装饰器说明

@controller(path, [middleware, ...])

将所装饰的类注册为具有根路径的控制器,并且可以为该控制器注册任意的全局中间件。

@httpMethod(method, path, [middleware, ...])

将所装饰的控制器方法注册为特定路径和请求方式的请求句柄,需要注意的是方法名应该是合法的express路由方法。

@SHORTCUT(path, [middleware, ...])

Shortcut装饰器是对@httpMethod的简单封装。它包括了@httpGet,@httpPost@httpPut@httpPatch@httpHead@httpDelete@All。如果想要这之外的功能,请使用@httpMethod(或者给我们提个PR 😄)。

@request()

将方法参数绑定到请求对象。

@response()

将方法参数绑定到响应对象。

@requestParam(name: string)

将方法参数绑定到request.params。如果传入了name值,则绑定到对应name值的参数。

@queryParam(name: string)

将方法参数绑定到request.query。如果传入了name值,则绑定到对应name值的查询参数。

@requestBody()

将方法参数绑定到request.body。如果express应用中没有使用bodyParser中间件,那么该方法将把参数绑定到express请求对象上。

@requestHeaders(name: string)

将方法参数绑定到请求头。

cookies(name: string)

将方法参数绑定到请求cookies。

@next()

将方法参数绑定到next函数。

@principal()

将方法参数绑定到从AuthProvider获得的用户主体。

BaseHttpController

BaseHttpController是一个基类,它内置了很多辅助函数,用以帮助编写可测试的控制器。当从我们获得从这些控制器中定义的方法所返回的响应时,您可以使用下面将要介绍的httpContext属性上可用的response对象,或是返回一个HttpResponseMessage,亦或是返回一个实现IHttpActionResult接口的对象。

后两种方法的好处是:因为你的控制器对请求httpContext发送一个响应不在是直接耦合了,所以不必模拟整个响应对象,您可以简单地在返回值上运行断言。此外,整个API还允许我们在这一领域进行进一步改进,同时添加一些相似框架(如 .NET WebAPI)的功能,如媒体格式化、内容协商等等。

import { injectable, inject } from "inversify";
import {
    controller, httpGet, BaseHttpController, HttpResponseMessage, StringContent
} from "inversify-express-utils";

@controller("/")
class ExampleController extends BaseHttpController {
    @httpGet("/")
    public async get() {
        const response = new HttpResponseMessage(200);
        response.content = new StringContent("foo");
        return response;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

BaseHttpController上,我们提供了成吨的辅助方法来简化返回常见IHttpActionResults的过程,包括

  • OkResult
  • OkNegotiatedContentResult
  • RedirectResult
  • ResponseMessageResult
  • StatusCodeResult
  • BadRequestErrorMessageResult
  • BadRequestResult
  • ConflictResult
  • CreatedNegotiatedContentResult
  • ExceptionResult
  • InternalServerError
  • NotFoundResult
  • JsonResult
import { injectable, inject } from "inversify";
import {
    controller, httpGet, BaseHttpController
} from "inversify-express-utils";

@controller("/")
class ExampleController extends BaseHttpController {
    @httpGet("/")
    public async get() {
        return this.ok("foo");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

JsonResult

在某些场景中,需要设置一下响应的状态码。可以通过使用BaseHttpController所提供的json辅助方法来实现。

import {
    controller, httpGet, BaseHttpController
} from "inversify-express-utils";

@controller("/")
export class ExampleController extends BaseHttpController {
    @httpGet("/")
    public async get() {
        const content = { foo: "bar" };
        const statusCode = 403;

        return this.json(content, statusCode);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这就让灵活地创建自定义响应成为可能,同时还能保持单元测试的简单性。

import { expect } from "chai";

import { ExampleController } from "./example-controller";
import { results } from "inversify-express-utils";

describe("ExampleController", () => {
    let controller: ExampleController;

    beforeEach(() => {
        controller = new ExampleController();
    });

    describe("#get", () => {
        it("should have a status code of 403", async () => {
            const response = await controller.get();

            expect(response).to.be.an.instanceof(results.JsonResult);
            expect(response.statusCode).to.equal(403);
        });
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

本示例使用了MochaChai作为单元测试框架。

HttpContext

HttpContext属性方便我们访问当前请求、响应和用户信息。HttpContextBaseHttpController派生的控制器中的一个属性。

import { injectable, inject } from "inversify";
import {
    controller, httpGet, BaseHttpController
} from "inversify-express-utils";

@controller("/")
class UserPreferencesController extends BaseHttpController {

    @inject("AuthService") private readonly _authService: AuthService;

    @httpGet("/")
    public async get() {
        const token = this.httpContext.request.headers["x-auth-token"];
        return await this._authService.getUserPreferences(token);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果您想要创建自定义控制器,您需要使用@injectHttpContext装饰器手动注入HttpContext

import { injectable, inject } from "inversify";
import {
    controller, httpGet, BaseHttpController, httpContext, interfaces
} from "inversify-express-utils";

const authService = inject("AuthService")

@controller("/")
class UserPreferencesController {

    @injectHttpContext private readonly _httpContext: interfaces.HttpContext;
    @authService private readonly _authService: AuthService;

    @httpGet("/")
    public async get() {
        const token = this.httpContext.request.headers["x-auth-token"];
        return await this._authService.getUserPreferences(token);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

AuthProvider

如果不创建自定义的AuthProvider实现的话,HttpContext将无法访问当前用户。

const server = new InversifyExpressServer(
    container, null, null, null, CustomAuthProvider
);
1
2
3

我们需要实现一下AuthProvider接口。

AuthProvider允许我们获取用户的主体(Principal);

import { injectable, inject } from "inversify";
import { interfaces } from "inversify-express-utils";

const authService = inject("AuthService");

@injectable()
class CustomAuthProvider implements interfaces.AuthProvider {

    @authService private readonly _authService: AuthService;

    public async getUser(
        req: express.Request,
        res: express.Response,
        next: express.NextFunction
    ): Promise<interfaces.Principal> {
        const token = req.headers["x-auth-token"]
        const user = await this._authService.getUser(token);
        const principal = new Principal(user);
        return principal;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

当然Principal接口也需要我们亲自操刀。它要满足的功能包括:

  • 访问用户的详细信息。
  • 检查其对某些资源的访问权限。
  • 检查其是否经过验证。
  • 检查其是否在用户角色中。
class Principal implements interfaces.Principal {
    public details: any;
    public constructor(details: any) {
        this.details = details;
    }
    public isAuthenticated(): Promise<boolean> {
        return Promise.resolve(true);
    }
    public isResourceOwner(resourceId: any): Promise<boolean> {
        return Promise.resolve(resourceId === 1111);
    }
    public isInRole(role: string): Promise<boolean> {
        return Promise.resolve(role === "admin");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

接下来我们就可以通过HttpContext来访问当前用户的主体(Principal):

@controller("/")
class UserDetailsController extends BaseHttpController {

    @inject("AuthService") private readonly _authService: AuthService;

    @httpGet("/")
    public async getUserDetails() {
        if (this.httpContext.user.isAuthenticated()) {
            return this._authService.getUserDetails(this.httpContext.user.details.id);
        } else {
            throw new Error();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

BaseMiddleware

BaseMiddleware的扩展将允许我们在Express的中间件函数内注入依赖并访问当前的HttpContext

import { BaseMiddleware } from "inversify-express-utils";

@injectable()
class LoggerMiddleware extends BaseMiddleware {
    @inject(TYPES.Logger) private readonly _logger: Logger;
    public handler(
        req: express.Request,
        res: express.Response,
        next: express.NextFunction
    ) {
        if (this.httpContext.user.isAuthenticated()) {
            this._logger.info(`${this.httpContext.user.details.email} => ${req.url}`);
        } else {
            this._logger.info(`Anonymous => ${req.url}`);
        }
        next();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们还需要声明一些类型绑定:

const container = new Container();

container.bind<Logger>(TYPES.Logger)
        .to(Logger);

container.bind<LoggerMiddleware>(TYPES.LoggerMiddleware)
         .to(LoggerMiddleware);
1
2
3
4
5
6
7

接着我们就可以将TYPES.LoggerMiddleware注入我们的控制器中了:

@injectable()
@controller("/")
class UserDetailsController extends BaseHttpController {

    @inject("AuthService") private readonly _authService: AuthService;

    @httpGet("/", TYPES.LoggerMiddleware)
    public async getUserDetails() {
        if (this.httpContext.user.isAuthenticated()) {
            return this._authService.getUserDetails(this.httpContext.user.details.id);
        } else {
            throw new Error();
        }
    }
}

container.bind<interfaces.Controller>(TYPE.Controller)
         .to(UserDetailsController)
         .whenTargetNamed("UserDetailsController");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

请求范围服务

扩展BaseMiddleware的中间件能够在HTTP请求范围内重新绑定服务。如果您需要访问服务中的HTTP请求或是服务中无法直接访问的特定上下文属性,那么这个功能就是为你准备的了。

来看一下下面的TracingMiddleware中间件。在本示例中,我们希望从传入的请求中捕获X-Trace-Id报头,并将其作为TYPES.TraceIdValue提供给IoC服务。

import { inject, injectable } from "inversify";
import { BaseHttpController, BaseMiddleware, controller, httpGet } from "inversify-express-utils";
import * as express from "express";

const TYPES = {
    TraceId: Symbol.for("TraceIdValue"),
    TracingMiddleware: Symbol.for("TracingMiddleware"),
    Service: Symbol.for("Service"),
};

@injectable()
class TracingMiddleware extends BaseMiddleware {

    public handler(
        req: express.Request,
        res: express.Response,
        next: express.NextFunction
    ) {
        this.bind<string>(TYPES.TraceIdValue)
            .toConstantValue(`${ req.header('X-Trace-Id') }`);

        next();
    }
}

@controller("/")
class TracingTestController extends BaseHttpController {

    constructor(@inject(TYPES.Service) private readonly service: Service) {
        super();
    }

    @httpGet(
        "/",
        TYPES.TracingMiddleware
    )
    public getTest() {
        return this.service.doSomethingThatRequiresTheTraceID();
    }
}

@injectable()
class Service {
    constructor(@inject(TYPES.TraceIdValue) private readonly traceID: string) {
    }

    public doSomethingThatRequiresTheTraceID() {
        // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

BaseMiddleware.bind()方法将:

  • 绑定TYPES.TraceIdValue(如果还没有绑定)。
  • 重新绑定TYPES.TraceIdValue(如果已经绑定)。

路由映射

如果我们有和下面类似情况的控制器:

@controller("/api/user")
class UserController extends BaseHttpController {
    @httpGet("/")
    public get() {
        return {};
    }
    @httpPost("/")
    public post() {
        return {};
    }
    @httpDelete("/:id")
    public delete(@requestParam("id") id: string) {
        return {};
    }
}

@controller("/api/order")
class OrderController extends BaseHttpController {
    @httpGet("/")
    public get() {
        return {};
    }
    @httpPost("/")
    public post() {
        return {};
    }
    @httpDelete("/:id")
    public delete(@requestParam("id") id: string) {
        return {};
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

我们可以使用prettyjson函数来查看所有可用的端点:

import { getRouteInfo } from "inversify-express-utils";
import * as prettyjson from "prettyjson";

// ...

let server = new InversifyExpressServer(container);
let app = server.build();
const routeInfo = getRouteInfo(container);

console.log(prettyjson.render({ routes: routeInfo }));

// ...
1
2
3
4
5
6
7
8
9
10
11
12

WARNING

请确保在调用server.build()之后调用getRouteInfo嗷!

prettyjson的输出格式如下:

routes:
  -
    controller: OrderController
    endpoints:
      -
        route: GET /api/order/
      -
        route: POST /api/order/
      -
        path: DELETE /api/order/:id
        route:
          - @requestParam id
  -
    controller: UserController
    endpoints:
      -
        route: GET /api/user/
      -
        route: POST /api/user/
      -
        route: DELETE /api/user/:id
        args:
          - @requestParam id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

示例

这里有一些示例可供参考。

上次更新: 1/14/2020, 4:49:04 PM