探索 F# 前端领域

2025-06-07

探索 F# 前端领域

大家好,好久不见了!
今天我们将讨论 F# 生态系统中前端开发的现状。在过去的几年里,Fable 已经变得非常强大,并且发布了更多的绑定。

免责声明 F# 生态系统稳定,您无需跳槽到下一个版本或类似的东西!即使有选择,也不意味着您需要抛弃现有知识去学习新知识。不妨把这想象成餐厅菜单:有很多选择,但最终由您决定选择哪一个,您甚至可以决定“我不想在这里吃饭”,这完全没问题。不会有小猫死去,世界也不会停止运转,所以如果您看到很多选择,我建议您不要感到压力!

谈到 F# 的前端格局,我们有三条主要路线:

  • 寓言
  • WebSharper
  • WebAssembly

这些网站对前端环境有不同的方法,但最终做同样的事情,其中​​一些选项允许您进行服务器端渲染,而另一些选项允许您进行单页应用程序。

寓言

在这个部分:

引人注目:

  • 费利兹
  • 寓言文学
  • 苏蒂尔

低调

  • Fable.Svelte
  • 费利兹·斯纳布多姆

寓言下一个:

  • Feliz.Solid

Fable 是一个 F# 到 JavaScript 的编译器(就像 Typescript 编译为 JavaScript 一样),它即将发布第四个主要版本,并且拥有基于React.js 的非常强大的生态系统,尽管最近也出现了其他选择。

为什么选择 Fable?谁从中受益最多?

Fable 是为那些需要使用 JavaScript 生态系统或希望从中受益的开发人员而设计的,否定其中诞生的众多优秀库和现有解决方案是不明智的。

Fable 并没有否定 JavaScript 的存在,而是在其基础上构建,并在 F# 前端开发方面为您提供最灵活的选择。
您可以继续使用 F# 的安全性(大多数情况下),并在必要时回退到 JavaScript 互操作(通过 emit、imports 或动态运算符),甚至 JavaScript 本身来填补可能存在的空白。

糟糕的是,JavaScript 生态系统如此庞大,多年来发展迅速,以至于如果你想要 X 库,可能没有对应的库。毕竟,相比之下,F# 的开发人员数量实在太少,所以我们的程序员实力根本无法与 F# 相提并论。

编写绑定并不复杂,但是如果您所针对的库太大,则可能会花费您相当多的开发时间,但是这笔费用只需支付一次,当绑定完成时,只需保持绑定为最新状态(这不是一件太大的苦差事)。

话虽如此!让我们深入了解一下 Fable 的选项:

费利兹

这是迄今为止 F# 前端生态系统中最受欢迎的库,它借鉴了 Fable React 的经验教训,并改进了其 DSL(领域特定语言),使其比现有替代方案更简洁、更简洁。它在 React 应用程序方面也采取了不同的方法,略微偏离了当时非常流行的 Elmish 架构(也称为 MVU),并提供了尽可能接近 React 本身的 API。

Feliz 引入了钩子,这有助于在某些情况下简化状态管理,并减少应用程序增长时 MVU 可能变得冗长,因为它与以前的 fable-react 绑定兼容,所以迁移到 Feliz 并不需要太大的努力。

典型的 Feliz UI 组件如下所示

[<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
    ]
Enter fullscreen mode Exit fullscreen mode

它的 DSL 基于每种 HTML 标签的属性列表,您只需编写函数和其他组件即可构建可重用的 UI 部分,考虑到 React 的性质,很明显为什么 Feliz 是最常用的,它完全符合 F# 的思维、数据和功能!

Feliz.UseElmishFeliz 通过软件包支持 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

[<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")
Enter fullscreen mode Exit fullscreen mode

如果您想深入了解前端 F#,那么 Feliz 是一个不错的选择,您将了解大多数 F# FE 开发人员使用的内容,并且它可以说是当今 Fable 领域中的最佳选择。

Feliz 的缺点就是使用 React 的缺点,因为 Feliz 与 React 是一对一绑定的,所以你会遇到 React 开发者遇到的同样问题:奇怪的钩子规则、容易错误地触发重新渲染,而且 React 生态系统中的效果尚未完全确定,你必须手动保存重新渲染 UI 所需的内容。React 使用虚拟 DOM,这在 2010 年代初是一种高效的 UI 渲染方式,但随着浏览器性能的提升,这种情况已不复存在。事实证明,在性能至关重要的情况下,与 React 不同的 VDOM 只是开销而不是优势。对于普通网站来说,这应该不是什么问题,但仍然值得一提。

寓言文学

说到 Fable,我个人最喜欢的是Fable.Lit 。它基于lit.dev构建,后者是一个基于 Web 标准的 Web 组件库。它为 F# FE 环境带来了高性能、简单易用且跨框架兼容的组件。由于 Lit 处理的是 DOM 元素本身,而不是抽象,您可以像使用原生 JavaScript 一样操作组件实例,只不过您可以使用 F# 的安全性来实现这一点。

在 Fable.Lit 中,我们并没有构建 F# DSL(我们尝试过),而是使用了一种基于字符串的替代方案,它接近于您所熟悉和喜爱的 HTML,当您必须使用来自shoelace.stylefast.designadobe spectrum components 等的 Web 组件时,这也有很大帮助,这将是未来几年非常重要和重要的一点,因为 Web 组件终于开始流行,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>
    """
Enter fullscreen mode Exit fullscreen mode

首先,如果你感到疑惑“呃,字符串”,“这没有任何亮点”,“这些洞没有被打出来”,我对此有几句话要说:

  1. 插值字符串不像 JS 标签模板那样灵活,所以在 .NET 中我们只能使用对象,这样就失去了类型安全性
  2. 我们实际上有两个扩展,可让您高亮显示这些 F# 字符串

事情是这样的(也是我喜欢它的主要原因):

  • 我们是否必须为其编写绑定sl-button
  • 我们是否需要为任何其他自定义元素/Web 组件编写绑定?

两者的答案都是“否”,我们仍然需要为可能使用的库的 JS 部分编写绑定,但当涉及到自定义元素和其他标准 HTML 元素时,我们不需要做任何事情,包括它的属性/特性。

权衡恰恰在于,我们可以访问大量的库,但在描述 UI 时我们会失去一些类型安全性。

在我忘记之前,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>
    """
Enter fullscreen mode Exit fullscreen mode

Lit 本身是一个相当安全的选择,并且建立在 Web 标准之上,因此它很可能拥有很长的寿命(如果你听说过的话,它最早是在 2013-2014 年左右以聚合物的形式出现的),并且已经随着 Web 浏览器的调整和改进而不断改进。

另一方面,Fable.Lit 还比较新,绑定可能仍有一些可以改进的地方,但底层技术已经可以投入生产。

苏蒂尔

当我第一次了解 Sutil 时,我就爱上了它,它将Svelte的很多概念带到了F# 前端领域,虽然它的开发速度比大多数都慢,但它有一些非常有趣的选择,比其他替代方案更适合一些人的想法。

Sutil 是我们拥有的第一个纯 F# 前端框架,它没有绑定到任何框架,因为它只是 F#。

Sutil 使用了 Feliz 的 DSL 变体Feliz.Engine,因此您可以通过响应式 UI 元素获得您熟悉和喜爱的 F# 类型安全性。Sutil
函数只运行一次,之后所有内容都是静态的,除非您选择通过 store 进行响应式处理。这有助于提高性能,并且更新仅在内容发生变化时应用。

与 Fable.Li 类似,Sutil 使用普通的 DOM 元素,这使得它也与 Web 组件兼容,它还提供使用它编写 Web 组件的功能!

如果您真的喜欢 svelte 或 rxjs(可观察对象)的编程模型,那么 Sutil 值得关注,它还具有内置动画、chrome 开发工具和其他不错的功能。

苏蒂尔的样子

// 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"
            ])
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

如前所述,每当我们挂载/调用时,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 "+"
            ]
        ]]
Enter fullscreen mode Exit fullscreen mode

Sutil 的部分缺点在于更新缓慢,尽管 David 最近提到他会继续开发,但还是希望有更多的维护人员参与。
此外,它已经测试了一段时间,所以可能还没有准备好迎接黄金时段的到来。
如果能有更多真实用户的测试就更好了,因为至少在我相对有限的测试中,它感觉和之前的任何选择一样稳定。

费利斯引擎

提到这些备受瞩目的项目后,有一个项目值得一提,如果你计划将另一个框架引入 F# 领域,那么你可以使用 Feliz.Engine

Feliz.Engine 是一个以标准方式定义元素、属性和样式的 F# DSL 库。它脱胎于原始的 Feliz DSL,但略作修改以适应更通用的用例。

Sutil、Feliz.Solid 和 Feliz.Snabdom 在底层使用 Feliz.Engine,您也可以使用它来将其他人带入其中!

这个项目值得一提,因为它有可能为生态系统带来更多东西(不要将其与 Feliz 本身混淆)

Fable.Svelte

这些绑定是让 F# 处理.svelte文件的一种方式。关于这一点,我没什么好说的,只是它确实存在,如果你愿意的话可以看看,但它的使用率相当低。

对于你的下一个重要项目来说,我认为它不是一个好的选择,也许适合在这里或那里进行实验,但考虑到它的使用率很低,可能存在一些尚未发现的错误,如果你喜欢类似 Svelte 的方式来做 UI,Sutil 会是一个更好的选择

Svelte 当然是一个可靠的选择,也是目前最流行的 JS 框架之一,问题在于绑定的成熟度以及它们经过的实战测试

费利兹·斯纳布多姆

使用 Feliz.Engine 附带的 Feliz.Snabdom,snabdom 也有一个虚拟 dom 实现,但处理 DOM 元素而不是抽象它们(如 react),这为您提供了更多与第三方组件互操作的回旋余地。它提供生命周期钩子、延迟加载元素和其他功能。

我自己并不是虚拟 dom 的粉丝,所以我并没有真正尝试过,只是尝试了几个例子,同时我也不确定绑定的成熟度,尽管 snabdom 已经问世多年,并且已被数千名开发人员使用,但问题在于代码的可移植性 + 绑定的成熟度。

寓言接下来!

这些新选项正在热火朝天地推出,为 Fable 在前端生态系统中的集成描绘出美好的未来!

Fable 4(蛇岛)将带来JSX编译功能,这意味着使用 JSX 作为其 DSL 和构建块的框架将更容易集成,这种支持也将出现在 Feliz.Engine 中,这意味着几件事

  • F# 端的稳定 API(Feliz.Engine)
  • 只需配置您想要使用的包(无论是 solidjs、vue jsx、inferno、preact 等)即可实现广泛的 UI 框架目标
  • 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!"
            ]
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

正如您所见,它与 Sutil 或 Feliz.Snabdom 非常相似,这是因为它也使用了 Feliz.Engine!虽然它们之间不容易互操作,因为每个库都定义了 DSL 实际发出的内容:DOM 元素、虚拟 DOM 元素,但它们确实使用相同的 DSL,因此学习一个基本上也会教您其他的!

它的主要缺点是它当然是新东西,只适用于《神鬼寓言 4》(撰写本文时为 Alpha 版本),因此不应该考虑将其用于你的下一个正式项目。等到《神鬼寓言 4》正式发布后,你才有可能认真考虑并参与其中。

寓言 + JSX

如果您认为“我最喜欢的框架不在列表中”,请不要担心,多年来为 Fable 编写绑定已经变得更加容易,特别是当您考虑到 Feliz.Engine 时,Fable 4 还将带来 JSX,这意味着它可以更简单地集成到 JavaScript 生态系统中。

也许你喜欢 Vue,但支持 Vue 文件太多了,也许你在工作中使用的框架支持 JSX,这有可能以最小的改变带来很多好处,就像Alfonso 所说的那样

这就像针对接口(JSX)而不是实现(编译后的 JS)进行编程

尽管他也表示每个框架及其工具处理 JSX 的方式存在细微差别和可能差异,因此 Feliz.Engine 接近通用,但还没有那么通用。

WebSharper

Web sharper 已经推出很长一段时间了,并且有一个有趣的 F# 优先 UI 方法,WebSharper 旨在实现全栈 F# 承诺,隐藏一些 JS 细节,但在需要与 javascript 互操作时具有一些相当不错的功能。

WebSharper 不是一组多个库和框架,而是一站式提供所有风格的框架

一个简单的 Web Sharper 应用程序如下所示:

[<Website>]
let Main =
    Application.SinglePage (fun ctx ->
        Content.Page(
            h1 [] [ text "Hello World!"]
        )
    )
Enter fullscreen mode Exit fullscreen mode

这将告诉 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"
Enter fullscreen mode Exit fullscreen mode

或者如果你更喜欢 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>
Enter fullscreen mode Exit fullscreen mode

[<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

Enter fullscreen mode Exit fullscreen mode

有一个完整的网站,其中包含您可以尝试的演示!

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()
Enter fullscreen mode Exit fullscreen mode

这种反应式风格类似于新的Vue 的 Composition API,因此无论您选择什么,WebSharper 都能满足您的需求。

话虽如此,在我的推特泡沫中,WebSharper 并不是最受欢迎的,我也不太清楚为什么,我的猜测是它试图尽可能地隐藏 JavaScript 以试图留在 F# 中,这可能会在某些情况下造成某种供应商锁定和摩擦,但这并不意味着它是一个糟糕的选择,如果你不想在 JavaScript 生态系统之上构建太多东西,它看起来是一项可靠的技术,特别是因为它提供付费支持,所以它更适合团队而不是个人。

如果您想了解有关 WebSharper 的更多信息,请告诉我,以便我可以进行进一步探索并为其撰写几篇博客文章。

WebAssembly

在这个部分:

  • 波莱罗舞曲
  • 乐趣.Blazor
  • Avalonia.FuncUI

Web Assembly 是游戏中的最新玩家,它将成为 Web 开发领域的真正游戏规则改变者,为了体验它的强大功能,您现在可以在浏览器中原生使用 Photoshop,这对于您作为 .NET 开发人员意味着什么?

这意味着您可以在浏览器上原生运行 F# 代码(或者 C#,如果您喜欢的话),无需接触中间 JavaScript,并且可以保持 F# 的安全性

为什么使用 Web Assembly?谁从中受益最多?

Web 程序集 (WASM) 适用于想要在浏览器中运行本机代码的用户,这有几个含义,WebAssembly 目前还无法访问 DOM 和垃圾收集器,因此要使 WASM 应用程序与 .NET 协同工作,您需要加载 .NET 运行时 + 应用程序的代码。这意味着每次有人访问您的网站时,您都必须等待几秒钟,以便您的 Web 应用程序加载运行时 + 您的代码。
任何时候这些技术都需要与 JS 世界共享信息,这可能是昂贵的,虽然您作为应用程序开发人员不必手动执行此操作,但您仍然需要警惕每次与 JS 世界共享信息时产生的序列化/反序列化成本,无论是大型 UI 树、大量/多行数据还是类似情况。

话虽如此,如果您能承受这些缺点,那么您将能够享受 F# 安全性的全部优势,不再需要发出奇怪的 JavaScript 代码,也不需要尝试将接口绑定到 JavaScript 对象,希望这在运行时也能实现。这才是真正的安全。

您可以利用 .NET 生态系统中的库,它们包含您可能已经掌握的所有模式和知识。这也意味着,由于您使用的是 .NET,因此可以与服务器 100% 共享逻辑和数据。毕竟,.NET6 库(除非它们使用特定于操作系统的 API)可以在服务器和 WASM 的 ASP.NET Core 上运行,这意味着无需共享路径经过调整的文件夹#if FABLE或类似指令,共享的只是程序集本身。

虽然您根本不需要与 JavaScript 交互,但如果必须,您可以这样做,有办法与全局范围内甚至 JavaScript 模块中声明的函数进行互操作。

因此,您并不是完全孤立的,如果需要,您可以与外界互动。

免责声明:大多数这些替代方案都依赖于Blazor,这是 Microsoft 提供的产品解决方案,用于像往常一样使用 C# 来利用 WebAssembly。F# 不在路线图中,但社区总是会伸出援手,拯救局面,并为 F# 开发人员提供他们应得的体验。

波莱罗舞曲

Bolero 是 F# Web Assembly 最稳定、最成熟的解决方案,它由 Web Sharper 的同一批开发人员开发,某种程度上,它可能是他们使用 F# 开发 Web 应用程序的下一步。Bolero 提供了 Web Sharper 的大部分功能,但这次是原生的,包括通过类型提供程序提供的 HTML 模板、带有可区分联合的客户端路由、F# DSL 和 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 }

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

Enter fullscreen mode Exit fullscreen mode

它与我们迄今为止见过的其他 MVU 示例非常相似,bolero 还提供与 Blazor 概念的互操作,例如来自第三方库的外部组件、远程处理(RPC 客户端-服务器风格的通信)以及纯 F#。

如果您喜欢 WebSharper 提供的方法,并且正在寻找更进一步的方法,那么 bolero 将是您的理想之选。您可以从那里学习概念和知识。对我来说,它的缺点是它在处理状态时缺乏响应式风格。MVU 很棒,但在大型应用程序中,它对我来说不够用。缺点中的亮点在于,您可以在应用程序中创建多个 Elmish 组件并使用参数,这样您就不必使用单个 Elmish 主更新函数,而是每个组件都有自己的状态。

乐趣.Blazor

这是另一个基于 Blazor 构建的抽象,它是新生事物,在状态处理方面拥有一些非常诱人的模型。Fun.Blazor 最近发布了 2.0.0 版本,该版本全面增强了许多功能,例如允许使用 F# 计算表达式 (CE) 以类似 bolero 的方式创建 UI,还可以使用字符串模板(很像 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"
    }
}
Enter fullscreen mode Exit fullscreen mode

FSharp.Data.AdaptiveFun.Blazor 提供了允许对视图进行增量更新的功能,该包的工作原理类似于 Excel 单元格,其中一个单元格可能是其他单元格的真实数据来源,其他单元格可以根据第一个单元格重新计算值。由于只有响应式部分发生变化,因此这可以实现 UI 的高性能更新。该模型与 Sutil 的模型非常接近。实际上,您也可以在 Fun.Blazor 中使用 store。

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"
            }
        }
    )
Enter fullscreen mode Exit fullscreen mode

自适应视图是一个概念,如果其他框架能够实现,我会很高兴,因为反应模型真的引起了我的共鸣,话虽如此,我知道你想看看 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"
        }
    }
)
Enter fullscreen mode Exit fullscreen mode

Fun.Blazor 具有很大的潜力,它还很年轻,需要更多的现实世界的使用来验证 v2.0.0 中所做的许多努力,虽然年轻,但它感觉是一个可靠的选择,但请记住,就像 Sutil 一样,它只有一个维护者,所以如果你喜欢它,你应该考虑为框架做出贡献,因为它感觉是一个非常好的选择。

Avalonia.FuncUI

这可能会让很多人感到惊讶,因为 Avalonia 是一个桌面应用程序框架!但正如在这个Avalonia.FuncUI WASM 模板中所见,可以通过 WASM 将 Avalonia 的强大功能带入桌面,Avalonia.FuncUI 的主要优势在于您将能够在浏览器、android、ios、mac、linux 和 windows 之间共享代码。

Avalonia.FuncUI 最近也进行了更新,并在 v0.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)
            ]
        ]
    ]
)
Enter fullscreen mode Exit fullscreen mode

IWritable<'T>和工作的概念IReadable<'T>就像我们之前在 sutil/fun.blazor 中看到的自适应/可变/存储/可观察对象一样,因此 Avalonia.FuncUI 可以开始成为 Web 领域的竞争对手,特别是如果您已经有一些桌面应用程序开发经验,这是 WASM 在实践中的强大功能的一部分,在 Avalonia.FuncUI WASM 的情况下,您实际上不需要了解任何 Web 开发知识就可以开始使用,直接加入!

话虽如此,Avalonia 使用 Skia 在画布上进行渲染(可能使用 webgl),因此您无需检查任何类型的 DOM 节点,而且据我所知(如有必要,我很乐意纠正),因此您将可访问性抛诸脑后,辅助技术将无法与此类网站配合使用,还值得注意的是,据我所知(在撰写本文时),Avalonia 中的 Web 支持处于测试状态,因此可能存在一些潜在的错误。

茂宜岛

这里最显而易见的可能是 MAUI,因为它也支持 Blazor,但我在这里对此非常不屑一顾,我对此表示歉意,因为它是微软的产品,我更希望微软走出其 .NET 生态系统,我很乐意培育更好的替代方案,如 Uno/Avalonia,但无论如何,对吧?

  • 祝 F# 支持顺利
  • 祝你好运获得 Linux 支持

这两件事都可以用 Avalonia 完成,不会出现任何大问题,而且这里讨论的所有其他替代方案都可以在三大操作系统中的任何一个上开发,甚至可以从你的 Raspberry Pi 4 上开发,这不是我期望在 MAUI 中很快实现的。

总结

话虽如此,F# 前端格局并不是那么大,到处都是推文和新闻可能会让人感到困惑,但值得庆幸的是,与大多数 F# 一样:我们已经基本确定了如何使用它们,即使存在多样性,大多数替代方案也可以以某种方式共存。

以下是tl;dr

如果出现以下情况,请使用 Fable:

  • 你想利用 JavaScript 生态系统
  • 如果需要的话,你希望能够从 F# 迁移出去
  • 您希望浏览器占用的资源 kb 最少。
  • 你必须频繁地与 JavaScript 交互
  • 你喜欢并想使用 React 或 Lit 或 Vanilla (Sutil)

如果出现以下情况,请勿使用 Fable:

  • 你真的不喜欢 JS
  • 你想要真正的类型安全
  • 你可以使用原生 F#(即不使用大多数 JS 生态系统)
  • 你不想学习或处理 JavaScript 工具

如果出现以下情况,请使用 WebSharper:

  • 你不介意先使用 F#,然后再使用 JS
  • 您希望通过类型提供程序实现类型安全的 HTML
  • 您希望模糊客户端 F# 和服务器端 F# 之间的界限
  • 你不太关心工具链,只关心最终可部署的资产

如果出现以下情况,请勿使用 WebSharper:

  • 你需要手动创建 js 文件并调整编译工具链
  • 你需要一个更加面向 JavaScript 的应用程序
  • 您需要在现有节点工具的基础上进行构建
  • 您关心应用程序的额外运行时间(约 8kb)

如果出现以下情况,请使用 Web Assembly:

  • 您希望在浏览器中运行原生 F#,而不是假的
  • 您想利用 .NET 生态系统
  • 您希望使用 .NET 工具来发布、分发和构建您的 F#
  • 您希望在桌面、服务器和移动设备之间共享代码(就像 Avalonia 允许您的那样)

如果出现以下情况,请勿使用 Web Assembly:

  • 你不想发布过于繁重的网站(即使某些库提供了精简支持)
  • 您需要较低的 TTI(交互时间)和 TFP(第一印象时间)
  • 你需要一个更成熟的生态系统
  • 你需要大量的 JS 互操作

个人观点

没人要求我这么做,也没人应该这么做,因为我的意见不应该影响你的决策。
话虽如此……

我个人的顶级是:

  1. 寓言文学
  2. Sutil-Fun.Blazor
  3. Feliz。固体吗?

主要原因是 Fable.Lit 符合 Web 标准,而我主要是一名前端开发人员,React 并不适合我,主要是因为它专注于钩子,在 React 的情况下,它们可能有意义,但太神奇了,如果使用不当,很容易出现错误和性能问题(我正在看你的 useFootGun,我的意思是 useEffect)

Sutil 排在第二位,因为它拥有一个纯 F# 框架,并且还提供了一个反应状态管理模型,这非常棒,它完全符合我做网站的思维模型。

Fun.Blazor 也迅速夺得 Tie 的第二名,因为它采用了与 sutil 相同的状态管理概念,并更进了一步

Feliz.Solid 位居第三,因为它也提供了一种响应式模型,未来很可能在很多地方和代码库中取代 React。它不会遇到 React 所面临的问题,而且前景光明。它的作者最近(在撰写本文时)被 Netlify 聘用,因此,随着 Feliz 集成的成熟,它的发展前景只会越来越好。

结论

所以我希望这篇文章能够让您了解 F# 前端的当前状态,以及如果您想选择我们提供的一个或其他替代方案,您应该考虑什么。

最终,您不应该被迫做出正确的选择。F# 解决方案即使不够“成熟”,也相当可靠,毕竟这是我们选择 F# 的主要原因,要么是为了工作,要么是为了娱乐(有时两者兼而有之)。如果您真的有理由不选择它,那么您应该避免使用其中一种替代方案,否则它很可能会满足您的需求。

这些框架是 F# 社区成员的杰出作品,即使它们看起来很年轻或处于测试阶段,这些工具也做得非常出色,并且功能比您听到这些词时想象的要强大得多,请尝试一下,向其作者提供反馈,并记住并非所有东西都是 React 或其衍生产品,您今天可以选择 :)

直到下一次,如果需要的话,请不要忘记留下您的评论和问题!

文章来源:https://dev.to/tunaxor/exploring-the-f-frontend-landscape-13aa
PREV
异步 JavaScript 终极指南
NEXT
JavaScript 中创建对象的不同方法总结