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

  1. doménový model
  2. build systém (Fake)
  3. nasazení (Farmer)
  4. 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#

  • 505