Реактивность
Реактивность в приложениях Genie позволяет разработчикам создавать интерактивные и адаптивные пользовательские интерфейсы, которые обновляются автоматически при изменении базовых данных или взаимодействии пользователя с пользовательским интерфейсом. Это достигается за счет комбинации реактивных переменных, обработчиков и компонентов пользовательского интерфейса. На этой странице представлены основные концепции реактивности в приложениях Genie и объясняется, как вместе они формируют динамический пользовательский интерфейс.
Реактивные переменные, обработчики и компоненты пользовательского интерфейса
Реактивные переменные
Реактивные переменные служат для хранения состояния компонентов пользовательского интерфейса, чтобы бэкенд мог получать информацию об изменениях, вносимых во фронтенде, и наоборот. Реактивные переменные определяются внутри блока @app
с помощью макросов @in
и @out
, которые означают следующее:
-
@in
: эту переменную можно изменить в пользовательском интерфейсе, и ее изменение будет передано в бэкенд. -
@out
: эта переменная доступна только для чтения, и ее нельзя изменить из пользовательского интерфейса. Однако ее можно обновить в бэкенде, чтобы отразить изменения в данных.
Кроме того, существует макрос @private
для определения реактивных переменных, которые не отображаются в пользовательском интерфейсе. Эти переменные будут копироваться в каждый сеанс пользователя, а внесенные в них изменения не будут передаваться в пользовательский интерфейс или другим пользователям. Тем не менее эти переменные могут активировать реактивный обработчик.
Реактивные переменные привязываются к компонентам пользовательского интерфейса для хранения информации об их состоянии, например числа, выбранного с помощью ползунка, или содержимого текстового поля. При каждом изменении содержимого такой переменной активируется обработчик. Например, компонент textfield
можно привязать к переменной следующим образом:
using GenieFramework
@app begin
@in msg = ""
end
ui() = textfield("Message", :msg )
@page("/", ui)
Каждый раз, когда пользователь вводит текст в поле в браузере, переменная msg
будет обновляться в бэкенде.
Реактивные обработчики
Реактивные обработчики определяют код, который выполняется при изменении значения реактивной переменной. Такие обработчики определяются с помощью макроса @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))]
Реактивные компоненты пользовательского интерфейса
Реактивные переменные привязываются к компонентам пользовательского интерфейса для хранения информации об их состоянии, например числа, выбранного с помощью ползунка, или содержимого текстового поля. При каждом изменении содержимого такой переменной активируется обработчик. Например, textfield
компонент можно привязать к реактивной переменной N
из предыдущего примера:
textfield("N", :N )
Полученный код HTML включает атрибут v-model
, который связывает поле ввода с реактивной переменной N:
<q-input label="N" v-model="N"></q-input>
Это гарантирует, что любое изменение значения поля ввода в браузере будет отражено в N
в коде Julia и блок реактивного кода будет выполнен соответствующим образом. Аналогичным образом, любое изменение 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 # Будет создана новая переменная 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 не приведет к передаче нового значения в браузер. К срабатыванию обработчика и передаче изменений приведет только замена всего словаря или изменение поля в браузере.
Это поведение показано в примере ниже. Для изменения поля data
в словаре есть три кнопки: одна запускает код в браузере, а две другие активируют обработчик в бэкенде. Реактивный обработчик, отслеживающий словарь 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()
Внутреннее устройство: реактивные модели
Для работы реактивных моделей в них поддерживается внутреннее представление реактивных переменных и блоков кода. При определении реактивных переменных и блоков кода они сохраняются в словарях REACTIVE_STORAGE
и HANDLERS
модуля GenieFramework.Stipple.ReactiveTools
. Например, хранилище для блока @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
. Это гарантирует наличие у каждого пользователя изолированного состояния и возможности взаимодействовать с приложением независимо от состояния других пользователей. Доступ к модели, созданной для запроса, можно получить с помощью @init
при использовании функции route
вместо @page
:
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 в соответствующий экземпляр реактивной модели. Связь между фронтендом и бэкендом осуществляется по протоколу WebSocket, который обеспечивает двунаправленные каналы связи в реальном времени между клиентом и сервером. При изменении значения реактивной переменной во фронтенде или бэкенде другой стороне отправляется сообщение WebSocket с новым значением.