Давайте представим, что нам нужно сделать небольшой фронтенд для нашего любимого бэкенд-сервиса Scala (например, который весь на Akka).
Для внутренних нужд, не заботясь о совместимости браузеров, и без дизайна, чтобы всё было совсем просто: пара знаков, пара форм, что-то обновлялось через сокеты, моргание, просто мелочи.
И тогда вы начинаете думать, что же происходит в мире js. Угловой? Угловой 2? Реагировать? Вуэ? jQuery? Или что-то другое? Или, может быть, просто сделать это с ванилью и не беспокоиться? Но к JavaScript у них руки уже не склоняются, они вообще его не помнят. Либо вы не можете поставить точку с запятой, либо кавычки неправильные, либо вы забыли вернуться, либо ваших любимых методов нет в коллекции.
Понятно, что ради такого можно и оплошать, но не хочешь, совсем не хочешь.
Вы начинаете писать, но все равно что-то не так.
И тут в голову закрадываются нехорошие мысли, может Scala.js? Ты их отгоняешь, но они не отпускают. Почему нет? Некоторые могут задаться вопросом, что это вообще такое? Короче говоря, это библиотека с привязкой к объектам браузера, dom-элементам и компилятору, который берет ваш Scala-код и компилирует его в JavaScript; вы также можете создать общие классы между jvm и js, например, класс Case с кодировщиком/декодером json. Звучит устрашающе, как и C++, который можно скомпилировать в JavaScript (хотя в связке с WebGL он работает вполне неплохо).
Итак, с чего нам начать? Давайте подключим его!
Давайте добавим индекс-dev.html// plugins.sbt addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.14") addSbtPlugin("com.lihaoyi" % "workbench" % "0.3.0") // build.sbt enablePlugins(ScalaJSPlugin, WorkbenchPlugin)
<!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())
Что произошло в конце
Также можно написать какой-нибудь простой роутер без слишком сложной структуры состояний, но это вопрос технологии.
В результате мы за 10 минут без особых затрат получили простую и понятную структуру, которую можно расширять и пополнять новыми компонентами и функционалом.
Scala.js — очень интересное направление развития технологий, но все же я бы не советовал использовать что-то подобное для проектов, состоящих более чем из 2 страниц.
Так как Scala.js пока, на мой взгляд, достаточно беден по инфраструктуре и по тому, где найти разработчиков для поддержки и развития проекта.
Технология крайне редкая, но для решения простых задач она имеет право на существование.
ПС.
Код может содержать ошибки, используйте на свой страх и риск.
Всего наилучшего! Теги: #scala #scalajs #JavaScript #.
bike #JavaScript #scala
-
Атомные Часы: Тикающий Мир
19 Oct, 24 -
Рпи
19 Oct, 24 -
Простая Настройка Asterisk + Fail2Ban
19 Oct, 24 -
Nvidia Поддержит Стартапы Финансами
19 Oct, 24