Продолжаем нашу рубрику «ни дня без Дерби». Сегодня мы начнем (наконец-то!) писать код и рассмотрим базовые моменты Derby.js. А так же вы узнаете почему Derby-программисты обычно одиноки, в то время как их более счастливые коллеги, использующие другие фреймворки, работают над аналогичными проектами в веселой дружной команде и с бОльшими сроками.
Как настроить окружение читайте тут.
Начнем с того, что создадим и запустим макет приложения:
derby bare habr
cd habr
npm start
Набираем в браузере http://localhost:3000/.
Видим надпись Bare.
Что только что произошло? Ваш запрос ушел на сервер, где был обработан всем connect middleware по очереди из /lib/server/index.js, вплоть до:
.use(app.router())
Что является роутером клиентского приложения app и находится здесь /lib/app/index.js в виде:
app.get('/', function(page) {
page.render();
});
Тут для пути '/' мы генерируем html из темплейта /views/app/index.html. Почему именно index.html? Это по дефолту. Можно написать так, ничего не изменится:
app.get('/', function(page) {
page.render('index');
});
В index.html у нас есть только секция Body. Еще могут быть Head, Header, Footer, Scripts, Title и т.п. Derby шаблонизатор, когда будет собирать нам html, найдет секцию Body и положит ее значение в соответсвующее место, а вместо других секций (так как они не заданы), положит пустые или дефолтные значения. В итоге обратно на клиент вернется:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style id="$_css"></style>
</head>
<!--$_page-->
<body>
Bare
<!--$$_page-->
<script defer="" async="" src="/derby/lib-app-index.js"></script>
</body>
</html>
Что видим? Title — пустой. Стилей нету. Head — тоже почти пустой. Как и следовало ожидать. Зато что-то лежит в body.
Почему стили будут inline, а не отдельным файлом? Nate где-то писал что когда он работал в Google Search, они там проводили кучу тестов по этому поводу и пришли в выводу что в абсолютном большинстве сценариев inline стили загружаются быстрее, чем отдельным файлом, поэтому все стили из папки /styles загружаются inline. Вам ничего не мешает добавлять ссылки на свои стили в секции Head.
Скрипт /derby/lib-app-index.js — это как раз наше клиентское приложение, которое подключится, как только загрузится и дальше вся генерация html будет уже на клиенте.
и — это служебные тэги шаблонизатора derby. Они используются для того, чтобы при динамической связи данных между html и моделью, при изменени модели не обновлять всю страницу полностью, а обновлять только часть html. В данном случае при срабатывании роутера, будет перегружаться только секция Body, потому что другие мы поленились, не задали и они одинаковы для всех страниц.
Вы можете изменять текст в темплейте (а также стили) и увидете мгновенно изменения в браузере. Это не имеет отношения к каким-то динамическим синхронизациям данных. Это для удобства разработчика. Если вы меняете html и css, то он автоматически компилируется, загружается на клиент и подставляется вместо старого. Если меняете js, то приложение перезагружается полностью.
Давайте отделим вид от данных. Для в Derby есть два способа. Начнем с контекста. Это объект, который мы можем добавить следующим аргументом в page.render() и данные из которого мы можем отображать в html.
app.get('/', function(page) {
page.render({text: 'text from Context'});
});
<Body:>
{{text}}
Двойные {{}} скобки в темплейте значат, что вид не связан динамически с данными. Шаблонизатор не следит за изменением данных или html. Для контекста это и не нужно, так как контекст — это простой js-объект и не меняется, в связи с этим использование данного способа вывода данных (контекста) довольно ограниченно и используется, возможно, только для каких-то статических страничек. Вся мощь работы с данными появляется при использовании объекта Model.
Тут уже что-то написано про модель, чтобы не повторяться.
Где взять модель? Есть много способов (в зависимости от того на клиенте вы или на сервере), но в роутере она идет вторым аргументом (после page) и это очень удобно для нас в данной ситуации:
app.get('/', function(page, model) {
page.render();
});
Давайте поместим в нее данные:
app.get('/', function(page, model) {
model.set('_page.text', 'text in model');
page.render();
});
_page.text — это path (путь) в модели, где будет лежать наш текст. Ему соответсвует объект json. В данном случае:
var obj = model.get('_page');
obj === {text: 'text in model'}
Нижнее подчеркивание означает что этот path — локальный. То есть он существует только в данной модели. И не синхронизируется с бд и другими моделями (на других клиентах). Вы можете создавать любые локальные path, но объект _page — немного особенный. Он очищается при каждом срабатывании роутера, поэтому в нем удобно хранить данные, связанные с данной страницей.
Для того, чтобы увидеть данные из модели, меняем темплейт:
<Body:>
{{_page.text}}
Давайте попробуем сделать динамическую связь данных и html:
<Body:>
<input type="text" value={_page.text} />
{_page.text}
Одиночные скобки значат, что данные из модели динамически привязаны к html. Если вы изменяете значение в input, изменяется значение в модели, что в свою очередь изменяет значение текста рядом с input. Вот так реализуется двухстороняя связка между данными и html.
Ну что вроде ничего сложного пока не было? 3 строчки кода? Как-то не серьезно даже?
Давайте сделаем по настоящему серьезную штуку! Мы сейчас создадим веб-приложение, клиенты которого синхронизированны между собой. Мы меняем html на одном клиенте, динамически изменяются данные на этом клиенте, потом эти данные летят на сервер, где срабатывает алгоритм разрешения конфликтов, находятся клиенты, которые также подписаны на эти данные и, наконец, данные рассылаются всем этим клиентам, где они превращаются в html. Подойдет? Сколько времени нужно чтобы написать такое на вашем любимом (до Derby) фреймворке? Сколько строчек кода?
app.get('/', function(page, model) {
model.subscribe('page.text', function(err) {
if (!model.get('page.text')) {
model.set('page.text', 'text in model');
}
page.render();
})
});
<Body:>
<input type="text" value={page.text} />
{page.text}
И это всё? o_O Ну в общем да. Откройте http://localhost:3000/ в нескольких окнах браузера и поиграйтесь.
page.text — это remote path. В отличие от локального path, он указывает на базу данных. В данном случае у нас создается коллекция path и объект с ид text. В реальной жизни remote paths выглядят вот так: 'users.8ddd02f1-b82d-4a9c-9253-fe5b3b86ef41.name', 'customers.8ddd02f1-b82d-4a9c-9253-fe5b3b86ef41.properties.isLead', 'products.8ddd02f1-b82d-4a9c-9253-fe5b3b86ef41.prices.1.value'.
model.subscribe — так мы подписываем нашего клиента на данные в path 'page.text'. При изменении этих данных в бд, сервер пришлет нам новую версию этих данных.
Исходники :-)
Не забудьте запустить npm install
Автор: vmakhaev