Dart 服务端开发 shelf_bind 包

限于喜欢 提交于 2020-03-14 12:29:00

介绍

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

shelf_bind赋予你:

  • 使用您自己的方法而不必担心shelf样板
  • 专注于使用您自己的类编写业务逻辑,并让shelf_bind处理将其装入shelf

shelf_bind倾向于约定优于配置,因此您可以编写必要的最小代码,但仍然可以根据需要覆盖默认值。

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

  • 绑定到简单类型
  •        包括类型转换
  • 绑定到您自己的域对象
  •       通过属性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中连接。

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

独立使用

如果您使用带有mojito或shelf_rest的shelf_bind,则可以跳过此独立使用部分。

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

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

这会创建一个等效于的 shelf Handler

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!