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が必要です。
- github/websharper.templates.gitをCloneします。
- websharper.templatesをビルドします。
- ビルドして出来たプログラムを実行して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 -> bool
と LoginRedirect : 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つのメリットがあります。
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ファイルでtitle
とbody
をプレースホルダーにします。
<!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のテンプレートを作ります。
プレースホルダーは、title
とcontent
です。
<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