Изучение ландшафта фронтенда на F#

Привет всем, давно не виделись!
Сегодня мы поговорим о том, что представляет собой текущий ландшафт Frontend-разработки для экосистемы F#. За последние несколько лет Fable стал достаточно функциональным, и было выпущено больше привязок.

Отказ от ответственности: Экосистема F# стабильна, вам не нужно пересаживаться на новый корабль или что-то в этом роде! То, что есть выбор, не означает, что вам нужно бросать то, что вы знаете, и изучать что-то новое. Думайте об этом как о меню ресторана: Есть варианты, но в конечном итоге вы сами решаете, какой из них выбрать, и вы даже можете решить: «Я не хочу здесь есть» — это совершенно нормально. Ни один котенок не умрет, и мир не остановится, так что если вы видите много вариантов, я бы посоветовал вам не чувствовать давления при выборе!

Когда речь заходит о ландшафте Frontend для F#, у нас есть три основных дороги:

  • Fable
  • WebSharper
  • Web Assembly

У них разные подходы к фронтенду, но в конечном счете они делают одно и то же. Некоторые из этих вариантов позволяют вам перейти к рендерингу на стороне сервера, а другие — к одностраничным приложениям.

Fable

В этом разделе

Высокий профиль:

  • Feliz
  • Fable.Lit
  • Sutil

Низкий профиль

  • Fable.Svelte
  • Feliz.Snabdom

Fable Next

  • Feliz.Solid

Fable — это компилятор F# в JavaScript (как Typescript компилируется в JavaScript). Он находится в стадии четвертого крупного релиза и имеет очень сильную экосистему, основанную на React.js, хотя в последнее время стали доступны и другие варианты.

Почему именно Fable и кому он больше всего подходит?

Fable предназначен для тех разработчиков, которым необходимо работать с экосистемой JavaScript или которые хотят извлечь из нее выгоду. Было бы неразумно отрицать количество хороших библиотек и существующих решений, которые родились в ней.

Fable вместо того, чтобы отрицать существование JavaScript, строит на его основе и предоставляет вам наиболее гибкий вариант, когда речь идет о разработке фронтенда на F#.
Вы продолжаете использовать безопасность F# (большую часть времени) и при необходимости можете вернуться к взаимодействию с JavaScript (через emit, imports или динамический оператор) или даже к самому JavaScript, чтобы заполнить недостающие пробелы, которые там могут быть.

Плохо то, что экосистема JavaScript настолько обширна и так сильно разрослась за эти годы, что может оказаться, что если вам нужна библиотека X, то для нее не будет привязки. В конце концов, число разработчиков F# слишком мало, так что у нас нет достаточной силы программистов, чтобы быть на равных.

Написание привязок не сложно, но это может занять довольно много времени от ваших усилий по разработке, если библиотеки, на которые вы нацеливаетесь, слишком велики, однако эти затраты оплачиваются только один раз, когда привязки готовы, остается только поддерживать их в актуальном состоянии (что не является слишком большой работой).

И это при том, что все уже сказано! Давайте рассмотрим варианты Fable:

Feliz

Это, безусловно, самая популярная библиотека в экосистеме F# frontend. Эта библиотека взяла уроки из Fable React и улучшила свой DSL (Domain Specific Language), чтобы он был более кратким и менее многословным, чем существующие альтернативы. Она также использовала другой подход, когда речь шла о приложениях React, она немного отошла от очень популярной в то время архитектуры Elmish (также известной как MVU) и предоставила API, максимально приближенный к самому React.

Feliz ввел хуки, которые помогли упростить управление состоянием в некоторых случаях, а также уменьшить многословность MVU при росте приложений. Поскольку он был совместим с предыдущими связками fable-react, переход на Feliz не потребовал больших усилий.

Ваш типичный компонент пользовательского интерфейса Feliz выглядит следующим образом

[<ReactComponent>]
let Counter() =
    let (count, setCount) = React.useState(0)
    Html.div [
        Html.button [
            prop.style [ style.marginRight 5 ]
            prop.onClick (fun _ -> setCount(count + 1))
            prop.text "Increment"
        ]

        Html.button [
            prop.style [ style.marginLeft 5 ]
            prop.onClick (fun _ -> setCount(count - 1))
            prop.text "Decrement"
        ]

        Html.h1 count
    ]
Вход в полноэкранный режим Выход из полноэкранного режима

Его DSL основан на списке свойств для каждого вида HTML-тегов, вы можете создавать многократно используемые части пользовательского интерфейса, просто написав функции и другие компоненты, учитывая природу React, понятно, почему Feliz является наиболее используемым, он просто подходит для F#, данные и функции!

Feliz поддерживает MVU с помощью пакета Feliz.UseElmish.

type Msg =
    | Increment
    | Decrement

type State = { Count : int }

let init() = { Count = 0 }, Cmd.none

let update msg state =
    match msg with
    | Increment -> { state with Count = state.Count + 1 }, Cmd.none
    | Decrement -> { state with Count = state.Count - 1 }, Cmd.none

[<ReactComponent>]
let Counter() =
    let state, dispatch = React.useElmish(init, update, [| |])
    Html.div [
        Html.h1 state.Count
        Html.button [
            prop.text "Increment"
            prop.onClick (fun _ -> dispatch Increment)
        ]

        Html.button [
            prop.text "Decrement"
            prop.onClick (fun _ -> dispatch Decrement)
        ]
    ]

// somewhere else
ReactDOM.render(Counter(), document.getElementById "feliz-app")
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы хотите погрузиться в Frontend F#, то Feliz — это отличный выбор, вы узнаете, что использует большинство разработчиков F# FE, и это, пожалуй, лучший выбор на сегодняшний день в сфере Fable.

Минусы Feliz — это минусы использования React, поскольку Feliz — это привязка 1-1 к React, вы получаете те же проблемы, что и разработчики React, странные правила для хуков, легко по ошибке спровоцировать повторный рендеринг, а эффекты все еще не до конца разобраны в экосистеме react, и вам придется вручную отслеживать, какие вещи нужно повторно рендерить в вашем UI. React использует Virtual DOM, который был производительным способом рендеринга пользовательского интерфейса в начале 2010-х годов, но теперь это не так, поскольку браузеры догнали его по производительности, и оказалось, что в критических по производительности ситуациях отличие VDOM от React — это просто накладные расходы, а не преимущество. Для обычного сайта это не должно вызывать беспокойства, но упомянуть об этом стоит.

Fable.Lit

Это мой личный фаворит, когда дело доходит до опций Fable. Fable.Lit строится поверх lit.dev, которая является библиотекой веб-компонентов, построенной на веб-стандартах. Она привносит в F# FE ландшафт производительные прямолинейные и межфреймворковые совместимые компоненты, поскольку Lit работает с самими элементами DOM, а не с абстракциями, вы можете манипулировать экземплярами компонентов, как если бы вы делали ванильный JavaScript, за исключением того, что вы можете использовать для этого безопасность F#.

В Fable.Lit вместо создания F# DSL (мы пытались) мы используем альтернативу на основе строк, которая близка к HTML, который вы знаете и любите. Это также очень помогает, когда вам приходится использовать веб-компоненты, такие как shoelace.style, fast.design, adobe spectrum components и другие, это будет очень важным и важным моментом в течение следующих нескольких лет, сейчас, когда веб-компоненты окончательно ушли в прошлое и их используют такие крупные компании, как Microsoft, Salesforce, Github, Adobe и другие.

Вот два способа, которыми вы можете использовать компоненты Fable.Lit

[<HookComponent>]
let functionCounter(initial: int) =
    let value, setValue = Hook.useState initial
    html
        $"""
        <!-- @<event name> means attach a handler to this event -->
        <sl-button outline variant="neutral" @click={fun _ -> setValue value + 1}>Increment</sl-button>
        <sl-button outline variant="neutral" @click={fun _ -> setValue value - 1}>Decrement</sl-button>
        <sl-badge>Count: {value}</sl-badge>
        """

[<LitElement("my-custom-element")>]
let MyCustomElement() =
    let host, props =
        LitElement.init(fun config ->
            config.props = {| initial = Prop.Of(0, attribute = "initial") |}
            // defaults to true if not set
            config.useShadowDom <- false
        )
    let value, setValue = Hook.useState props.initial.Value

    html
        $"""
        <sl-button outline variant="neutral" @click={fun _ -> setValue value + 1}>Increment</sl-button>
        <sl-button outline variant="neutral" @click={fun _ -> setValue value - 1}>Decrement</sl-button>
        <sl-badge>Count: {value}</sl-badge>
        """

// using both somewhere
html
    $"""
    Function Component:
    {functionCounter 20}
    <br>
    <!-- .initial means bind to "initial" property -->
    <my-custom-element .initial={10}></my-custom-element>
    """
Войти в полноэкранный режим Выйти из полноэкранного режима

Прежде всего, если вы задаетесь вопросами «фу, строки», «это не дает никакого выделения», «дырки не пропечатываются», у меня есть несколько слов на этот счет:

  1. Интерполированные строки не так гибки, как шаблоны с метками JS, поэтому в .NET мы возвращаемся к использованию только объектов и теряем безопасность типов.
  2. На самом деле у нас есть два расширения для того, чтобы дать вам возможность выделять эти строки в F#
    • Выделение шаблонов HTML/SQL в F#
    • Html для F# (Lit Template)
    • Для Rider и других редакторов никто не пытался создать плагин, насколько я знаю.

Вот в чем дело (и главная причина, по которой мне это нравится):

  • Нужно ли нам писать привязки для sl-button?
  • Нужно ли нам писать привязки для любого другого пользовательского элемента/веб-компонента?

Ответ на оба вопроса — нет, мы все еще должны писать привязки для JS-частей библиотек, которые мы можем использовать, но когда дело доходит до пользовательских элементов и других стандартных элементов HTML, нам не нужно ничего делать, включая их атрибуты/свойства.

Компромисс заключается именно в том, что мы получаем доступ к огромному количеству библиотек, но теряем некоторую безопасность при описании пользовательского интерфейса.

И пока я не забыл об этом, Fable.Lit также поддерживает архитектуру Elmish

type Msg =
    | Increment
    | Decrement

type State = { Count : int }

let init() = { Count = 0 }, Cmd.none

let update msg state =
    match msg with
    | Increment -> { state with Count = state.Count + 1 }, Cmd.none
    | Decrement -> { state with Count = state.Count - 1 }, Cmd.none

[<HookComponent>]
let Counter() =
    let model, dispatch = Hook.useElmish(init, update)
    html $"""
        <h1>{model.Count}</h1>
        <button @click={fun _ -> dispatch Increment}>Increment</button>
        <button @click={fun _ -> dispatch Decrement}>Decrement</button>
    """
Вход в полноэкранный режим Выйти из полноэкранного режима

Lit сам по себе является довольно безопасной ставкой — это надежный выбор и построен на веб-стандартах, так что он, скорее всего, будет иметь действительно долгую жизнь (он впервые вышел примерно в 2013-2014 годах как полимер, если вы когда-нибудь задумывались об этом) и корректировался и улучшался вместе с веб-браузерами.

Fable.Lit, с другой стороны, довольно новый, и привязка, возможно, все еще имеет некоторые области, где мы можем улучшить, но технология, лежащая в основе, уже готова к производству.

Sutil

Когда я впервые узнал о Sutil, я влюбился в него, он привнес много концепций из Svelte в ландшафт фронтенда F#, и хотя его развитие шло медленнее, чем у большинства, у него есть несколько действительно интересных вариантов, которые могут подойти некоторым умам лучше, чем другие альтернативы.

Sutil — это первый чистый F# фреймворк для фронтенда, который у нас есть, у него нет привязки к какому-либо фреймворку, потому что это просто F#.

Sutil использует вариацию DSL под названием Feliz.Engine, поэтому вы получаете безопасность типа F#, которую вы знаете и любите, с реактивными элементами пользовательского интерфейса.
Функции Sutil выполняются только один раз, а затем все остается статичным, если только вы не решите быть реактивным с помощью магазинов. Это помогает в производительности, а обновления применяются только там, где что-то меняется.

Подобно Fable.Li, Sutil работает с обычными элементами DOM, что делает его совместимым с веб-компонентами, а также предоставляет возможности для написания веб-компонентов!

Если вам нравится модель программирования svelte или rxjs (observables), то Sutil — это то, на что стоит обратить внимание, он также имеет встроенные анимации, инструменты разработчика chrome и другие приятные возможности.

Вот как выглядит Sutil

// functions can be separated from UI logic
// with some thought we can make these very reusable
// and even UI agnostic
let increment (counter: IStore<int>) =
    counter
    |> Store.modify (fun count -> count + 1)

let decrement (counter: IStore<int>) =
    counter
    |> Store.modify (fun count -> count - 1)

let view() =
    let counter = Store.make 0

    Html.div [
        // make this element reactive
        Bind.el(counter, fun count -> Html.p $"Counter: {count}")
        Html.div [
            // using stablished HTML elements
            Html.button [
                onClick (fun _ -> increment counter ) []
                text "Increment"
            ]
            // interoperation with custom elements
            Html.custom("sl-button", [
                Attr.custom("variation", "neutral")
                onClick (fun _ -> decrement counter) []
                text "Decrement"
            ])
        ]
    ]
Вход в полноэкранный режим Выход из полноэкранного режима

Как уже упоминалось ранее, когда мы монтируем/вызываем view, он отрисовывается один раз, а когда магазины/наблюдаемые объекты выдают новое значение, обновляются только реактивные части, что позволяет выполнять мелкозернистые обновления.

Sutil также предлагает поддержку MVU:

type Msg =
    | Increment
    | Decrement

type State = { Count : int }

let init() = { Count = 0 }, Cmd.none

let update msg state =
    match msg with
    | Increment -> { state with Count = state.Count + 1 }, Cmd.none
    | Decrement -> { state with Count = state.Count - 1 }, Cmd.none

let Counter() =
    let model, dispatch = () |> Store.makeElmishSimple init update ignore
    Html.div [
        disposeOnUnmount [ model ]
        Bind.fragment (model |> Store.map getCounter) <| fun n ->
            Html.h1 [ text $"Counter = {n}" ]

        Html.div [
            Html.button [
                onClick (fun _ -> dispatch Decrement) []
                text "-"
            ]

            Html.button [
                onClick (fun _ -> dispatch Increment) []
                text "+"
            ]
        ]]
Вход в полноэкранный режим Выход из полноэкранного режима

Одним из недостатков Sutil является медленное обновление, хотя Дэвид недавно упомянул, что он будет работать над этим, но было бы желательно больше сопровождающих.
Кроме того, он находится в бета-версии уже некоторое время, так что, возможно, он еще не готов к прайм-тайму.
Было бы неплохо получить больше тестов от реальных пользователей, потому что, по крайней мере, в моем относительно ограниченном тестировании он кажется таким же надежным, как и любой из предыдущих вариантов.

Feliz.Engine

После того, как были упомянуты проекты высокого уровня, стоит упомянуть еще один проект, который можно использовать, если вы планируете когда-либо привнести другой фреймворк в пространство F# — Feliz.Engine.

Feliz.Engine — это библиотека, которая стандартным образом определяет F# DSL для элементов, атрибутов и стилей. Она родилась из оригинальной библиотеки Feliz DSL, но была немного изменена, чтобы соответствовать более общему сценарию использования.

Sutil, Feliz.Solid и Feliz.Snabdom используют Feliz.Engine под капотом, вы также можете использовать его для привлечения других!

Этот проект заслуживает упоминания только за его потенциал привнести больше в экосистему (и не путать его с самим Feliz)

Fable.Svelte

Эта связка была способом заставить F# работать с файлами .svelte. Мне нечего сказать об этом, кроме того, что он существует, и вы можете взглянуть на него, если хотите, но его использование довольно незначительно.

Я не думаю, что это хороший выбор для вашего следующего серьезного проекта, может быть для экспериментов, но учитывая его низкое использование, могут быть некоторые ошибки, которые еще не найдены, Sutil будет лучшим выбором, если вам нравится Svelte-подобный способ создания пользовательского интерфейса.

Svelte, конечно, является надежным выбором и одним из самых популярных JS фреймворков, проблема заключается в том, насколько зрелыми являются привязки и насколько они протестированы.

Feliz.Snabdom

Используя Feliz.Engine, Feliz.Snabdom также имеет реализацию виртуального dom, но работает с элементами DOM, а не абстрагируется над ними (как react), что дает вам больше возможностей для взаимодействия со сторонними компонентами. Он предоставляет крючки жизненного цикла, ленивую загрузку элементов и другие возможности.

Я сам не являюсь поклонником virtual dom, поэтому я не пробовал его больше, чем просто несколько примеров, в то же время я не уверен в зрелости привязок, хотя snabdom выпускается уже несколько лет и используется тысячами разработчиков, но меня беспокоит то, насколько переносимым является код + насколько зрелыми могут быть привязки.

Fable Next!

Эти новые опции выходят горячими из печи и рисуют светлое будущее для интеграции Fable в экосистему Frontend!

Fable 4 (остров змеи) собирается принести JSX компиляцию, что означает, что фреймворки, которые используют JSX в качестве DSL и строительных блоков, будут иметь еще более легкое время интеграции, эта поддержка придет и в Feliz.Engine, что означает несколько вещей

  • Стабильный API для стороны F# (Feliz.Engine)
  • Широкий выбор UI фреймворков путем простой настройки пакетов, которые вы хотите использовать (будь то solidjs, vue jsx, inferno, preact и т.д.)
  • Более легкие пути миграции между F# <-> JS

Учитывая, что JSX все еще является этапом компиляции, вы всегда можете вернуться к ручному JSX и продолжить работу с/на JSX, если это необходимо.

Feliz.Solid

Это очень интересно, [solid.js] в последнее время набирает популярность, потому что это то, чем мог бы быть react.

  • Истинная и предсказуемая реактивность
  • Отсутствие ручного отслеживания зависимостей
  • Поддержка наблюдаемых
  • Отсутствие виртуального DOM
  • Быстрый и эффективный
  • Небольшая библиотека

Итак, если модель react — это то, что вам нужно, и вы хотите избежать многих из особенностей react, то это то, на что стоит обратить внимание.

Сплошной код выглядит следующим образом:

[<JSX.Component>]
let Counter() =
    let count, setCount = Solid.createSignal(0)
    let doubled() = count() * 2
    let quadrupled() = doubled() * 2

    Html.fragment [
        Html.p $"{count()} * 2 = {doubled()}"
        Html.p $"{doubled()} * 2 = {quadrupled()}"
        Html.br []
        Html.button [
            Attr.className "button"
            Ev.onClick(fun _ -> count() + 1 |> setCount)
            Html.children [
                Html.text $"Click me!"
            ]
        ]
    ]
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, он очень похож на Sutil или Feliz.Snabdom, и это потому, что он также использует Feliz.Engine! Хотя они не взаимодействуют между собой просто потому, что каждая библиотека определяет, что DSL на самом деле излучает: DOM-элементы, виртуальные DOM-элементы, они используют один и тот же DSL, так что изучив одну из них, вы, по сути, научитесь и другим!

Основным недостатком этого является то, что это, конечно, новинка, она работает только на Fable 4 (Alpha на момент написания статьи) и поэтому не должна рассматриваться для вашего следующего серьезного проекта. Как только Fable 4 выйдет по-настоящему, тогда это может стать тем, что действительно стоит рассмотреть и внести свой вклад.

Fable + JSX

Если вы думаете: «Моего любимого фреймворка нет в списке», не волнуйтесь, писать привязки для Fable стало проще с годами, особенно если принять во внимание Feliz.Engine. Fable 4 также принесет JSX, что означает, что интеграция в экосистему JavaScript может стать еще проще.

Возможно, вам нравится vue, но поддержка файлов vue — это слишком много, возможно, фреймворк, который вы используете на работе, поддерживает JSX, что потенциально может принести многое с минимальными изменениями, как сказал Альфонсо.

Это как программирование на основе интерфейса (JSX) вместо реализации (скомпилированного JS).

Хотя он также сказал, что есть нюансы и вероятные различия в том, как каждый фреймворк и его инструментарий относится к JSX, так что Feliz.Engine близок к универсальному, но не настолько универсален.

WebSharper

WebSharper был выпущен уже довольно давно и имеет интересный подход к пользовательскому интерфейсу, основанный на F#. WebSharper стремится выполнить обещание полного стека F#, скрывая некоторые детали JS, но имеет некоторые довольно хорошие возможности, когда ему нужно взаимодействовать с javascript.

WebSharper — это не набор множества библиотек и фреймворков, а универсальный фреймворк.

Простое приложение WebSharper выглядит следующим образом:

[<Website>]
let Main =
    Application.SinglePage (fun ctx ->
        Content.Page(
            h1 [] [ text "Hello World!"]
        )
    )
Вход в полноэкранный режим Выйти из полноэкранного режима

Это позволит WebSharper сгенерировать некоторый JavaScript и запустить его непосредственно в теле вашего приложения.

Хотя WebSharper имеет стратегию ViewModel, он также предлагает поддержку MVU, например, счетчик может выглядеть следующим образом

[<JavaScript>]
module Counter =

    type Model = { Counter : int }

    type Message = Increment | Decrement

    let Update (msg: Message) (model: Model) =
        match msg with
        | Increment -> { model with Counter = model.Counter + 1 }
        | Decrement -> { model with Counter = model.Counter - 1 }

    let Render (dispatch: Dispatch<Message>) (model: View<Model>) =
        div [] [
            button [on.click (fun _ _ -> dispatch Decrement)] [text "-"]
            span [] [text (sprintf " %i " model.V.Counter)]
            button [on.click (fun _ _ -> dispatch Increment)] [text "+"]
        ]

    let Main =
        App.CreateSimple { Counter = 0 } Update Render
        |> App.Run
        |> Doc.RunById "main"
Вход в полноэкранный режим Выход из полноэкранного режима

или, если вы предпочитаете HTML

<!-- this is inside the HTML page you're serving -->
<body>
  <button ws-onclick="OnDecrement">-</button>
  <div>${Counter}</div>
  <button ws-onclick="OnIncrement">+</button>
  <script type="text/javascript" src="Content/Counter.min.js"></script>
  <!--[BODY]-->
</body>
Войдите в полноэкранный режим Выход из полноэкранного режима

[<JavaScript>]
module Client =
    // The templates are loaded from the DOM, so you just can edit index.html
    // and refresh your browser, no need to recompile unless you add or remove holes.
    type MySPA = Template<Snippet.IndexHtml, ClientLoad.FromDocument>

    type Model = int

    type Message =
        | Increment
        | Decrement

    let update msg model =
        match msg with
        | Message.Increment -> model + 1
        | Message.Decrement -> model - 1

    let view =
        let vmodel = Var.Create 0

        let handle msg =
            let model = update msg vmodel.Value

            vmodel.Value <- model

        MySPA()
            .OnIncrement(fun _ -> handle Message.Increment)
            .OnDecrement(fun _ -> handle Message.Decrement)
            .Counter(V(string vmodel.V))
            .Bind()

        fun model ->
            vmodel.Value <- model

    let Main =
        view init

Войти в полноэкранный режим Выход из полноэкранного режима

Есть целый сайт с демо-версиями, которые вы можете попробовать!

WebSharper также предлагает реактивную модель, которая может быть использована вместо архитектуры MVU, последний пример также может быть упрощен до следующего примера:

[<JavaScript>]
module Client =
    // The templates are loaded from the DOM, so you just can edit index.html
    // and refresh your browser, no need to recompile unless you add or remove holes.
    type MySPA = Template<Snippet.IndexHtml, ClientLoad.FromDocument>
    let counter = Var.Create 0
    let Main =
        MySPA()
            .OnIncrement(fun _ -> counter.Value <- counter.Value + 1)
            .OnDecrement(fun _ -> counter.Value <- counter.Value - 1)
            .Counter(V(string counter.V))
            .Bind()
Вход в полноэкранный режим Выход из полноэкранного режима

Этот реактивный стиль похож на новый Vue’s Composition API, так что независимо от вашего выбора WebSharper обеспечит вас всем необходимым.

Учитывая это, в моем твиттере WebSharper не является одним из самых популярных, и я не совсем понимаю, почему, моя догадка заключается в том, что он пытается скрыть JavaScript настолько, насколько это возможно, чтобы попытаться остаться в F#, и это может создать своего рода блокировку поставщика и трения в некоторых случаях, но это не означает, что это плохой выбор, хотя, если вы не хотите строить поверх экосистемы JavaScript настолько сильно, это выглядит как надежная технология, которую можно взять специально, потому что она предлагает платную поддержку, так что это может быть хорошим вариантом для команд, а не для отдельных пользователей.

Если вы хотите узнать больше о WebSharper, дайте мне знать, чтобы я мог изучить его подробнее и посвятить ему пару постов в блоге.

Веб-сборка

В этом разделе:

  • Bolero
  • Fun.Blazor
  • Avalonia.FuncUI

Web Assembly — это новейший игрок в игре, который был/будет настоящим игровым чейнджером в веб-разработке. Чтобы убедиться в его силе, вы можете использовать фотошоп нативно в браузере, а что это значит для вас как .NET разработчика?

Это значит, что вы можете запускать код на F# (или C#, если это ваш конек) нативно в браузере, без промежуточного JavaScript, который вы должны трогать, и вы сохраняете безопасность F#.

Почему именно веб-ассемблер и кто получает от него больше всего пользы?

Веб-сборка (WASM) предназначена для пользователей, которые хотят запускать нативный код в браузере, это имеет несколько последствий, WebAssembly пока не имеет доступа к DOM и сборщику мусора, поэтому для работы WASM приложений с .NET вам нужно загрузить .NET runtime + код вашего приложения. Это означает, что при каждом посещении вашего сайта вам придется ждать несколько секунд, пока ваше веб-приложение загрузит среду выполнения + ваш код.
В любое время, когда любая из этих технологий должна обмениваться информацией с миром JS, и это может быть дорогостоящим, хотя вам, как разработчику приложений, не нужно делать это вручную, вы все равно должны опасаться расходов на сериализацию/десериализацию, которые возникают всякий раз, когда вы обмениваетесь информацией с миром JS, будь то в виде больших деревьев пользовательского интерфейса, больших объемов/строк данных и подобных ситуаций.

Тем не менее, если вы можете позволить себе эти недостатки, то вы сможете насладиться безопасностью F# во всей ее красе, больше никаких странных JavaScript emits, или попыток привязать интерфейс к JavaScript объекту и надеяться, что это будет работать во время выполнения. Все по-настоящему.

Вы можете использовать экосистему .NET, когда речь идет о библиотеках, со всеми их шаблонами и знаниями, которые у вас уже есть. Это также означает, что поскольку вы используете .NET, вы можете на 100% обмениваться логикой и данными с сервером, ведь библиотеки .NET6 (если они не используют специфичные для ОС API) работают на ядре ASP.NET для сервера и WASM, это означает отсутствие общих папок с настраиваемыми путями и #if FABLE или подобными директивами, это просто сборка, используемая как есть.

Хотя вам совсем не обязательно взаимодействовать с JavaScript, вы можете это сделать, если это необходимо, есть способы взаимодействия с функциями, объявленными в глобальной области видимости или даже в модулях JavaScript.

Таким образом, вы не полностью изолированы и можете взаимодействовать с внешним миром, если это необходимо.

DISCLAIMER: Большинство из этих альтернатив полагаются на Blazor, который является продуктом, предлагаемым Microsoft для подключения к WebAssembly с C#, как обычно, F# не входит в дорожную карту, но сообщество всегда вклинивается, чтобы спасти положение и предложить разработчикам F# опыт, которого они заслуживают.

Bolero

Bolero — это наиболее стабильное и зрелое решение для F# веб-ассемблера, оно создано теми же разработчиками, что и web sharper, в некотором смысле это может быть следующим шагом на их пути к созданию веб-приложений на F#. Bolero предлагает большинство функциональных возможностей web sharper, но на этот раз нативные, HTML шаблоны через Type Provider, маршрутизацию клиентов с дискриминированными союзами, F# DSL, MVU.

Ваш типичный вид Bolero выглядит следующим образом


type Model = { value: int }
let initModel = { value = 0 }

type Message = Increment | Decrement
let update message model =
    match message with
    | Increment -> { model with value = model.value + 1 }
    | Decrement -> { model with value = model.value - 1 }

let view model dispatch =
    div {
        button { on.click (fun _ -> dispatch Decrement); "-" }
        string model.value
        button { on.click (fun _ -> dispatch Increment); "+" }
    }

let program =
    Program.mkSimple (fun _ -> initModel) update view

type MyApp() =
    inherit ProgramComponent<Model, Message>()

    override this.Program = program

Вход в полноэкранный режим Выход из полноэкранного режима

Это довольно похоже на другие примеры MVU, которые мы видели до сих пор, Bolero также предлагает взаимодействие с концепциями Blazor, такими как внешние компоненты из сторонних библиотек, remoting (RPC клиент-серверный стиль взаимодействия) и, конечно же, Pure F#.

Если вам понравился подход, который дал вам WebSharper, и вы ищете следующий шаг, то bolero будет именно тем, что вам нужно, вы сможете перенести концепции и знания оттуда. Печальная часть для меня в том, что у него нет реактивного стиля, когда дело доходит до обработки состояния MVU великолепен, но на больших приложениях он просто не подходит для меня, хорошая вещь из плохого в том, что вы можете создать несколько компонентов Elmish в вашем приложении и использовать параметры, так что у вас нет одной главной функции обновления Elmish и скорее каждый компонент имеет свое собственное состояние.

Fun.Blazor

Это еще одна абстракция, построенная на основе Blazor, это новый ребенок в городе и имеет несколько очень привлекательных моделей, когда речь идет об обработке состояния. Fun.Blazor недавно выпустил свою версию 2.0.0, которая добавляет кучу приятных вещей, позволяет создавать пользовательский интерфейс с помощью F# Computation Expressions (CE), аналогично bolero, имеет возможность использовать шаблоны строк (подобно Fable.Lit) и некоторые опции для маршрутизации, например, маршрутизатор в стиле жирафа, предлагает простую интеграцию с инъекцией зависимостей Blazor, что очень полезно для взаимодействия с JavaScript и другими сервисами Blazor.

Ваш типичный компонент Fun.Blazor выглядит следующим образом:

adaptiview() {
    let count, setCount = cval 0 // changeable value

    h1 { $"Counter: {count}"}

    button {
        onclick (fun _ -> setCount count + 1)
        "Increment"
    }

    button {
        onclick (fun _ -> setCount count - 1)
        "Decrement"
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Fun.Blazor использует возможности FSharp.Data.Adaptive, чтобы позволить инкрементные обновления представления, этот пакет работает как ячейки excel, где одна ячейка может быть источником истины для других, другие ячейки могут пересчитывать значения в зависимости от первой ячейки, что позволяет быстро обновлять пользовательский интерфейс, потому что меняются только реактивные части. Эта модель очень близка к модели Сутила. Фактически, вы можете использовать магазины и в Fun.Blazor.

let myComponent() =
    html.comp(fun (hook: IComponentHook) ->
        let counter = hook.UseStore 0
        let double =
            store.Observable
            |> Observable.map(fun n -> n * n)
            |> AVal.ofObservable counter.Current hook.AddDispose

        adaptiview() {
            let! countValue, setCount = counter.WithSetter()
            let! doubleValue = double

            h1 { $"Counter: {countValue}, Double: {doubleValue}"}

            button {
                onclick (fun _ -> setCount countValue + 1)
                "Increment"
            }

            button {
                onclick (fun _ -> setCount countValue - 1)
                "Decrement"
            }
        }
    )
Вход в полноэкранный режим Выход из полноэкранного режима

Адаптивные представления — это концепция, которую я бы очень хотел, чтобы реализовали другие фреймворки, потому что реактивная модель действительно мне нравится. Но я знаю, что вы хотите увидеть elmish в действии, поэтому я рад сообщить вам, что да, он также поддерживает MVU

type Model = { value: int }
let initModel = { value = 0 }

type Message = Increment | Decrement
let update message model =
    match message with
    | Increment -> { model with value = model.value + 1 }
    | Decrement -> { model with value = model.value - 1 }
// using elmish directly
html.elmish (init, update, fun state dispatch ->
    div {
        h1 { $"Count: {state.value}" }
        button {
            onclick (fun _ -> dispatch Increment)
            "Increment"
        }
        button {
            onclick (fun _ -> dispatch Decrement)
            "Decrement"
        }
    }
)

// using elmish with adaptive views
html.comp (fun (hook: IComponentHook) ->
    let state, dispatch = hook.UseElmish(init, update)
    div {
        adaptiview() {
            let! count = state
            h1 { $"Count: {count.value}" }
        }
        button {
            onclick (fun _ -> dispatch Increment)
            "Increment"
        }
        button {
            onclick (fun _ -> dispatch Decrement)
            "Decrement"
        }
    }
)
Вход в полноэкранный режим Выход из полноэкранного режима

Fun.Blazor имеет большой потенциал, он еще молод, ему нужно больше реального использования в мире, чтобы подтвердить многие усилия, предпринятые в версии 2.0.0, хотя он и молод, но не забывайте, что, как и у Sutil, у него только один сопровождающий, так что если вам нравится этот фреймворк, вы должны рассмотреть возможность участия в его развитии, потому что он кажется действительно хорошим вариантом.

Avalonia.FuncUI

Для многих это может стать сюрпризом, потому что Avalonia — это фреймворк для настольных приложений! Но, как видно из этого WASM-шаблона Avalonia.FuncUI, можно использовать возможности Avalonia для настольных приложений через WASM. Основное преимущество Avalonia.FuncUI в том, что вы сможете обмениваться кодом между браузерами, android, ios, mac, linux и windows.

Avalonia.FuncUI также был недавно обновлен и добавил эту реактивно-подобную модель в версии 0.5.0

Component(fun ctx ->
    let state = ctx.useState 0
    DockPanel.create [
        DockPanel.verticalAlignment VerticalAlignment.Center
        DockPanel.horizontalAlignment HorizontalAlignment.Center
        DockPanel.children [
            TextBlock.create [
                TextBlock.dock Dock.Top
                TextBlock.text (string state.Current)
            ]
            Button.create [
                Button.dock Dock.Bottom
                Button.onClick (fun _ -> state.Current - 1 |> state.Set)
                Button.content "-"
            ]
            Button.create [
                Button.dock Dock.Bottom
                Button.content "+"
                Button.onClick (fun _ -> state.Current + 1 |> state.Set)
            ]
        ]
    ]
)
Вход в полноэкранный режим Выход из полноэкранного режима

Концепции IWritable<'T> и IReadable<'T> работают так же, как адаптивные/изменяемые/хранилища/наблюдения, которые мы уже видели в sutil/fun.blazor, поэтому Avalonia. FuncUI может начать становиться конкурентом в веб-ландшафте, особенно если у вас уже есть некоторый опыт разработки настольных приложений, это часть силы WASM на практике в случае Avalonia.FuncUI WASM, вам не нужно знать ничего о веб-разработке, чтобы начать, просто прыгайте!

Учитывая это, Avalonia использует Skia для рендеринга в холсте (вероятно, используя webgl), поэтому у вас не будет никаких узлов DOM для осмотра, а также, насколько я знаю (и я буду рад, если меня поправят, если это необходимо), вы выбрасываете доступность из окна из-за этого, поэтому вспомогательные технологии не будут работать с таким типом веб-сайтов, также стоит отметить, что, насколько я знаю (на момент написания статьи), веб-поддержка в Avalonia находится в состоянии бета-версии, поэтому там может скрываться пара ошибок.

MAUI

Слоном в комнате здесь, скорее всего, будет MAUI, потому что он также поддерживает blazor, но я буду очень пренебрежителен и признаю свою вину, потому что это продукт Microsoft, я бы предпочел, чтобы Microsoft ушла со своего пути в экосистеме .NET, и я бы хотел лучше развивать альтернативы, такие как Uno/Avalonia, но все равно, верно?

  • Удачи с поддержкой F#
  • Удачи с поддержкой linux

Обе вещи могут быть сделаны с Avalonia без каких-либо серьезных проблем, также все другие альтернативы, обсуждаемые здесь, могут быть разработаны на любой из трех основных операционных систем, даже на вашей Raspberry Pi 4, чего я не ожидаю в MAUI в ближайшее время.

Выводы

С учетом вышесказанного, ландшафт F# Frontend Landscape не так уж велик, он может показаться запутанным со всеми этими твитами и новостями, идущими повсюду, но, к счастью, как и большинство вещей F#: мы в значительной степени определились с тем, как мы используем вещи, и даже если есть разнообразие, большинство альтернатив могут жить вместе в том или ином виде.

Вот что я хочу сказать:

Используйте Fable, если:

  • Вы хотите воспользоваться преимуществами экосистемы JavaScript
  • Вы хотите иметь возможность перейти от F# в случае необходимости
  • Вам нужно наименьшее количество кб ресурсов для браузера.
  • Вам приходится много работать с JavaScript
  • Вам нравится и вы хотите использовать React, Lit или Vanilla (Sutil).

Не используйте Fable, если:

  • Вам действительно не нравится JS
  • Вам нужна настоящая безопасность типов
  • Вы можете позволить себе перейти на ванильный F# (т.е. не использовать большую часть экосистемы JS)
  • Вы не хотите изучать или иметь дело с инструментарием JavaScript.

Используйте WebSharper, если:

  • Вы не возражаете против перехода на F# в первую очередь, JS во вторую.
  • Вам нужен безопасный HTML с помощью провайдеров типов
  • Вы хотите размыть границы между F# на стороне клиента и F# на стороне сервера
  • Вас не слишком волнует инструментарий и вы заботитесь только о конечных развертываемых активах.

Не используйте WebSharper, если:

  • Вам нужно буквально вручную создавать js-файлы и вносить коррективы в инструментарий компиляции
  • Вам нужно приложение, более ориентированное на JavaScript
  • Вам нужно строить поверх существующего инструментария node
  • Вам важно дополнительное время выполнения для вашего приложения (около 8kb).

Используйте Web Assembly, если:

  • Вы хотите запускать родной F# в браузере, а не поддельный.
  • Вы хотите использовать экосистему .NET
  • Вы хотите пользоваться инструментарием .NET для публикации, распространения и сборки вашего F#
  • Вы хотите обмениваться кодом между настольным компьютером, сервером и мобильным устройством (как это позволяет Avalonia).

Не используйте Web Assembly, если:

  • Вы не хотите поставлять тяжелые веб-сайты (даже если есть поддержка обрезки для некоторых либ)
  • Вам нужно низкое TTI (время до взаимодействия) и TFP (время до рисования кулаком)
  • Вам нужна более зрелая экосистема
  • Вам нужен тяжелый JS interop

Личное мнение

Никто не просил об этом, и никто не должен, потому что мое мнение не должно иметь никакого веса на принятие вами решения.
Тем не менее…

Мой личный топ:

  1. Fable.Lit
  2. Сутил — Fun.Blazor
  3. Feliz.Solid?

Основная причина в том, что Fable.Lit соответствует всем требованиям веб-стандартов, а я в первую очередь фронтенд-разработчик, и React просто не мой конек, в основном из-за его ориентации на хуки, которые в случае React могут иметь смысл, но они слишком магические и очень склонны к ошибкам и проблемам с производительностью при плохом использовании (я смотрю на вас useFootGun, я имею в виду useEffect).

Sutil занимает второе место, потому что иметь чистый F# фреймворк, который также предлагает реактивную модель управления состоянием, это просто соответствует моей ментальной модели создания веб-сайтов.

Fun.Blazor быстро занимает второе место, потому что он берет те же концепции, которые использует Sutil для управления состояниями, и делает шаг вперед.

Feliz.Solid занимает третье место, потому что он также предлагает реактивную модель, которая, вероятно, заменит React во многих местах и кодовых базах в будущем. Она не страдает от тех же проблем, что и React, и ее будущее выглядит светлым, ее автор недавно был нанят (на момент написания статьи) в Netlify, так что она может только расти отсюда, пока интеграция Feliz будет созревать.

Заключение

Ну вот и все, надеюсь, этот пост пролил свет на текущее состояние F# frontend и на то, что вам следует принять во внимание, если вы хотите выбрать одну или другую альтернативу.

В конечном итоге на вас не должно оказываться давление, чтобы вы выбрали правильный выбор. Решения на F#, даже если они не являются «зрелыми», довольно надежны, в конце концов, это основная причина, по которой мы выбрали F#, чтобы либо работать с ним, либо получать удовольствие (иногда и то, и другое). Вам следует избегать одной из этих альтернатив, если у вас действительно есть причины не выбирать ее, в противном случае она, скорее всего, будет работать на ваши нужды.

Эти фреймворки — отличная работа членов сообщества F#, даже если они выглядят молодыми или находятся в бета-версии, эти инструменты очень хорошо сделаны и обладают гораздо большими возможностями, чем вы могли бы подумать, услышав эти слова, попробуйте их, дайте обратную связь их авторам и помните, что не все является React или производными, у вас есть выбор сегодня 🙂

До следующего выпуска, не забывайте оставлять свои комментарии и вопросы, если это необходимо!

Оцените статью
Procodings.ru
Добавить комментарий