介绍
提供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目前支持JSON和FORM编码的主体。
默认情况下,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
来源:oschina
链接:https://my.oschina.net/u/3647851/blog/1862680