Blazor a F#
Lukáš Grolig
Něco málo o mně
- mnoho let SW developer a architekt
- v poslední době se věnuji hlavně masivně škálovatelným systémům využívajícím ML
Proč F#?
méně bugů v aplikaci:
- silnější typová kontrola
- Option type oproti null
- algebraické typy
- expression oproti statement
- lepší pattern matching
- immutable by default
Projekty používají obvykle F# a C# dohromady
- doménový model
- build systém (Fake)
- nasazení (Farmer)
- testy
A teď k webu
Javascriptová trojce
Nevýhody JS
- dynamický
- slabě typovaný
- immutabilita
- paralelismus
- this
- pattern matching
- ADT
- ...
Výhody C# oproti JS
- kompilovaný
- silně typový
- podporuje koncepty FP (nic extra)
- teoreticky lepší performance
- a jak je zvykem u .NET - unifikovaný systém vývoje
A přišel Webassembly
Teď k Blazoru
A samozřejmě to má i své nedostatky
- stažení velkého balíku při otevření appky
- všechny funkce dostupné JS nejsou podporovány => občas je nutné volat JS
- SEO
- není tolik zdrojů
- stále existuje null
server side Blazor
2 modely
client side Blazor
Jaké jsou limitace klienta oproti serveru?
Bolero
Bolero je postaveno nad Blazorem a přidává mnoho funkcí navržených speciálně pro práci ve F#:
- Elmish Model-View-Update architektura pro funkcionální přístup k reaktivnímu obsahu.
- Syntaxe HTML-in-F# inspirovaná WebSharper.UI a Fable.
Případně šablony HTML s funkcí hot reloading, které poskytují komfortní návrhové prostředí. - Směrování URL na způsob F#, automatické přiřazování URL k union typům F#.
- Easy Remoting
Teď k vytvoření projektu
dotnet new blazorwasm
dotnet new blazorserver
C# Blazor
dotnet new -i Bolero.Templates
dotnet new bolero-app -o HelloWorld
F# Bolero
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
}
}
type Startup() =
member this.ConfigureServices(services: IServiceCollection) =
services.AddMvc() |> ignore
services.AddServerSideBlazor() |> ignore
services.AddBoleroHost(server = false) |> ignore
module Program =
[<EntryPoint>]
let Main args =
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build()
.Run()
0
Vytvoření stránky v Pages
file s příponou .razor
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Fs Page (HTML template)
// hello.html
<div id="${Id}">Hello, ${Who}!</div>
// hello.fs
type Hello = Template<"hello.html">
let hello =
Hello()
.Id("hello")
.Who("world")
.Elt()
Fs Page
let myElement name =
div [] [
h1 [] [text "My app"]
p [] [textf "Hello %s and welcome to my app!" name]
]
Kód a HTML v jednom souboru?
A nebo to rozdělit?
Předání parametrů
public class CounterClass : BlazorComponent
{
public int CurrentCount { get; set; }
[Parameter]
protected string SubTitle { get; set; }
public void IncrementCount()
{
CurrentCount += 5;
}
}
<Counter SubTitle="Subtitle from Index (Home) page"/>
Zpracování událostí
<button class="btn btn-primary" @onclick="UpdateHeading">
Update heading
</button>
@code {
private async Task UpdateHeading(MouseEventArgs e)
{
await ...
}
}
Elmish
type Model = { firstName: string; lastName: string }
let initModel = { firstName = ""; lastName = "" }
Doménový model
type Message = SetFirstName of string | SetLastName of string
let update message model =
match message with
| SetFirstName n -> { model with firstName = n } // <-- kopie objektu se zmenou property
| SetLastName n -> { model with lastName = n }
Message a update
let view model dispatch =
div [] [
viewInput model.firstName (fun n -> dispatch (SetFirstName n))
viewInput model.lastName (fun n -> dispatch (SetLastName n))
text (sprintf "Hello, %s %s!" model.firstName model.lastName)
]
A zobrazení
Načítání dat
builder.Services.AddScoped(sp =>
new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
Napřed konfigurace HTTP klienta
private class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
Potom vytvoření modelu
Načítání dat
@using System.Net.Http
@inject HttpClient Http
@code {
private TodoItem[] todoItems;
protected override async Task OnInitializedAsync() =>
todoItems = await Http.GetFromJsonAsync<TodoItem[]>(
"api/TodoItems");
}
A samotný request
Načítání dat F#
type GitHub = JsonProvider<"https://api.github.com/.../issues">
let topRecentlyUpdatedIssues =
GitHub.GetSamples()
Načítání dat F#
open Bolero.Remoting
type MyService =
{
getEntry : string -> Async<string option> // Served at /myService/getEntry
setEntry : string * string -> Async<unit> // Served at /myService/setEntry
deleteEntry : string -> Async<unit> // Served at /myService/deleteEntry
}
interface IRemoteService with member this.BasePath = "/myService"
let myService = this.Remote<MyService>()
myService.getEntry key
Error Handling
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject IProductRepository ProductRepository
@inject ILogger<ProductDetails> Logger
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
Error Handling
@code {
private ProductDetails details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
}
F# way
type Model =
{ latestRetrievedEntry : string * string
latestError : exn option }
type Message =
// Trigger a `getEntry` request
| GetEntry of key: string
// Received response of a `getEntry` request
| GotEntry of key: string * value: string
// A request threw an error
| Error of exn
let update myService message model =
match message with
| GetEntry key ->
model,
Cmd.ofAsync
myService.getEntry key // async call and argument
(fun value -> GotEntry(key, value)) // message to dispatch on response
Error // message to dispatch on error
| GotEntry(key, value) ->
{ model with latestRetrievedEntry = (key, value) }, []
| Error exn ->
{ model with latestError = Some exn }, []
Lokalizace a globalizace
Hotové komponenty
Například Telerik UI for Blazor
- super pro rychlo vytvoření projektu
- levnější než si dělat vlastní komponenty
- ale horší customizace komponent
Deployment
varianta server side:
- prostředí s nainstalovaným .NET
- je vhodné buildit a nasazovat přes docker kontejnery
varianta client side:
- pokud nepočítáme BE, tak stačí mít CDN
Farmer
// Create a storage account with a container
let myStorageAccount = storageAccount {
name "myTestStorage"
add_public_container "myContainer"
}
// Create a web app with application insights that's connected to the storage account.
let myWebApp = webApp {
name "myTestWebApp"
setting "storageKey" myStorageAccount.Key
}
// Create an ARM template
let deployment = arm {
location Location.NorthEurope
add_resources [
myStorageAccount
myWebApp
]
}
// Deploy it to Azure!
deployment
|> Writer.quickDeploy "myResourceGroup" Deploy.NoParameters
PWA
Webová aplikace chovající se jako desktopová appka
PWA
Webová aplikace chovající se jako desktopová appka
PWA
Je třeba přidat app manifest (manifest.json)
a ideálně vyřešit podporu pro offline práci (service workers a storage)
A jako u každého webového projektu musíme řešit mraky optimalizací
Doporučení pro projekty
- vyřešit autentizace a autorizaci hned na začátku (IdentityServer)
- hned udělat CI/CD
- deployment do AppService (kašlat na Kubernetes)
- kašlat na různý prostředí = dark deployment
- použít rozumný feature flag tool
- čas switchnout na Rider
Shrnutí
-
Existuje F#
-
Doménové modelování přes ADT
-
Nepotřebujete design patterny
-
Nádstavba nad Blazorem je Bolero
-
Pro state management se využívá Elmish
To je pro dnešek vše
Dotazy?
Děkuji za pozornost
Blazor a F#
By Lukáš Grolig
Blazor a F#
- 534