https://habr.com/ru/post/480912/- Разработка веб-сайтов
- JavaScript
- Angular
- ReactJS
Первая
статья про
dap, очевидно, не стала моим писательским успехом: подавляющее большинство коментов к ней свелись к «ниасилил» и «ниасилил, но осуждаю». А приз за
самый единственный конструктивный комментарий верхнего уровня достается
OldVitus, за совет продемонстрировать dap на примере TodoMVC, чтобы было с чем сравнить. Чем я в этой статье и займусь.
TodoMVC, если кто не знает, это такой стандартный UI-хелловорлд, позволяющий сравнить решения одной и той же задачи — условного «Списка дел» — средствами разных фреймворков. Задачка, при всей своей простоте (ее
решение на dap влезает «в один экран»), весьма иллюстративна. Поэтому на ее примере я попробую показать, как типичные для веб-фронтенда задачи реализуются с помощью dap.
Искать и изучать формальное описание задачи я не стал, а решил просто среверсить один из примеров. Бэкенд в рамках этой статьи нам не интересен, поэтому сами мы его писать не будем, а воспользуемся
одним из готовых с сайта
www.todobackend.com, оттуда же возьмем и
пример клиента и стандартный
CSS-файл.
Для использования dap вам не нужно ничего скачивать и устанавливать. Никаких
npm install
и вот этого всего. Не требуется создавать никаких проектов с определенной структурой каталогов, манифестами и прочей атрибутикой IT-успеха. Достаточно текcтового редактора и браузера. Для отладки XHR-запросов может еще потребоваться веб-сервер — достаточно простейшего, типа вот этого
расширения для Chrome. Весь наш фронтенд будет состоять из одного-единственного .html-файла (разумеется, ссылающегося на скрипт dap-движка и на стандартный CSS-файл TodoMVC)
Итак, с чистого листа.
1. Создаем .html файл
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo -- dap sample</title>
<link rel="stylesheet" href="https://www.todobackend.com/client/css/vendor/todomvc-common.css"/>
<script src="https://dap.js.org/0.4.js"></script>
</head>
<body>
<script>
// здесь будет dap
</script>
</body>
</html>
Обычная html-заготовка, в которой подключаем CSS-файл, любезно предоставляемый сайтом
www.todobackend.com и dap-движок, не менее любезно предоставляемый сайтом
dap.js.org
2. Копируем DOM-структуру оригинального примера
Чтобы пользоваться стандартным CSS-файлом без переделок, будем придерживаться той же DOM-структуры, что и
оригинальный пример. Открываем его в браузере Chrome, жмем Ctr+Shift+I, выбираем вкладку Elements и видим, что собственно приложение находится в элементе
section id="todo-app">
Последовательно раскрывая это поддерево, переписываем его структуру в наш .html файл. Сейчас мы просто срисовываем по-быстренькому, а не пишем код, поэтому просто пишем сигнатуры элементов в 'одинарных кавычках', а в скобках их детей. Если детей нет — рисуем пустые скобочки. Следим за индентами и балансом скобок.
// здесь будет dap
'#todoapp'(
'#header'(
'H1'()
'INPUT#new-todo placeholder="What needs to be done?" autofocus'()
)
'#main'(
'#toggle-all type=checkbox'()
'UL#todo-list'(
'LI'(
'INPUT.toggle type=checkbox'()
'LABEL'()
'BUTTON.destroy'()
)
)
)
'#footer'(
'#todo-count'()
'UL#filters'(
'LI'()
)
'#clear-completed'()
)
)
Oбратите внимание: повторяющиеся элементы (например, здесь это элементы
LI
) мы пишем в структуру по одному разу, даже если в оригинале их несколько; очевидно, что это массивы из одного и того же шаблона.
Формат сигнатур, думаю, понятен любому, кто писал руками HTML и CSS, поэтому останавливаться на нем подробно пока не буду. Скажу лишь, что теги пишутся ЗАГЛАВНЫМИ буквами, а отсутствие тега равносильно наличию тега DIV. Обилие здесь #-элементов (имеющих id) обусловлено спецификой подключаемого CSS-файла, в котором используются в основном как раз id-селекторы.
3. Вспоминаем, что dap-программа — это Javascript
Чтобы избавить нас от лишних скобочек в коде, dap-движок внедряет прямо в
String.prototype
несколько методов (я в курсе, что внедрять свои методы в стандартные объекты — это айяйяй, но… короче, проехали), которые преобразует строку-сигнатуру в dap-шаблон. Один из таких методов —
.d(rule, ...children)
. Первым аргументом он принимает правило генерации (
d-правило), и остальными аргументами — произвольное число чайлдов.
Исходя из этого нового знания, дописываем наш код так, чтобы вместо каждой открывающей скобки у нас была последовательность
.d(""
, а перед каждой открывающей одинарной кавычкой, кроме самой первой, была запятая. Лайфхак: можно воспользоваться автозаменой.
'#todoapp'.d(""
,'#header'.d(""
,'H1'.d("")
,'INPUT#new-todo placeholder="What needs to be done?" autofocus'.d("")
)
,'#main'.d(""
,'#toggle-all type=checkbox'.d("")
,'UL#todo-list'.d(""
,'LI'.d(""
,'INPUT.toggle type=checkbox'.d("")
,'LABEL'.d("")
,'BUTTON.destroy'.d("")
)
)
)
,'#footer'.d(""
,'#todo-count'.d("")
,'UL#filters'.d(""
,'LI'.d("")
)
,'#clear-completed'.d("")
)
)
Вуаля! Мы получили дерево вызовов метода
.d
, которое уже готово трансформироваться в dap-шаблон. Пустые строки
""
— это зародыши будущих d-правил, а чайлды стали перечисленными через запятую аргументами. Формально, это уже валидная dap-программа, хоть пока и не совсем с тем выхлопом, который нам нужен. Но ее уже можно запустить! Для этого после закрывающей корневой скобки дописываем метод
.RENDER()
. Этот метод, как понятно из его названия, рендерит полученный шаблон.
Итак, на данном этапе имеем .html-файл вот с таким содержанием:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo -- dap sample</title>
<link rel="stylesheet" href="https://www.todobackend.com/client/css/vendor/todomvc-common.css"/>
<script src="https://dap.js.org/0.4.js"></script>
</head>
<body>
<script>
'#todoapp'.d(""
,'#header'.d(""
,'H1'.d("")
,'INPUT#new-todo placeholder="What needs to be done?" autofocus'.d("")
)
,'#main'.d(""
,'#toggle-all type=checkbox'.d("")
,'UL#todo-list'.d(""
,'LI'.d(""
,'INPUT.toggle type=checkbox'.d("")
,'LABEL'.d("")
,'BUTTON.destroy'.d("")
)
)
)
,'#footer'.d(""
,'#todo-count'.d("")
,'UL#filters'.d(""
,'LI'.d("")
)
,'#clear-completed'.d("")
)
)
.RENDER() // рендерим полученный dap в документ
</script>
</body>
</html>
Можно
открыть его в браузере, чтобы убедиться, что DOM-элементы генерятся, CSS-стили применяются, осталось только наполнить этот шаблон данными.
4. Получаем данные
Идем на
страничку-оригинал, открываем в инструментах вкладку Network, включаем фильтр XHR, и смотрим, откуда берутся данные, и в каком виде.
Окей, понятненько. Список дел берется прямо из
todo-backend-express.herokuapp.com в виде json-массива объектов. Замечательно.
Для получения данных в dap имеется встроенный конвертор
:query
который асинхронно «конвертирует» URL в данные, с него полученные. Сам URL мы не будем писать прямо в правиле, а обозначим его константой
todos
; тогда вся конструкция по добыче данных будет выглядеть так:
todos:query
а саму константу
todos
пропишем словаре — в секции
.DICT
, прямо перед
.RENDER()
:
'#todoapp'.d(""
...
)
.DICT({
todos : "https://todo-backend-express.herokuapp.com/"
})
.RENDER()
Получив массив
todos
, строим из него список дел: для каждого дела берем название из поля
.title
и пишем его в элемент
LABEL
, а из поля
.completed
берем признак «завершенности» — и пишем в свойство
checked
элемента-чекбокса
INPUT.toggle
. Делается это так:
,'UL#todo-list'.d("*@ todos:query" // Оператор * выполняет повтор для всех элементов массива
,'LI'.d(""
,'INPUT.toggle type=checkbox'.d("#.checked=.completed") // # обозначает "этот элемент"
,'LABEL'.d("! .title") // Оператор ! просто добавляет текст в элемент
,'BUTTON.destroy'.d("")
)
)
Обновляем эту нашу страничку в браузере и… если вы запускаете ее из файловой системы, то ничего не происходит. Проблема в том, что современные браузеры не разрешают кросс-доменные XHR-запросы из локальных документов.
Пришло время смотреть нашу страничку через http — с помощью любого локального вебсервера. Ну, или если вы пока не готовы писать dap своими руками, смотрите последовательные версии странички по моим ссылкам (не забывайте смотреть исходники — в Хроме это делается с помощью Ctrl+U)
Итак,
заходим на нашу страничку по http:// и видим, что данные приходят, список строится. Отлично! Вы уже освоили операторы
*
и
!
, конвертор
:query
, константы и доступ к полям текущего элемента массива. Посмотрите еще раз на получающийся код. Он вам все еще кажется нечитаемым?
5. Добавляем состояние
Возможно, вы уже попробовали понажимать на галочки в списке дел. Сами галочки меняют цвет, но, в отличие от оригинала, родительский элемент
LI
не меняет свой стиль («завершенное дело» должно становиться серым и зачеркнутым, но этого не происходит) — дела не меняют свое
состояние. А никакого состояния эти элементы пока и не имеют и, соответственно, не могут его менять. Сейчас мы это поправим.
Добавим элементу
LI
состояние «завершенности». Для этого определим в его d-правиле
переменную состояния $completed
. Элементу
INPUT.toggle
, который может это состояние менять, назначим соответствующее правило реакции (
ui-правило), которое будет устанавливать переменную
$completed
в соответствии с собственным признаком
checked
(«галка включена»). В зависимости от состояния
$completed
элементу
LI
будем либо включать, либо выключать CSS-класс «completed».
,'UL#todo-list'.d("*@ todos:query"
,'LI'.d("$completed=.completed"// Переменная состояния, инициализируем из поля .completed
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed") // Начальное состояние галочки берем из данных
.ui("$completed=#.checked") // при нажатии обновляем $completed
,'LABEL'.d("! .title")
,'BUTTON.destroy'.d("")
)
.a("!? $completed") // в зависимости от значения $completed, включаем или выключаем css-класс completed
)
Подобные манипуляции с CSS-классами — вещь довольно частая, поэтому для них в dap имеется специальный оператор
!?
Обратите внимание, делаем мы это в
а-правиле (от слова accumulate). Почему не в d-правиле? Отличие между этими двумя типами правил в том, что d-правило при обновлении полностью перестраивает содержимое элемента, удаляя старое и генеря все заново, тогда как a-правило не трогает имеющееся содержимое элемента, а «дописывает» результат к тому, что уже есть. Смена отдельного атрибута элемента
LI
не требует перестройки остального его содержимого, поэтому рациональней это делать именно в a-правиле.
Смотрим на
результат. Уже лучше: нажатия на галочки меняют состояние соответствующего элемента списка дел, и в соответствии с этим состоянием меняется и визуальный стиль элемента. Но все еще есть проблема: если в списке изначально присутствовали завершенные дела — они не будут серенькими, т. к. по умолчанию a-правило не исполняется при генерации элемента. Чтобы исполнить его и при генерации, допишем в d-правило элемента
LI
оператор
a!
,'LI'.d("$completed=.completed; a!" // Сразу же после инициализации переменной $completed используем ее в a-правиле
Смотрим. Окей. С состоянием
$completed
разобрались. Завершенные дела стилизуются корректно и при начальной загрузке, и при последующих ручных переключениях.
6. Редактирование названий дел
Вернемся к
оригиналу. При двойном клике по названию дела включается режим редактирования, в котором это название можно поменять. Там это реализовано так, что шаблон режима просмотра «view» (с галкой, названием и кнопкой удаления) целиком прячется, а показывается элемент
INPUT class="edit"
. Мы сделаем чуть иначе — прятать будем только элемент
LABEL
, т. к. остальные два элемента нам при редактировании не мешают. Просто допишем класс
view
элементу
LABEL
Для состояния «редактирование» определим в элементе
LI
переменную
$editing
. Изначально оно (состояние) сброшено, включается по
dblclick
на элементе
LABEL
, а выключается при расфокусе элемента
INPUT.edit
. Так и запишем:
,'LI'.d("$completed=.completed $editing=; a!" // Теперь у нас две переменные состояния
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$completed=#.checked")
,'LABEL.view'
.d("? $editing:!; ! .title") // Если $editing сброшена, то показываем этот элемент
.e("dblclick","$editing=`yes") // По dblclick включаем $editing
,'INPUT.edit'
.d("? $editing; !! .title@value") // Если $editing непустой
.ui(".title=#.value") // обновляем .title по событию change (ui событие по умолчанию для INPUT)
.e("blur","$editing=") // сбрасываем $editing по событию blur
,'BUTTON.destroy'.d("")
).a("!? $completed $editing") // отображаем состояния $completed и $editing в css-классе элемента 'LI'
Теперь мы
можем редактировать названия дел.
7. Отправка данных на сервер
Ок, в браузере мы дела редактировать уже можем, но эти изменения нужно еще и передавать на сервер. Смотрим, как это делает оригинал:
Внесенные изменения отправляются на сервер методом PATCH с неким URL вида
http://todo-backend-express.herokuapp.com/28185
, который, очевидно, является уникальным для каждого дела. Этот URL указывается сервером в поле
.url
для каждого дела, присутствующего в списке. То есть все, что от нас требуется для обновления дела на сервере — это отправить PATCH-запрос по адресу, указанному в поле
.url
, с измененными данными в формате JSON:
,'INPUT.edit'
.d("? $editing; !! .title@value")
.ui(".title=#.value; (@method`PATCH .url (@Content-type`application/json)@headers (.title):json.encode@body):query")
.e("blur","$editing=")
Здесь мы используем все тот же конвертор
:query
, но в более развернутом варианте. Когда
:query
применяется к простой строке, эта строка трактуется как URL и выполняется GET-запрос. Если же
:query
получает сложный объект, как в данном случае, он трактует его как детальное описание запроса, содержащее поля
.method
,
.url
,
.headers
и
.body
, и выполняет запрос в соответствии с ними. Здесь мы сразу после обновления
.title
отправляем серверу PATCH-запрос c этим обновленным
.title
Но есть нюанс. Поле
.url
мы получаем от сервера, оно выглядит примерно так:
http://todo-backend-express.herokuapp.com/28185
, то есть в нем жестко прописан протокол http:// Если наш клиент тоже открыт по http://, то все нормально. Но если клиент открыт по https:// — то возникает проблема: по соображениям безопасности браузер блокирует http-трафик от https-источника.
Решается это просто: если убрать из
.url
протокол, то запрос будет проходить по протоколу страницы. Так и сделаем: напишем соответствующий конвертер —
dehttp
, и будем пропускать
.url
через него. Собственные конверторы (и прочий функционал) прописывается в секции
.FUNC
:
.ui(".title=#.value; (@method`PATCH .url:dehttp (@Content-type`application/json)@headers (.title):json.encode@body):query")
...
.FUNC({
convert:{ // конверторы - это функции с одним входом и одним выходом
dehhtp: url=>url.replace(/^https?\:/,'')// удаляем протокол из URL
}
})
Еще имеет смысл вынести объект headers в словарь, чтобы использовать его и в других запросах:
.ui(".title=#.value; (@method`PATCH .url:dehttp headers (.title):json.encode@body):query")
...
.DICT({
todos : "//todo-backend-express.herokuapp.com/",
headers: {"Content-type":"application/json"}
})
Ну и для полного фэншуя воспользуемся еще одним полезным свойством конвертора
:query
— автоматическим кодированием тела запроса в json в соответствии с заголовком
Content-type:application/json
. В итоге правило будет выглядеть так:
.ui(".title=#.value; (@method`PATCH .url:dehttp headers (.title)):query")
Итак,
смотрим. Окей, названия дел теперь меняются не только в браузере, но и на сервере. Но! Меняться-то может не только название дела, но и его состояние завершенности —
completed
. Значит, его тоже нужно отправлять серверу.
Можно элементу
INPUT.toggle
дописать аналогичный PATCH-запрос, просто вместо
(.title)
отправлять
(.completed)
:
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$completed=#.checked; (@method`PATCH .url:dehttp headers (.completed:?)):query")
А можно вынести этот PATCH-запрос «за скобки»:
,'LI'.d("$completed=.completed $editing= $patch=; a!" // $patch - "посылка" для сервера
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$patch=($completed=#.checked)") // кладем в $patch измененный completed
,'LABEL.view'
.d("? $editing:!; ! .title")
.e("dblclick","$editing=`yes")
,'INPUT.edit'
.d("? $editing; !! .title@value")
.ui("$patch=(.title=#.value)") // кладем в $patch измененный title
.e("blur","$editing=")
,'BUTTON.destroy'.d("")
)
.a("!? $completed $editing")
// если $patch не пустой, отправляем его серверу, потом сбрасываем
.u("? $patch; (@method`PATCH .url:dehttp headers $patch@):query $patch=")
Тут дело вот в чем. Правила реакции относятся к группе «up-правил», которые исполняются «снизу вверх» — от потомка к родителю, до самого корня (эта последовательность может быть прервана при необходимости). Это чем-то похоже на «всплывающие» события в DOM. Поэтому какие-то фрагменты реакции, общие для нескольких потомков, можно поручить их общему предку.
Конкретно в нашем случае выигрыш от такого делегирования не особо заметный, но если бы редактируемых полей было больше, то вынос этого громоздкого (по меркам dap, конечно) запроса в одно общее правило сильно помог бы сохранять код простым и читабельным. Так что рекомендую.
Смотрим: Теперь на сервер отправляются и изменения названия, и изменения статуса.
В следующей статье, если будет интерес, рассмотрим добавление, удаление и фильтрацию дел. А пока можно посмотреть
финальный результат и другие примеры dap-кода на
dap.js.org/docs