最近、Swift向けのWebアプリケーションサーバ・フレームワークである"Perfect"を触っている。
http://perfect.org/perfect.org
ドキュメントやサンプルも分かりやすく(まだまだ情報量は少ないものの、それはこれから)、
Swiftに触り始めたばかりの私でも触りやすく、
かつ世界的にもかなり注目されているようなので、
そのうちデファクト・スタンダードの座にどかっと腰を下ろすかもしれない、
そんなフレームワークの一つ。個人的には大歓迎。
さて、このPerfectでのクライアントとサーバ側のやり取りの仕組みについてちょっと調べてみた。
そもそものきっかけはPerfectのExampleの中に入っていたTapTracker内での
クライアントとサーバとのやり取りが、どのような仕組みになっているかが気になったため。
以下は、私がPerfectを触った際の備忘録的な記事となります。
もし記載している内容に誤りがありましたら、コメントなどからご指摘頂けますと幸いです。
ちなみにこのサンプルについては下記の記事で分かりやすく解説してくれています。
私は始めにこの記事を読んで、Perfectに興味を持ちました。
早速Tap Trackerを触ってみたところ、"ViewController.swift"にて指定している通信先のエンドポイント。
これって押下するボタンによって、処理を変えられないのかしら?と思った。
同じくPerfectのExamplesとして入っている"URL Routing"では
下記のような手順でアクセスするURLによって処理を分岐させている。
下記、PerfectHandlers.swift内のサンプルコードをそのまま抜粋
// This is the function which all Perfect Server modules must expose. // The system will load the module and call this function. // In here, register any handlers or perform any one-time tasks. public func PerfectServerModuleInit() { // Install the built-in routing handler. // Using this system is optional and you could install your own system if desired. Routing.Handler.registerGlobally() Routing.Routes["GET", ["/", "index.html"] ] = { (_:WebResponse) in return IndexHandler() } Routing.Routes["/foo/*/baz"] = { _ in return EchoHandler() } Routing.Routes["/foo/bar/baz"] = { _ in return EchoHandler() } Routing.Routes["GET", "/user/{id}/baz"] = { _ in return Echo2Handler() } Routing.Routes["GET", "/user/{id}"] = { _ in return Echo2Handler() } Routing.Routes["POST", "/user/{id}/baz"] = { _ in return Echo3Handler() } // Test this one via command line with curl: // curl --data "{\"id\":123}" http://0.0.0.0:8181/raw --header "Content-Type:application/json" Routing.Routes["POST", "/raw"] = { _ in return RawPOSTHandler() } // Check the console to see the logical structure of what was installed. print("\(Routing.Routes.description)") }
これはソースの内容から、処理はだいたい想像できる。
が、Tap Tracker内ではこのような処理は行われておらず、
TTHandlers.swift内のPerfectServerModuleInitにて、以下の様な処理が行われていた。
以下、サンプルコードをそのまま抜粋
// This is the function which all Perfect Server modules must expose. // The system will load the module and call this function. // In here, register any handlers or perform any one-time tasks. public func PerfectServerModuleInit() { // Register our handler class with the PageHandlerRegistry. // The name "TTHandler", which we supply here, is used within a mustache template to associate the template with the handler. PageHandlerRegistry.addPageHandler("TTHandler") { // This closure is called in order to create the handler object. // It is called once for each relevant request. // The supplied WebResponse object can be used to tailor the return value. // However, all request processing should take place in the `valuesForResponse` function. (r:WebResponse) -> PageHandler in return TTHandler() } // Create our SQLite tracking database. do { let sqlite = try SQLite(TTHandler.trackerDbPath) try sqlite.execute("CREATE TABLE IF NOT EXISTS taps (id INTEGER PRIMARY KEY, time REAL, lat REAL, long REAL)") } catch { print("Failure creating tracker database at " + TTHandler.trackerDbPath) } }
"TTHandler"を"PageHandler"に追加しているんだろうなーとは分かるのだが、
では、このTTHandlerはどうやって呼ばれるのか?
"TapTracker.mustache"のコードを下記に掲載する。
{{% handler:TTHandler}}{{! This is the mustache template file for the tap tracker example. }}{"resultSets":[{{#resultSets}}{"time":"{{time}}","lat":{{lat}},"long":{{long}} }{{^last}},{{/last}}{{/resultSets}}]}
このコード内の"{% handler:TTHandler}"で紐付けているのかな?と推測。
クライアント側からは"http://localhost:8181/TapTracker"でアクセスしている。
1."http://localhost:8181/TapTracker"でアクセス。
2."TapTracker.mustache(エンドポイント=>TapTrackerの部分とmustacheファイル名が紐付けられている)が呼ばれ、
3.mustacheファイル内に記載された"{% handler:TTHandler}"の記述を元に、
4.PageHandlerに追加されている"TTHandler"が呼び出される。
5.TTHandler内のvaluesForResponseにて、処理が行われる
恐らくはこんな感じなのではないだろうか?
(自信が持ちきれていないのは、充分な検証ができていないため)
というわけで、自身で新たな"TapTracker"とは別に、エンドポイントを作成してみる。
ここでは"Test"とする。
("http://localhost:8181/Test"でアクセスすると、処理が返ってくるようにしてみる)
1.アクセスするURLと同名の"Test.mustache"を用意する。
2.mustacheファイルの中身を下記のように記載する。
{{% handler:TestHandler}}{{! This is the mustache template file for the tap tracker example. }}{"resultSetsTest":[{{#resultSetsTest}}{"value":"{{value}}","value2":{{value2}},{{/resultSetsTest}}]}
3.PageHandlerクラスを継承した、TestHandler.swiftファイルを作成。
4.TestHandlerクラス内にvaluesForResponseメソッドを作成し、このメソッド内に下記のように処理を書く。
import PerfectLib class TestHandler: PageHandler { func valuesForResponse(context: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) throws -> MustacheEvaluationContext.MapType { print("TEST") // The dictionary which we will return var values = MustacheEvaluationContext.MapType() let resultSetsTest: [[String:Any]] = [["value": 999, "value2":999]] values["resultSetsTest"] = resultSetsTest return values } }
これで、"{"resultSetsTest":[{"value":"999","value2":999,]}"という値が返ってくるはず。
5."TTHandlers.swift"のPerfectServerModuleInitメソッド内にて、TestHandlerを追加してやる
PageHandlerRegistry.addPageHandler("TestHandler") { (r:WebResponse) -> PageHandler in return TestHandler() }
これでOKだと思ったが、
The file /Test was not found.
と処理が返ってくる。
試行錯誤してみたところ、XCode上で下記の処理が必要だった。
6.XCodeから下記キャプチャの手順をふみ、"Test.mustache"ファイルを追加してやる。
これで再度、ビルド⇒実行。
"http://localhost:8181/Test"にアクセスすると、TestHandlerクラスのvaluesForResponseが呼び出されるため、
下記のような応答となる。
curl http://localhost:8181/Test
{"resultSetsTest":[{"value":"999","value2":999,]}
※追記
上で紹介させていただいたnirazoさんの記事にも記載ありましたね。
1行目の{{% handler:TTHandler}}で対応するHandler名を指定しているのがわかります。
なお、クライアントサイドからリクエストを投げる際のエンドポイント(TapTracker)の部分をmustacheのファイル名と同一にする必要があるようですね。
この方の記事、とても丁寧に説明されているので、特に私のようなSwift初学者にとっては
読み返すたびに新たな発見がある。というか、上記の件は私の単なる読み飛ばしなだけか。。