サーバ編 |
ShelfはHTTPサーバの為のミドルウエア・フレームワークのライブラリで、Dartチームが2014年1月から開発し、現在も維持更新されている。これは単なるミドルウエア・フレームワークではなく、HttpServerを使う場合よりもずっと簡単にHTTPサーバを書くことができることに加え、複数のサーバを実装出来るように配慮されているので、このライブラリが将来Dartに於けるHTTPサーバの主流になることが期待されている。Shelfは従ってdart:ioのHTTP APIとは独立している。クッキー・ベースのセッション管理もHttpServerの実装とは独立したミドルウエアとなる。
Node.jsはChromeブラウザ用に開発されたV8エンジンをサーバ・サイドでも使えるように拡張したものであるが、これにはConnectと呼ばれるウェブ・サーバの為のミドルウエア・フレームワークが存在する。Shelfはこれに影響を受けたパッケージ・ライブラリである。
Shelfを使うとウェブ・サーバあるいはウェブ・サーバの要素部品をより簡便に構成・作成できるようになる。
用意されているシンプルなクラスたちの小規模なセットを使うことができる
サーバ・ロジックをシンプルな関数(即ちハンドラ)にマッピングしている:要求が単一の引数になり、応答は値として返される。
同期と非同期の処理を容易に組み合わせることができる。返す応答はFutureとすることもできる。
同じモデルでシンプルな文字列あるいはバイト・ストリームで返すことができる柔軟性を持っている。
このパッケージを使ったシンプルなエコー・サーバの記述例として、このパッケージに含まれているexample/example_server.dartを試してみよう。
このパッケージのリポジトリからZIPファイルをダウンロードして解凍する
奥のshelf-masterフォルダの名前をshelfに変更しIDE上で展開する
pubspec.yamlファイルを開き、Enable Dart support及びGet dependenciesを実行すると下図のように展開される:
|
example.dartを実行させるとコンソールには以下のように表示される:
|
ブラウザからhttp://localhost:8080/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()はDartのHttpServerインスタンスのラッパである。このメソッドの引数はハンドラ、サーバ・アドレス、及びサーバ・ポートである。
このライブラリにあるRequestとResponseは追って解説する
下図はShelfの基本的な構成を示す:
|
ミドルウエアのパイプラインはJava Servetのフィルタと似たコンセプトでもある。
いわゆる要求処理(サーバ・ロジック)はハンドラが行い、その前後処理(例えばキャッシュ、ログ、認証など)はミドルウエアが受け持つ。ハンドラはユーザが作成するが、ミドルウエアは通常Dartチームまたはサード・パーティが開発したものを使用する。アダプタはShelfが持っている基本機能ではあるが、ユーザが専用に用意することも可能である。
要求の内容によってその処理を振り分けるルータのようなミドルウエアは複数のハンドラをラップする。
ミドルウエアからハンドラに何らかの情報を渡したいときは、ShelfのRequestオブジェクトにコンテキスト(contxt)を付加する。このコンテキストは(名前、値)のMapである。逆にハンドラからミドルウエアに何らかの情報を渡したいときは、ShelfのResponseオブジェクトにコンテキストを付加する。ミドルウエアまたはアダプタに返されるShelfの応答はオブジェクトまたはそのFutureの形で渡される。一般にハンドラやミドルウエアは何らかのイベント待ちを有することが多く、Futureの形式で返すほうが便利である。
アダプタはshelf_ioはshelfをdart:ioの環境の中で使うためのアダプタである。殆どのアプリケーションはこの図のようにHttpServerのHttpRequestをshelf.Requestに変換してハンドラに渡す。
shelfの作成者(Kevin Moor)はハンドラとミドルウエアは同じ構造にしており、ハンドラはむしろその特別なものとして抽象化している。ハンドラをミドルウエアでラップしたものもハンドラである。これを理解すれば、以下のAPIドキュメントのトップにある記述の翻訳は面食らわなくて済む:
ハンドラはshelf.Requestを処理してshelf.Responseを返す関数である。これには要求そのものを処理するもの(例えば要求URIをファイル・システム上で検索する静的なファイル・サーバ)と、要求に対し何らかの処理をして他のハンドラに渡すもの(例えば該要求と応答に関する情報をコンソールに出力するロガー)がある。
後者の類のハンドラは「ミドルウエア」と呼ばれている。これはサーバ・スタックの途中に存在することに依る。ミドルウエアは、あるハンドラに対し更に付加的な機能を持たせるためにそれをラップして別のハンドラにするための関数だと見做すことができる。Shelfのアプリケーションは通常複数のハンドラたちを中心にしたミドルウエアの多くのレイヤで構成されることになり、その為にこの種のアプリケーションを構成しやすくするshelf.Pipelineというクラスが用意されている。
ミドルウエアのなかには複数のハンドラたちを対象にしていて、各要求に対しそれらのハンドラを選択的に呼び出すものもある。例えば、ルーティング・ミドルウエア(routing middleware)では、到来要求のURIまたはHTTPメソッド(POSTとかGETとか)に基づいてどのハンドラを呼び出すかを判断する。一方カスケード化のミドルウエア(cascading middleware)では、どれかが応答を返すまで順番に各ハンドラを呼び出す。
ミドルウエアとしては、キャッシュ、ロガー、あるいは認証などが典型的である。各種ミドルウエアは将来Shelfに含められよう。またサードパーティのミドルウエアも拡充されよう。現在pub.dartlang.orgに登録されているものとしては以下のものがある(2019年4月時点):
ハンドラ
shelf_static : 静的ファイル・サーバ
shelf_web_socket : WebSocket接続を確立しクライアントと交信する
shelf_packages_handler:packages/ディレクトリ要求に対応する
shelf_proxy:要求を外部サーバにプロキシする
ミドルウエア
shelf_router:サード・パーティのルータ
shelf_route:サード・パーティのルータ(Dart 2非対応)
shelf_rest:RESTハンドラ(Dart 2非対応)
shelf_gzip:GZIP圧縮した応答を返すミドルウエア
shelf_exception_handler:HttpExceptionを投げるミドルウエア
mojito:マイクロフレームワーク
shelf_bind:通常のDartの関数を使ってshelfのハンドラが書けるようにする(Dart 2非対応)
通常のサーバ・アプリケーションでは複数のミドルウエアのチェイン(パイプ)の最後にひとつ(ルーティングのミドルウエアがある場合は複数)のハンドラが置かれた構成になろう。そのためにPipelineというクラスが用意されている。パイプライン(Pipeline)というクラスは、ハンドラと幾つかのミドルウエアのセットを構成しやすくするためのヘルパ・クラスである。たとえば:
var handler = const Pipeline() .addMiddleware(loggingMiddleware) .addMiddleware(cachingMiddleware) .addHandler(application); |
と記述することで、applicationというハンドラに、cachingMiddlewareおよびloggingMiddlewareというミドルウエアからなるハンドラを構成することができる。
アダプタはshelf.Requestオブジェクトを生成し、それをハンドラに渡し、またその結果のshelf.Responseオブジェクトを取り扱うコードのことを言う。その殆どの部分は、下位のHTTPサーバからの要求オブジェクトをハンドラに渡す。shelf_io.serveがこの種のアダプタになる。アダプタにはクライアント・サイド(ブラウザ)でwindow.locationとwindow.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に渡す。
Shelfは”shelf”と”shelf.io”の2つのライブラリで構成されている。
“shelf”はミドルウエア・フレームワークの本体である。
“shelf.io”はdart:ioからのHttpRequestのオブジェクトたちを処理するためのアダプタである。serveRequests関数のなかのrequestsパラメタとしてHttpServerのインスタンスを指定できる(HttpServerがHttpRequestのストリームを実装しているため)。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 |
あるsocketでHijackCallbackを準備するためにhijackメソッドが使うcallbackで、Shelfのアダプタが用意する。 |
||
shelf.io |
Functions |
handleRequest |
HTTP要求(HttpRequest)を処理するためにhandlerを使用する。 |
serve |
指定したIPアドレスとTCPポート上で要求到来を待ち、その要求をHandlerに送信するHttpServerを起動する。 |
||
serveRequests |
HttpRequestのStreamに対処する。 |
ここで重要な関数はミドルウエアを生成するためのcreateMiddlewareである。更により高度なミドルウエア(例えば要求を加工してハンドラに渡す)を作るにはTypeDefのMiddlewareを使用する。
|
requestHandler を指定したときは、これはRequestオブジェクトを受理する。このrequestHandler 関数はこの要求に対処しResponseまたはFuture<Response>を返すことができる。requestHandlerはnullを返すことができるが、その場合はそのrequestは内側のハンドラに送られる。但しそのrequestをrequestHandlerで加工しても、それは内側のハンドラに渡されるRequestオブジェクトには反映されない。requestHandlerで加工したrequestを内部ハンドラに渡したい場合は、次に示すMiddlewareの型エイリアスを使うことになる。
ここではコンセプト図の上側のハンドラをinnerHandler(内側のハンドラ)と表現しているが、これは次のような関数型エイリアスで上側のハンドラをラップしているからである:
|
responseHandlerが指定されているときは、このresponseHandler関数は内側のハンドラで生成されたResponseオブジェクトで呼ばれる。requestHandlerで作られたResponseオブジェクトはresponseHandlerには送られない。
responseHandlerはResponseまたはFuture<Response>を返さなければならない。このresponseHandler関数はそれが受信した応答パラメタを返すか新しい応答オブジェクトを生成するかする。
errorHandlerが指定されているときは、このerrorHandler関数は内側のハンドラでスローされたエラーを受理する。このerrorHandler関数はrequestHandlerまたはresponseHandlerがスローしたエラーは受信しないしHijackExceptionsも受信しない。このerrorHandler関数は新しい応答を返すかまたはエラーをスローするかする。
以上の説明から、API上はミドルウエアとハンドラは次のような構成になっていることが理解されよう:
|
ミドルウエアもハンドラも到来要求を処理し、応答を返すことができる
ミドルウエアはハンドラをラップする
ハンドラをラップしたミドルウエアもハンドラである
Middleware 1の内部ハンドラはHandlerである
Middleware 2の内部ハンドラはHandlerをラップしたMiddleware 1である
このようにして一連のミドルウエアとハンドラのセットが構成される
dart:ioのHttpRequestはチャンク形式のHTTP要求に対応するためにStreamを実装していたが、shelf.Requestは単にMessageを継承している。RequestのオブジェクトからHTTPボディ部をバイト列あるいは文字列として取り出すには、Messageのread(Streamベース)またはreadAsString(Futureベース)メソッドを使用する。
もうひとつの特徴はハイジャック機能である。これは要求socketの制御をハイジャックするものである。SocketレベルでのHTTP要求の操作が可能となる。
なおdart:ioのHttpSessionなどHttpRequestのオブジェクトを使うときには新たなミドルウエアを使用しなければならない。shelf_simple_sessionというセッション・マネージャのミドルウエアの開発は止まっており、代わりにshelf_auth_sessionがDart 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とクエリ文字列を除いた部分で不変変数。 |
ResponseはHTTP応答を返すためのクラスである。このクラスもMessageを継承している。このクラスのコンストラクタは次のようになっている:
|
これは指定したステータス・コード(statusCode)を持ったHTTP応答を生成する。
statusCode ステータス・コード(100またはそれ以上)
body HTTP応答のボディ部でString、Stream<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応答。サーバは該要求に応えることを拒否する:
|
302 Found応答。要求されているリソースが一時的に新しいURIに移っていることを示す:
|
500 Internal Server Error応答。該要求処理中に内部エラーが起きたことを示す:
|
301 Moved Permanently応答。要求されているリソースが新しいURIに恒久的に移ってしまっていることを示す:
|
404 Not Found応答。要求されているURIに合致したリソースが見つからないことを示す:
|
304 Not Modified応答。指定した時刻以降要求したリソースが存在するかのGET要求に対し、変更されていないことを示す:
|
200 OK応答。これは最も良く使われる応答で、該要求を成功裏に処理したことを示す:
|
303 See Other応答。該要求の応答は別の新しいURIで得られることを示す:
|
メソッドとプロパティは現在以下のようになっている:
メソッド |
|
change |
このオブジェクトの内容を変更した新しいResponseオブジェクトをつくる。ミドルウエアで使われる。 |
read |
バイト列としてボディ部をとりだす(Streamベース)。 |
readAsString |
文字列としてボディ部をとりだす(Futureベース)。 |
プロパティ |
|
contentLength |
ボディ部のコンテント長。 |
context |
ミドルウエアとハンドラ間のデータで、名前と値のペアのMap。 |
encoding |
ボディ部のエンコーディング。 |
expires |
該応答の有効期限 |
headers |
ヘッダ部の名前と値のMapで不変変数。 |
lastModified |
lastModifiedSincヘッダ行で該応答が最後に変更された時刻。 |
mimeType |
Content-Typeヘッダから取得。 |
statusCode |
ステータス・コード。 |
ウェブ・アプリケーションはよりリッチな体験をユーザに与えるために、通常動的コンテンツと静的コンテンツが組み合わされる。静的コンテンツをアプリケーション・サーバから提供するには、ファイル・サーバ機能が必要になるが、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);
|
このハンドラは
|
というメソッドで生成される。サービスしたい静的リソースへのファイル・パスはfileSystemPathで、ファイル名が指定されていないときのデフォルトのファイルをdefaultDocumentで指定する。serveFilesOutsidePathは指定したパス以外のファイルにアクセスするかを指定する。
この章の読者の理解を早めるために、本章の添付サンプル・コードとして、shelf_testというリポジトリがgithubにアップロードされている。これをダウンロードし、自分のIDE上で活用されたい。
shelf_testというパッケージは次のような構成になっている(Get dependenciesを実行して必要な外部パッケージを取り込んだ後):
|
shelf_test/binのフォルダなかのコードたちがサンプル・サーバである。これらのサーバのテストにはChromeを使用することをお勧めする。
handler_sample_1は簡単なエコー・サーバあるいはpingサーバと呼ばれるもので、このサーバにアクセスすると簡単なテキスト・メッセージをクライアントに返す。ブラウザはtext/plainをデフォルトのコンテント形式としているので、この応答を次のように表示する。
|
このコードは非常に簡単なものではあるが、このミドルウエア・フレームワークの基本的な使い方を示している。
ハンドラは次のように記述されている:
|
この関数はResponse.okという便利なコンストラクタを使ってFutureOr<Response>の型のオブジェクトを返す。
このハンドラを動作させるためには'package:shelf/shelf_io.dart'にあるserveというアダプタが使われる:
|
このメソッドはFuture<HttpServer>を返すので、awaitを使ってすの完了を受けてコンソールにこのサーバが起動したことを表示している。
handler_sample_2はより一般的なアプリケーションの構成を示す為のサンプルである。即ち:
最初にアプリケーションの入り口のHTMLページを渡す。
そのHTMLページからアプリケーション(到来shelf.Requestオブジェクトの内容をテキストまたはHTMLとしてクライアントに返す)を呼ぶ。
クライアントからのfavicon.ico要求に対応する。
具体的にこのアプリケーションを試してみよう:
handler_sample_2を実行すると、コンソールには'Serving at http://127.0.0.1:8080'と表示される。
ブラウザから'http://localhost:8080/'でこのサーバを呼ぶと次のような画面となる:
|
これはこのガイドの読者には馴染みのパージであるが、\shelf_test\resources\ShelfRequestDump.htmlをshelf_staticハンドラを介してクライアントに返したものである。
もう一つこの画面で注意することは、ブラウザのタブの左に小さなアイコンが表示されていることである。これはブラウザがhttp://localhost:8080/favicon.icoで要求したもので、\shelf_test\resources\favicon.icoを同じくshelf_staticハンドラを介してクライアントに返したものである。この下手くそなアイコンはダートとそれを置く為の棚(dart shelf)を示したものだが、どなたかより適したアイコンを創作して頂きたい。
このフロント画面で適当なテキストを入力しサブミット・ボタンを押すと、このサーバは次のような到来shelf.requestオブジェクトの内容を報告する応答を返す:
|
このアプリケーションの核となっているmyHandllerというハンドラは次のようになっている:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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メソッドで生成される。
|
引き数の'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 myHandlerはFuture<shelf.Response>を返す。最後のshelf.Responseオブジェクトを渡しそのFutureを完了させる。
11 これをStringに変換したものは、これをコンソールに打ち出してデバッグに活用すると便利である。
12 util.createHtmlResponse(sb)はブラウザ用にHTMLテキストに変換するツールである。
13 shelf.Response.okはheaders:でHTMLであることを指定しないとブラウザは単なるテキストだと判断してしまうので注意
このプログラムはまたstaticHandlerとmyHandlerの二つのハンドラがあり、myHandlerがstaticHandlerをラップした感じになっているので、いわゆるルータのミドルウエアの機能を有している。本格的なルータのミドルウエア(例えばshelf_router)を使わなくてもこのような簡便なものでも有用であろう。
middleware_sample_1はshelf.Requestオブジェクトを加工して内部ハンドラに渡すミドルウエアのサンプルである。このサーバを起動し、ブラウザから'http://localhost:8080/test'でアクセスすると、次のような応答が返される:
|
ここで
|
とコンテキストにはtestContextData : added by myMiddlewareというこのミドルウエアが追加したコンテキストが入っている。通常このようなコンテキストはこのミドルウエアがラップしているミドルウエアまたはハンドラに渡す情報として使われる。
このデータを作っているのはmodifyRequestという関数である。
|
それではミドルウエアの生成はどのように記述されているだろうか:
|
一見このコードは複雑であるが、
|
という関数型エイリアスを思いだせば理解されよう。すなわちあるinnerHandlerが与えられたとき、あるrequestが与えられたときにそのrequestをmodifyRequestで加工したものでinnerHandlerを呼んだ結果としてのshelf.Responseを返す関数(即ちミドルウエア)を返す関数である。こうすればmodifyRequest(request)で加工したshelf.requestオブジェクトが確実にinnerHandlerに渡されることになる。
どうしてcreateMiddleware関数を使わないのか疑問に思われるかもしれない。しかしこの関数はshelfが用意したRequestオブジェクトを使うことを前提としており、contextを変更することはできない。何故ならAPIで分かるようにこれらのプロパティは総てfinalであり、変更できない。
このミドルウエアと内部ハンドラであるrequestDumpは次のようにPipelineを使って組み合わされ、myHandlerとなっている:
|
middleware_sample_2は要求パスが'/middleware'の時に限り直接
|
という応答をクライアントに返すミドルウエアである。それ以外の要求パスに対しては該要求は内部ハンドラであるsimpleHandlerに渡され、このハンドラは
|
とういう応答を返している。
そのようなミドルウエアを作成するには
|
これはhandler_sample_2の手法をもっとスマートに構成する手法である。
ここでは次のように生成している:
|
即ちrequestHandlerの部分は、request.requestedUri.path == '/middleware'の時に限り直接shelf.Response.ok応答を返している。それ以外の場合はnullを返すことで該要求は内部ハンドラに渡されることを指示している。
/middlewareでアクセスしたとき:
|
それ以外でアクセスしたとき:
|
middleware_sample_3は内部ハンドラからのshelf.Responseを加工するミドルウエアの例である。このサーバを起動させ、ブラウザから'http://localhost:8080/123'でアクセスすると、ブラウザには次のようなテキストが表示される:
|
最初の行はsimpleHandlerが返したテキストであり、次の行がmyMiddlewareというミドルウエアが追加したものである。
このミドルウエアは次の関数で生成される:
shelf.Middleware myMiddleware = shelf.createMiddleware(responseHandler: modifyResponse); |
このようにshelf.createMiddlewareという簡便なメソッドを使うことができる。これはcontextを加工する必要がないからである。もし外側のミドルウエアにcontextを使って情報を渡したい場合は次のような記述が必要になる:
|
即ちこれは、要求が到来したらinnerHandlerをその要求で呼び、返されたFutureで待ち、応答がinnerHandlerから戻されたら、その応答でmodifyResponseを呼んで、結果としての応答(またはそれのFuture)を返す関数(即ちミドルウエア)を返す関数である。
modifyRequestは次のようになっている:
|
以下はこのサーバへのアクセス例である:
|
ミドルウエアがsimpleHandlerからの応答に1行追加している:
|