SAFE Stack (link) のサンプルの ToDo アプリを眺める続き。
アプリ本体のソースを見ていく。
コメントはファイル全般を見たときより輪をかけて怪しくて、憶測や感想でしかないものが増えた。
対象のファイルは以下の 4 個。
大した量ではない。
ファイル | 行数 |
src/Client/App.fs | 19 |
src/Client/Index.fs | 125 |
src/Server/Server.fs | 57 |
src/Shared/Shared.fs | 21 |
合計 | 222 |
src/Client/App.fs
1 module App
2
3 open Elmish
4 open Elmish.React
5
6 #if DEBUG
7 open Elmish.Debug
8 open Elmish.HMR
9 #endif
10
11 Program.mkProgram Index.init Index.update Index.view
12 #if DEBUG
13 |> Program.withConsoleTrace
14 #endif
15 |> Program.withReactSynchronous "elmish-app"
16 #if DEBUG
17 |> Program.withDebugger
18 #endif
19 |> Program.run
行番号 | コメント |
7 | Elmish にデバッガがあることに驚いた。ここに載っている。 |
8 | “HMR” の意味は “Hot Module Replacement” だった。ここに載っている。 |
11 | Elmish プログラムの生成。Program.mkProgram についてはここを見るのがよさそう。ソースが載っているのだけど。3つある引数の型は、それぞれ init : 'arg -> 'model * Cmd<'msg> update : 'msg -> 'model -> 'model * Cmd<'msg> view : 'model -> Dispatch<'msg> -> 'view になっている。 |
13 | Program.withConsoleTrace の中で Log.toConsole というのを呼んでいる。 |
15 | Program.withReactSynchronous はここを見ると Elmish.React の中で定義されている。”elmish-app” は src/Client/index.html にある div 要素の ID と一致。 |
17 | Program.withDebugger については 7 行目と同じく、ここを見るのよさそう。 |
19 | Elmish プログラムの開始。 |
src/Client/Index.fs
1 module Index
2
3 open Elmish
4 open Fable.Remoting.Client
5 open Shared
6
7 type Model = { Todos: Todo list; Input: string }
8
9 type Msg =
10 | GotTodos of Todo list
11 | SetInput of string
12 | AddTodo
13 | AddedTodo of Todo
14
行番号 | コメント |
4 | Fable.Remoting を使っている。確か Elmish.Remoting というのもあったけど、ここでは Fable.Remoting だった。両者の違いは何だろうか。 |
7, 8 | モデルとメッセージの定義。 |
15 let todosApi =
16 Remoting.createApi ()
17 |> Remoting.withRouteBuilder Route.builder
18 |> Remoting.buildProxy<ITodosApi>
19
行番号 | コメント |
15 | todosApi という名前で、サーバの API を Remoting 経由で呼ぶ窓を作っている。サーバにも同名の todosApi というものがあるけど、todosApi の役割がクライアントとサーバとで異なっている。クライアントでは todosApi が Remoting をラップしている。サーバで Remoting をラップするのは webApp というやつで、todosApi は webApp を組み立てる部品になっている。 |
18 | ITodosApi は Shared.fs で定義されている。……のだけど、使っているのはクライアントだけで、サーバでは使われていない。 |
20 let init () : Model * Cmd<Msg> =
21 let model = { Todos = []; Input = "" }
22
23 let cmd =
24 Cmd.OfAsync.perform todosApi.getTodos () GotTodos
25
26 model, cmd
27
行番号 | コメント |
20 | init 関数の定義。 |
21 | モデルの初期状態。 |
23 | 最初のコマンド。コマンドは何かアクションをして、メッセージを返すもの、という感じ。 |
24 | Cmd についてはここに載っている。Cmd.OfAsync.perform の3つの引数はそれぞれ task: 'a -> Async<_> arg: 'a ofSuccess: _ -> 'msg になっている。 |
28 let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
29 match msg with
30 | GotTodos todos -> { model with Todos = todos }, Cmd.none
31 | SetInput value -> { model with Input = value }, Cmd.none
32 | AddTodo ->
33 let todo = Todo.create model.Input
34
35 let cmd =
36 Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
37
38 { model with Input = "" }, cmd
39 | AddedTodo todo ->
40 { model with
41 Todos = model.Todos @ [ todo ] },
42 Cmd.none
43
行番号 | コメント |
28 | update 関数の定義。メッセージとモデルを受け取って、新しいモデルとコマンドを返す。 |
32-42 | ここでも Cmd.OfAsync.perform を使っている。サーバの API を呼ぶときは非同期にして、結果を別のメッセージ (過去分詞で命名) で受け取る、というのが基本形ということか。 |
44 open Feliz
45 open Feliz.Bulma
46
行番号 | コメント |
44, 45 | SAFE Stack v3 からサンプルアプリケーションで Feliz を使うようになった。 |
47 let navBrand =
48 Bulma.navbarBrand.div [
49 Bulma.navbarItem.a [
50 prop.href "https://safe-stack.github.io/"
51 navbarItem.isActive
52 prop.children [
53 Html.img [
54 prop.src "/favicon.png"
55 prop.alt "Logo"
56 ]
57 ]
58 ]
59 ]
60
行番号 | コメント |
47 | navBrand 関数は view 関数から呼ばれる。モデルとメッセージに非依存の固定の画面部品。 |
61 let containerBox (model: Model) (dispatch: Msg -> unit) =
62 Bulma.box [
63 Bulma.content [
64 Html.ol [
65 for todo in model.Todos do
66 Html.li [ prop.text todo.Description ]
67 ]
68 ]
69 Bulma.field.div [
70 field.isGrouped
71 prop.children [
72 Bulma.control.p [
73 control.isExpanded
74 prop.children [
75 Bulma.input.text [
76 prop.value model.Input
77 prop.placeholder "What needs to be done?"
78 prop.onChange (fun x -> SetInput x |> dispatch)
79 ]
80 ]
81 ]
82 Bulma.control.p [
83 Bulma.button.a [
84 color.isPrimary
85 prop.disabled (Todo.isValid model.Input |> not)
86 prop.onClick (fun _ -> dispatch AddTodo)
87 prop.text "Add"
88 ]
89 ]
90 ]
91 ]
92 ]
93
行番号 | コメント |
61 | containerBox 関数は view 関数から呼ばれる。モデルに応じて内容が変化する画面の部品。また、on… 系のイベントで dispatch 関数にメッセージを渡す。 |
65-67 | モデルから todo の各項目を取り出して li 要素にする。 |
78 | onChange イベントで dispatch 関数を呼び出し。 |
86 | onClick イベントで dispatch 関数を呼び出し。 |
94 let view (model: Model) (dispatch: Msg -> unit) =
95 Bulma.hero [
96 hero.isFullHeight
97 color.isPrimary
98 prop.style [
99 style.backgroundSize "cover"
100 style.backgroundImageUrl "https://unsplash.it/1200/900?random"
101 style.backgroundPosition "no-repeat center center fixed"
102 ]
103 prop.children [
104 Bulma.heroHead [
105 Bulma.navbar [
106 Bulma.container [ navBrand ]
107 ]
108 ]
109 Bulma.heroBody [
110 Bulma.container [
111 Bulma.column [
112 column.is6
113 column.isOffset3
114 prop.children [
115 Bulma.title [
116 text.hasTextCentered
117 prop.text "test_safe_stack_20210724"
118 ]
119 containerBox model dispatch
120 ]
121 ]
122 ]
123 ]
124 ]
125 ]
行番号 | コメント |
94 | view 関数の定義。 |
95-125 | 画面全体の骨格なので行数は多いけど、重要で動的なところは containerBox 関数に任せている。 |
src/Server/Server.fs
1 module Server
2
3 open Fable.Remoting.Server
4 open Fable.Remoting.Giraffe
5 open Saturn
6
7 open Shared
8
行番号 | コメント |
3, 4 | Fable.Remoting のサーバ側。 |
5 | Saturn は Web サーバのフレームワーク。Web サイト (link) を見ると SAFE Stack で使われることを強く意識しているっぽい。 |
9 type Storage() =
10 let todos = ResizeArray<_>()
11
12 member __.GetTodos() = List.ofSeq todos
13
14 member __.AddTodo(todo: Todo) =
15 if Todo.isValid todo.Description then
16 todos.Add todo
17 Ok()
18 else
19 Error "Invalid todo"
20
行番号 | コメント |
9 | Storage クラスの定義。 |
10 | 保存先はメモリ上の ResizeArray のインスタンス。 |
12, 14 | メンバ関数は API と一対一で対応するシンプルなもの。 |
21 let storage = Storage()
22
23 storage.AddTodo(Todo.create "Create new SAFE project")
24 |> ignore
25
26 storage.AddTodo(Todo.create "Write your app")
27 |> ignore
28
29 storage.AddTodo(Todo.create "Ship it !!!")
30 |> ignore
31
行番号 | コメント |
21 | Storage のインスタンスを作成。 |
23-30 | 初期状態で 3 個の ToDo 項目を登録。 |
32 let todosApi =
33 { getTodos = fun () -> async { return storage.GetTodos() }
34 addTodo =
35 fun todo ->
36 async {
37 match storage.AddTodo todo with
38 | Ok () -> return todo
39 | Error e -> return failwith e
40 } }
41
42 let webApp =
43 Remoting.createApi ()
44 |> Remoting.withRouteBuilder Route.builder
45 |> Remoting.fromValue todosApi
46 |> Remoting.buildHttpHandler
47
行番号 | コメント |
32 | todosApi は API の呼び出しの受け口みたいな感じ。クライアントにも同名のものがある。レコードのラベルの名前が、サーバのそれと、クライアントで参照する ITodosApi とで同じになっている。 |
42 | webApp は todosApi を Remoting でラップしている。 |
48 let app =
49 application {
50 url "http://0.0.0.0:8085"
51 use_router webApp
52 memory_cache
53 use_static "public"
54 use_gzip
55 }
56
57 run app
src/Shared/Shared.fs
1 namespace Shared
2
3 open System
4
5 type Todo = { Id: Guid; Description: string }
6
7 module Todo =
8 let isValid (description: string) =
9 String.IsNullOrWhiteSpace description |> not
10
11 let create (description: string) =
12 { Id = Guid.NewGuid()
13 Description = description }
14
行番号 | コメント |
5 | 個々の ToDo 項目の型の定義。 |
7-13 | ToDo 項目のバリデータと作成。 |
15 module Route =
16 let builder typeName methodName =
17 sprintf "/api/%s/%s" typeName methodName
18
19 type ITodosApi =
20 { getTodos: unit -> Async<Todo list>
21 addTodo: Todo -> Async<Todo> }
行番号 | コメント |
19-21 | ITodosApi を実際に参照しているのはクライアントだけ。(Shared.fs に定義されているけど) サーバでは ITodosApi を使わずに、型を一致させた todosApi を定義している。 |