Skip to content

Instantly share code, notes, and snippets.

@crecotun
Last active May 30, 2016 22:55
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save crecotun/c7055175327aaa9a2dbe to your computer and use it in GitHub Desktop.
Save crecotun/c7055175327aaa9a2dbe to your computer and use it in GitHub Desktop.
Backbone.js: recommendations

Backbone.js: рекомендации

Оглавление

Модуль

Файловая структура

Я рекомендую следующую структуру для одного модуля

views/
  base.coffee

models/
  base.coffee

collections/
  items.coffee

router/
  router.coffee

templates/
  base.html

module_name.coffee
  1. module_name.coffee — основной файл вызова модуля, который возвращает функцию-конструктор
  2. views/base.coffee — основная вьюшка, внутри которой инициализируются подмодули
  3. 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

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-код куда-либо на страницу самостоятельно.

View

initialize

Самый минимальный набор для initialize метода.

initialize: (options) ->
  Cocktail.mixin( @, Mixins.Views.Base, Mixins.Common ) # add Mixins

  @setOptions(options)
  @initEventsListeners()
  @initStickitBindings()
  @addValidation()
  1. Подключили миксины
  2. Начали слушать события
  3. Привязали модель
  4. Добавили валидацию

Старайтесь не вызывать render при инициализации вьюшки, а только в месте инициализации модуля.

initEventsListeners / addEventListeners / addListeners

Метод для инициализации прослушивания событий. Раньше я использовал названия addEventListeners и addListeners, но initEventsListeners подходит больше.

initEventsListeners: ->
  @listenTo @, 'rendered', =>
    @cacheDOM()
    @attachDOMEvents()
    @stickit()

initStickitBindings / addStickit

Раньше использовал addStickit, рекомендую initStickitBindings. Внутри только объект @bindings

initStickitBindings: ->
  @bindings =
    '[name="title"]': 'title'

  @

render

Внутри метода 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()

  @

cacheDOM

Кэшируем jquery-объекты в переменные для быстрого доступа.
Я использую @$el в качестве пространсва имен (например, @$el.$list).

cacheDOM: ->
  @$el.$list = @$('.module--list')
  @$el.$button = @$('module--button')

  @

attachDOMEvents

Разделяем события приложения и события DOM.

attachDOMEvents: ->
  @$el.$button.on "click", (e) =>
    console.log e.target

Model / Collection

Основные методы работы с данными (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

Сейчас в common у меня только 1 метод setOptions. Нужен для того, чтобы все опции, который были переданы в объекте стали доступны через this
Инициализирую я его в самом начале модуля, в initialize.

Пример набора опций:

options = {
  model: new Backbone.Model(),
  parentView: parentView
}

После @setOptions(options) мы получим @model и @parentView.

Views

Views по идее должен делится на несколько подкатегорий, сейчас это только Base — обязательный набор любой вью.

subViews

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

addValidation

Метод, добавляющий валидацию во вьюшку. Для валидации я использую плагин Backbone.Validation

Метод принимает объект options, в котором можно передать:

  1. selector - для поиска элемента по заданному аттрибуту. По умолчанию это name ( $('name="model_field"') ).
    Например, selector: 'data-selector' будет искать так $('data-selector="model_field"').
    Поиск элемента расчитан максимум на 1 вложенность 'one.two' = name="one[two]"

  2. valid/invalid — переписывают поведение стандартных методов для поиска и вывода ошибок.

close

Метод, который отвечает за удаление подвьюшек в массиве subViews (для поддержки старого кода он обходит массив instances), удаление текущей вью и модели.

removeSubView

Добавляет возможность удалить конкретную вью из определенного массива.

showErrors / clearErrors

Методы для показа/чистки ошибок серверной валидации. showErrors работает также как backbone.validation. В аргументах принимает errors и options. options используется только для задания кастомного селектора как и в addValidation.

Улучшения и советы

  1. Я бы разобрался с рекурсивными зависимостями в require.js. Это позволит избавиться от захламления window модулями.
  2. Подстроить бекенд под бэкбон и пользоваться родными методами на полную катушку
  3. Улучшил архитектуру приложения с применением техник и паттернов из книги Эдди Османи
  4. Unit-тестирование
  5. Перечитал бы книгу внимательнее и до конца, многие вещи я придумывал сам или вычитывал откуда-то
  6. Не забывать про Backbone zombie views
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment