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 Aug 11, 2015 · 9 revisions

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

Reactive Kittens

Введение

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

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

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

Код на языке Scala компилируется с помощью Scala.js в JavaScript. Получившийся *opt.js загружается в Web Worker с помощью очень легкого загрузчика 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

Давайте попробуем написать немного кода. В Reactive Kittens встроен DSL, позволяющий описывать DOM непосредственно в коде вашего приложения.

import felix._

object Main exentds FelixApplication {
  def start = {
    'div(
      'header(
        'h1('class /= "title", "Hello world"),
        'ul(
          'li('a('href /= "#", "Item 1")),
          'li('a('href /= "#", "Item 2")),
          'li('a('href /= "#", "Item 3"))
        )
      ),
      'content("Lorem ipsum dolor"),
      'footer("http://example.com (c) 2015")
    )
  }
}

'div() это тэг. /= это атрибут. Все просто. DOM описанный таким образом будет преобразован в команды для vaska.js и отрендерен в основном потоке выполнения с помощью методов Element таких как createElement() и applendChild(). Важно понимать, что это обычный код на Scala без какого либо препроцессинга. Это значит, что вы можете выносить части DOM в функции, генерировать его из коллекций, с помощью комбинаторов map, filter и т.п.

val items = Seq("Item 1", "Item 2", "Item 3")
'ul(items.map(x => 'li('a('href /= "#", x))))

Мы намеренно не стали пытаться делать статически-типизированную модель DOM, что бы облегчить использование современных веб-стандартов. Это значит что вы можете использовать собственные HTML-тэги описанные с помощью Web Components.

'polymerMenu(
  'polymerMenuItem('link /= "#", 'label /= "Item 1"),
  'polymerMenuItem('link /= "#", 'label /= "Item 2")
)
<polymer-menu>
  <polymer-menu-item link="#" label="Item 1"/>
  <polymer-menu-item link="#" label="Item 1"/>
</polymer-menu>

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

Добавим немного реактивности. Напишем простую программу, складывающая два числа, введенные в текстовые поля и выводящая результат.

import moorka._

Этот импорт позволит нам использовать функции Rx.

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. Давайте разберемся, что здесь происходит. В первую очередь обратим внимание на объявление реактивных переменных Var.

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

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

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

Здесь интересны две вещи. Сначала посмотрим на код в блоке for. Для значений переменных firstNumber и secondNumber будет применяется код написанный в блоке yield. Так for/yield создают новое реактивное значение, которое будет изменяться каждый раз при обновлении соответствующих реактивных переменных. Теперь обратите внимание на оператор :=. Здесь он связывает реактивное значение полученное с помощью for/yield с со свойством элемента (не атрибутом). Таким образом сумма двух реактивных переменных всегда будет попадать в виде текста в поле Sum.

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

val clear = Channel.signal()
val (firstNumber, secondNumber) = {
  val empties = clear.map(_ => "0")
  def newVar() = {
    val x = Var("0")
    x pull empties
    x
  }
  (newVar(), newVar())
}

...

'div('class := "form-controls"
  'button('click listen clear, "Clear")
)

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

Обратите внимание, как мы создаем реактивные переменные. Через pull() мы указываем, что все значения, которые появятся в empties попадут в наш Var. По этому каждый раз когда пользователь кликает кнопку Clear, в firstNumber и secondNumber попадет значение "0".

Форма входа

Теперь давайте возьмем более жизненный пример и напишем форму входа. Работать она будет следующим образом. Пользователь вводит адрес электронной почты и пароль и нажимает кнопку "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 в реактивном канале, мы ходим на сервер, подобно тому, как мы очищали значение полей ввода.