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

相關文章
相關標籤/搜索