Изучаем Derby 0.6 — разбор примеров — #1
Последние несколько месяцев я участвую в нескольких проектах, разрабатываемых на Derby (реактивный fullstack javascript-фреймворк). Часть из них вполне успешно работает в продакшине, часть стартует в близжайшее время.Пока я изучал данню технологию возникло несколько мыслей. Во-первых, информации о данной технологии крайне мало: документация скудана, а статьи, которые написаны, обычно пишутся людьми, потратишвими от силы день-два на ее изучени. Во-вторых, есть небольшая группа людей, которые отлично знают данную технологию и используют ее в своих проектах, успешно решив все проблемы, о которые спотыкаются начинающие.
Идея у меня проста — поделиться полученными знаниями, если это конечно будет интересно и востребовано. Я хочу взять несколько примеров из проекта derby-examples и разобрать их по полочкам. Либо, воссоздавая их с нуля, попутно объясняя логику создания, с точнки зрения специалиста, либо же, беря уже готовый пример, и объясняя те моменты, которые были не раскрыты в предыдущих примерах. Короче, если понравится, думаю разобрать 5–6 примеров, по одному в неделю.
Версия 0.6 Изучать будем derby версии 0.6. На данный момент выпущена версия 0.6 alpha 5. Она достаточно стабильна для того, чтобы мы начали переводить все свои проекты с 0.5 на нее. Новые же проекты все создаются исключительно на 0.6. Вот, например, type4fame — хобби-проект моего друга, сделан на 0.6.В сравнении с версией 0.5, все, конечно, поменялось в лучшую сторону. Код фреймворка структурирован и понятен, Рендеринг ускорился, появилась возможность использовать произольные выражения во вьюхах. Новая система компонент позволяет хорошо структурировать большие приложения. Делить их на части с низкой связанностью. Короче, у меня только положительные впечатления.
Ради справедливости, здесь стоит добавить, что это все-таки еще альфа-версия, и у создателей есть определенный набор нереализованных TODO, и ошибки тоже встречаются (и исправляются).
Уровень подготовки Перечислю основное, что нужно знать и понимать, чтобы быстро въехать в derby: базовые знания по веб-разработке (html, css, javascript); nodejs — нужно понимать commonjs-модули, npm, знать, как запустить стандартный http-сервер; expressjs — приложения derby строятся поверх приложений express, поэтому хорошо бы иметь об экспресс и серверном js какие-то базовые знания (подключение модулей, обработка запроссов и т.д.) Есть так же определенный набор технологий, которые используются в derby и знакомство с ними здорово бы помогло (хотя и не является обязательным, мы постепенно будем всего этого касаться)реактивное программирование изоморфные javascript приложения операциональное преобразование — технология, разработанная google, для разрешения конфликтов при одновременном редактировании документов в google docs browserify mongodb Платформа Нам подойдет linux, mac. C windows могут быть проблемы — версия 0.5 работала нормально и под windows (правда приходилось самому компилить redis версии 2.6), версия 0.6 тоже к этому идет, но есть парочка pull-request-ов, которые еще не попали в основной репозиторий, поэтому начинающим пока стоит подождать.Перед началом разработки нужно убедиться, что на компьютере установлены:
nodejs mongodb redis 2.6 (обратите внимание, версия 2.4 не подходит) Все ставится с настройками по умолчанию. Монга и редис в момент работы приложения должны быть запущены.Итак, начинаем Разберем пример: https://github.com/zag2art/derby-example-hello. Он очень простой, зато разъяснений будет много.Можно скопировать себе готовый пример и изучить его, а можно набить его с нуля самому. Советую второй вариант, так лучше отложится в памяти, но выбирайте сами, я разъяснию оба варианта.
Копируем из моего репозитория:
# делаем себе копию git clone https://github.com/zag2art/derby-example-hello.git
# заходим в папочку проекта cd derby-example-hello
# устанавливаем зависимости (там два пакета derby и derby-starter) npm install
# чтобы запустить нужно будет набрать команду снизу и открыть бруазер на странице localhost:3000 npm start Создаем все файлы сами (предварительно создав папочку и войдя в нее):
package.json создаем стандартно, используя npm init (в качестве файла запуска указываем server.js), далее добавляем два модуля тоже через npm:
# ставим последнюю версию дерби из ветки 0.6 (если версию не указать npm отдает версию 0.5), сохраняем ее в package.json npm install derby@~0.6 -S
# ставим derby-starter npm install derby-starter -S Несколько слов о derby-starter: скорее всего вы никогда не будете использовать его в своих проектах. Гляньте если хотите его код, здесь по большей части инициализируется стандартное expressjs-приложение, к которому в качестве middle-ware цепляется дерби. Здесь же настраивается подключение дерби к данным (монге и редису), ну и настраивается серверная часть дерби.
В обычном случае этот код будет находится внути дерби-приложения, так как его нужно будет дополнять, например, для подключения авторизации через passport, ограничений доступа к данным через racer-access, или же, если вам захочется обрабатывать часть запросов express-ом, а не дерби (например, если нужен дублирующий restful-api). Но для наших целей всего этого пока не нужно, и, благодаря этому модулю наша серверная часть сокращается до одной строки:
server.js
require ('derby-starter').run (__dirname+'/index.js'); Запускаем серверную часть дерби указывая в качетсве параметра путь к файлу index.js, в котором и находтся само наше derbyjs-приложение.Итак, приложение сотоит из 2-х файлов: index.js
var app = module.exports = require ('derby').createApp ('hello', __filename); app.loadViews (__dirname);
// Маршрут рендерится на клиене и на сервере app.get ('/', function (page, model) { // Подписка обеспечивает синхронизацию данных model.subscribe ('hello.message', function () { page.render (); }); }); и index.html
{{hello.message}}
Давайте сначала наберем их, запустим приложение, поиграемся с ним, а потом разберем исходный код. Итак, все файлы находятся в одной папке: package.json, server.js, index.js и index.html. Модули derby и derby-starter установлены и находятся в подкаталоге node_modules, mongo и redis установлены и запущены.
Запускаем приложение (npm start, ну или напрямую — node server.js):
zag2art@laptop:~/work/derby-example-hello$ npm start
> derby-example-hello@0.0.0 start /home/zag2art/work/derby-example-hello > node server.js
Master pid 11291 11293 listening. Go to: http://localhost:3000/ Открываем браузер, вводим адрес http://localhost:3000/, и пришем что-нибудь в input-е, например:
Что видим, введенный нами текст, повторился в блоке h2, ниже input-а, здесь срабатывают реактивные привязки. Еще один эксперемент, попробуем открыть еще одну вкладку с этим же адресом http://localhost:3000/. Видим, введенный нами ранее текст. То есть данные синхронизируются между вкладками (а если бы наше приложение было выложено в Интернете, то и между абсолютно всеми клиентами). Попробуем поменять данные. Изменение сразу же отразится на соседней вкладке, без каки-либо лишних миганий, то-есть при обновлении данных, нужная часть страницы пересоздается прямо на клиенте. Далее, давайте посмотрим на исходный код страницы на второй вкладке:

Делаем вывод о том, что сервер отдавал нам уже сформированную заполненную страницу — что очень хорошо для индексации страниц поисковиками. Тоесть, у нас есть как клиентский, так и серверный рендеринг — быстное обновление для пользователей и доступ к данным для поисковиков.
Теперь давайте разберем принципы, заложенные в derby, а так же исходный код. Первое о чем стоит задуматься — это то, какие требования к системе были у создателей derby:
Дерби, предназначена для создания SPA (сложных, богатых элементами веб-приложений) приложений с сильной перерисовкой внутри страницы Для удобства пользователей, приложени должно позволять делать закладки на свое текущее состояние —, а значит при важном изменении состояния, должен меняться url То же самое касается и инексации поисковиками. Приложение должно индексироваться, отсюда вытекает, что при проектировании приложения, создатель должен четко для себя решить, какие состояния приложения должны попадать в индекс и запланировать для них соответствующие url. Итак, создатели поняли следующее, если страница запрашивается у сервера, нужно генерировать ее там и отдавать уже готовой, если же пользователь в браузере работает в приложении он не должен при смене url-каждый раз обращаться к серверу, ждать, получать мелькание экрана. Так же очень не хотелось бы дублировать код рендеринга страницы. То есть нам получается нужно иметь возможность отрисовывать страницу и на сервере и на клиенте, и не дублировать код. И на клиенте и на сервере у нас javascript — это облегчает дело. Но можно ли вообще так сделать. Получается нам нужно, чтобы и на клиенте и на сервере одинаково работал код, отвечающий за рендеринг страниц (шаблонизация, работа с html), маршрутизация (работа с url при смене адреса на клиенте), причем, получается, что и там и там нужно единообразно организовать доступ к данным. Такой код, работающий одинаково и на клиенте и на сервере, называется изоморфным, можете прочитать об этом тут. В дерби это реализовано.Получается в дерби есть 2 части:
Часть относящаяся только к серверу (всевозможные настройки, подключение модулей, привязка к expressjs). Обратите внимание здесь нет ни рендеринга, ни маршрутизации. В нашем случае это файл server.js c модулем derby-starter. Само изоморфное приложение. Его код исполняется на сервере, когда запрашивается какая-либо страница с сервера, а дальше, код работает уже на клиента. В нашем случае, это index.js и index.html. Давайте я попытаюсь подробней рассказать о второй части. Представим, что мы создали дерби приложение, работающее с 3-мя страницами:/ — домашняя страница сайта /about — страница с информацией о создателе /projects — страница с проектами Все страницы имеют ссылки друг на друга. Как будет работать с таким сайтом поисковик. Он сделает 3 запроса, по одному к каждой странице, и нормально их индексирует. Как будет с этим работать пользователь. (обратите внимание — важный момент). Пользователь войдет на сайт (либо по ссылке, либо введя адрес в адресную строку), на любую из этих трех страниц. Это будет единственным запросом к серверу. Потому, что вместе с готовой странице, пользователь получит в нагрузку все изоморфное приложение (так называемый бандл, включающий в себя в укакованном виде все, что мы написали — js-файлы в которых прописана обработка url, html-файлы с шаблонами, css-файл, а так же часть дерби, которая позволяет всему этому работать). Таким образом, изначально запросив, допустим страницу /about, пользователь получает на клиенте готовое SPA-приложение. И теперь, если он нажмент (на свое страничке about) на ссылку / или /projects — запросов к серверу больше не будет (здесь должна быть звездочка, которую я расшифрую потом). Страница сгенерируется напрямую на клиенте, url в браузере поменяется. Ну и так далее: пользователь может бегать по этим трем страницами, без всяких запросов к серверу.Так, теперь давайте посмотрим на код index.js:
var app = module.exports = require ('derby').createApp ('hello', __filename); app.loadViews (__dirname);
// Маршрут рендерится на клиене и на сервере app.get ('/', function (page, model) { // Подписка обеспечивает синхронизацию данных model.subscribe ('hello.message', function () { page.render (); }); }); Во-первых, нужно сказать, что index.js — это common.js модуль, то-есть, мы, например, легко можем подключить сюда любые модули, которые могут работать, как на клиенте, так и на сервере. Вот, например, так: var _ = require ('underscore');. В первой строке подкючается и инициализируется дерби. app.loadViews (__dirname); Здесь мы показываем, где у нас находятся файлы с шаблонами. В данном случаем мы указали на ту же папку, в которой лежит index.js, дебри поищет там и найдет index.html. У нас минимальное учебное приложение, поэтому все в одной папке, обычно же шаблоны лежать отдельно в подпапочке views. По идее, если бы у нас были css-стили, здесь же мы бы прописали что-то типа: // пример из нормального приложения побольше app.loadViews (path.join (__dirname, '…/views')); app.loadStyles (path.join (__dirname, '…/styles')); Там бы мог находится, например, файл index.css (ну или index.styl или index.less — дерби их поддерживает). Естественно в этом файле могут быть include-ы со своей структурой подпапок.Идем дальше, а дальше код обработки url. Схематично, если бы мы делали то приложение, которое я описал (на 3 стараницы) — здесь было бы:
app.get ('/', function (page, model) { … });
app.get ('/about', function (page, model) { … });
app.get ('/projects', function (page, model) { … }); Понятно да, то-есть на все, что мы назвали отдельной страницей здесь должен быть обработчик (кто-то скажет контроллер). Да, еще зесь стоит упомянуть то, что все дело очень похоже на expressjs, здесь можно использовать параметры, например: app.get ('/users/: user', getUser); Подробней смотрите здесьЧто делаем внутри обработчика? Вообще по аналогии с expressjs мы должны подготовить данные для рендеринга страницы, а потом вызвать функцию render с соответствующим шаблоном и данными в качестве параметров. Схема здесь та же, но есть и свои особенности. Сначала разберемся с данными. Для работы с ними используется model (передается в обработчик параметром). Дерби — реактивный фреймворк, поэтому у нас при работе с данными всегда есть два варинта: либо мы просто получаем данные, такими какие они есть да данный момент и отдаем их дальше шаблонизатору, либо же мы их получаем, но при этом еще и подписываемя на их обнолвение… Что это значит. Преставим себе страницу с каким-нибудь списком: если мы просто получили и вывели данные, то все — страница будет статично, если же мы еще и подписались на обновления, то список на нашей странице будет обновляться (если, например, другой пользователь, имеющий доступ к данному списку, поменял его). В коде это выглядет примерно так:
// подтягиваем данные без подписки на обновление model.fetch ('list', function (){ // Здесь данные уже подтянуты // Можно вызывать render });
// подтягиваем данные c подпиской на обновление model.subscribe ('list', function (){ // Здесь данные уже подтянуты, // подписка на будущие обновления зарегистрирована // Можно вызывать render }); Итак, код из нашего примера: model.subscribe ('hello.message', function () { page.render (); }); Мы подписываемя на 'hello.message', и в функции обратного вызова (когда данные подтянуты) вызываем render. Здесь возникает несоколько вопросов: что такое 'hello.message' и почему render без параметров (нет ни имени шаблона, ни данных)? В дерби за работу с реактивными данными отвечает модуль racer, в котором все обращения к данным происходят с использованием так называемых «путей». У нас 'hello.message' — это путь, где 'hello' — это имя коллекции в mongo (в sql была бы таблица), 'message' — это id записи в коллекции. То-есть мы здесь подписались на одну единственную запись в коллекции hello. Именно эти данные и будут попадать в наш input в шаблоне. Подробнее о работе с данными и путях смотрите тут.
Далее, в render мы ничего не передали по следующим причинам: html для url=/ у нас задан в главном html-файле (index.html — корневой, могло бы быть еще несколько, подключенных к нему через import), он будет исползован по умолчанию, а данные мы не передали, потому что в шаблонах есть доступ к «путям».
Теперь файл с шаблонами — index.html:
{{hello.message}}
Итак, сам файл состит из нескольких секций (у нас только одна — Body), в терминологии дерби, они и называются шаблонами. То-есть так: у нас есть файл шалонов, в котором есть шаблон Body, как выпонимаете, все что мы в него положили, окажется в body результирующего html. В рамках данного примера, я не буду вам объяснять всю систему шаблонизации дерби, включая пространства имен, наследование и т.д. (пример неподходящий), добавлю лишь несколько моментов. В двойных фигурных скобках — привязки к данным (те самые наши пути). Для задания title страницы, например, мы могли бы поспользоваться еще одним предопределенным шаблоном Title, например, так:
{{hello.message}}
Внутри шаблонизатор очень похож на handlebars. Приведу несколько примеров для наглядности (не стоит в них вдумываться особо):
Hello, friend!
{{/}}
{{each _page.todos as #todo, #index}}
{{#todo.text}}
{{if #index % 5 === 0}} // 0, 5, 10, 15 Важно> {{/}} {{/}} Ну и пример привязки к событиям: Ok Здесь мы забежали немножко вперед, но думаю это оправдано. На следующей неделе, сделаем «список дел» (TODO-list), там все это будет использовано и объяснено.А пока поиграйтесь с кодом, попробуйте добавить, что-нибудь в шаблон, поменяйте title (обратите внимание дерби поддерживает livereloading, то есть как только вы измените html-файл и запишите изменение — браузер сам обновит страницу). Попробуйте подключить стили.P.S. Если вам захочется почитать стандартную документацию по дерби, имейте ввиду, что она написана под версию 0.5, некоторые части там неактуальны.Что там читать можно: контроллеры, модели. Что не стоит: представления, шаблоны, компоненты — все уже поменялось, вы только запутаетесь.
P.P. S.В комментариях, пожалуйста напишите, о чем бы вы хотели услышать по derby. Что наиболее интересно?
