Backbone.js

Backbone это javascript-библиотека для тяжёлых frontend javascript приложений, как например gmail или twitter. В таких приложениях вся логика интерфейса ложится на браузер, что впрочем даёт очень значительное преимущество в скорости интерфейса. А как организовать код, для таких достаточно больших проектов? Который не просто набор глобальных переменных и jquery—DOM связей. Для меня перенос бэкэнда вперёд был довольно непривычен.

Backbone близок по духу еще одной библиотеке для богатого фронта - knockout, но как можно судить по названиям, они отличаются по замыслу. Нокаут занимается активной синхронизацией данных между DOM элементами через ModelView объекты с бэкэндом. "Позвоночник" же отвечает только за достаточную многослойность архитектуры, которую надо программисту унаследовать. 

Основные классы

В качестве ядра используются наследуемые "классы"

  • Router и History - принимают url и говорят какой view надо запустить
  • View - привязывается к dom-элементам и в зависимости от их вложённости отвечает за их данные. Именно View вызываются из Route что-бы открыть прошлое состояние. И именно View реагирует на пользователя через подписку на события ( поле events)
  • Collection - массив моделей (не обязательно одинаковых), привязывается к View и может оповещать его об изменениях
  • Model - основные классы сущностей, имеют url для получения и изменения данных по RESTful http с backend


Концептуальная диаграмма сущностей в типичном проекте

View, будучи конечным результатом и наиболее нагруженным классом, напрямую зависит от js-шаблонизатора из underscore библиотеки. Шаблонизатор ничем практически не отличается - только тем что все шаблоны подгружаются сразу в script-тэгах и переменные выделяются <%=foobar%> тэгами с мини-логикой. Начало выглядит примерно так..

MyApp.EditableSpanView = Backbone.View.extend({
template: _.template($('#span_wrapper').html()), 
initialize: function(){
this.render();
},
render:function(){
$(this.el).html(this.template(this.model.toJSON()));
}
});

Это объявление View с 1-1 зависимостью с DOM элементом #span_wrapper откуда мы возьмём html конструкцию и модель this.model с данными, которая ещё не задана. Документация backbone не блещет деталями, о том как надо код писать. При создании нового шаблона ему имеет смысл присвоить model или collection если он не общий. Как-то так:

realSpanView = new MyApp.EditableSpanView({model: ItemModel, id:'output' });

Это реализация View. Тут выполнится унаследованные конструктор initialize и рисование в render. Результат будет в элементе с id output — если он уже на странице имеется то в нем, если нет то появится.

События

Backbone имеет своё представление о том как должны распространятся события. Я когда-то писал о сложности этого в DOM. Тут же всё проще. События распространяются двумя путями

  1. От DOM-элемента через View.events в соответствующий написанный вами View-метод
  2. От модели или коллекции через подписанные на неё объекты выше (см. диаграмму выше), в т.ч. View

Насчёт первого пункта можно добавить что имеет смысл делать вложенные View-объекты (как группы DOM-деревьев). Например MyBodyView для глобально позиционируемых слоёв, MyListView с коллекцией и MyItemView для конкретного элемента.

По второму пункту - есть магический метод this.bind, который связывает два типа объектов, обычно в initialize. Например MyListView.initialize имеет смысл связать view с коллекцией, что-бы изменение коллекции вызывало изменение view. 

this.collection.bind('add', this.add, this);
this.collection.bind('remove', this.remove, this);

Кажется таким очевидным и простым, но этого нет нигде по умолчанию, вы сами должны определить что должно кого оповещать. И хуже всего то, что порой не понимаешь механизма работы платформы. Модели не должны ничего знать о своём отображении, управление идёт наверху - view должен привязать себя к уничтожению и всякому изменению модели

Столкнулся с проблемами

Например вам необходимо постоянно поддерживать свежесть коллекции. Коллекция загружается по url параметру из fetch и дальше вызывается reset где обычно коллекция наполняется опять моделями. 

Так вот для того что-бы сделать преобработку данных без перезаписи собственно reset метода, я вешаю bind на reset, вот только аргумент результатов - не json-список и не список переконвертированных моделей у которых распечатать свойства можно только через .get(), а непонятный формат в котором впрочем есть models параметр по которому можной пройтись.

И такого непривычного поведения достаточно что-бы выбить из колеи. То же ключевое слово this - при bind теряется и на самом деле используется связанный объект (коллекция), для этого есть третий параметр или же bindAll из underscore.

Вывод

Библиотека интересная, ограничивающая программистов привыкших к расхлябанному jquery-подходу подвешивания событий на DOM и принуждающая frontend писать едва ли не так же качественно как и backend. Поэтому использовать её стоит в ограниченных условиях, когда надо написать больше JS-кода чем обычно. Альтернативы Backbone — Spine.js, Sproutcore, Cappuccino, EJS. Из "почитать по теме" советую подкаст sitepoint 145bbtutorialsхабр, а для особо одарённых — про тестирование с Jasmine.

Якорная навигация

В последнее время участилось использование мной как подгрузки содержания ajax'ом в админке, так и использование спрятанных блоков, которые показываются в зависимости от нажатий пользователя на всякие табы и кнопки. Возникает проблема что с обновлением страницы открытое содержание пропадёт. Проблема наглядно решается при помощи якорей в gmail.

Например так можно открыть слайд сделанный на jQuery со спрятанным содержанием (например в FAQ)

    var URL = document.location.toString();
    if (URL.match('#')) {
        var anchor = '#' + URL.split('#')[1];
        if (anchor) $(anchor).parent().click();
    }

А для простых смертных без jQuery, так можно получить ID редактируемого элемента и передать на рекурсивное открытие дерева до нужного элемента

   var anchor;
    var URL = document.location.toString().split('#');
    if(URL.length > 1) {
        anchor= URL[1].split('/');
        var intNode=1*anchor[1];
        if (intNode>0) MenuTree.editDeepChild(intNode);
    }

Для записи в URL не обязательно иметь anchor-элемент, можно вполне javascript'ом менять якорь через window.location

Читайте также:

Редакторы кода с помощью javascript

Очень часто в web-проектах надо использовать визуальный редактор кода (richtext code editor), похожий на существующие IDE, с номерами строк и подсветкой кода. Наиболее часто он используется в редактировании исходного кода статьи или шаблонов в админке. В этой статье я перечислю существующие скрипты по аналогии со списком WYSIWYG-редакторов.

Практически все существующие визуальные редакторы создают iframe и генерируют внутри страницу в соответсвии с данными javascript-объекта, занимающимся всеми задачами по генерации кода и обработке клавиш.

Основанные на iframe:

  • EditArea — наиболее популярный редактор благодаря табуляции, gzip, совместимость пространства имён с другими библиотеками
  • CodePress — парсит SQL, Perl, C#, XSL, ASP, VBscript. Отдельные движки для разных браузеров (gecko=firefox, ie, opera). Создаётся iframe со внутренним CodePress'объектом с обращением через contentWindow. Сразу внутри скрипта идёт поиск textarea-элементов по классу, так что с динамическими ajax-редакторами прийдётся изменять скрипт. Кроме того нумерация (до 1500) строк сделана при помощи одной картинки
  • CodeMirror — парсит JS, HTML, CSS

Напомню что iframe не соответсвует XHTML спецификации, а с использовать предложенный тэг object с не сильно получится, из-за ограничения на доступ внутренних dom-элементов (поправьте если я неправ).

Основанные на div'ах

  • Simple CodeArea 0.1b - очень простой но помоему самый логичный подход использовать подсветку синтаксиса благодаря полупрозрачной textarea и div-элементу с форматированным текстом под ней. Единственная проблема - запаздывание при скролле, но в будующем я думаю это перспективный продукт
  • MDK-editor — относительно навороченный редактор, есть контекстное меню. В минусы можно отнести некрасивый скин, небольшую тормознутость, неизвестную лицензию на использование, перезапись window.onload, размер кода свыше 100 кб. Достаточно много классов и обычных функций, просто так не разобраться, тем более без документации.
  • Helene — вместо iframe используется фоновый div и динамически позиционируемая textarea поверх. Поскольку изменяется только один ряд, то невозможно выделить нескольких строк сразу
  • 9ne — похож на консоль, но нет возможности выделения всей строки Shift+End. Эмулирует каретку мигающим div'ом.

PS. Аннонсирую что в скором времени выйдет блог-движок моего производства…

Читайте также

jQuery для продолжающих (с плагинами)

jQuery - библиотека о которой в последнее время говорит практически каждый web-разработчик, верстальщик и дизайнер. Написанная с учётом CSS, она упрощает доступ к одному или нескольким DOM-элементам. Если вы ещё используете prototype, то можно использовать режим совместимости (правда не факт что у вас будут работать плагины). Стандартно доступ происходит благодаря функции $ или JQuery. Элементу можно добавить (.addClass) или отнять (.removeClass) CSS-класс. Если это input-элемент, то запись и чтение происходит в аттрибуты элемента (.attr) и напрямую к значению (.val) . Внутренние элементы можно задать как через (.html). Ajax очень просто вызывается функцией $.post (url, val, callback)

Кроме минимализма, ускоренности и CSS-селекторов библиотека мало чем по функциональности отличается от prototype, mootools. Она не расширяет родные JS-объекты, как это делает protype и существует в своём пространстве переменных, поэтому не конфликтует с другими библиотеками.

Порядок выполнения событий в DOM

Столкнулся с проблемой в своём календарике - есть два элемента, один из которых позиционируется абсолютно на весь экран, полупрозрачная затемняющая занавеска а второй - форма. Вы наверняка видели такие решения при показе картинок в lightbox или аутидентификации на habrahabr'е..

<div id='form_bg' onclick="$('form_bg').hide();$('form').hide();"> <div id='form'></div> </div>

Проблема в том, что при любом нажатии на внутренний элемент, событие автоматом вызывается на родительском элементе. Это заставило меня задуматься как над решением, так и над теорией как браузеры работают.