Pull to refresh

Изучаем Derby 0.6, пример #1

Reading time 11 min
Views 16K
image
Последние несколько месяцев я участвую в нескольких проектах, разрабатываемых на 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:
  1. базовые знания по веб-разработке (html, css, javascript);
  2. nodejs — нужно понимать commonjs-модули, npm, знать, как запустить стандартный http-сервер;
  3. expressjs — приложения derby строятся поверх приложений express, поэтому хорошо бы иметь об экспресс и серверном js какие-то базовые знания (подключение модулей, обработка запроссов и т.д.)

Есть так же определенный набор технологий, которые используются в derby и знакомство с ними здорово бы помогло (хотя и не является обязательным, мы постепенно будем всего этого касаться)
  1. реактивное программирование
  2. изоморфные javascript приложения
  3. операциональное преобразование — технология, разработанная google, для разрешения конфликтов при одновременном редактировании документов в google docs
  4. browserify
  5. mongodb

Платформа


Нам подойдет linux, mac. C windows могут быть проблемы — версия 0.5 работала нормально и под windows (правда приходилось самому компилить redis версии 2.6), версия 0.6 тоже к этому идет, но есть парочка pull-request-ов, которые еще не попали в основной репозиторий, поэтому начинающим пока стоит подождать.

Перед началом разработки нужно убедиться, что на компьютере установлены:
  1. nodejs
  2. mongodb
  3. redis 2.6 (обратите внимание, версия 2.4 не подходит) — upd redis уже не обязателен

Все ставится с настройками по умолчанию. Монга и редис в момент работы приложения должны быть запущены.

Итак, начинаем


Разберем пример: 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

<Body:>
  <!-- в шаблоне прописывается html и привязки к данным -->
  Holler: <input value="{{hello.message}}">
  <h2>{{hello.message}}</h2>


Давайте сначала наберем их, запустим приложение, поиграемся с ним, а потом разберем исходный код. Итак, все файлы находятся в одной папке: 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:
  1. Дерби, предназначена для создания SPA (сложных, богатых элементами веб-приложений) приложений с сильной перерисовкой внутри страницы
  2. Для удобства пользователей, приложение должно позволять делать закладки на свое текущее состояние — а значит при важном изменении состояния, должен меняться url
  3. То же самое касается и инексации поисковиками. Приложение должно индексироваться, отсюда вытекает, что при проектировании приложения, создатель должен четко для себя решить, какие состояния приложения должны попадать в индекс и запланировать для них соответствующие url.

Итак, создатели поняли следующее, если страница запрашивается у сервера, нужно генерировать ее там и отдавать уже готовой, если же пользователь в браузере работает в приложении он не должен при смене url-каждый раз обращаться к серверу, ждать, получать мелькание экрана. Так же очень не хотелось бы дублировать код рендеринга страницы. То есть нам получается нужно иметь возможность отрисовывать страницу и на сервере и на клиенте, и не дублировать код. И на клиенте и на сервере у нас javascript — это облегчает дело. Но можно ли вообще так сделать. Получается нам нужно, чтобы и на клиенте и на сервере одинаково работал код, отвечающий за рендеринг страниц (шаблонизация, работа с html), маршрутизация (работа с url при смене адреса на клиенте), причем, получается, что и там и там нужно единообразно организовать доступ к данным. Такой код, работающий одинаково и на клиенте и на сервере, называется изоморфным, можете прочитать об этом тут. В дерби это реализовано.

Получается в дерби есть 2 части:
  1. Часть относящаяся только к серверу (всевозможные настройки, подключение модулей, привязка к expressjs). Обратите внимание здесь нет ни рендеринга, ни маршрутизации. В нашем случае это файл server.js c модулем derby-starter.
  2. Само изоморфное приложение. Его код исполняется на сервере, когда запрашивается какая-либо страница с сервера, а дальше, код работает уже на клиента. В нашем случае, это файлы — index.js и index.html.

Давайте я попытаюсь подробней рассказать о второй части. Представим, что мы создали дерби приложение, работающее с 3-мя страницами:
  1. / — домашняя страница сайта
  2. /about — страница с информацией о создателе
  3. /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:
<Body:>
  <!-- в шаблоне прописывается html и привязки к данным -->
  Holler: <input value="{{hello.message}}">
  <h2>{{hello.message}}</h2>

Итак, сам файл состоит из нескольких секций (у нас только одна — Body), в терминологии дерби, они и называются шаблонами. То есть так: у нас есть файл шаблонов, в котором есть шаблон Body, как вы понимаете, все что мы в него положили, окажется в body результирующего html. В рамках данного примера, я не буду вам объяснять всю систему шаблонизации дерби, включая пространства имен, наследование и т.д. (пример неподходящий), добавлю лишь несколько моментов. В двойных фигурных скобках — привязки к данным (те самые наши пути). Для задания title страницы, например, мы могли бы воспользоваться еще одним предопределенным шаблоном Title, например, так:
<Title:>
  Мое первое дерби-приложение
<Body:>
  <!-- в шаблоне прописывается html и привязки к данным -->
  Holler: <input value="{{hello.message}}">
  <h2>{{hello.message}}</h2>

Внутри шаблонизатор очень похож на handlebars. Приведу несколько примеров для наглядности (не стоит в них вдумываться особо):

<Body:>
  {{if _page.login}}
    <h3> Hello, friend! </h3>
  {{/}}  

  {{each _page.todos as #todo, #index}}
    <p>{{#todo.text}}</p>
    {{if #index % 5 === 0}}
        <!-- 0, 5, 10, 15 -->
        <b>Важно</b>
    {{/}}
  {{/}}

Ну и пример привязки к событиям:

  <a href="#" on-click="{{removeTopic(topic[#index])}}">Ok</a>

Здесь мы забежали немножко вперед, но думаю это оправдано. На следующей неделе, сделаем «список дел» (TODO-list), там все это будет использовано и объяснено.

А пока поиграйтесь с кодом, попробуйте добавить, что-нибудь в шаблон, поменяйте title (обратите внимание дерби поддерживает livereloading, то есть как только вы измените html-файл и запишите изменение — браузер сам обновит страницу). Попробуйте подключить стили.

P. S.
Если вам захочется почитать стандартную документацию по дерби, имейте ввиду, что она написана под версию 0.5, некоторые части там неактуальны.
Что там читать можно: контроллеры, модели. Что не стоит: представления, шаблоны, компоненты — все уже поменялось, вы только запутаетесь.

P. P. S.
В комментариях, пожалуйста, напишите, о чем бы вы хотели услышать по derby. Что наиболее интересно? Может быть есть какие-то вопросы? Если есть — задавайте их полюбому, важно заложить прочную основу.

update
Второй пример — http://habrahabr.ru/post/221703/
Третий пример — http://habrahabr.ru/post/222399/

Если нравится derbyjs — не сочтите за труд поставить звездочку на github
Only registered users can participate in poll. Log in, please.
Интересна ли тема, стоит ли продолжать серию?
91.63% Да 219
8.37% Нет 20
239 users voted. 53 users abstained.
Tags:
Hubs:
+42
Comments 42
Comments Comments 42

Articles