Я рекомендую следующую структуру для одного модуля
views/
base.coffee
models/
base.coffee
collections/
items.coffee
router/
router.coffee
templates/
base.html
module_name.coffee
module_name.coffee
— основной файл вызова модуля, который возвращает функцию-конструкторviews/base.coffee
— основная вьюшка, внутри которой инициализируются подмодулиmodels/base.coffee
— основная модель. Нужна, чтобы получить данные с сервера и передать подмодулям данные и сохранять данные всех подмодулей.
Файлы модуля находятся в одном пространстве имен.
Этот объект хранится в window
, так легче брать нужные части из любого места в приложении (например, компиляция шаблона window.ModuleName.Templates.Base()
).
window.ModuleName = ModuleName =
Views:
Base: BaseView
Models:
Base: BaseModel
Collections: {}
Templates:
Base: Handlebars.compile BaseTmpl
Я считаю это анти-паттерном, но другой вариант в requirejs я не пробовал.
App - функция-конструктор, запускающая модуль (инициализирует основную вью и данные).
Возвращает вьюшку/модель/коллекцию и метод close
.
class ModuleName.App
constructor: ->
@model = new ModuleName.Models.Base()
@view = new ModuleName.Views.Base
model: @model
return {
# data
model: @model
view: @view
# methods
close: @close
}
close: ->
@view.close?()
@view.remove?()
Настоятельно рекомендую инициализировать и добавлять модуль на страницу следующий способом.
module = new ModuleName.App()
$( module.view.render().el ).appendTo( @$el )
Метод render
внутри модуля не должен добавлять html-код куда-либо на страницу самостоятельно.
Самый минимальный набор для initialize
метода.
initialize: (options) ->
Cocktail.mixin( @, Mixins.Views.Base, Mixins.Common ) # add Mixins
@setOptions(options)
@initEventsListeners()
@initStickitBindings()
@addValidation()
- Подключили миксины
- Начали слушать события
- Привязали модель
- Добавили валидацию
Старайтесь не вызывать render
при инициализации вьюшки, а только в месте инициализации модуля.
Метод для инициализации прослушивания событий.
Раньше я использовал названия addEventListeners
и addListeners
, но initEventsListeners
подходит больше.
initEventsListeners: ->
@listenTo @, 'rendered', =>
@cacheDOM()
@attachDOMEvents()
@stickit()
Раньше использовал addStickit
, рекомендую initStickitBindings
.
Внутри только объект @bindings
initStickitBindings: ->
@bindings =
'[name="title"]': 'title'
@
Внутри метода render
происходит только рендер шаблона @$el
и вызов события о завершении.
Так вьюшка узнает, что ей можно связывать модель и кешировать DOM.
Хорошо
render: ->
@$el.html ModuleName.Templates.Base()
@trigger 'rendered'
@
Плохо
Метод вызывает побочные методы, что усложняет чтение кода.
render: ->
@$el.html ModuleName.Templates.Base(response.data)
@stickit()
@cacheDOM()
@
Плохо
Запросы к серверу нужно делать внутри модели или коллекции и потом вызывать рендеринг или перерендеринг.
render: ->
$.when( @getData() )
.then
(response) =>
@$el.html ModuleName.Templates.Base(response.data)
@stickit()
@cacheDOM()
@
Кэшируем jquery-объекты в переменные для быстрого доступа.
Я использую @$el
в качестве пространсва имен (например, @$el.$list
).
cacheDOM: ->
@$el.$list = @$('.module--list')
@$el.$button = @$('module--button')
@
Разделяем события приложения и события DOM.
attachDOMEvents: ->
@$el.$button.on "click", (e) =>
console.log e.target
Основные методы работы с данными (fetch
, save
и тд.) я переписывал. С самого начала бекенд не был заточен под архитектуру бэкбона, но сейчас ситуация стала лучше и я бы рекомендовал использовать подход бэкбона.
Вместо стандартного метода fetch
я использовал свой метод getData
. Cейчас я считаю, что лучше переписывать поведение fetch
, так в любой момент можно будет отказаться от своей реализации в пользу нативной без замены названия.
Часто, внутри большого модуля есть подмодули. Каждый подмодуль независим от родителя и имеет свои методы получения данных с сервера. Чтобы избежать большого количества запросов, я рекомендую делать 1 запрос у модуля родителя и передавать данные в подмодули.
Иницализация и передача данных
module = new Module.App({
data: @model.toJSON()
})
Потом эти данные передаем в модель и уже там вызываем fetch
с ними.
# переписанный метод fetch, который вызывается при инициализации модели/коллекции
fetch: (data) ->
if data?
# записываем данные в модель или коллекцию
else
# делаем аякс запрос или вызываем стандартный fetch
От модуля к модулю используются одни и те же функции: удалить вьюшку с подвьюшками, сделать опции "глобальными", инициализировать валидацию и тд. Чтобы все это не копировать от файла к файлу, нужно использовать миксины. Миксин — класс/объект с набором методов, которые добавляются в модуль для расширения его возможостей.
Для работы с миксинами в Backbone я использую плагин Cocktail. В использовании он до безобразия прост:
Cocktail.mixin(@, Mixins.Common, Mixins.Views.Base)
Я разбил файл на несколько частей: Common
, Model
, View
. Нет смысла подключать в View
методы для Model
и наоборот. Общий набор методов я вынес в Common
.
Сейчас в common
у меня только 1 метод setOptions
. Нужен для того, чтобы все опции, который были переданы в объекте стали доступны через this
Инициализирую я его в самом начале модуля, в initialize
.
Пример набора опций:
options = {
model: new Backbone.Model(),
parentView: parentView
}
После @setOptions(options)
мы получим @model
и @parentView
.
Views
по идее должен делится на несколько подкатегорий, сейчас это только Base
— обязательный набор любой вью.
Массив в который добавляем все подвью, нужен для того, чтобы в будущем легко убрать все сущности.
Метод, добавляющий валидацию во вьюшку. Для валидации я использую плагин Backbone.Validation
Метод принимает объект options
, в котором можно передать:
-
selector
- для поиска элемента по заданному аттрибуту. По умолчанию этоname
($('name="model_field"')
).
Например,selector: 'data-selector'
будет искать так$('data-selector="model_field"')
.
Поиск элемента расчитан максимум на 1 вложенность'one.two'
=name="one[two]"
-
valid
/invalid
— переписывают поведение стандартных методов для поиска и вывода ошибок.
Метод, который отвечает за удаление подвьюшек в массиве subViews
(для поддержки старого кода он обходит массив instances
), удаление текущей вью и модели.
Добавляет возможность удалить конкретную вью из определенного массива.
Методы для показа/чистки ошибок серверной валидации.
showErrors
работает также как backbone.validation. В аргументах принимает errors
и options
. options
используется только для задания кастомного селектора как и в addValidation
.
- Я бы разобрался с рекурсивными зависимостями в require.js. Это позволит избавиться от захламления
window
модулями. - Подстроить бекенд под бэкбон и пользоваться родными методами на полную катушку
- Улучшил архитектуру приложения с применением техник и паттернов из книги Эдди Османи
- Unit-тестирование
- Перечитал бы книгу внимательнее и до конца, многие вещи я придумывал сам или вычитывал откуда-то
- Не забывать про Backbone zombie views