今回は 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でやってしまった人もいるようで、海外では結構人気のアーキテクチャーなんですかねぇ。