今回は SAFE Template で生成したプロジェクトの構造を俯瞰してみます。
前回の記事、SAFE Stack開発環境構築の最後の方で記載したコマンドを実行して作成された状態のプロジェクトを基にしていますので、同様に試していただければ理解が進むのではないかと思います。

作成されたプロジェクトをビルドした状態から始めたいので、ビルドや実行をしていない方はプロジェクトのルートフォルダをカレントにして以下のコマンドを実行してください。

fake build --target build

プロジェクトディレクトリの構成

概ね、以下のようになっていると思います。

/{project}
│  .editorconfig
│  .gitignore
│  build.fsx
│  Dockerfile
│  package.json
│  paket.dependencies
│  paket.lock
│  yarn.lock
│  
├─.fake
├─.paket
│      paket.exe
│      Paket.Restore.targets
│      
├─deploy
├─node_modules
├─paket-files
│      paket.restore.cached
│      
└─src
    │  safe.sln
    │  
    ├─Client
    │  │  Client.fs
    │  │  Client.fsproj
    │  │  paket.references
    │  │  webpack.config.js
    │  │  
    │  ├─bin
    │  ├─obj
    │  │  
    │  └─public
    │      │  admin.css
    │      │  index.html
    │      │  
    │      ├─Images
    │      │      safe_favicon.png
    │      │      
    │      └─js
    │              bundle.js
    │              bundle.js.map
    │              
    ├─Server
    │  │  paket.references
    │  │  Server.fs
    │  │  Server.fsproj
    │  │  
    │  ├─bin
    │  └─obj
    │                  
    └─Shared
            Shared.fs
            

プロジェクトルート

ディレクトリ/ファイル 説明
build.fsx FAKEのビルドスクリプト
Dockerfile テンプレート作成時、--deploy docker を指定すると作成される
package.json npm の設定ファイル
paket.dependencies Paket の設定ファイル
.fake/ FAKE 関連のキャッシュディレクトリ
.paket/ paket.exe 格納ディレクトリ
deploy/ 配布ファイル作成先ディレクトリ
paket-files/ Paket のキャッシュディレクトリ?
src/ ソースファイル格納ディレクトリ
src/Client/ クライアント側ソースファイル格納ディレクトリ
src/Server/ サーバー側ソースファイル格納ディレクトリ
src/Shared/ クライアント&サーバー共用ソースファイル格納ディレクトリ

プロジェクトのルートには FAKEのビルド用スクリプトやDockerイメージ作成のためのDockerfileが置いてあります。
この中で、Paketという、また見慣れないものがあります。
Paket はリンク先にもあるように、依存ファイルをダウンロード&インストールしてくれるツールです。
.NET では NuGetが使われていますが、Paket は NuGet、GitHub等から依存ファイルをダウンロードしてくることが出来ます。
.paketディレクトリにある paket.exeが実行ファイルとなっており、設定しだいでは fake.exeをダウンロードしてきてビルドを行うbootstrapperとしても機能します。
F#のコミュニティではFAKEと同様によく使われているもののようです。

肝心のソースファイルは src/ ディレクトリ以下に格納されています。
src/Client/ にはFableによってJavaScriptにコンパイルされるクライアント側ソースファイルが格納されています。
src/Server/ にはWebサーバー Saturn によるサーバー側ソースファイルが格納されています。こちらは純粋にASP.NET Coreで動作します。
src/Shared/ はクライアント側とサーバー側で共用するソースファイルを格納するディレクトリになっています。

src/Client

ディレクトリ/ファイル 説明
Client.fs クライアント側ソースファイル
Client.fsproj F#プロジェクトファイル
webpack.config.js webpackの設定ファイル
src/Client/Public/ メインのhtmlファイルやImage等のリソース、コンパイル結果のjs格納ディレクトリ

Fableによって、Client側ソースはJavaScriptに変換され、webpackでバンドルされます。
このテンプレートはシンプルなので Client.fsというソースファイルひとつだけになります。

src/Server

ディレクトリ/ファイル 説明
Server.fs サーバー側ソースファイル
Server.fsproj F#プロジェクトファイル

サーバー側はSaturnの実行ソースになります。
わずか40行弱のとてもシンプルなソースになっています。

src/Shared

ディレクトリ/ファイル 説明
Shared.fs クライアントとサーバーで共用するソース。モデルやFable.RemotingのAPI定義等

表の説明欄の通りです。
クライアント側とサーバー側で同じモデルを共用できるのが最大の強みですよね。

サーバー側のソース

短いのでそのまま載っけてみます。

open System.IO
open System.Threading.Tasks

open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2
open Giraffe
open Saturn
open Shared

open Fable.Remoting.Server
open Fable.Remoting.Giraffe

let publicPath = Path.GetFullPath "../Client/public"
let port = 8085us

let getInitCounter() : Task<Counter> = task { return 42 }

let counterApi = {
    initialCounter = getInitCounter >> Async.AwaitTask
}

let webApp =
    Remoting.createApi()
    |> Remoting.withRouteBuilder Route.builder
    |> Remoting.fromValue counterApi
    |> Remoting.buildHttpHandler

let app = application {
    url ("http://0.0.0.0:" + port.ToString() + "/")
    use_router webApp
    memory_cache
    use_static publicPath
    use_gzip
}

run app

最後の run app にてサーバーが起動しているわけですが、run関数に指定されているappで基本的な設定が行われているのがわかります。
詳細についてはApplication - Saturn Docを見ていただくか、Saturnのソースを見てみても良いかもしれません。
appの中で、use_router webAppというところがありますが、これがトップルーターの指定になります。
SAFE Templateでは唯一 Fable.Remotingの HttpHandler が指定されています。
let webApp = ...の部分を見ると、|> Remoting.fromValue counterApiという部分があります。
これがカウンターのAPIとなっており、initialCounterというメソッドが定義されているのがわかります。
同じインタフェースでクライアント側でも呼び出すので、counterApiの型は共用ソースのShared.fsに定義されてるICounterApiになります。

namespace Shared

type Counter = int

module Route =
    /// Defines how routes are generated on server and mapped from client
    let builder typeName methodName =
        sprintf "/api/%s/%s" typeName methodName

/// A type that specifies the communication protocol between client and server
/// to learn more, read the docs at https://zaid-ajaj.github.io/Fable.Remoting/src/basics.html
type ICounterApi =
    { initialCounter : unit -> Async<Counter> }

Server.fsに戻って、initialCounterの実装は、getInitCounter関数で定義されています。
42を固定で返しています。

また、webAppに指定されている |> Remoting.withRouteBuilder Route.builderのRoute.builder関数はShared.fsに定義されています。
これもクライアント側で呼び出す際に使用します。

サーバー側のコードはクライアントからのAPIコールに応答するだけなので割とシンプルな構造です。
C#しか知らなかった私でも、F#の本を傍に置きながら、なんとなく理解しました(していると思います...)。


クライアント側のソースはもうちょっと長めなので、また次回とします。

SAFE Stackのクライアント側では Fable.Elmish という Elm Architecture のF#実装が使用されています。

elm チュートリアルのページを見ると、Elmは驚くほどF#にそっくりな言語だと感じると思います。
F#でElm Architectureはとても自然に実装できているのではないでしょうか。

Elm ArchitectureをJavaScriptでやってしまった人もいるようで、海外では結構人気のアーキテクチャーなんですかねぇ。