Engee 文档

反应性

Genie 应用程序中的反应性允许开发人员创建交互式响应用户界面,在底层数据发生变化或用户与用户界面交互时自动更新。这可以通过结合使用反应式变量、处理程序和用户界面组件来实现。本页介绍 Genie 应用程序中反应性的核心概念,并解释它们如何协同工作以创建动态用户界面。

反应式变量、处理程序和用户界面组件

Reactive variables

反应式变量用于存储用户界面组件的状态,使后端能够了解前端所做的更改,反之亦然。反应式变量是使用`@app` 块内的`@in` 和`@out` 宏定义的,每个宏都表示以下内容:

  • `@in`该变量可从用户界面修改,其更改将传播到后台。

  • @out:此变量为只读变量,不能从用户界面修改。但可以从后台更新,以反映数据的变化。

此外,还可以使用`@private` 宏来定义不向用户界面公开的反应式变量。这些变量将被复制到每个用户会话中,对它们所做的任何更改都不会传播到用户界面或其他用户。不过,这些变量仍然可以触发反应式处理程序。

反应式变量与用户界面组件绑定,以存储其状态信息,如滑块选择的数字或文本字段的内容,并在变量内容发生变化时触发处理程序。例如,我们可以将`textfield` 组件与变量绑定为

using GenieFramework
@app begin
    @in msg = ""
end
ui() = textfield("Message", :msg )
@page("/", ui)

每当用户在浏览器的字段中输入文本时,后台就会更新`msg` 变量。 Rective handlers

反应式处理程序定义了当反应式变量的值发生变化时执行的代码。处理程序使用`@onchange` 或`@onbutton` 宏定义,只要指定的反应式变量的值发生变化,前台或后台就会触发处理程序。

@app begin
    @in msg = ""
    @out msg_length = 0
    @onchange N begin
        msg_length = length(msg)
    end
end
ui = [textfield("Message", :msg), p("Length: {{msg_length}}")]

@onbutton 宏用于监视布尔值,并在处理程序执行后将其值设置为`false` 。

    @in trigger = false
    @onbutton trigger begin
        print("Action triggered")
    end
ui = [btn("Trigger action", @click(:trigger))]

Reactive UI components

反应式变量与用户界面组件绑定,用于存储其状态信息,如滑块选择的数字或文本字段的内容,并在变量内容发生变化时触发处理程序。例如,我们可以将`textfield` 组件与上一个示例中的反应式变量`N` 绑定:

textfield("N", :N )

生成的 HTML 代码包含`v-model` 属性,它将输入字段与 N 反应变量连接起来:

<q-input label="N" v-model="N"></q-input>

这样就能确保浏览器中输入字段值的任何变化都会反映到 Julia 代码中的`N` 上,并相应地执行反应代码块。同样,后台`N` 的任何变化都会更新浏览器中的输入框。

定义被动变量

定义新的反应式变量需要相应类型的初始值。例如,在上面的示例中,N 和`total` 的类型都是`Int` 。如果在用户界面中为`N` 引入的值是`Float` ,应用程序就会出错。此外,您不能使用一个反应式变量来初始化另一个变量,如下例所示:

@in N = 0
@in M = N + 1

由于`N` 不存在,这段代码会出错。

反应式变量只能在使用`@onchange` 或`@onbutton` 宏实现的处理程序中修改。在处理程序之外进行的任何更改都不会反映在用户界面上。这是因为这些变量位于`@app` 块中,而且它们是为每个用户会话实例化的。

最后,在`@onchange` 块中声明的变量是有作用域的,这意味着它们不会覆盖全局变量,而是会创建新变量。要修改全局变量,必须使用`global` 关键字:

N = 0
M = 0
@app begin
    @in toggle = false
    @onchange toggle begin
        global N = N + 1
        M = M+1 # This will create a new variable M
    end
end

变量类型

反应式变量以 Javascript 对象的形式序列化并发送到浏览器。大多数基本 Julia 类型,例如`Int`,StringVector{String}``Dict ,都可以声明为反应式。此外,自定义结构定义也可以像本例中一样暴露在用户界面上:

using GenieFramework
@genietools

mutable struct MyContent
    c::Int
end
mutable struct MyData
    description::String
    data::MyContent
end

@app begin
    @out d = MyData("hello", MyContent(1))
end

ui() = [p("{{d.description}}"),p("{{d.data}}"),p("{{d.data.c}}")]
@page("/", ui)
up()

如果某些对象无法序列化,则需要对`Stipple.render` 进行特殊化才能使其正常工作。

递归反应性

一般来说,复合对象不具有递归反应性。这意味着更改其中一个字段并不总是会触发反应式处理程序。以字典为例,在 Julia 代码中更改 dict 字段不会将新值传播到浏览器。只有替换整个 dict 或更改浏览器中的某个字段才会触发处理程序并传播更改。

下面的示例描述了这种行为。有三个按钮可以修改 dictionary 中的`data` 字段:其中一个在浏览器中运行代码,另外两个在后台触发一个处理程序。只有在按下第一个(前台)和第三个(dict 替换)按钮时,才会触发监视字典`d` 的反应式处理程序。使用第二个按钮从后台修改字段会增加计数器,但不会触发处理程序。

using GenieFramework
@genietools

@app begin
    @in d = Dict("description" => "hello", "data" => 1)
    @in change_field = false
    @in replace_dict = false
    @onchange d begin
        @show d
    end
    @onbutton change_field begin
        d["data"] += 1
    end
    @onbutton replace_dict begin
        d = Dict("description" => d["description"], "data" => d["data"] + 1)
    end
end

ui() = [p("{{d.data}}"), btn("Frontend +1 to field", @click("d.data += 1")), br(), btn("backend +1 to field", @click(:change_field)), br(), btn("backend replace dict", @click(:replace_dict))]
@page("/", ui)
up()

引擎盖下:反应模型

反应模型通过维护反应变量和代码块的内部表示来工作。定义反应式变量和代码块时,它们会存储在`GenieFramework.Stipple.ReactiveTools` 模块的`REACTIVE_STORAGE` 和`HANDLERS` 字典中。例如,上一个示例中`@app` 代码块的存储空间包括

julia> GenieFramework.Stipple.ReactiveTools.REACTIVE_STORAGE[Main]
LittleDict{Symbol, Expr, Vector{Symbol}, Vector{Expr}} with 6 entries:
  :channel__    => :(channel__::String = Stipple.channelfactory())
  :modes__      => :(modes__::Stipple.LittleDict{Symbol, Any} = $(QuoteNode(LittleDict{Symbol, Any, Vector{Symbol}, Vector{Any}}())))
  :isready      => :(isready::Stipple.R{Bool} = false)
  :isprocessing => :(isprocessing::Stipple.R{Bool} = false)
  :N            => :(N::R{Int64} = R(0, 1, false, false, "REPL[2]:2"))
  :total        => :(total::R{Int64} = R(0, 4, false, false, "REPL[2]:3"))

julia> GenieFramework.Stipple.ReactiveTools.HANDLERS[Main]
1-element Vector{Expr}:
 quote
    #= /Users/pere/.julia/packages/Stipple/pgem3/src/ReactiveTools.jl:689 =#
    on(__model__.N) do N
        #= /Users/pere/.julia/packages/Stipple/pgem3/src/ReactiveTools.jl:690 =#
        #= REPL[2]:5 =#
        print("N value changed to $(N)")
        #= REPL[2]:6 =#
        __model__.total[] = __model__.total[] + N
    end
end

当用户向路由发出 HTTP 请求时,会从存储中为该特定用户会话创建一个新的`ReactiveModel` 实例。这确保了每个用户都有独立的状态,可以独立地与应用程序交互,而不会影响其他用户的状态。当使用`route` 函数而不是`@page` 时,可通过`@init` 访问为请求实例化的模型:

route("/") do
    model = @init
    @show model
    page(model, ui()) |> html
end
var"##Main_ReactiveModel!#292"("OSINKNHRJHNKBFXCFZVKSWQVMWTUMUNN", LittleDict{Symbol, Any, Vector{Symbol},
Vector{Any}}(), Reactive{Bool}(Observable(false), 1, false, false, ""), Reactive{Bool}(Observable(false), 1, false, false, ""),
Reactive{Int64}(Observable(0), 1, false, false, "REPL[2]:2"), Reactive{Int64}(Observable(0), 4, false, false, "REPL[2]:3"))

在创建新的反应模型实例时,会为其分配一个唯一的标识符,用于跟踪用户会话并在整个会话期间保持状态。Genie 服务器使用该标识符将 websocket 消息路由到相应的反应模型实例。websockets 为前端和后端之间的通信提供了便利,它在客户端和服务器之间提供了实时、双向的通信通道。当前端或后台的反应式变量值发生变化时,就会向对方发送包含更新值的 websocket 消息。