前のページへ

サーバ編

次のページ





ミドルウエア・フレームワーク (shelf)

ShelfHTTPサーバの為のミドルウエア・フレームワークのライブラリで、Dartチームが20141月から開発し、現在も維持更新されている。これは単なるミドルウエア・フレームワークではなく、HttpServerを使う場合よりもずっと簡単にHTTPサーバを書くことができることに加え、複数のサーバを実装出来るように配慮されているので、このライブラリが将来Dartに於けるHTTPサーバの主流になることが期待されている。Shelfは従ってdart:ioHTTP APIとは独立している。クッキー・ベースのセッション管理もHttpServerの実装とは独立したミドルウエアとなる。

Node.jsChromeブラウザ用に開発されたV8エンジンをサーバ・サイドでも使えるように拡張したものであるが、これにはConnectと呼ばれるウェブ・サーバの為のミドルウエア・フレームワークが存在する。Shelfはこれに影響を受けたパッケージ・ライブラリである。

Shelfを使うとウェブ・サーバあるいはウェブ・サーバの要素部品をより簡便に構成・作成できるようになる。

  • 用意されているシンプルなクラスたちの小規模なセットを使うことができる

  • サーバ・ロジックをシンプルな関数(即ちハンドラ)にマッピングしている:要求単一の引数になり、応答は値として返される。

  • 同期と非同期の処理を容易に組み合わせることができる。返す応答はFutureとすることもできる。

  • 同じモデルでシンプルな文字列あるいはバイト・ストリーム返すことができる柔軟性を持っている。

サンプル・コードでその概要を知る

このパッケージを使ったシンプルなエコー・サーバの記述例として、このパッケージに含まれているexample/example_server.dart試してみよう。

  1. このパッケージのリポジトリからZIPファイルをダウンロードして解凍する

  2. 奥のshelf-masterフォルダの名前をshelfに変更しIDE上で展開する

  3. pubspec.yamlファイルを開き、Enable Dart support及びGet dependenciesを実行すると下図のように展開される:




  4. example.dartを実行させるとコンソールには以下のように表示される:

    Serving at http://localhost:8080

  5. ブラウザからhttp://localhost:8080/hello_world.htmlでこのサーバをアクセスすると、次のように表示される:




コンソールには次のような記録が残される:

2019-04-14T10:24:46.791297 0:00:00.000912 GET [200] /hello_world.html

このコードを見てみよう:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;

main() async {
  var handler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addHandler(_echoRequest);
  var server = await io.serve(handler, 'localhost', 8080);
  // Enable content compression
  server.autoCompress = true;
  print('Serving at http://${server.address.host}:${server.port}');
}

shelf.Response _echoRequest(shelf.Request request) {
  return shelf.Response.ok('Request for "${request.url}"');
}

Middleware

  • 6行目でshelfライブラリが持っているミドルウエアのlogRequest()という関数をPipelineに付加している。このミドルウエアはshelfウェブ・サーバがクライアントからの各要求を処理するたびにログ・データを残す一般的なものである。さらにミドルウエアを追加したときは6行目の記述を使ってそのハンドラを追加すればよい。

Handler:

  • 5行目のhandlerハンドラである。このハンドラはPipelineにミドルエアのlogRequest()を付加し、更に_echoRequestというHandlerを付加したものである。

Pipeline:

  • 7行目で付加されるHandlerはクライアントからのHTTP要求を処理するために実行される関数である。従ってこのハンドラをラップしたミドルウエアもハンドラともいえる。ミドルウエアの場合はハンドラたちは他のハンドラたちをラップして順番に実行するチェインを構成できる。パイプラインの場合は最後に実行されるハンドラであるメソッド(ここでは_echoRequest)を最後に付加する。_echoRequestはサーバ・ロジックの関数である。引数はShelf要求オブジェクトで、戻り値はShelf応答オブジェクトである。handlerはログの為の組み込みミドルウエアshelf.logRequests()と、サーバ・ロジック(ハンドラ)の_echoRequestが付加されている。このように、Pipelineはミドルウエアとハンドラを付加するためのヘルパ・クラスである。io.serveはこのHTTPサーバを開始させるメソッドである。

io.serve()

  • 8行目のメソッドio.serve()DartHttpServerインスタンスのラッパである。このメソッドの引数はハンドラ、サーバ・アドレス、及びサーバ・ポートである。

このライブラリにあるRequestResponseは追って解説する



基本的なコンセプト

下図はShelfの基本的な構成を示す:




ミドルウエアのパイプラインはJava Servetのフィルタと似たコンセプトでもある。

いわゆる要求処理(サーバ・ロジック)はハンドラが行い、その前後処理(例えばキャッシュ、ログ、認証など)はミドルウエアが受け持つ。ハンドラはユーザが作成するが、ミドルウエアは通常Dartチームまたはサード・パーティが開発したものを使用する。アダプタはShelfが持っている基本機能ではあるが、ユーザが専用に用意することも可能である。

要求の内容によってその処理を振り分けるルータのようなミドルウエアは複数のハンドラをラップする。

ミドルウエアからハンドラに何らかの情報を渡したいときは、ShelfRequestオブジェクトにコンテキスト(contxt)を付加する。このコンテキストは(名前、値)のMapである。逆にハンドラからミドルウエアに何らかの情報を渡したいときは、ShelfResponseオブジェクトにコンテキストを付加する。ミドルウエアまたはアダプタに返されるShelfの応答はオブジェクトまたはそのFutureの形で渡される。一般にハンドラやミドルウエアは何らかのイベント待ちを有することが多く、Futureの形式で返すほうが便利である。

アダプタはshelf_ioshelfdart:ioの環境の中で使うためのアダプタである。殆どのアプリケーションはこの図のようにHttpServerHttpRequestshelf.Requestに変換してハンドラに渡す。

ハンドラ(Handlers)とミドルウエア(Middleware)

shelfの作成者(Kevin Moor)はハンドラとミドルウエアは同じ構造にしており、ハンドラはむしろその特別なものとして抽象化している。ハンドラをミドルウエアでラップしたものもハンドラである。これを理解すれば、以下のAPIドキュメントのトップにある記述の翻訳は面食らわなくて済む:

ハンドラはshelf.Requestを処理してshelf.Responseを返す関数である。これには要求そのものを処理するもの(例えば要求URIをファイル・システム上で検索する静的なファイル・サーバ)と、要求に対し何らかの処理をして他のハンドラに渡すもの(例えば該要求と応答に関する情報をコンソールに出力するロガー)がある。

後者の類のハンドラは「ミドルウエア」と呼ばれている。これはサーバ・スタックの途中に存在することに依る。ミドルウエアはあるハンドラに対し更に付加的な機能を持たせるためにそれをラップして別のハンドラにするための関数だと見做すことができる。Shelfのアプリケーションは通常複数のハンドラたちを中心にしたミドルウエアの多くのレイヤで構成されることになり、その為にこの種のアプリケーションを構成しやすくするshelf.Pipelineというクラスが用意されている。

ミドルウエアのなかには複数のハンドラたちを対象にしていて、各要求に対しそれらのハンドラを選択的に呼び出すものもある。例えば、ルーティング・ミドルウエア(routing middleware)では、到来要求のURIまたはHTTPメソッド(POSTとかGETとか)に基づいてどのハンドラを呼び出すかを判断する。一方カスケード化のミドルウエア(cascading middleware)では、どれかが応答を返すまで順番に各ハンドラを呼び出す。

ミドルウエアとしては、キャッシュ、ロガー、あるいは認証などが典型的である。各種ミドルウエアは将来Shelfに含められよう。またサードパーティのミドルウエアも拡充されよう。現在pub.dartlang.orgに登録されているものとしては以下のものがある(20194月時点):

  • ハンドラ

  • ミドルウエア

    • shelf_router:サード・パーティのルータ

    • shelf_route:サード・パーティのルータ(Dart 2非対応)

    • shelf_restRESTハンドラ(Dart 2非対応)

    • shelf_gzipGZIP圧縮した応答を返すミドルウエア

    • shelf_exception_handlerHttpExceptionを投げるミドルウエア

    • mojito:マイクロフレームワーク

    • shelf_bind:通常のDartの関数を使ってshelfのハンドラが書けるようにする(Dart 2非対応)

パイプライン

通常のサーバ・アプリケーションでは複数のミドルウエアのチェイン(パイプ)の最後にひとつ(ルーティングのミドルウエアがある場合は複数)のハンドラが置かれた構成になろう。そのためにPipelineというクラスが用意されている。パイプライン(Pipeline)というクラスは、ハンドラと幾つかのミドルウエアのセットを構成しやすくするためのヘルパ・クラスである。たとえば:

var handler = const Pipeline()

.addMiddleware(loggingMiddleware)

.addMiddleware(cachingMiddleware)

.addHandler(application);

と記述することで、applicationというハンドラに、cachingMiddlewareおよびloggingMiddlewareというミドルウエアからなるハンドラを構成することができる。

ダプタ(Adapters)

アダプタはshelf.Requestオブジェクトを生成し、それをハンドラに渡し、またその結果のshelf.Responseオブジェクトを取り扱うコードのことを言う。その殆どの部分は、下位のHTTPサーバからの要求オブジェクトをハンドラに渡す。shelf_io.serveがこの種のアダプタになる。アダプタにはクライアント・サイド(ブラウザ)でwindow.locationwindow.historyを使ってHTTP要求を合成するもの、あるいはHTTPクライアントからの要求を直接Shelfのハンドラにパイプするものもあり得る。

ユーザがアダプタを実装する場合には、幾つかの規則に従わねばならないい。アダプタはurlまたはscriptNameパラメタを新しいshelf.Requestに渡してはならず、requestedUriのみが渡すことが可能である。またコンテキスト・パラメタを渡すときは、総てのキーはそのアダプタのパッケージ名とそれに続くピリオドで始まらねばならない。同じ名前を持った複数のヘッダを受信したときは、RFC 2616 section 4.2に従ってカンマで区切った単一のヘッダにまとめて渡さねばならない。

アダプタはnull応答を返すものを含めそのハンドラからの総てのエラーを処理しなければならない。可能であれば各エラーをコンソールに出力し、次にあたかも500(サーバー・エラーの応答コード)応答をそのハンドラが返したかのごとく動作しなければならない。アダプタは500応答のボディ部を含めることは可能であるが、そのボディ部データは発生したエラーに関す売る情報を含めてはいけない。これは予期されていないエラーの結果デフォルトで内部情報が外部から見えてしまわないようにするためである。もしユーザが詳細なエラー記述を返したいときは、そうするためのミドルウエアを明示的に含めるべきである。

アダプタはデフォルトでHTTP応答のServerヘッダ行のなかに自分自身の情報を含めるべきである。そのハンドラがServerヘッダ行を返してくるときは、それはアダプタのデフォルトのヘッダ行より優先されなければならない。

アダプタはハンドラが応答を返した時刻をDateヘッダ行に含めるべきである。そのハンドラがDateヘッダ行を返してくるときは、そちらが優先されねばならない。

アダプタはたとえそれがFutureチェインによって報告されていないものであっても、ハンドラがスローした同期エラーたちによりアプリケーションが絶対クラッシュしないようにしなければならない。特に、これらのエラーはルート・ゾーンのエラー・ハンドラに渡されてはいけない:しかしそのアダプタが別のエラー・ゾーンのなかで走っているときは、それらのエラーはそのゾーンに渡すようにしなければならない。次の例では、そうしなければトップ・レベルに渡されてしまうエラーのみを捕捉するのに使える関数である:

/// Run [callback] and capture any errors that would otherwise be top-leveled.

///

/// If [this] is called in a non-root error zone, it will just run [callback]

/// and return the result. Otherwise, it will capture any errors using

/// [runZoned] and pass them to [onError].

catchTopLevelErrors(callback(), void onError(error, StackTrace stackTrace)) {

if (Zone.current.inSameErrorZone(Zone.ROOT)) {

return runZoned(callback, onError: onError);

} else {

return callback();

}

}

この関数はcallbackを実行し、そうでなければトップ・レベルに渡されてしまうエラーを捕捉する。この関数が非ルート・ゾーンの中で呼ばれた時は、単にcallbackを呼びその結果を返すのみである。それ以外のときは、runZonedを使ってエラーを捕捉し、それをonErrorに渡す。

shelfAPI

Shelfは”shelf”と”shelf.io”2つのライブラリで構成されている。

  • shelf”はミドルウエア・フレームワークの本体である。

  • shelf.io”dart:ioからのHttpRequestのオブジェクトたちを処理するためのアダプタである。serveRequests関数のなかのrequestsパラメタとしてHttpServerのインスタンスを指定できる(HttpServerHttpRequestのストリームを実装しているため)。dart:ioアダプタは要求ハイジャック(request hijacking)に対応している。

その基本構成は次のようになっている:

ライブラリ

区分

名称

概要

shelf

関数

createMiddleware

要求ハンドラ、応答ハンドラ、およびエラー・ハンドラの関数を指定してMiddlewareを生成する。

logRequests

要求の到来時刻、内側ハンドラの経過時間、応答のステータス・コード、要求URIをプリントする組み込み済みのミドルウエア。将来各種のミドルウエアが関数として拡充されよう。

クラス

Cascade

幾つかのハンドラたちを順番に呼び出し、最初に受理可能な応答を返すヘルパ・クラス。

HijackException

ある要求がハイジャックされたことを示すのに使われる例外。

Pipeline

MiddlewareのセットとHandlerからなる構成を記述し易くする為のヘルパ・クラス。

Request

Shelfアプリケーションが処理するHTTP要求を表現したもの。

Response

ハンドラが返す応答を表現したもの。

Typedefs

Handler

Requestを処理する関数のシグネチュア。ハンドラはHTTPサーバから直接要求を受信しても良いし、大きなアプリケーションの要素として構成しても良い。ResponseまたはFuture<Response>を返す。

HijackCallback

Shelfのハンドラが用意し、hijackメソッドに渡されるcallback

Middleware

あるHandlerをラップして新規のHandlerを生成する関数。あるHandlerをラップしてMiddlewareとすることで、そのHandlerの関数を拡張し、あるハンドラに渡す前に割り込んで要求を処理する、またはあるハンドラが応答を返す前に割り込んでその応答を処理するようにできる。

OnHijackCallback

あるsocketHijackCallbackを準備するためにhijackメソッドが使うcallbackで、Shelfのアダプタが用意する。

shelf.io

Functions

handleRequest

HTTP要求(HttpRequest)を処理するためにhandlerを使用する。

serve

指定したIPアドレスとTCPポート上で要求到来を待ち、その要求をHandlerに送信するHttpServerを起動する。

serveRequests

HttpRequestStreamに対処する。



ここで重要な関数はミドルウエアを生成するためのcreateMiddlewareである。更により高度なミドルウエア(例えば要求を加工してハンドラに渡す)を作るにはTypeDefMiddlewareを使用する。

Middleware createMiddleware({Function requestHandler(Request request),

Function responseHandler(Response response),

Function errorHandler(error, StackTrace stackTrace)})

requestHandler を指定したときは、これはRequestオブジェクトを受理する。このrequestHandler 関数はこの要求に対処しResponseまたはFuture<Response>を返すことができる。requestHandlernullを返すことができるが、その場合はそのrequestは内側のハンドラに送られる。但しそのrequestrequestHandlerで加工しても、それは内側のハンドラに渡されるRequestオブジェクトには反映されない。requestHandlerで加工したrequestを内部ハンドラに渡したい場合は、次に示すMiddlewareの型エイリアスを使うことになる。

ここではコンセプト図の上側のハンドラをinnerHandler(内側のハンドラ)と表現しているが、これは次のような関数型エイリアスで上側のハンドラをラップしているからである:

typedef Handler Middleware(Handler innerHandler);

responseHandlerが指定されているときは、このresponseHandler関数は内側のハンドラで生成されたResponseオブジェクトで呼ばれる。requestHandlerで作られたResponseオブジェクトはresponseHandlerには送られない。

responseHandlerResponseまたはFuture<Response>を返さなければならない。このresponseHandler関数はそれが受信した応答パラメタを返すか新しい応答オブジェクトを生成するかする。

errorHandlerが指定されているときは、このerrorHandler関数は内側のハンドラでスローされたエラーを受理する。このerrorHandler関数はrequestHandlerまたはresponseHandlerがスローしたエラーは受信しないしHijackExceptionsも受信しない。このerrorHandler関数は新しい応答を返すかまたはエラーをスローするかする。

以上の説明から、API上はミドルウエアとハンドラは次のような構成になっていることが理解されよう:




  • ミドルウエアもハンドラも到来要求を処理し、応答を返すことができる

  • ミドルウエアはハンドラをラップする

  • ハンドラをラップしたミドルウエアもハンドラである

  • Middleware 1の内部ハンドラはHandlerである

  • Middleware 2の内部ハンドラはHandlerをラップしたMiddleware 1である

  • このようにして一連のミドルウエアとハンドラのセットが構成される

shelfRequest

dart:ioHttpRequestはチャンク形式のHTTP要求に対応するためにStreamを実装していたが、shelf.Requestは単にMessageを継承している。RequestのオブジェクトからHTTPボディ部をバイト列あるいは文字列として取り出すには、MessagereadStreamベース)またはreadAsStringFutureベース)メソッドを使用する。

もうひとつの特徴はハイジャック機能である。これは要求socketの制御をハイジャックするものである。SocketレベルでのHTTP要求の操作が可能となる。

なおdart:ioHttpSessionなどHttpRequestのオブジェクトを使うときには新たなミドルウエアを使用しなければならない。shelf_simple_sessionというセッション・マネージャのミドルウエアの開発は止まっており、代わりにshelf_auth_sessionDart 2対応で存在している。

Requestクラスは次のような構成になっている:

コンストラクタ

Request

ユーザは通常使う必要はない。アダプタで使われる。

メソッド

change

このオブジェクトの内容を変更した新しいRequestオブジェクトをつくる。ミドルウエアで使われる。

hijack

ハイジャック。

read

バイト列としてボディ部をとりだす(Streamベース)。

readAsString

文字列としてボディ部をとりだす(Futureベース)。

プロパティ

canHijack

ハイジャック可能かどうか。

contentLength

ボディ部のコンテント長。

context

ミドルウエアとハンドラ間のデータで、名前と値のペアのMap

encoding

ボディ部のエンコーディング。

headers

ヘッダ部の名前と値のMapで不変変数。

ifModifiedSince

ifModifiedSincヘッダ行。

method

GETあるいはPOST等の要求メソッド。

mimeType

Content-Typeヘッダから取得。

protocolVersion

HTTPバージョンで1.0または1.1

requestedUri

要求URIで不変変数。

scriptName

requestedUri のパスで不変変数。

url

要求URIとクエリ文字列を除いた部分で不変変数。

shelfResponse

ResponseHTTP応答を返すためのクラスである。このクラスもMessageを継承している。このクラスのコンストラクタは次のようになっている:

Response(int statusCode, {body, Map<String, String> headers,

Encoding encoding, Map<String, Object> context})

これは指定したステータス・コード(statusCode)を持ったHTTP応答を生成する。

  • statusCode ステータス・コード(100またはそれ以上)

  • body HTTP応答のボディ部でStringStream<dart-core<dart-core>>またはnull(ボディ部がないことを示す)である。nullまたはbodyが渡されないときはデフォルトのエラー・メッセージが使われる。

  • headers 追加のHTTPヘッダ行を付加したいとき指定する。

  • encoding 送信されるバイト列のストリームに対するエンコーディングで、指定しないときはUTF-8が使われる。encodingが指定されているとそれを示すContent-Typeヘッダ行がHTTP応答に付加される。指定されていないときは"Content-Type : application/octet-stream"というヘッダ行が付加される。

  • context これはミドルウエアに情報を渡すために使われる。

サーバは通常200(OK)応答を返すが、必要に応じ別の応答を返す必要が出る。Shelfではそれらの応答のための指名コンストラクタたちが用意されている:

  • 403 Forbidden応答。サーバは該要求に応えることを拒否する:

Response.forbidden(body, {Map<String, String> headers, Encoding encoding,

Map<String, Object> context})

  • 302 Found応答。要求されているリソースが一時的に新しいURIに移っていることを示す:

Response.found(location, {body, Map<String, String> headers, Encoding encoding,

Map<String, Object> context})

  • 500 Internal Server Error応答。該要求処理中に内部エラーが起きたことを示す:

Response.internalServerError({body, Map<String, String> headers,

Encoding encoding, Map<String, Object> context})

  • 301 Moved Permanently応答。要求されているリソースが新しいURIに恒久的に移ってしまっていることを示す:

Response.movedPermanently(location, {body, Map<String, String> headers, Encoding encoding, Map<String, Object> context})

  • 404 Not Found応答。要求されているURIに合致したリソースが見つからないことを示す:

Response.notFound(body, {Map<String, String> headers, Encoding encoding,

Map<String, Object> context})

  • 304 Not Modified応答。指定した時刻以降要求したリソースが存在するかのGET要求に対し、変更されていないことを示す:

Response.notModified({Map<String, String> headers, Map<String, Object> context})

  • 200 OK応答。これは最も良く使われる応答で、該要求を成功裏に処理したことを示す:

Response.ok(body, {Map<String, String> headers, Encoding encoding,

Map<String, Object> context})

  • 303 See Other応答。該要求の応答は別の新しいURIで得られることを示す:

Response.seeOther(location, {body, Map<String, String> headers, Encoding encoding, Map<String, Object> context})

メソッドとプロパティは現在以下のようになっている:

メソッド

change

このオブジェクトの内容を変更した新しいResponseオブジェクトをつくる。ミドルウエアで使われる。

read

バイト列としてボディ部をとりだす(Streamベース)。

readAsString

文字列としてボディ部をとりだす(Futureベース)。

プロパティ

contentLength

ボディ部のコンテント長。

context

ミドルウエアとハンドラ間のデータで、名前と値のペアのMap

encoding

ボディ部のエンコーディング。

expires

該応答の有効期限

headers

ヘッダ部の名前と値のMapで不変変数。

lastModified

lastModifiedSincヘッダ行で該応答が最後に変更された時刻。

mimeType

Content-Typeヘッダから取得。

statusCode

ステータス・コード。



shelf_staticハンドラ

ウェブ・アプリケーションはよりリッチな体験をユーザに与えるために、通常動的コンテンツと静的コンテンツが組み合わされる。静的コンテンツをアプリケーション・サーバから提供するには、ファイル・サーバ機能が必要になるが、sfelfではそのためのshelf_staticというファイル・ハンドラが用意されている。これはGoogleの技術者のKevin Mooreが作成したシンプルなものである。

以下のコードを見て頂きたい:

import 'package:shelf/shelf_io.dart' as io;

import 'package:shelf_static/shelf_static.dart';


void main() {

var handler = createStaticHandler('example/files',

defaultDocument: 'index.html')


io.serve(handler, 'localhost', 8080);

}

このハンドラは

Handler createStaticHandler(String fileSystemPath,{bool serveFilesOutsidePath:

false, String defaultDocument})

というメソッドで生成される。サービスしたい静的リソースへのファイル・パスはfileSystemPathで、ファイル名が指定されていないときのデフォルトのファイルをdefaultDocumentで指定する。serveFilesOutsidePathは指定したパス以外のファイルにアクセスするかを指定する。

テスト・アプリケーション

この章の読者の理解を早めるために、本章の添付サンプル・コードとして、shelf_testというリポジトリgithubにアップロードされている。これをダウンロードし、自分のIDE上で活用されたい。

shelf_testというパッケージは次のような構成になっている(Get dependenciesを実行して必要な外部パッケージを取り込んだ後):




shelf_test/binのフォルダなかのコードたちがサンプル・サーバである。これらのサーバのテストにはChromeを使用することをお勧めする。

handler_sample_1

handler_sample_1は簡単なエコー・サーバあるいはpingサーバと呼ばれるもので、このサーバにアクセスすると簡単なテキスト・メッセージをクライアントに返す。ブラウザはtext/plainをデフォルトのコンテント形式としているので、この応答を次のように表示する。




このコードは非常に簡単なものではあるが、このミドルウエア・フレームワークの基本的な使い方を示している。

ハンドラは次のように記述されている:

shelf.Response myHandler(shelf.Request request) =>
    new shelf.Response.ok('Hello from handler_sample_1.');

この関数はResponse.okという便利なコンストラクタを使ってFutureOr<Response>の型のオブジェクトを返す。

このハンドラを動作させるためには'package:shelf/shelf_io.dart'にあるserveというアダプタが使われる:

var server = await io.serve(myHandler, '127.0.0.1', 8080);
print('Serving at http://${server.address.host}:${server.port}');

このメソッドはFuture<HttpServer>を返すので、awaitを使ってすの完了を受けてコンソールにこのサーバが起動したことを表示している。

handler_sample_2

handler_sample_2はより一般的なアプリケーションの構成を示す為のサンプルである。即ち:

  • 最初にアプリケーションの入り口のHTMLページを渡す。

  • そのHTMLページからアプリケーション(到来shelf.Requestオブジェクトの内容をテキストまたはHTMLとしてクライアントに返す)を呼ぶ。

  • クライアントからのfavicon.ico要求に対応する。

具体的にこのアプリケーションを試してみよう:

  1. handler_sample_2を実行すると、コンソールには'Serving at http://127.0.0.1:8080'と表示される。

  2. ブラウザから'http://localhost:8080/'でこのサーバを呼ぶと次のような画面となる:




これはこのガイドの読者には馴染みのパージであるが、\shelf_test\resources\ShelfRequestDump.htmlshelf_staticハンドラを介してクライアントに返したものである。

もう一つこの画面で注意することは、ブラウザのタブの左に小さなアイコンが表示されていることである。これはブラウザがhttp://localhost:8080/favicon.icoで要求したもので、\shelf_test\resources\favicon.icoを同じくshelf_staticハンドラを介してクライアントに返したものである。この下手くそなアイコンはダートとそれを置く為の棚(dart shelf)を示したものだが、どなたかより適したアイコンを創作して頂きたい。

  1. このフロント画面で適当なテキストを入力しサブミット・ボタンを押すと、このサーバは次のような到来shelf.requestオブジェクトの内容を報告する応答を返す:




このアプリケーションの核となっているmyHandllerというハンドラは次のようになっている:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Future<shelf.Response> myHandler(shelf.Request request) async {
  var path = request.requestedUri.path;
  if (path == '/' || path == '/favicon.ico' || path == '') {
    print('* static file request ${request.requestedUri.path} arrived');
    return staticHandler(request);
  } else {
    print('* request_dump request arrived');
    String data;
    StringBuffer sb =  await util.reqInfo(request);
    data = sb.toString(); // return plain text
    print(data); // console out for debugging
    data = util.createHtmlResponse(sb); // or, return html text
    return shelf.Response.ok(data, headers:{"Content-Type":"text/html"});
  }
}
  • 01 戻りの型shelf.ResponseまたはFuture<shelf.Response>でなければならない

  • 02 要求パスが単に'/'のとき、および'/favicon.ico'のときはstaticHandlerというハンドラを呼んだ結果を返す。staticHandlerは次のようにshelf_staticライブラリのcreateStaticHandlerメソッドで生成される。

var staticHandler = static_handler.createStaticHandler('resources',
    defaultDocument: 'ShelfRequestDump.html');

引き数の'resources'は自分のコードからのリソースへの相対パスを示している。またdefaultDocumentはファイルが指定されていないときにデフォルトとして取り出すファイルを指定している。したがってこの場合は'http://localhost:8080/'というHTTP要求に対してフロント・ページが返されることになる。なおブラウザはこのようなホストを要求パスなしで呼び出した応答をキャッシュしてしまうことに注意のこと。もしこのファイルを加工した場合、あるいは'http://localhost:8080/'を別のアプリケーションで使う場合は、ブラウザの経歴を削除する必要がある。

  • 07 – 13 ここではそれ以外の要求パスを持ったHTTP要求に対し、そのshelf.Requestの内容をHTMLテキストで返している。

  • 09 util.reqInfo(request)という関数は、指定したshelf.Requestの内容をStringBufferでテキストとして報告する。しかしながらshelf.Requestのボディを読みだすread()およびreadAsString([Encoding encoding])はともにFutureを返す。したがってこの関数もFuture<StringBuffer>を返しているので、awaitで受けている。

  • 01 myHandlerFuture<shelf.Response>を返最後のshelf.Responseオブジェクトを渡しそのFutureを完了させる

  • 11 これをStringに変換したものは、これをコンソールに打ち出してデバッグに活用すると便利である。

  • 12 util.createHtmlResponse(sb)はブラウザ用にHTMLテキストに変換するツールである。

  • 13 shelf.Response.okheaders:HTMLであることを指定しないとブラウザは単なるテキストだと判断してしまうので注意

このプログラムはまたstaticHandlermyHandlerの二つのハンドラがあり、myHandlerstaticHandlerをラップした感じになっているので、いわゆるルータのミドルウエアの機能を有している。本格的なルータのミドルウエア(例えばshelf_router)を使わなくてもこのような簡便なものでも有用であろう。

middleware_sample_1

middleware_sample_1shelf.Requestオブジェクトを加工して内部ハンドラに渡すミドルウエアのサンプルである。このサーバを起動し、ブラウザから'http://localhost:8080/test'でアクセスすると、次のような応答が返される:




ここで

shelf.request.context :

shelf.io.connection_info : Instance of '_HttpConnectionInfo'

testContextData : added by myMiddleware

とコンテキストにはtestContextData : added by myMiddlewareというこのミドルウエアが追加したコンテキストが入っている。通常このようなコンテキストはこのミドルウエアがラップしているミドルウエアまたはハンドラに渡す情報として使われる。

このデータを作っているのはmodifyRequestという関数である。

shelf.Request modifyRequest(shelf.Request request) {
  return request.change(context: {'testContextData': 'added by myMiddleware'});
}

それではミドルウエアの生成はどのように記述されているだろうか:

/**
 * Middleware to modify incoming request and hands it to the innerHandler.
 */
shelf.Middleware myMiddleware() {
  return (shelf.Handler innerHandler) {
    return (shelf.Request request) {
      return innerHandler(modifyRequest(request));
    };
  };
}

一見このコードは複雑であるが、

typedef Handler Middleware(Handler innerHandler);

という関数型エイリアスを思いだせば理解されよう。すなわちあるinnerHandlerが与えられたとき、あるrequestが与えられたときにそのrequestmodifyRequestで加工したものでinnerHandlerを呼んだ結果としてのshelf.Responseを返す関数(即ちミドルウエア)を返す関数である。こうすればmodifyRequest(request)で加工したshelf.requestオブジェクトが確実にinnerHandlerに渡されることになる。

どうしてcreateMiddleware関数を使わないのか疑問に思われるかもしれない。しかしこの関数はshelfが用意したRequestオブジェクトを使うことを前提としており、contextを変更することはできない。何故ならAPIで分かるようにこれらのプロパティは総てfinalであり、変更できない。

このミドルウエアと内部ハンドラであるrequestDumpは次のようにPipelineを使って組み合わされ、myHandlerとなっている:

/**
 * Compose a set of Middleware and a Handler.
 */
var myHandler = const shelf.Pipeline()
    .addMiddleware(myMiddleware())
    .addHandler(requestDump);

middleware_sample_2

middleware_sample_2は要求パスが'/middleware'の時に限り直接

Response for "http://localhost:8080/middleware" from myMiddleware.

という応答をクライアントに返すミドルウエアである。それ以外の要求パスに対しては該要求は内部ハンドラであるsimpleHandlerに渡され、このハンドラは

Response for "http://localhost:8080/test" from simpleHandler.

とういう応答を返している。

そのようなミドルウエアを作成するには

Middleware createMiddleware({Function requestHandler(Request request),

Function responseHandler(Response response),

Function errorHandler(error, StackTrace stackTrace)})

これはhandler_sample_2の手法をもっとスマートに構成する手法である。

ここでは次のように生成している:

/**
 * Middleware that returns response for '/middleware' request.
 */
shelf.Middleware myMiddleware =
    shelf.createMiddleware(requestHandler: (shelf.Request request) {
  if (request.requestedUri.path == '/middleware') {
    // direct response
    return new shelf.Response.ok(
        'Response for "${request.requestedUri}" from myMiddleware.');
  } else
    return null; // call inner handler
});

即ちrequestHandlerの部分は、request.requestedUri.path == '/middleware'の時に限り直接shelf.Response.ok応答を返している。それ以外の場合はnullを返すことで該要求は内部ハンドラに渡されることを指示している。

/middlewareでアクセスしたとき:




それ以外でアクセスしたとき:




middleware_sample_3

middleware_sample_3は内部ハンドラからのshelf.Responseを加工するミドルウエアの例である。このサーバを起動させ、ブラウザから'http://localhost:8080/123'でアクセスすると、ブラウザには次のようなテキストが表示される:

Response for "http://localhost:8080/123" from simpleHandler.

Time : 20:50:17.261 ... added by myMiddleware.

最初の行はsimpleHandlerが返したテキストであり、次の行がmyMiddlewareというミドルウエアが追加したものである。

このミドルウエアは次の関数で生成される:

shelf.Middleware myMiddleware =
    shelf.createMiddleware(responseHandler: modifyResponse);

このようにshelf.createMiddlewareという簡便なメソッドを使うことができる。これはcontextを加工する必要がないからである。もし外側のミドルウエアにcontextを使って情報を渡したい場合は次のような記述が必要になる:

/**
 * Middleware to modify incoming request and hands it to the innerHandler.
 * Returns new Future<shelf.Response> object.
 */
shelf.Middleware myMiddleware() {
  return (shelf.Handler innerHandler) {
    return (shelf.Request request) {
      return new Future.sync(() => innerHandler(request))
          .then((shelf.Response response) {
        return modifyResponse(response);
      });
    };
  };
}

即ちこれは、要求が到来したらinnerHandlerをその要求で呼び、返されたFutureで待ち、応答がinnerHandlerから戻されたら、その応答でmodifyResponseを呼んで、結果としての応答(またはそれのFuture)を返す関数(即ちミドルウエア)を返す関数である。

modifyRequestは次のようになっている:

/**
 * Small function that modifies an outgoing response.
 */
Future<shelf.Response> modifyResponse(shelf.Response response) async {
  var newBody =
      'Time : ${new DateTime.now().toString().substring(11)} ... added by myMiddleware.';
  var data = await response.readAsString();
  newBody = '${data}\n${newBody}';
  return shelf.Response.ok(newBody, headers: response.headers);
}

以下はこのサーバへのアクセス例である:




ミドルウエアがsimpleHandlerからの応答に1行追加している:

Time : 21:07:37.029906 ... added by myMiddleware.





前のページへ

次のページ