F# Bolero - A cóż to takiego? - remoting
Posty z tej serii:
- F# Bolero - A cóż to takiego? - wprowadzenie
- F# Bolero - A cóż to takiego? - HTML templates
- F# Bolero - A cóż to takiego? - view components
- F# Bolero - A cóż to takiego? - routing
- F# Bolero - A cóż to takiego? - routing - page models
- F# Bolero - A cóż to takiego? - remoting
Aplikacja webowa składa się (w dużym uproszczeniu) z dwóch części:
- Widoków
- Danych, prezentowanych na widokach
Dane dla widoków możemy dostarczyć co najmniej na trzy sposoby:
- Wołamy zewnętrzne API bezpośrednio z aplikacji
- Tworzymy własne API, które dla aplikacji będzie punktem wejścia do wołania innych API
- Mix dwóch powyższych
Tworząc własne API dla aplikacji pisanej w Bolero, możemy skorzystać z mechanizmu Remoting, który w mojej subiektywnej opinii posiada trzy bardzo fajne właściwości:
- Szczegóły komunikacji między klientem a serwerem ukryte są pod warstwą abstrakcji
- Ten sam sposób używania, niezależnie od tego, czy pobieramy, czy wysyłamy dane
- Dwa wynikowe stany - udało się lub nie
W szczegółach, dane serilizowane są do formatu JSON oraz wymieniane przez protokół HTTP(s) z użyciem metody POST.
Zobaczmy, w jaki sposób możemy zaimplementować najprostsze API z jedną metodą, która zwraca tekst, a także, w jaki sposób możemy wywołać zaimplementowaną metodę.
Bolero Template umożliwia wygenerowanie kodu, gdzie serwer oraz klient skonfigurowani są w modelu hosted. My zrealizujemy scenariusz, w którym klient oraz serwer hostowani są niezależnie od siebie.
Cały kod znajdziesz na GitHub’e.
Zacznijmy od utworzenia trzech projektów:
-
kontrakty pomiędzy klientem, a serwerem - dll
dotnet new classlib -n Remoting.Contracts -lang f#
-
host serwera - ASP.NET Web API
dotnet new webapi -n Remoting.Server -lang f#
-
host klienta - Bolero minimal
dotnet new bolero-app -n Remoting.Client --minimal=true --server=false
Następnie, w projekcie Remoting.Contracts:
- dodajemy paczkę Bolero
dotnet add package Bolero
-
usuwamy plik
Library.fs
-
dodajemy plik
SimpleService.fs
z deklaracją API
module Remoting.Contracts
open Bolero.Remoting
type SimpleService =
{
GetHello: unit -> Async<string>
}
interface IRemoteService with
member this.BasePath = "/simpleservice"
Record SimpleService
zawiera pole GetHello
, które reprezentuje metodę API. Wynikiem wywołania GetHello
będzie zwrócenie przykładowego napisu. SimpleService
dziedziczy po interfejsie IRemoteService
z przestrzeni nazw Bolero.Remoting
. Member this.BasePath
wskazuje ścieżkę API. W ten sposób tworzymy deklarację, którą rozpozna Bolero.
Do projektu Remoting.Server:
- dodajemy paczkę Bolero.Server
dotnet add package Bolero.Server
- dodajemy referencję do projektu Remoting.Contracts:
dotnet add reference ../Remoting.Contracts
- dodajemy plik
SimpleServiceHandler.fs
z implementacją API
namespace Remoting.Server
open Bolero.Remoting.Server
open Microsoft.AspNetCore.Hosting
open Remoting.Contracts
open Microsoft.Extensions.Primitives
type SimpleServiceHandler(ctx: IRemoteContext, env: IWebHostEnvironment) =
inherit RemoteHandler<SimpleService>()
override this.Handler =
{
GetHello = fun _ -> async {
ctx.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", StringValues("*"))
return "Hello from Simple Service!"
}
}
Klasa SimpleServiceHandler
dziedziczy po klasie RemoteHandler
z przestrzeni nazw Bolero.Remoting.Server
. Pod Handler
podstawiamy definicję metody API. Bolero umożliwia skorzystanie z dwóch zależności, dla których dostarcza implementacje: IRemoteContext
oraz IWebHostEnvironment
. W przykładzie pierwszą z nich wykorzystujemy, aby do odpowiedzi HTTP(s) dodać nagłówek Access-Control-Allow-Origin
.
- w pliku
Startup.fs
konfigurujemy ASP.NET Web API włączając Bolero Remoting oraz definiując CORS
type Startup(configuration: IConfiguration) =
member _.Configuration = configuration
// This method gets called by the runtime. Use this method to add services to the container.
member _.ConfigureServices(services: IServiceCollection) =
// Add framework services.
services.AddControllers() |> ignore
services.AddRemoting<SimpleServiceHandler>()
.AddCors() |> ignore
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
if (env.IsDevelopment()) then
app.UseDeveloperExceptionPage() |> ignore
app.UseRemoting()
.UseHttpsRedirection()
.UseRouting()
.UseAuthorization()
.UseEndpoints(fun endpoints ->
endpoints.MapControllers() |> ignore
)
.UseCors(fun policy ->
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader() |> ignore
) |> ignore
Remoting włączamy poprzez wywołanie metod:
- services.AddControllers()
- app.UseRemoting()
CORS definiujemy poprzez wywołanie metod:
- services..AddCors()
- app.UseCors(…)
W konfiguracji hosted CORS nie jest potrzebny, ponieważ zarówno serwer, jak i klient uruchamiane są z tej samej domeny.
Na koniec, w projekcie Remoting.Client:
- dodajemy referencję do projektu Remoting.Contracts:
dotnet add reference ../../../Remoting.Contracts
- w pliku
Startup.fs
, włączamy Bolero Remoting , podając bazowe URI pod którym dostępne jest API
builder.Services.AddRemoting((fun httpClient ->
httpClient.BaseAddress <- Uri("http://localhost:1234"))) |> ignore
- w pliku
main.fs
, realizujemy aplikację, które zawoła API za pomocą Elmish Commands:
Definiujemy model
type Model =
{
Text: string
}
Ustawiamy początkowe wartości modelu
let initModel =
{
Text = ""
}, Cmd.none
Nowością w stosunku do przykładów z poprzednich artykułów tej serii jest to, że wraz z wartościami modelu podajemy jaki Command ma się wykonać. W momencie inicjalizowania początkowych wartości nie wykonujemy żadnej akcji, oznaczając to przez Cmd.none
Definiujemy Messages
type Message =
| GetHello
| ShowHello of string
| ShowError of exn
- GetHello - zawołanie API
- ShowHello - wykonanie działania, w przypadku, gdy API zwróci sukces
- ShowError - wykonanie działania, w przypadku, gdy API zwróci błąd
Definiujemy funkcję update
let update simpleService message model =
match message with
| GetHello ->
model, Cmd.OfAsync.either simpleService.GetHello () ShowHello ShowError
| ShowHello text ->
{model with Text = text}, Cmd.none
| ShowError exn ->
{model with Text = exn.Message}, Cmd.none
Oprócz standardowych parametrów message
oraz model
, funkcja przyjmuje parametr simpleService
. Parametr jest typem SimpleService
z projektu Remoting.Contracts
. Po nadejściu Message’a GetHello
wołamy API, za pomocą Command’a Cmd.OfAsync.either
. W przypadku prawidłowego zakończenia wywołania zostanie zgłoszony Message ShowHello
wpp. zostanie zgłoszony Message ShowError
. Przy sukcesie wypełniamy model zwróconym rezultatem wpp. opisem błędu.
Definiujemy funkcję view
let view model dispatch =
concat [
div [] [
button [on.click (fun _ -> dispatch GetHello)] [text "GetHello"]
]
div [] [
text model.Text
]
]
Widok zawiera dwa elementy:
- przycisk z akcją
on.Click
, która wysyła MessageGetHello
- pole
text
, które wyświetla zawartość modelu
Na koniec zmieniamy inicjalizowanie głównego komponentu z mkSimple
na mkProgram
type MyApp() =
inherit ProgramComponent<Model, Message>()
override this.Program =
let simpleService = this.Remote<SimpleService>()
Program.mkProgram (fun _ -> initModel) (update simpleService) view
W ten sposób wstrzykujemy klienta API do funkcji update oraz dostajemy możliwość używania Elmish Commands.
Koniec Serii
Tak, oto dotarliśmy do końca serii, która pokazuje podstawowe możliwości Bolero. Dzięki przedstawionym mechanizmom możemy tworzyć rozwiązania webowe w podejściu SPA, pisząc kod w języku F#.
Bolero posiada o wiele więcej możliwości. Jest to jednak temat na osobną serię artykułów.
Jeśli chodzi o mnie, to zostaję przy frameworku. Pamiętam, jak pierwszy raz przeczytałem o jego koncepcjach i doznałem tego fajnego efektu “aha…ciekawe…”. Teraz kiedy znam Bolero trochę lepiej, nic się nie zmieniło :)
Kolejnym etapem rozwoju jest wymyślenie tematu na aplikację oraz jej realizacja. Efektami prac podzielę się na blogu, którego w dalszej perspektywie będę chciał przepisać właśnie w Bolero.
=