WebSharper(F#)でWebSocket
WebSharperって何ぞやという方や触ったことのない方は下記otfさんの記事を先に読んでください。
F# WebSharperで関数型的ウェブ開発 - 無料でプログラミングを教えます日記
ところでこのWebSharperさんは、HTML5やWebSocketにしっかり対応しています。
これは試してみないわけにはいかないと思うので、サンプルを作ってみましょう。
プロジェクト生成など
WebSharper 2.4 HTML Application (Sitelets)テンプレートを使ってプロジェクトを作成作成してください。
次にhtmlファイルの修正。
<!DOCTYPE html> <html> <head> <title>${Title}</title> <meta name="generator" content="WebSharper" data-replace="scripts" /> </head> <body> <div> <table> <tr> <td data-hole="Caption"> </td> </tr> <tr> <td data-hole="Content"> </td> </tr> </table> </div> </body> </html>
わりとひどい気はしますがスルーして外枠の作成に移ります。
namespace Sample open System open System.IO open System.Web open IntelliFactory.Html open IntelliFactory.WebSharper.Sitelets open IntelliFactory.WebSharper type Action = | Index module Site = let index = PageContent <| fun context -> let t = typeof<SampleCanvasViewer> let element = Activator.CreateInstance(t) :?> Web.Control { Page.Default with Body = [Div [element]] Title = Some "Sample Canvas" } let controller = let handler = function | Index -> index { Handle = handler } type Website() = interface IWebsite<Action> with member this.Actions = [] member this.Sitelet = { Controller = Site.controller Router = Router.Table [Index, "/"] <|> Router.Infer() } [<assembly: WebsiteAttribute(typeof<Website>)>] do ()
調査用に書いたコードを流用したため少し冗長になっています。気になる方は自分で削ってみてください。
完成物
先に完成物を貼り付けておきます。これ見て理解できるなら後半の説明部分は読まなくても問題ないです。
namespace Sample open System.Net open IntelliFactory.WebSharper open IntelliFactory.WebSharper.Html open IntelliFactory.WebSharper.Html5 open IntelliFactory.WebSharper.JQuery module SampleCanvas = type Point = { x : float y : float } [<JavaScript>] let width = 400 [<JavaScript>] let height = 600 [<JavaScript>] let drawBackground (context : CanvasRenderingContext2D) = context.BeginPath() context.ClearRect(0., 0., float width, float height) context.Rect(0., 0., float width, float height) context.FillStyle <- "rgb(0, 0, 0)" context.Fill() module Tri = [<JavaScript>] let position (offset : Position) x y = let nextX = float (x - offset.Left) let nextY = float (y - offset.Top) { x = nextX; y = nextY } [<JavaScript>] let draw (context : CanvasRenderingContext2D) playerShip = context.Save() context.BeginPath() context.Translate(playerShip.x, playerShip.y) context.MoveTo(0., -10.) context.LineTo(-10., 10.) context.LineTo(10., 10.) context.FillStyle <- "rgb(64, 64, 255)" context.Fill() context.Restore() [<JavaScript>] let initSocket context = let socket = WebSocket("ws://localhost:19860/shooting") socket.Onopen <- (fun () -> {x = 0; y = 0} |> Json.Stringify |> socket.Send ) socket.Onmessage <- (fun msg -> drawBackground context msg.Data |> (string >> Json.Parse >> As<Point>) |> (Tri.draw context) ) socket [<JavaScript>] let animatedCanvas width height = // キャンバスの設定 let element = Tags.NewTag "Canvas" [] let canvas = As<CanvasElement> element.Dom canvas.Width <- width canvas.Height <- height let context = canvas.GetContext "2d" // ソケットの準備 let socket = initSocket context Div [ Width (string width); Attr.Style "float:left" ] -< [ Div [ Attr.Style "float:center" ] -< [ element |>! OnMouseMove (fun _ arg -> let offset = JQuery.JQuery.Of(element.Dom).Offset() (arg.X, arg.Y) ||> Tri.position offset |> Json.Stringify |> socket.Send ) ] ] [<JavaScript>] let Main () = Div [ animatedCanvas width height Div [Attr.Style "clear:both"] ] type SampleCanvasViewer() = inherit Web.Control() [<JavaScript>] override this.Body = SampleCanvas.Main () :> _
ここではJQueryを利用している部分とWebSocket部分を簡単に説明します。
canvas部分などはJavaScriptでのHTML5ほぼそのままな感じなので説明を省略します。
WebSocket部分
initSocket関数の一部分のみ抜粋して再掲します。
let socket = WebSocket("ws://localhost:19860/websocket") socket.Onopen <- (fun () -> {x = 0; y = 0} |> Json.Stringify |> socket.Send ) socket.Onmessage <- (fun msg -> drawBackground context msg.Data |> (string >> Json.Parse >> As<Point>) |> (Tri.draw context) )
JavaScriptでWebSocketプログラムを書いたことがある人にとっては簡単だと思います。WebSocketオブジェクトを生成して、接続が確率したときに実行する関数とサーバ側からメッセージが送られてきたときに実行する関数を設定しているだけですからね。
ちなみに今回はJSONを使ってデータを送受信しています。
msg.Dataはobj型でJSON.Parseのシグネチャがobj -> stringなので、仕方なくstringに型変換しています。binaryが送られてくる可能性もあるので、もっときちんと書くならパターンマッチを使ったほうがいいでしょう。
As
JQuery部分
Canvas上でマウスがmoveしたときの動作を記述している部分を再掲します。
element |>! OnMouseMove (fun _ arg -> let offset = JQuery.JQuery.Of(element.Dom).Offset() (arg.X, arg.Y) ||> Tri.position offset |> Json.Stringify |> socket.Send )
OnMauseHoge系で取得できる座標は絶対座標なのに対し、必要なのはcanvas上での相対座標なのでcanvasのoffsetを使って相対座標を計算し、その後JSONデータに変換してサーバに送信しています。
一つ目のJQueryはモジュール名で、二つ目のJQueryはクラス名です。モジュールのインポートをうまくやらないとこういった残念なコードを書かないといけなくなります。
JQuery.OfでJQueryオブジェクトを取得し、Offsetでcanvasの配置位置を取得しています。
完全に余談ですが、JQueryメソッドの多くはJQueryオブジェクトを引数にとりJQueryオブジェクトを返していたりします。つまりモナd…時間が足りないのでこの話は横においておきます。
実行
実行するにはサーバ側のコードが必要になるので、今回は下記記事のコードを利用します。
Cowboy(Erlang)でWebSocket - pocketberserkerの爆走
黒いcanvas上でマウスを動かすと、三角形も移動すると思います(サーバ側が1秒後との更新になっているのでカクカクですが)。
というわけで
WebSocketで送信されてきたデータはobj型なのでアレですが(仕方ないけど)、As関数を使うことで型レベルプログラミングできるようになります。他のコードもJavaScriptをそのまま記述するよりは安心して書けるのではないでしょうか。
まぁ、JavaScriptまともに書いたことないから比較できないのですけど!
それはさておき、WebSharperはHTML5が使えたりWebSocketも最新ドラフトに対応しているっぽかったりと、わりと面白いです。
これを機会に、皆さんもF#でJSなコードを書いてみませんか?