なんか気付いたら3月なんですけど、誰か時計の針こっそり1日12時間ずつ進めてたりしませんか?
最近そんな気分になることがあるのがマジで怖くなってきました。本当にTime is money.
Service Workerを知った
つい最近、Chrome 40 で今すぐ ServiceWorker を試すという記事を読んで、寡聞にして知りませんでしたが、ServiceWorkerという仕様を知りました。
最近プロジェクトでiPad対応・・・という声がささやかれるようになり、またマスタ系統のキャッシュをなんとかしてぇなぁ・・・と常々思っていましたが、これを使えばいけるんじゃね!?しかも無ければ普通にアクセスするだけだし!と思い、目論見通りに触ってみることにしました。
・・・が、ただ触るだけじゃ面白くない。Googlingしてみると、結構いろいろ触っている人もいるし、仕様に関する話だけなら2014年からもありました。
特にこのService Workerは、フロントエンドとバックエンドの間でいろいろできる、ということもありますが、DOMを触れないとか、Web Workerを彷彿とさせる感じです。
・・・ん?ということは型付言語でかっちり作れるんじゃね? ならばjs_of_ocamlだ!
Service Worker in js_of_ocaml
js_of_ocaml(http://ocsigen.org/js_of_ocaml/) は、Facebookのflowでも利用されている、OCamlのバイトコードからJavaScriptを吐いてしまうクレイジーなライブラリです。
ただし、直近のOCaml 4.02系列では標準で添付されなくなったcamlp4にがっつり依存してるのと、ppxの話が進んだりしてるようなしてないようなよくわかんない感じでした。ppxで利用出来る拡張が出来たらそれはそれで使ってみようかと思います。多分使いづらいですが。
前置きは置いておいて、とりあえずサンプルを作ってみました。キャッシュとかを扱うのは、普通にOCamlのclassにエンコードすればできますが、そこまでやる気力が無かった(というか単純に面倒)だったので、至極シンプルにこんなサンプルです。
- index.htmlにアクセスすると、普通(=>index.htmlの内容が表示される)
- /testとか深い階層に移動すると、自動的に生成されたレスポンスが表示される
これだけでも以外と面倒でした。
実際のコード
まず、超簡単にpromiseとconsole(ログ用)の定義を作りました。
(* console.ml *)
open Js
class type console = object
method log : 'a t -> unit meth
end
let console : console t = Unsafe.variable "console"
(* promise.ml *)
open Js
class type promise = object
method _then: ('a t -> unit) -> promise t meth
method _catch: ('a t -> unit) -> promise t meth
end
注意)js_of_ocamlの拡張を使っているせいなのかどうかわかりませんが、シンタックスハイライトがおかしいです。すみません。
そのまんまですね。console.logに複数の引数を渡せるようにするのは、ものすごく面倒になりそうだったんでさっくり無視しました。
promiseについても、単純にインターフェースだけで、コンストラクタは定義していません。というかできないし。
では、ServiceWorkerを登録する側です。登録する側は別に普通に書けばいいんですが、とりあえず今回はサンプルなんで。
open Js
class type setting = object
method scope : js_string t prop
end
let navigator = Unsafe.variable "navigator"
let register service setting =
let console = Console.console in
let m = Unsafe.get navigator "serviceWorker" |> def in
Optdef.iter m (fun f ->
let promise : Promise.promise t = Unsafe.meth_call m "register" [| Unsafe.inject service; setting; |] in
(promise##_then (fun _ -> console##log (string "registered")))##_catch (fun e -> console##log (string "can not registered"); console##log (e)) |> ignore
)
let () = ignore begin
register "/service_worker.js" (Unsafe.obj [| ("scope", Unsafe.inject (string "/")) |])
end
navigatorにserviceWorkerがあるかどうかをチェックして、存在していたらサービスを設定する、という感じです。
navigatorにあるかどうかをチェックするのに、Optdefというundefinedをoptionのように利用できるものを利用してます。
ちなみに、(promise##_then ...)##_catch (...) となっている部分ですが、ここはpromiseの仕様をよく確認してないので、もしかしたら分けてもいけるのかもしれませんし、いけないのかもしれません。とりあえずここでは愚直にJavaScriptをエンコードしてます。
さて、実際のWorker側です。若干でかいです。
et version = "1.0"
class type installEvent = object
inherit Dom_html.event
(* Wait install until successful callback. *)
method waitUntil: 'a -> unit Js.meth
end
class type _request = object
method url: Js.js_string Js.t Js.readonly_prop
method _Method: Js.js_string Js.t Js.readonly_prop
end
class type _response = object
end
class type fetchEvent = object
inherit Dom_html.event
method respondWith_promise: Promise.promise Js.t -> unit Js.meth
method respondWith_response: _response Js.t -> unit Js.meth
method default: _request Js.t -> 'a Js.meth
method request: _request Js.t Js.readonly_prop
end
(* Define events for service worker *)
module Service_worker_events = struct
let install : installEvent Js.t Dom_html.Event.typ = Dom_html.Event.make "install"
let fetch : fetchEvent Js.t Dom_html.Event.typ = Dom_html.Event.make "fetch"
end
(* ServiceWorkerでのイベント登録を楽にする?ためのモジュール。 *)
module Service_worker : sig
val install: ((installEvent as 'b) Js.t -> bool Js.t) -> Dom_html.event_listener_id
val fetch: ((fetchEvent as 'b) Js.t -> bool Js.t) -> Dom_html.event_listener_id
end = struct
module Event = Service_worker_events
let addEvent e h =
let h = Dom_html.handler h in
let open Js in
Dom_html.addEventListener Dom_html.window e h _false
let install handler = addEvent Event.install handler
let fetch handler = addEvent Event.fetch handler
end
let _Response : (Js.js_string Js.t -> _response Js.t) Js.constr = Js.Unsafe.global##_Response
let () = ignore begin
let console = Console.console in
Service_worker.install (fun _ ->
let open Js in
console##log (string ("installed " ^ version)); _false) |> ignore;
Service_worker.fetch (fun e ->
let url = Js.to_string e##request##url |> Url.url_of_string in
match url with
| None -> assert false
| Some url -> begin match url with
| Url.Http url | Url.Https url when url.Url.hu_path_string <> "" ->
let response = "You access to " ^ url.Url.hu_path_string in
let res = jsnew (_Response) ((Js.string response)) in
e##respondWith_response (res) |> ignore;
| _ -> e##default (e##request)
end;
Js._false
) |> ignore;
end
残念というか当然というか、install/fetchというイベントはまだjs_of_ocamlに存在しないので、installEvent、fetchEventというclassにエンコードしてます。
Service_workerというモジュールはコメントに書いてある通りです。addEventListenerが結構めんどくさいもんで・・・。
それと、fetchEventのrespondWithについては、promiseとresponseいずれも受け取れるようにしてます。若干冗長になりますが仕方ない。
fetchでの処理が、若干OCamlらしいといえばらしいでしょうか。urlかどうかをmatchで分解して、http/httpsの場合だけ別の処理をやって、それ以外はデフォルトの処理をやる、みたいなかんじになってます。
js_of_ocamlでやる意義はあるか
インターフェースをclassにエンコードしきったライブラリがあるのであれば(多分面倒だけどそれほど難しくない)、恐らくやる意義はあるでしょう。多分。
このシンプルな例でも、urlの型とかそういったものからちょっとでもずれると、すぐにエラーになるのは嬉しいです。
ただ、プロダクションでは利用しない(モックサーバーやテストで利用する場合など)は、これで実装する意義は全くないので、そういう場合は大人しくtry&errorする方が良いかと思います。
実際にこの上にキャッシュを実装する上でも最も難関になるであろう部分は、キャッシュから返ってくるデータをどうエンコードするか、という部分になるかと思います。無視してresponseにぶち込んでしまえばいいと思いますが、細かい制御をしたい場合は、型が仇になって実装が結構面倒になるとおもいます。
ただ、試してみると遅延とかは全く感じないので、キャッシュを作成するだけでも効果のほどがうかがえます。
js_of_ocamlでやったほうがいい部分は?
キャッシュを返すだけの部分とかは、JavaScriptで書いても別段変わんないと思うので、細かい制御がいらないのならCoffeeScriptでもTypeScriptでもいいと思います。
それ以外、Offlineの場合における制御やら処理やら、難しい部分になったら、js_of_ocamlの利用価値はあると思います。型があることによる安心が欲しい部分になりますので。でも大変なのは請け合い(特に常駐とかでやると確実に後が大変なのでやめようね★)
それでも(できるだけ)型が欲しい
欲しいんです。もう何が入っているかわからないObjectと戦いたくないんだ・・・!
でも最近React.jsをがっつり使ってJavaScript塗れなのでした。オチはない。