反应性
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`,String
、 Vector{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 消息。