Skip to content
This repository has been archived by the owner on Mar 25, 2018. It is now read-only.
Aleksey Fomkin edited this page May 12, 2015 · 9 revisions

ЧЕРНОВИК БУДУЩЕЙ ВЕРСИИ. ПЕРЕВОД НА АНГЛИЙСКИЙ ПОСЛЕ ВЫЧИТКИ

Reactive Kittens

Введение

Reactive Kittens, это набор библиотек, позволяющий разрабатывать современные одностраничные веб-приложения (SPA) на языка Scala с применением функционального реактивного программирования (FRP). Фокус набора -- мобильные приложения, основанные на вэб-технологиях. Основной особенностью набора является возможность разделить потоки выполнения для рендеринга DOM и для выполнения логики приложения. Это позволяет сделать приложения более отзывчивыми, избавиться от "залипания" интерфейса и пользоваться ресурсами нескольких ядер CPU для выпонения веб приложений без написания дополнительного кода. Мотивацией для создания набора послужил ряд потребностей не решенных современными библиотеками.

  1. Иметь гарантию отсутствия "залипаний"
  2. Получить легковесную FRP-библиотеку
  3. Получить тесную интеграцию FRP-библиотеки с системой шаблонизации

Как это работает

Код на языке Scala компилируется с помощью Scala.js в язык JavaScript. Получившийся *opt.js загружается в WebWorker с помощью очень легкого загрузчика vaska.js и далее работает изолированно от остальной страницы. Программа в воркере шлет команды в основной поток испольнеия, управляя ими DOM моделью и получая из нее пользовательский ввод. Таким образом мы добиваемся высокой отзывчивости (отсутствия "залипаний") и отсутствия стартового лага на компиляцию JavaScipt в основном потоке исполнения.

Такой подход накладывает некоторые ограничения. В первую очередь это отсутствие возможности работать с некоторой частью API браузера на прямую. В основоном это касается самого DOM, системы событий, localStorage и т.д. Список API доступого для использования можно увидеть на этой странице. Для того, что бы использовать полный API, вам необхомо будем вызывать специальную прослойку, которая будет преобразовывать ваши вызовы в команды для vaska.js. Посылка и итерпретация команды пожирает значительно больше ресурсов, чем прямой вызов. Вторым, неочевидным, ограничением является отсутствие возможности вызвать что либо зависимое от стека вызовов на стороне основного потока выполнения. В таким вещам относятся исключения и разнообразные функции производящие побочные эффекты вроде event.preventDefault(). В свете выше сказанного вы не можете линковать библиотеки использующие запрещенный API в ваше Scala.js приложение.

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

Объяснение на пальцах

Убедитесь что вы знакомы с языком Scala. Необходимо понимать как работают и уметь пользоваться функциями map и flatMap. Желательно иметь представление о нотации for-comprehensions, так как примеры будут даны именно в ней. Для начала вам необходимо сколонировать типовой проект и инпортировать в свою любимую IDE, соместимоую со Scala и SBT. Теперь давайте попробуем написать немного кода. Нашей целью станет очень простая программа, складывающая два числа, введенные в два поля ввода и выводящая результат.

Описываем DOM в Scala

import felix._
import moorka._

object Main exentds Application {
  def start() = {
  }
}

Складываем два числа

val firstNumber = Var("0")
val secondNumber = Var("0")

'div('class := "form"
  'div('class := "form-item"
    'span('class := "form-label", "First Number"),
    'input('value =:= firstNumber)
  ),
  'div('class := "form-item"
    'span('class := "form-label", "Second Number"),
    'input('value =:= secondNumber)
  ),
  'div('class := "form-item"
    'span('class := "form-label", "Sum"),
    'textContent = for (a <- firstNumber; b <- secondNumber) yield {
      try { (a.toInt + b.toInt).toString } catch { _ =>
        "Please, enter valid values"
      }
    }
  )
)

Что мы видим? Как только оба поля ввода заполнены, ответ сразу же записывается в поле Sum. Давайте размеремся, что здесь происходит. В первую очередь давайте обратим внимание на объявление реактивных переменных Rx

val firstNumber = Var("0")
val secondNumber = Var("0")

Реактивные переменные это такие объекты, которые могут сообщаться о своих изменеиях. Когда мы "присваиваем" их в DOM с помощью оператора =:= просходит связывание значения в DOM и значения реактивной переменной. Стоит обратить внимание, что количество связываний не повляет на скорость работы приложения. Во первых они работают отдельно друг от друга, во пторых выполняются в воркере. Для основного потока это просто событие change или input. Теперь давайте посмотрим как они используются для выыода.

'textContent = for (a <- firstNumber; b <- secondNumber) yield {
  try { (a.toInt + b.toInt).toString } catch { _ =>
    "Please, enter valid values"
  }
}

То что вы видите называется производным связыванием. Для значения этих двух реактивных переменных будет применеятся код написанный в блоке yield, после чего он будет попадать в виде текста в поле Sum. В случае если сложение не получится, а оно не получится, если пользователь введет не число, будет выведена строка "Please, enter valid values".

Теперь давайте усложним пример добавив кнопку "Clear". Для этого мы немного поменяем объявление рективных переменных и добавим кое-что новое.

val clear = Channel.signal()
val (firstNumber, secondNumber) = {
  val empties = clear.map(_ => "0")
  (Var.withDefaultMod("0")(_ => empties), Var.withDefaultMod("0")(_ => empties))
}
...
'div('class := "form-controls"
  'button('click listen clear, "Clear")
)

Мы объявили новое значение clear и переписали опредение реактивных переменных. Сначала давайте разберемся, что такое clear. Итак clear это реактивный канал, который тоже является Rx. Каналы работают так же как реактивные переменные, но не держат в себе значения. Оно появляется в них только в тот момент, когда кто-то туда что-то кладет и затем изчезает. В нашм случае кладет оператор listen который связывет событие click с сигналом clear.

Следующим важным дополнением в код является withMod. Такой способ создания реактивных переменных позволяет менять их всзязи с измениями в других Rx. На вход withMod подается функция которая принимает текущее значение реактивной переменной и возвращает реактивное значение. В нашем случае это empties -- производное от канала clear. Каждый раз, когда в clear кладут пустое значение Unitlisten кладет туда именно пустое значение) в empties попадает "0". За счет withMod эта строка перь будет подадать еще в наши реактивные переменные.

Форма входа

Теперь давайте возьмем более жизненный пример и напишем форму входа. Работать она будет следующим образом. Пользователь вводит адрес электронной почты и пароль и нажимает кнопку "Sign in". Запрос уходит на гипотетический сервер, ответ от которого выводится сверху формы.

val signIn = Channel.signal()
val email = Var("")
val password = Var("")

'div('class := "form"
  'span(class := "form-result"
    'textContent := for { 
      email <- email
      password <- password
      _ <- signIn
      url = s"https://example.com/signin?email=$email,passsword=$password"
      response <- Ajax.get(url).map(_.responseText).toRx
    } yield {
      response
    }
  ),
  'div('class := "form-item"
    'span('class := "form-label", "Email"),
    'input('value =:= email)
  ),
  'div('class := "form-item"
    'span('class := "form-label", "Password"),
    'input('value =:= password, 'type := "password")
  ),
  'div('class := "form-controls"
    'button('click listen signIn, "Sign in")
  )
)

В чем отличие от первого примера? Мы дожидаемся нажатия кнопки и ходим на сервер за ответом. Однако, если внимательно присмотреться, то мы поймем, что отличия не столь значительны. Как и в "калькуляторе" мы имеем две реактивные переменные и реактивный канал. При появлении значения Unit в реактивном канале, мы ходим на сервер, подобно тому, как мы очищали значение полей ввода.

Clone this wiki locally