Derby.js Путь воина

  • Tutorial


Продолжаем нашу рубрику «ни дня без Дерби». Сегодня мы начнем (наконец-то!) писать код и рассмотрим базовые моменты 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, он указывает на базу данных. В данном случае у нас создается коллекция page и объект с ид 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

Материалы по Derby.js
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 29
  • 0
    Спасибо! Очень доступно изложено!
    • 0
      Рад, что понравилось :-)
    • –1
      Кто все туториалы по Derby минусует? Неужели проделки любителей Angular? Не хочется на них думать :-)
      • +2
        Это хабр, привыкайте.
        • +1
          Привыкаю…
          Еще раз спасибо за инвайт.
          • 0
            Рад, что кому-то успел отдать, пока не слили. =) Тем более derby.js действительно показался интересным на тот момент.
            • 0
              Тем более derby.js действительно показался интересным на тот момент.

              А на текущий момент? :)
              • 0
                На текущий момент наиболее насущные проблемы это архитектурная организация кода. Ужас как не люблю спагетти-код. Больше смотрю сейчас в сторону marionette.js. Но к дерби возможно и вернусь когда-то.
                • 0
                  Вот кстати хороший вопрос в тему…
                  Как грамотно в Derby структурировать код Избегая «спагетти» и соответствую SOLID?
                  • 0
                    «Спагетти» — это больше навыки программирования (в данном случае на js/node.js). Стркутура кода сильно зависит от приложения. Для социальной сети она будет одна, для игры другая. Ну а SOLID конечно соответствуйте, Derby вам тут, как минимум, не помешает.
                  • 0
                    Тут писал о marionette.js
        • 0
          Нет у меня такой команды derby bare

          D:\node\derby>derby --help
          
            Usage: derby [options] [command]
          
            Commands:
          
              new [dir] [app]
              Create a new Derby project. If no directory name is specified, or the
              name `.` is used, the project will be created in the current directory.
              A name for the default app may be specified optionally.
          
            Options:
          
              -h, --help       output usage information
              -V, --version    output the version number
              -c, --coffee     create files using CoffeeScript
              -n, --noinstall  do not run `npm install`
          
          
          D:\node\derby>derby -V
          0.5.9
          • +1
            Команда была добавлена месяц назад. Обновите Дерби, пожалуйста.
            • 0
              Спасибо, я вроде 0.5.10 видел мельком на github-е, но на сайте 0.5.9, — думал последняя стабильная…
              • 0
                К сожалению сайт и документация не всегда успевают за github.
          • 0
            Ээх! Засада. Статика — работает. Локальная «динамика» — работает. А многопользовательская — нет. Надо курить мануал — что-то не то на win7-64. Или переходить на linux :)
            Вот дамп:
            Trace: [Error: ERR unknown command 'evalsha']
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\node_modules\share\lib\server\session.js:485:19
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\node_modules\share\lib\server\useragent.js:185:23
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\node_modules\share\node_modules\livedb\lib\index.js:475:53
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\node_modules\share\node_modules\livedb\lib\index.js:696:53
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\node_modules\share\node_modules\livedb\lib\index.js:185:16
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\node_modules\share\node_modules\livedb\lib\index.js:144:16
                at Command.callback (C:\Dropbox\web\node\Derby\habr\node_modules\redis\index.js:1140:13)
                at RedisClient.return_error (C:\Dropbox\web\node\Derby\habr\node_modules\redis\index.js:531:25)
                at ReplyParser.<anonymous> (C:\Dropbox\web\node\Derby\habr\node_modules\redis\index.js:282:14)
                at ReplyParser.EventEmitter.emit (events.js:95:17)
            Operation was rejected (Error: ERR unknown command 'evalsha'). Trying to rollback change locally.
            [Error: ERR unknown command 'evalsha']
            
            C:\Dropbox\web\node\Derby\habr\node_modules\derby\lib\View.server.js:41
              return json.replace(/[&']/g, function(match) {
                          ^
            TypeError: Cannot call method 'replace' of undefined
                at stringifyData (C:\Dropbox\web\node\Derby\habr\node_modules\derby\lib\View.server.js:41:15)
                at Object.View._renderScripts (C:\Dropbox\web\node\Derby\habr\node_modules\derby\lib\View.server.js:176:7)
                at C:\Dropbox\web\node\Derby\habr\node_modules\derby\lib\View.server.js:156:10
                at null._onTimeout (C:\Dropbox\web\node\Derby\habr\node_modules\derby\node_modules\racer\lib\Model\bundle.js:14:5)
                at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
            
            

            Для начала буду ставить VirtualBox…
            • 0
              У меня все ОК на win 7- 64
              • 0
                У вас видимо Redis 2.4

                В live-db используются комманда evalsha (выполнение Lua скриптов). Ее добавили только в Redis 2.6
                • 0
                  Да, точно. Вот, ведь, невнимательный я :) Сходу нашёл только x86 установщик. Ссылочку не подскажете? Ставить VS и «билдить» что-то не хочется ;)
                  • 0
                    Я вот отсюда собирал: github.com/MSOpenTech/redis

                    И это было последней каплей для перехода на Linux :-) Рекомендую.
                    • 0
                      Да, там есть .exe в архивах. Попробую подменить. Спасибо!
                      • 0
                        Заработало. Щастье :)
                        • 0
                          А не могли бы Вы детальней рассказать, как собирать derbyjs под windows? Я вот просто не могу понять, как hiredis собрать правильней.
                  • 0
                    создается коллекция path = создается коллекция **page**
                  • 0
                    Спасибо за труды, очень интересно! Объясните пожалуйста, зачем делать render в subscribe? Если вынести, работает так же:
                    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();
                    });
                    
                    • 0
                      Нашёл ответ в следующей статье: habrahabr.ru/post/196088/#comment_6801840
                      • 0
                        Ну да, здесь общая логика такая, чтобы при первом запросе на сервере отдать уже полностью готовую страницу (чтобы поисковики нормально индексировали), нужно делать render после того, как все данные в модель уже загрузились.

                        subscribe регистрирует подписку на данные, но кроме того его callback срабатывает тогда, когда данные полностью загрузились в модель.

                        Если же вынести рендер, то пользователь все-равно увидит все то же самое, да и в случае, если этот код будет на клиенте — тоже разницы не будет. Вопрос именно в случае сервера, что отправит сервер клиенту и важно это лишь для индексации.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.