LoginSignup
8
8

More than 5 years have passed since last update.

js_of_ocamlで触るService Worker

Last updated at Posted at 2015-02-28

なんか気付いたら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塗れなのでした。オチはない。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8