Scala.js — Это Легко И Просто

Давайте представим, что нам нужно сделать небольшой фронтенд для нашего любимого бэкенд-сервиса Scala (например, который весь на Akka).

Для внутренних нужд, не заботясь о совместимости браузеров, и без дизайна, чтобы всё было совсем просто: пара знаков, пара форм, что-то обновлялось через сокеты, моргание, просто мелочи.

И тогда вы начинаете думать, что же происходит в мире js. Угловой? Угловой 2? Реагировать? Вуэ? jQuery? Или что-то другое? Или, может быть, просто сделать это с ванилью и не беспокоиться? Но к JavaScript у них руки уже не склоняются, они вообще его не помнят. Либо вы не можете поставить точку с запятой, либо кавычки неправильные, либо вы забыли вернуться, либо ваших любимых методов нет в коллекции.

Понятно, что ради такого можно и оплошать, но не хочешь, совсем не хочешь.

Вы начинаете писать, но все равно что-то не так.

И тут в голову закрадываются нехорошие мысли, может Scala.js? Ты их отгоняешь, но они не отпускают. Почему нет? Некоторые могут задаться вопросом, что это вообще такое? Короче говоря, это библиотека с привязкой к объектам браузера, dom-элементам и компилятору, который берет ваш Scala-код и компилирует его в JavaScript; вы также можете создать общие классы между jvm и js, например, класс Case с кодировщиком/декодером json. Звучит устрашающе, как и C++, который можно скомпилировать в JavaScript (хотя в связке с WebGL он работает вполне неплохо).

Итак, с чего нам начать? Давайте подключим его!

  
  
  
  
  
  
  
   

// plugins.sbt addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.14") addSbtPlugin("com.lihaoyi" % "workbench" % "0.3.0") // build.sbt enablePlugins(ScalaJSPlugin, WorkbenchPlugin)

Давайте добавим индекс-dev.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Example</title> <link rel="stylesheet" type="text/css" href=".

/main.css" /> </head> <body class="loading"> <div>Loading.</div> <script type="text/javascript" src=".

/ui-fastopt.js"></script> <script type="text/javascript" src="/workbench.js"></script> <script> example.WebApp().

main(); </script> </body> </html>

Заметил верстак ? Этот плагин позволяет автоматически компилировать и обновлять страницу, когда вы что-то меняете.

Очень удобно.

Теперь пришло время добавить точку входа пример.

WebApp Веб-приложение

import scala.scalajs.js.JSApp import scala.scalajs.js.annotation.JSExport import org.scalajs.dom import org.scalajs.dom.Event import org.scalajs.dom.raw.HTMLElement @JSExport object WebApp extends JSApp { @JSExport override def main(): Unit = { dom.document.addEventListener("DOMContentLoaded", (_: Event) ⇒ { dom.document.body.outerHTML = "<body></body>" bootstrap(dom.document.body) }) } def bootstrap(root: HTMLElement): Unit = { println("loaded") } }

В сети есть довольно подробная инструкция.

Ханс-он Scala.js , так что мы не опоздаем.

Наш проект загружен, обновлен, пришло время писать код. Но поскольку у нас тут vanilla js, то, в общем-то, особо не заморачиваешься.

Крайне неудобно создавать dom-элементы через document.createElement("div") и вообще работать с ними.

Просто для такого рода вещей есть как минимум парочка готовых решений, и да, опять же веб-фреймворки.

Мы не ищем легких путей, не хотим иметь дело с большими монстрами и тащить их за собой нам нужно легкое и небольшое приложение.

Давайте сделаем все сами.

Нам нужно какое-то более привычное и удобное представление dom, мы хотим простого связывания, а также можем немного скопировать принцип и подход из ScalaFX, чтобы сделать его более привычным.

Во-первых, давайте сделаем это просто Список наблюдаемых И Наблюдаемое значение .

EventListener, ObservedList, ObservedValue

class EventListener[T] { private var list: mutable.ListBuffer[T ⇒ Unit] = mutable.ListBuffer.empty def bind(f: T ⇒ Unit): Unit = list += f def unbind(f: T ⇒ Unit): Unit = list -= f def emit(a: T): Unit = list.foreach(f ⇒ f(a)) } class ObservedList[T] { import ObservedList._ private var list: mutable.ListBuffer[T] = mutable.ListBuffer.empty val onChange: EventListener[(ObservedList[T], Seq[Change[T]])] = new EventListener def +=(a: T): Unit = { list += a onChange.emit(this, Seq(Add(a))) } def -=(a: T): Unit = { if (list.contains(a)) { list -= a onChange.emit(this, Seq(Remove(a))) } } def ++=(a: Seq[T]): Unit = { list ++= a onChange.emit(this, a.map(Add(_))) } def :=(a: T): Unit = this := Seq(a) def :=(a: Seq[T]): Unit = { val toAdd = a.filter(el ⇒ !list.contains(el)) val toRemove = list.filter(el ⇒ !a.contains(el)) toRemove.foreach(el ⇒ list -= el) toAdd.foreach(el ⇒ list += el) onChange.emit(this, toAdd.map(Add(_)) ++ toRemove.map(Remove(_))) } def values: Seq[T] = list } object ObservedList { sealed trait Change[T] final case class Add[T](e: T) extends Change[T] final case class Remove[T](e: T) extends Change[T] } class ObservedValue[T](default: T, valid: (T) ⇒ Boolean = (_: T) ⇒ true) { private var _value: T = default private var _valid = valid(default) val onChange: EventListener[T] = new EventListener val onValidChange: EventListener[Boolean] = new EventListener def isValid: Boolean = _valid def :=(a: T): Unit = { if (_value != a) { _valid = valid(a) onValidChange.emit(_valid) _value = a onChange.emit(a) } } def value: T = _value def ==>(p: ObservedValue[T]): Unit = { onChange.bind(d ⇒ p := d) } def <==(p: ObservedValue[T]): Unit = { p.onChange.bind(d ⇒ this := d) } def <==>(p: ObservedValue[T]): Unit = { onChange.bind(d ⇒ p := d) p.onChange.bind(d ⇒ this := d) } } object ObservedValue { implicit def str2prop(s: String): ObservedValue[String] = new ObservedValue(s) implicit def int2prop(s: Int): ObservedValue[Int] = new ObservedValue(s) implicit def long2prop(s: Long): ObservedValue[Long] = new ObservedValue(s) implicit def double2prop(s: Double): ObservedValue[Double] = new ObservedValue(s) implicit def bool2prop(s: Boolean): ObservedValue[Boolean] = new ObservedValue(s) def attribute[T](el: Element, name: String, default: T)(implicit convert: String ⇒ T, unConvert: T ⇒ String): ObservedValue[T] = { val defValue = if (el.hasAttribute(name)) convert(el.getAttribute(name)) else convert("") val res = new ObservedValue[T](defValue) res.onChange.bind(v ⇒ el.setAttribute(name, unConvert(v))) res } }

Теперь пришло время создать базовый класс для всех элементов dom:

abstract class Node(tagName: String) { protected val dom: Element = document.createElement(tagName) val className: ObservedList[String] = new ObservedList val id: ObservedValue[String] = ObservedValue.attribute(dom, "id", "")(s ⇒ s, s ⇒ s) val text: ObservedValue[String] = new ObservedValue[String]("") text.onChange.bind(s ⇒ dom.textContent = s) className.onChange.bind { case (_, changes) ⇒ changes.foreach { case ObservedList.Add(n) ⇒ dom.classList.add(n) case ObservedList.Remove(n) ⇒ dom.classList.remove(n) } } } object Node { implicit def node2raw(n: Node): Element = n.dom }

И некоторые компоненты пользовательского интерфейса, такие как Pane, Input, Button: Панель, Ввод, Кнопка

class Pane extends Node("div") { val children: ObservedList[Node] = new ObservedList children.onChange.bind { case (_, changes) ⇒ changes.foreach { case ObservedList.Add(n) ⇒ dom.appendChild(n) case ObservedList.Remove(n) ⇒ dom.removeChild(n) } } } class Button extends Node("button") { val style = new ObservedValue[ButtonStyle.Value](ButtonStyle.Default) style.onChange.bind { v ⇒ val styleClasses = ButtonStyle.values.map(_.toString) className.values.foreach { c ⇒ if (styleClasses.contains(c)) className -= c } if (v != ButtonStyle.Default) className += v.toString } } class Input extends Node("input") { val value: ObservedValue[String] = new ObservedValue("", isValid) val inputType: ObservedValue[InputType.Value] = ObservedValue.attribute(dom, "type", InputType.Text)(s ⇒ InputType.values.find(_.toString == s).

getOrElse(InputType.Text), s ⇒ s.toString) Seq("change", "keydown", "keypress", "keyup", "mousedown", "click", "mouseup").

foreach { e ⇒ dom.addEventListener(e, (_: Event) ⇒ value := dom.asInstanceOf[HTMLInputElement].

value) } value.onChange.bind(s ⇒ dom.asInstanceOf[HTMLInputElement].

value = s) value.onValidChange.bind(onValidChange) onValidChange(value.isValid) private def onValidChange(b: Boolean): Unit = if (b) { className -= "invalid" } else { className += "invalid" } def isValid(s: String): Boolean = true }

После этого вы можете создать свою первую страницу:

class LoginController() extends Pane { className += "wnd" val email = new ObservedValue[String]("") val password = new ObservedValue[String]("") children := Seq( new Pane { className += "inputs" children := Seq( new Span { text := "Email" }, new Input { value <==> email inputType := InputType.Email override def isValid(s: String): Boolean = validators.isEmail(s) } ) }, new Pane { className += "inputs" children := Seq( new Span { text := "Password" }, new Input { value <==> password inputType := InputType.Password override def isValid(s: String): Boolean = validators.minLength(6)(s) } ) }, new Pane { className += "buttons" children := Seq( new Button { text := "Login" style := ButtonStyle.Primary }, new Button { text := "Register" } ) } ) }

И последний штрих:

def bootstrap(root: HTMLElement): Unit = root.appendChild(new LoginController())

Что произошло в конце

Scala.js — это легко и просто

Также можно написать какой-нибудь простой роутер без слишком сложной структуры состояний, но это вопрос технологии.

В результате мы за 10 минут без особых затрат получили простую и понятную структуру, которую можно расширять и пополнять новыми компонентами и функционалом.

Scala.js — очень интересное направление развития технологий, но все же я бы не советовал использовать что-то подобное для проектов, состоящих более чем из 2 страниц.

Так как Scala.js пока, на мой взгляд, достаточно беден по инфраструктуре и по тому, где найти разработчиков для поддержки и развития проекта.

Технология крайне редкая, но для решения простых задач она имеет право на существование.

ПС.

Код может содержать ошибки, используйте на свой страх и риск.

Всего наилучшего! Теги: #scala #scalajs #JavaScript #.

bike #JavaScript #scala

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.