Dart 服务端开发 shelf_bind 包

介绍

提供shelf中间件,容许您将普通Dart功能用做货架处理程序。java

shelf_bind赋予你:数据库

  • 使用您本身的方法而没必要担忧shelf样板
  • 专一于使用您本身的类编写业务逻辑,并让shelf_bind处理将其装入shelf

shelf_bind倾向于约定优于配置,所以您能够编写必要的最小代码,但仍然能够根据须要覆盖默认值。json

shelf_bind是一个强大的绑定框架,支持:app

  • 绑定到简单类型
  •        包括类型转换
  • 绑定到您本身的域对象
  •       经过属性setter方法
  •       经过构造函数
  • 来自请求path,query,body和header字段的绑定
  • 注入本身的自定义参数,如http clients
  • 与shelf_route无缝集成(并与mojito和shelf_rest捆绑在一块儿)
  • 带约束的自动参数验证
  • snake_case和camelCase之间的自动转换,用于查询参数以及kebab-case和camelCase之间的标头
  • 合理的默认值意味着大多数状况下不须要注释,可是在须要时可使用注释。

它能够用做独立的shelf组件,也能够做为将其与其余组件集成的框架的一部分。框架

将它与shelf_route一块儿使用的最简单方法是使用mojito或shelf_rest,由于他们的路由器已经在shelf_bind中链接。async

若是您刚开始,我建议首先查看mojito并使用此README做为有关处理程序绑定的更多详细信息。函数

独立使用

若是您使用带有mojito或shelf_rest的shelf_bind,则能够跳过此独立使用部分。post

bind函数从普通的dart函数建立一个shelf Handler。ui

var handler = bind(() => "Hello World");

这会建立一个等效于的 shelf Handlerthis

var handler = (Request request) => new Response.ok("Hello World");

若是函数返回Future,那么它将映射到Future <Response>

bind(() => new Future.value("Hello World"))

如今你能够设置一个shelf-io server来为你带来急需的问候世界(awthanks)

io.serve(bind(() => "Hello World"), 'localhost', 8080);

路径参数

添加到函数中的任何简单类型参数都将与同名的路径参数匹配。

名称将自动在snake_case和camelCase之间转换

(String name) => "Hello $name"

shelf_bind支持绑定到任何路径参数,包括:

  • path segments 如 /greeting/fred
  • query parameters 如 /greeting?name=fred

它使用shelf_path访问路径参数,这意味着它将与任何使用shelf_path在Request上下文属性中存储路径参数的中间件(例如shelf_route)一块儿使用。

这也意味着它不依赖于任何特定的表示路径的格式。 例如,路径是否认义为/ greeting /:name或/ greeting / {name}或/ person {?name}或其余什么并不重要。

简单类型

您还能够绑定到int这样的简单类型

(String name, int age) => "Hello $name of age $age"

支持

  • num
  • int
  • double
  • bool
  • DateTime
  • Uri

若是您想要支持新类型,请提交功能请求(或pull请求)

可选的命名参数

您也可使用带有默认值的可选命名参数。

(String name, {int age: 20}) => "Hello $name of age $age"

若是在上下文中未提供(或为null)命名参数,则将使用默认值。

将多个路径参数绑定到您的类中

您能够将多个路径参数绑定到您本身的类中。 高级部分对此进行了描述。

Request Body

默认状况下,非简单类型的处理程序参数来自body。

这包括:

  • Map
  • List
  • 您的任何类(未注册为自定义对象)。

例如,下面的处理程序参数都将被假定为来自request body。

(Map myMap) => ...

(List myList) => ...

(Person myMap) => ...

shelf_bind目前支持JSONFORM编码的主体。

默认状况下,shelf_bind尝试肯定请求内容类型的编码,以下所示:

  • 若是没有,则假定body为JSON
  • 若是设置了content-type而且是FORM或JSON,那么它将做为该类型处理
  • 若是是任何其余内容类型,则返回400响应

您可使用@RequestBody注解覆盖此行为。 若是存在@RequestBody注解,则内容将被视为注解中提供的类型。

例如,不管请求内容类型如何,如下内容都将被视为FORM编码

(@RequestBody(format: ContentType.FORM) Map myMap) => ...

Shelf Request Object

只需将其做为参数添加到函数中,便可访问shelf Request对象。

注意:因为您能够直接访问请求的全部部分,包括标题,所以您不多须要这样作。

(String name, Request request) => "Hello $name ${request.method}"

Response

Response Body

默认状况下,经过调用JSON.encode将函数的返回值编码为JSON。

例如,您能够返回地图

() => { "greeting" : "Hello World" }

这适用于任何能够编码为JSON的内容,包括任何自定义类

class SayHello {
  String greeting;

  Map toJson() => { 'greeting': greeting };
}

SayHello myGreeter() => new SayHello()..greeting = "Hello World"

Response Status

您能够按照“注解一节中的说明覆盖默认状态代码。

Shelf Response

若是要彻底控制响应,能够直接返回Shelf Response

() => new Response.ok("Hello World")

Error Response

shelf_bind不会对错误执行任何特定格式设置。 相反,它将它留给上游中间件来处理,例如shelf_exception_handler

这容许您将全部错误处理保存在一个位置。

import 'package:http_exception/http_exception.dart';

() => throw new BadRequestException()

在一些shelf_exception_handler中间件中补救

var handler = const Pipeline()
    .addMiddleware(exceptionHandler())
    .addHandler(bind(() => throw new BadRequestException()));

咱们获得一个将返回400响应的处理程序。

用注解调整

Path 参数

要调整如何执行请求路径参数的绑定,请使用@PathParam注解。

您能够更改路径名的默认映射。 例如,若是您有一个名为argOne的处理程序参数,则默认状况下会映射到名为arg_one的请求路径参数

若是您但愿将其映射到arg1,则能够按以下方式指定

(@PathParam(pathName: 'arg1') String argOne) => ...

Request Body

要调整如何执行请求正文的绑定,请使用@RequestBody批注。

注意,只有一个处理程序参数能够映射到正文。

#### JSON

要强制将body始终解释为JSON,请将格式设置以下

bind(@RequestBody(format: ContentType.JSON) Person person) => "Hello ${person.name}")

####Form

bind(@RequestBody(format: ContentType.FORM) Person person) => "Hello ${person.name}")

Response Headers

您可使用ResponseHeaders批注覆盖成功返回处理程序方法时设置的默认状态(200)。 您还能够将location header设置为传入请求网址。

@ResponseHeaders.created()
String _create(String name) => "Hello $name";

final handler = bind(_create);

您能够将状态设置为您喜欢的任何内容

@ResponseHeaders(successStatus: 204)
String _whatever(String name) => "Hello $name";

在POST上设置location字段时,返回对象上的主键字段用于路径的最后一段。

默认状况下,主键字段为id,但能够经过指定idField参数来覆盖它。

@ResponseHeaders.created(idField: #name)
Person _create(@RequestBody() Person person) => person;

name字段如今用于最后一个段。 例如,若是对http://localhost/person进行POST而且名称为fred,则该位置将设置为

location: http://localhost/person/fred

与Shelf Route一并使用

shelf_bind的主要用途之一是使用像shelf_route这样的路由器。

最简单的方法就是使用mojito或shelf_rest,由于它们提供了开箱即用的功能

当bind返回一个Handler时,你能够简单地将该处理程序传递给shelf_route的Router方法

var myRouter = router()
  ..get('/', bind(() => "Hello World"));

不可能轻松多了。 可是,必须将全部处理程序包装在绑定中会增长一些噪音。 为避免这种状况,咱们能够先将HandlerAdapter安装到路由中。 shelf_bind提供了一个开箱即用的功能。

var myRouter = router(handlerAdapter: handlerAdapter())
  ..get('/', () => "Hello World");

Example

如下显示了使用shelf_route做为路由的上述全部示例处理程序

import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_route/shelf_route.dart' as route;
import 'package:shelf_bind/shelf_bind.dart';
import 'package:http_exception/http_exception.dart';
import 'package:shelf_exception_handler/shelf_exception_handler.dart';
import 'dart:async';

void main() {
  var router = route.router(handlerAdapter: handlerAdapter())
      ..get('/', () => "Hello World")
      ..get('/later', () => new Future.value("Hello World"))
      ..get('/map', () => {"greeting": "Hello World"})
      ..get('/object', () => new SayHello()..greeting = "Hello World")
      ..get('/ohnoes', () => throw new BadRequestException())
      ..get('/response', () => new Response.ok("Hello World"))
      ..get('/greeting/{name}', (String name) => "Hello $name")
      ..get('/greeting2/{name}{?age}',
          (String name, int age) => "Hello $name of age $age")
      ..get('/greeting3/{name}', (Person person) => "Hello ${person.name}")
      ..get(
          '/greeting5/{name}',
          (String name, Request request) => "Hello $name ${request.method}")
      ..post('/greeting6', (Person person) => "Hello ${person.name}")
      ..get('/greeting8{?name}',
          (@PathParams() Person person) => "Hello ${person.name}");

  var handler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addMiddleware(exceptionHandler())
      .addHandler(router.handler);

  route.printRoutes(router);

  io.serve(handler, 'localhost', 8080).then((server) {
    print('Serving at http://${server.address.host}:${server.port}');
  });
}

class SayHello {
  String greeting;

  Map toJson() => { 'greeting': greeting };
}

class Person {
  final String name;

  Person.build({this.name});

  Person.fromJson(Map json) : this.name = json['name'];

  Map toJson() => { 'name': name };
}

请参阅example/binding_example.dart中项目中的更多详细示例

高级用法

将多个路径参数绑定到您的类中

您可使用@PathParams注解将路径变量绑定到类的属性。

class Person {
  String name;
}

bind((@PathParams() Person person) => "Hello ${person.name}")

若是您更喜欢不可变类,那么您能够绑定到构造函数

class Person {
  final String name;

  Person.build({this.name});
}

构造函数必须对全部属性使用命名参数,而且名称必须与请求路径参数名称匹配。

默认状况下,构造函数必须称为build。 未来可使用注解覆盖它。

Validation

shelf_bind与强大的Constrain包集成,以支持处理程序函数参数的自动验证。

经过validateParameters属性启用验证到绑定功能

bind((Person person) => "Hello ${person.name}", validateParameters: true)

或者在使用shelf Router时,您能够在handlerAdapter上设置它以应用于全部路由(请参阅下面的shelf Route集成部分)

handlerAdapter: handlerAdapter(validateParameters: true)

如今让咱们用一些(人为的)约束来为Person类增添趣味。

class Person {
  @NotNull()
  @Ensure(nameIsAtLeast3Chars, description: 'name must be at least 3 characters')
  final String name;

  @NotNull()
  @Ensure(isNotEmpty)
  @Ensure(allStreetsStartWith15, description: "All streets must start with 15")
  List<Address> addresses;


  Person.build({this.name});

  Person.fromJson(Map json) :
    this.name = json['name'],
    this.addresses = _addressesFromJson(json['addresses']);

  static List<Address> _addressesFromJson(json) {
    if (json == null || json is! List) {
      return null;
    }

    return json.map((a) => new Address.fromJson(a)).toList(growable: false);
  }

  Map toJson() => { 'name': name, 'addresses':  addresses };

  String toString() => 'Person[name: $name]';
}


class Address {
  @Ensure(streetIsAtLeast10Characters)
  String street;

  Address.fromJson(Map json) : this.street = json['street'];

  Map toJson() => { 'street': street };

  String toString() => 'Address[street: $street]';
}

// The constraint functions

Matcher nameIsAtLeast3Chars() => hasLength(greaterThan(3));

bool allStreetsStartWith15(List<Address> addresses) =>
  addresses.every((a) => a.street == null || a.street.startsWith("15"));

Matcher streetIsAtLeast10Characters() => hasLength(greaterThanOrEqualTo(10));

如今每当调用处理程序时,Person对象将在传递给Dart函数以前进行验证。 若是验证失败,将抛出BadRequestException(来自http_exception包),其中包含详细的约束违规。

若是你已正确配置了shelf_exception_handler,你会收到相似的响应

HTTP/1.1 400 Bad Request
content-type: application/json

{
    "errors": [
        {
            "constraint": {
                "description": "all streets must start with 15",
                "group": "DefaultGroup",
                "type": "Ensure"
            },
            "details": null,
            "invalidValue": {
                "type": "List",
                "value": [
                    "Address[street: blah blah st]"
                ]
            },
            "leafObject": {
                "type": "Person",
                "value": "Person[name: fred]"
            },
            "message": "Constraint violated at path addresses\nall streets must start with 15\n",
            "propertyPath": "addresses",
            "reason": null,
            "rootObject": {
                "type": "Person",
                "value": "Person[name: fred]"
            }
        }
    ],
    "message": "Bad Request",
    "status": 400
}

Response Validation

与处理程序函数参数验证相似,您可使用constrain包启用响应验证。 这是为了确保您永远不会发送无效数据。

经过validateReturn属性启用响应验证到绑定功能

(String name) => new Person(name)

若是验证失败,将抛出具备500状态的HttpException(来自http_exception包),由于这意味着您已经弄乱了代码;-)。

有关验证的更详细说明,请参阅“路径参数”部分的“验证”部分。

注入自定义参数

除了正常的请求相关数据(如路径参数,主体和头)以外,shelf_bind还支持将任意对象注入处理函数。 这些被称为自定义对象

一般,这些对象是从与请求相关的数据中实例化的,但这不是必需的。

常见的用法是将客户端注入HTTP客户端和数据库客户端等远程服务。 可能须要以通过身份验证的用户身份调用这些服务。

将customObjects参数用于handlerAdapter或bind觉得这些对象注入您本身的工厂

bind((String name, PersonLookupClient client) => client.lookup(name),
    customObjects: customObjects);
var adapter = handlerAdapter(customObjects: customObjects);

customObjects参数只是从类型到工厂的映射。 工厂采用Request参数。

var customObjects = {
    PersonLookupClient: (req) => new Future.value(new PersonLookupClient())
};

class PersonLookupClient {
  Future<Person> lookup(String name) =>
      new Future.value(new Person.build(name: name));
}

工厂可能会返回Future,在这种状况下,在将已解析的对象传递给处理程序方法以前将会解决future问题。

像mojito和shelf_rest这样的软件包会注入本身的自定义对象

更多信息

有关全部选项的更多详细信息,请参阅Wiki

TODO

查看未解决的问题

个人博客即将搬运同步至腾讯云+社区,邀请你们一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2tt7f9yv2ry8g

相关文章
相关标签/搜索