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 ()

調査用に書いたコードを流用したため少し冗長になっています。気になる方は自分で削ってみてください。

本題

今回はサンプルとして、canvas上でマウスポインタの指している場所に三角形を描画します。ただし、クライアント側に状態をもちたくないので座標はWebSocketでサーバ側で保持してもらいます。

完成物

先に完成物を貼り付けておきます。これ見て理解できるなら後半の説明部分は読まなくても問題ないです。

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でobj型のオブジェクトをHoge型に変換します。JSONデータがレコードに変換できるなんてどんな黒魔術を使っているのでしょうね。

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なコードを書いてみませんか?