WebSharperの基本

はじめに

この記事は F# Advent Calendar 2015の11日目です。 4年前に、WebSharperの紹介記事を書きましたが内容が陳腐化してしまったで、またWebSharperについて書きます。

まだ書き途中なので随時更新します。

WebSharperとは

WebSharperは、F#でWebアプリケーションを作るためのフレームワークです。 Client-SideもF#で記述するので、型の恩恵を受けれます。

Visual StudioでWebSharperを使う

現在、WebShaper 公式サイトからダウンロードできるVSIXは、WebSharper 3.x系に対応していません。 WebSharper 3.xのVSIXを入手するには、websharper.templatesが必要です。

  1. github/websharper.templates.gitをCloneします。
  2. websharper.templatesをビルドします。
  3. ビルドして出来たプログラムを実行してvsixを生成します。

Siteletsでウェブサイトを定義する

WebSharperがウェブサイトを定義するためにSiteletsという方法を提供しています。 ウェブサイトは、リクエストからレスポンスへの変換を定義します。 Website属性のつけた変数がウェブサイトになります。

[<Website>]
let main : Sitelet = (* ここにSiteletを定義する *)

Website属性をつけた変数は、Sitelet型である必要があります。 Siteletは、下記の役割をもっています。

  • リクエストからアクションを生成する
  • アクションからコンテントを生成する

WebSharperは、HTTPのルーティングを型として抽象的に表現します。 そしてアクションは、そのルーティングの結果です。 コンテントは、レスポンスを得るための関数です。 つまりSiteletは、アクションやコンテントという抽象を介してリクエストをレスポンスに変換するための型です。

Siteletの具体的な定義を示します。

type Sitelet<'Action when 'Action : equality> =
    {
        Router : Router<'Action>
        Controller : Controller<'Action>
    }

Siteletを構成する型を説明します。

Siteletを構成する型 定義
Router Request -> 'Action
Controller 'Action -> Content<'Action>

Controllerの戻り値であるContent<'Action>型は、下記のように定義されています。

type Content<'Action> =
      CustomContent of (Context<'Action> -> Http.Response)
      CustomContentAsync of (Context<'Action> -> Async<Http.Response>)

Content<'Action>には、同期版と非同期版がありますが、どちらもレスポンスを生成するという点では同じです。 下記は、Content<'Action>を生成するためのメソッド一覧です。コンテントの種類によって使い分けましょう。

メソッド 説明
Content.Page HTMLのボディ、HTMLのヘッド部、HTMLのタイトル、HTMLのDoctypeを指定してHTMLのコンテントを生成します |  
Content.Json JSONのコンテントを生成します
Content.Text テキストのコンテントを生成します
Content.File ファイルを読み込んでコンテントを生成します
Content.Custom HTTPステータス、HTTPヘッダー、ボディを指定してコンテントを生成します

コンテントを返すSiteletを定義する

コンテントを返すSiteletは、Sitelet.Contentメソッドで定義します。 Sitelet.Contentメソッドの型は、location: string -> action: 'T -> cnt: (Context<'T> -> Async<Content<'T>>) -> Sitelet<'T>です。

引数名 説明
location string コンテントの場所を指定します。
action 'T コンテントのアクションを指定します。
cnt Context<'T> -> Async<Content<'T>> コンテキストからコンテントの変換を指定します。

下記は、ルート(GET /)に対してページ(HTML)を返すSiteletの定義例です。

type EndPoints = Index

[<Website>]
let main = 
    Sitelet.Content "/" Index (fun _ -> Content.Page(Body=[Text "login helo"], Title="login page"))

Siteletを合成する

SiteletはSitelet.Sumメソッドで合成できます。

[<Website>]
let main = 
  Sitelet.Sum [
    Sitelet.Content "/" Login (fun _ -> Content.Page(Body=[Text "login helo"], Title="login page"))
    Sitelet.Content "/my" My (fun _ -> Content.Text "my") 
  ] 

Siteletのルーティングを型で定義する

Siteletのルーティングは、抽象化されていて型で定義できます。 Sitelet.Infer メソッドは、定義したルーティングとURLとのマッピングを推論します。 型がどんなふうにルーティングを表現するかは下記のチートシートを参照するとよいでしょう。

http://websharper.com/docs/sitelets-ref

認証されたSiteletを定義する

Sitelet.Protectメソッドは、認証されたSiteletに変換します。 第一引数にはFilterを適用します。 Filterは、VerifyUser : string -> boolLoginRedirect : Endpoint -> Endpointで定義します。

(* 変換したいSitelet *) 
|> Sitelet.Protect ({ 
       VerifyUser = (fun _-> false)
       LoginRedirect = (fun _ -> Login)  
    } : Sitelet.Filter<EndPoints>)

Applicationモジュール

WebSharper.Applicationモジュールは、Siteletのファサードです。

HTML Combinators

WebSharperで扱うHTMLは、HTML Combinatorsによって生成します。 HTML Combinatorsは2種類あります。

  • Server-Side HTML Combinator
  • Client-Side HTML Combinator

Client-Side HTML Combinatorは、Pageletとも呼びます。

Server-SideのHTML Combinator

Server-SideのHTMLは、WebSharper.Html.ServerのHTML Combinatorを使って生成します。

  • HTML要素の型 : Element
  • HTML属性の型 : Attriute

ElementとAttributeは、INodeを実装しています。

let serverSideHtml : Element = 
  Article [
    H1 [Text "hello serverSideHtml"]

    // 下記のように属性を定義する場合は、アップキャストするのがめんどくさいので -< 演算子が定義されている
    P [Class "paragraph"] 
    -< [Text "This is a paragraph."]
    // HTML: <p class="paragraph">This is a paragraph.</p>

    // 下記のように Tags、Attrを修飾するとインテリセンスが効いて便利。
    // AutoOpenなので、修飾しなくても大丈夫。
    Tags.P [Attr.Class "paragraph"] 
    -< [Tags.Text "This is a paragraph."]

    embedClientSide
  ]

Client-SideのHTML Combinator (Pagelet)

Client-SideのHTMLは、WebSharper.Html.ClientのHTML Combinatorを使って生成します。 Server-Sideとは名前空間が違うだけで同じ関数名でHTMLを生成できます。

  • HTML要素の型 : Element
  • HTML属性の型 : Attriute

ElementとAttributeは、Pageletを実装しています。

JavaScript属性をつけておくことで、ビルド時にWebSharperがILからjavascriptコンパイルします。

[<JavaScript>]
module ClientSideHtml =
  open WebSharper.Html.Client

  let clientSideHtml () : Element =
    Article [
      H1 [Text "hello clientSideHtml"]
    ]

サーバーサイドのHTML Combinatorは、変数でよいですがクライアントサイドの場合は関数かプロパティでないといけません。。 Server-Sideとは違いClient-SideのHTMLは、(|>!) 演算子 と OnXXX系関数 でイベントハンドラーを定義します。

let clientSideHtmlWithEventHandler () =
  Article [
    H1 [Text "hello clientSideHtmlWithEventHandler"]
    Button [Text "Click Me"]
    |>! OnClick (fun button event ->
      WebSharper.JavaScript.JS.Alert("clicked!")
    )
  ]

PageletはRenderメソッドを持っていて、レンダリングが終わったことをOnAfterRenderでフックできます。

let clientSideHtmlWithHookRendering () =
  Article []
  |>! OnAfterRender (fun article ->
    H1 [Text "hello clientSideHtmlWithHookRendering"]
    |> article.Append
  )

Pageletは、動的なDOM操作をサポートしています。

let clientSideHtmlWithDynamicDom () =
  Article []
  |>! OnAfterRender (fun article ->
    // 要素を追加したり
    H1 [Text "hello clientSideHtmlWithDynamicDom"]
    |> article.Append

    // 要素のテキストノードを読み取ったり
    Div [
      H2 [Text "Text property"]
      Div [Text (P [Text "text property value"]).Text]
    ]
    |> article.Append

    // 要素のinner HTMLを読み取ったり
    Div [
      H2 [Text "Html property"]
      Div [Text (P [Text "Html property value"]).Html]
    ]
    |> article.Append

    // JSとしてのDOMを読み取ったり
    (P [Text "DOM property value"]).Dom // JavaScript.Dom.Element
    |> article.Append

    // JSとしてのBodyを読み取ったり
    (P [Text "Body property value"]).Body // JavaScript.Dom.Node
    |> article.Append

  )

PageletをServer-Sideに埋め込む

Client-SideのHTML ConbinatorsをControlとして定義する

Controlとして、Client-SideのHTMLを定義することもできます。 Controlを定義することで、つぎの2つのメリットがあります。

  • ClientSide関数で埋め込む場合より、複雑な値をコンストラクタに渡せる。
  • ASP.NET Pagesと統合できるようになる。
module ClientSideHtmlAsControl =
  open WebSharper.Html.Client

  type ClientSideControl() =
    inherit WebSharper.Web.Control ()

    [<JavaScript>]
    override this.Body =
      Article [
        H1 [Text "hello clientSideHtml"]
      ] :> _

RPC(Remote Procedure Call)

関数にRemote属性をつけると、Pageletから呼び出せるようになります。 Rpc属性というものもありますが、Remote属性と同じ意味です。 RPCには、下記の3種類があります。

  • メッセージパッシング
  • 非同期呼び出し
  • 同期呼び出し

WebSharper 2からサポートされたHandlerObjects機能を使うとメソッド呼び出しもできるらしいが、使い方はよくわかりません。

メッセージパッシング

* -> unitなRemote関数は、メッセージパッシングになります。 つまり、クライアントはRemote関数の終了に対してブロックをしません。

[<Remote>]
let callMessagePassing () = 
  Thread.Sleep(5000)

非同期呼び出し

* -> Async<*>なRemote関数は、非同期呼び出しになります。 つまり、クライアントはRemote関数の終了に対してブロックをしません。 クライアントサイドのコードは、Asyncを使って戻り値を取得できます。

[<Remote>]
let callAsync () = 
  async {
    // async中で実行される処理は、スレッドがリクエストのスレッドとは異なるので
    // System.Web.HttpContext.Currentのようなスレッドローカルなものに注意する。
    // コンテキストを取得するにはWeb.IContextを使うこと。
    Thread.Sleep(5000)
    return "ok"
  }

同期呼び出し

* -> *なRemote関数は、同期呼び出しになります。 つまり、クライアントはRemote関数の終了に対してブロックします。 同期呼び出しは、呼び出し中にブラウザが固まるので推奨されません。

[<Remote>]
let callSync () = 
  Thread.Sleep(5000)
  "ok"

WebContext

WebContextを使ってRpcでユーザーを取得します。

[<Rpc>]
let showLoginUser () =
    // Rpc(Remote Procedure Call)でIContextを取得するにはRemoting.GetContextメソッドを使う。
    let ctx = Remoting.GetContext()
    ctx.UserSession.GetLoggedInUser()

RpcをClient-Sideから呼び出します。

[<JavaScript>]
module ClientSideSample =
    open WebSharper.Html.Client
     
    let printLoginUser () =
        H1 []
        |>! OnAfterRender (fun p ->
            async {
              let! user = RemoteSample.showLoginUser()
              p.Append("Welcome : " + user.Value)
            } |> Async.Start
        )

SiteletにClientSideSampleを埋め込みます。 Siteletの中では、IContext経由でUserSession機能を使えます。

module Site =
    open WebSharper.Html.Server

    type EndPoints = string

    [<Website>]
    let Main =
        Sitelet.Infer (fun ctx user ->
            async {
                // Siteletの中では、IContext経由でUserSession機能を使える。
                do! ctx.UserSession.LoginUser(user)
                return! Content.Page(Body=[Div [ClientSide <@ ClientSideSample.printLoginUser () @>]], Title="hoge")
            }
        )

HTML Template

HTML Templateは、HTMLに下記の3つの方法でプレースホルダーを埋め込むことができます。

プレースホルダーの種類 指定方法
子要素プレースホルダ data-hole属性をHTML要素につける
要素そのもののプレースホルダ data-replace属性をHTML要素につける
文字列プレースホルダ HTML要素のインナー要素として${xxxx}を記述する

下記のようなhtmlファイルでtitlebodyプレースホルダーにします。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8" />
    <title>${title}</title>
</head>
<body>
  <div data-replace="body">
  </div>
</body>
</html>

Main.htmlのテンプレートを埋めるための型を作ります。 Pageのメンバは、プレースホルダーにした要素になります。

type Page = {
  Title : string
  Body: list<Element>
}

最後にContent.Template関数でContent.Templateオブジェクトを作成します。 プレースホルダーをどのように置換するかWithメソッドで記述します。

let MainTemplate : Content.Template<Page> =
  Content.Template<Page>("~/Main.html")
    .With("title", fun x -> x.Title)
    .With("body", fun x -> x.Body)

次にArticle.htmlのテンプレートを作ります。 プレースホルダーは、titlecontentです。

<article>
<h2>${title}</h2>
<section class="main" data-hole="content"></section>
</article>

Article.htmlのテンプレートを埋めるための型を作ります。

type Article = {
  Title: string
  Content: list<Element>
}
let ArticleTemplate: Content.Template<Article> =
  Content.Template<Article>("~/Article.html")
    .With("title", fun x -> x.Title)
    .With("content", fun x -> x.Content)
let MainPage (ctx: Context<Application.SPA.EndPoint>) : Async<Content<Application.SPA.EndPoint>> =
  let articles : Article list = [
    { Title = "Article1"; Content = [Text "this is the article1"]}
    { Title = "Article2"; Content = [Text "this is the article2"]}
  ]

MainTemplateをContent.WithTemplateでContentを生成します。

  • 第一引数にContent.Template
  • 第二引数にT
Content.WithTemplate MainTemplate {
  Title = "My Page"
  Body = [
    yield H1 [Text "Body of the page"]
    for article in articles do
      // Content.Templateに対してRunすることでインスタンス化できる
      // thisにContent.Template<T>
      // 第一引数にT
      // 第二引数にルートフォルダ(string)
      yield! ArticleTemplate.Run(article, ctx.RootFolder)
  ]
}

UI.Next

UI Nextは、リアクティブなUIを構築するためのクライアントサイド向けのライブラリです。

名前空間
WebSharper.UI.Next.Server
WebSharper.UI.Next.Html
WebSharper.UI.Next.Client

Siteletを定義する

Siteletを定義します。

module Site =
  open WebSharper.UI.Next.Server
  open WebSharper.UI.Next.Html

  [<Website>]
  let Main =
    Application.SinglePage (fun ctx ->
      // client関数で、クライアントサイドのDocをサーバーサイドに埋め込みます。
      Content.Doc(client <@ ClientSideSample.firstSample () @>)
    )

Client-Sideでリアクティブ変数を作る

Client-SideでUI.Nextを使うには下記の名前空間を開きましょう。

open WebSharper.UI.Next.Client
open WebSharper.JavaScript

リアクティブ変数(Var)を作る。 rvプリフィックスは、Reactive Variableの意味。

let rvInput = Var.Create ""

View.FromVarで VarからViewを取得できる。

let vInput = View.FromVar rvInput

View.Mapで入力入力された文字列を大文字化します。

let vUpperInput = vInput |> View.Map (fun str -> str.ToUpper())

UI.Nextは、Virtual Domを扱うための型としてDoc型を用意しています。 Doc型は、IControlBodyを実装しています。 Doc.AsPageletでPageletに変換できる。 Docは、Concatで結合できます。

Doc.Concat [
  // Doc.Inputは、リアクティブ変数と結びつくインプット要素を生成します。
  Doc.Input [] rvInput

  // View<string>からテキストノードを生成します。
  Doc.TextView vUpperInput
]

HTML要素は、Elmを返す関数で表します。 divAttrのようにAttrサフィックスの関数は、Attrを受け取ります。

divAttr [attr.styleDyn vSearchEngineColor] [
  Doc.Select [] (function |Google -> "Google" | Yahoo -> "Yahoo") [Google; Yahoo] rvSearchEngine
  Doc.TextView (rvSearchEngine.View |> View.Map (function |Google -> "Google" | Yahoo -> "Yahoo"))
]

SubmitterでDataflowの変更を通知する

Submitterは、Dataflowレイヤーの特別なノードです。 SubmitterのViewは、最後にTriggerを呼ばれた時点のViewになります。

作成時にViewと初期値を渡します。

let submitter = Submitter.Create rvInput.View ""

ButtonのアクションとしてSubmitterのTriggerを渡せます。

Doc.Concat [
  Doc.Input [] rvInput
  Doc.Button "submit" [] submitter.Trigger
  Doc.TextView submitter.View
]

UI.NextのTemplating

// TODO