Pull to refresh

Sqimitive.js — Frontend Primitive или «Backbone без фантиков»

Reading time 31 min
Views 19K
Уже довольно давно большинство сайтов перестало быть набором HTML/PHP/CSS/JS-файлов, которые достаточно просто загрузить на сервер. Bower, Grunt, Component.js, AMD, Require.js, CoffeeScript, Clojure, Composer, npm, LESS и ещё 100500 инструментов — всё это сегодня применяется для сборки проектов, обновления компонентов, загрузки зависимостей, сжатия кода, компиляции из одного JavaScript в другой, подтасовки карт, прополки огорода и даже готовки яичницы.

Многих людей это вдохновляет. Да что там — 95% моих знакомых в один голос твердят, как подключив всего пару-тройку библиотек с особой, уличной магией можно забабахать сайт на over-9000 зелёных австралийских долларов — и всего за один вечер, с перерывом на кофе и бублики.

А я — странный человек. Не люблю смешения языков, технологий, библиотек. Angular, Knockout, React — они все хороши, но каждая — по-своему сложна. А ведь есть и «гибриды», где сходится сразу несколько миров — как Ember и Knockout.Bootstrap. Вдобавок, многие построены на jQuery — впрочем, к ней даже у меня претензий нет; наверное, таким и должен был быть JavaScript.

Как бы то ни было, реальность беззастенчиво входит в контакт с мечтами и расставляет точки над «i». Мне так же приходится писать на «new & popular» — а когда пишешь, душа томится и просится создать очередной велосипед… а ей разве откажешь? Она ведь как дитя малое.

Велосипед был создан. Велосипед без фантиков. Такой же простой, как автомат Калашникова, и многогранный, как швейцарский нож, где вместо наследования — события, вместо моделей, коллекций и представлений — один класс, с неограниченной вложенностью и полной свободой действий, почти в два раза меньший Backbone.js, использующий Underscore.js и, необязательно, jQuery/Zepto.

Добро пожаловать в Sqimitive.

Как всё начиналось


Я — фрилансер. «Фри» в данном случае обозначает прямо противоположное, поэтому по долгу службы я работаю над многими проектами, со многими технологиями и иногда подолгу. Последние два года было много работы именно с Backbone. В течении этого времени у меня накопился вагон и маленькая тележка наблюдений и замечаний касательно этой, в общем-то, хорошей библиотеки. В итоге они вылились в нечто новое. То, что я назвал «Sqimitive».

Почти весь 2014 год мне повезло проработать в нью-йоркской фирме «Belstone Capital». Это превосходное место для серьёзной деятельности. Идеал души фрилансера (хотя эта же душа не даёт подолгу работать в одном, даже самом идеальном, месте). Sqimitive была создана именно там.

Сердечное спасибо коллегам из Belstone, которые на мою просьбу выложить часть внутреннего кода в открытый доступ ответили: «Go ahead».

Сейчас Sqimitive около 9 месяцев от роду. Она лежит в основе двух проектах этой компании, в сумме на 15-20 тысяч строк (это забавно, учитывая её собственный размер в 700 строк без комментариев). API последние месяцы не менялся, серьёзных ошибок замечено не было уже совсем давно. Код готов к использованию в производственной среде. Его можно найти на GitHub, полную документацию на 55 страниц — на squizzle.me и там же можно посмотреть пример простой To-Do App.

Ниже в этой статье я опишу 90% возможностей библиотеки, с уймой примеров, теории и лирики. А начнём мы с фундаментальных ям JavaScript и Backbone. Кстати, если вы знаете Backbone — Sqimitive вам покажется очень знакомым, тёплым и почти ламповым.

(Здесь и далее — моя субъективная точка зрения, которая может не совпадать с вашей, Хабра, президента или Космической коалиции. Читайте на свой страх и риск, не забывайте комментировать и помните о своей карме!)

Оглавление:

  1. Наследование как фактор выживания
  2. Превращение методов в события
  3. Наследование на лету или прототипирование-2.0
  4. О важных связях с общественностью
  5. Вложенность — наше всё
  6. UnderGoodness
  7. Что в опциях тебе моём?
  8. Нерадивые родители: деревья и списки
  9. Представляем виды
  10. О бренности жизни без сохранения данных
  11. assignResp и _respToOpt — карта ответа API
  12. И это что, всё? _shareProps, _mergeProps, masker()


Наследование как фактор выживания


Если бы JavaScript был Haskel, то у него бы не было этой проблемы (отчасти потому, что на нем бы никто не писал). Если бы JavaScript был Си, то у него были бы другие проблемы — может, много проблем, но точно не таких.

Но JavaScript — это и не Haskel, и не Си. У него было тяжёлое детство с непостоянными родителями, которые навсегда повредили его это this. Так что теперь это непостоянство расхлёбывают программисты.

Функции в JavaScript — это обычные значения вроде чисел и строк (т.н. first class citizen). Функции можно присваивать, удалять, копировать, даже преобразовывать в строку. Плюс к этому функция имеет неявный параметр — this — который по задумке авторов языка должен указывать на объект, который вызвал срабатывание этой функции — а никак не на объект, к которому эта функция была привязана.

Таким образом, объекты в JavaScript как бы есть, но получить контекст объекта — невозможно. Объекты в этом смысле — это те же ассоциативные массивы. Видимо, в этом суть концепции Java (ООП) + Script (ФП). Шутка.

(Прошу не принимать мой сарказм близко к сердцу людям с плохим чувством юмора. Я люблю и JavaScript, и Haskel, но я точно так же осведомлён об их… особенностях и стараюсь их чётко обозначить, чтобы найти им хорошее решение. А вообще, пора бы уже Ruby захватить мир.)

Классический пример (JSFiddle):

function Obj() {
  this.property = 'Hello!'

  this.show = function () {
    alert(this.property)
  }
}

var obj = new Obj
setTimeout(obj.show, 100)
  // alert('undefined')

Причина — в том, что мы считали значение-функцию, которое записано в свойстве show объекта Obj, и передали её в setTimeout, которая просто её вызвала — в отрыве от Obj, в контексте window. Здесь obj для нас — всё равно, что безликий массив.

Впрочем, проблема с непостоянным this худо-бедно, но решается — и даже товарищи из ECMAScript в конце концов сдались и спустя каких-то 16 лет (в 2011 вместе с 5.1) к Function был добавлен bind(), фиксирующий this в одном положении.

Другая особенность JavaScript — отсутствие в языке ссылки на базовый класс и вообще понятия «базового класса». JavaScript использует прототипное наследование, что в общем означает следующее: каждая функция (она же конструктор, который вы вызываете как new Func) имеет так называемый «прототип». При new Func происходит копирование полей этого прототипа в новый экземпляр объекта. «Наследование» в понятиях традиционного ООП — отсутствует, вместо этого прототип копируется в другой прототип, то есть все его поля копируются в другой объект: переменные и методы-функции — которые, как уже сказано, обычные значения, которыми можно манипулировать. Затем на новом прототипе делаются все изменения, которые предписывает «наследование» (перекрываются методы, добавляются поля и т.п.).

Фактически же мы получаем два независимых класса-прототипа.

Эта техника призвана бороться с некоторыми недостатками классического ООП — хрупким базовым классом, коллизиях при множественном наследовании и другими нюансами. Важную проблему в JavaScript решали, в целом, правильными методами, но плавающий this, функции-значения (невозможность определить своё имя в рамках объекта без прямого перебора всех полей) и отсутствие простых штатных связей между прототипами в сумме вызывают кровь и ярость.

В традиционном ООП наследование — это когда один объект копирует другой (возможно, с изменениями), но между ними сохраняется связь родитель-потомок. Теперь посмотрим на ООП в JavaScript (JSFiddle):

function Base() {
  // Пустой конструктор.
}

Base.prototype.property = 'Hello!'

Base.prototype.show = function () {
  alert(this.property)
}

function Child() {
  // Пустой конструктор.
}

// Копируем прототип базового "класса".
for (var prop in Base.prototype) {
  Child.prototype[prop] = Base.prototype[prop]
}

// Так мы можем не указывать базовый класс явно при перекрытых вызовах (см. ниже).
Child.__super__ = Base.prototype

Child.prototype.show = function () {
  // Вызвать унаследованный код?
  Child.__super__.show.call(this)
}

Как видно, здесь функция show, которая привязана к Child, не знает ни своего имени (она может быть привязана под разными именами, много раз, к разным прототипам), ни имени базового класса, ни даже this, если мы сделаем что-то вроде setTimeout((new Child).show, 100).

Нам остаётся только вшить (hardcode) эти значения в код самой функции. Понятно, что это — плохой путь:

  • Меняется имя класса — нужно изменить все ссылки на него
  • Меняется имя функции — нужно также изменить все ссылки
  • Копируется функция — нужно менять имя (как часто это забывается)
  • Копируется класс — ну, вы поняли

Это не говоря о том, что писать Foo.__super__.bar.apply(this, arguments) — как минимум утомительно и неэстетично. А отладка забытых непереименованных ссылок может сравниться разве что с изучением чёрной магии…

Тем не менее, именно такой принцип используется в Backbone (в Angular, Knockout, React вы фактически пишите на «полу-ООП», где явно указываете предков при вызове, что не многим лучше). Хорошее решение — у Ember, с его автоматическим this._super:

Child.reopen({
  show: function (msg) {
    this._super(msg + '123')
  },
})

Но Ember — это 48 000 строк чистого JavaScript. Неужели нельзя проще?..

Можно. Sqimitive решает эту проблему так (JSFiddle):

var Base = Sqimitive.Sqimitive.extend({
  property: 'Hello',

  show: function (right) {
    alert(this.property + right)
  },
})

var Child = Base.extend({
  property: 'Bye',

  events: {
    '=show': function (sup, right) {
      sup(this, [' World' + right])
    },
  },
})

;(new Base).show('123')
  // alert('Hello123')

;(new Child).show('123')
  // alert('Bye World123')

Кроме того, вы можете использовать стандартный вариант с __super__ — оставлен для любителей стрелять себе по ногам и для поддержки legacy-кода (JSFiddle):

var Base = Sqimitive.Sqimitive.extend({
  // Как выше.
})

var Child = Base.extend({
  property: 'Bye',

  show: function (right) {
    Child.__super__.show.call(this, ' World' + right)
  },
})


Превращение методов в события


Блок events в Sqimitive определяет новые обработчики, которые вызываются для событий объектов этого класса. Когда имя события совпадает с именем уже существующего метода — этот метод (в примере — show) заменяется на firer('show') — функцию, которая при вызове инициирует одноимённое событие. Заменённый метод (например, унаследованный) ставится в начало цепочки обработки, а заменяемый — после него. Таким образом, сохраняется логика выполнения. Нет нужды изменять что-либо при изменении структуры базового или наследующего класса.

Если же метода не было — новый метод становится единственным обработчиком. Если же под данным именем значится не метод, то это свойство перезаписано не будет и событие можно будет вызвать только явно, через fire().

Таким образом, любой метод класса — возможная точка привязки события, а сами события можно возбуждать как явно через fire('event'), так и вызывая метод на самом объекте — если это событие, то оно будет инициировано благодаря firer(), а если нет — функция будет вызвана напрямую (фактически это единственный обработчик события). Трансформация метода в событие делается прозрачно и на лету.

При этом стоит отметить, что для пуристов, которые борются за наносекунды — всё чисто. Если вам важна производительность конкретного метода или класса — просто определите его как обычно, без события (см. пример выше с __super__) — тогда он будет вызываться напрямую, минуя fire(). Причём сделать это можно и в базовом классе, и в потомках, и уже имея перекрытые методы-события. Нужно только следить, чтобы в последствии никто не создал из этого метода событие, иначе наносекунды потекут не в ту сторону.

Как показал мой опыт, полная замена метода, как в примере выше — штука довольно редкая. В Sqimitive есть ещё три типа добавления обработчика, которые различаются префиксом (знак равно выше — один из них):

  • Без префикса — самый часто используемый тип. Добавляет обработчик после существующих и игнорирует результат вызова.
  • Минус (-) — добавляет обработчик перед существующими и игнорирует результат.
  • Плюс (+) — как без префикса, только передаёт текущий результат в первом параметре и ожидает получить новый результат от функции (если она вернёт undefined — сохраняется прежний; именно это и происходит, если функция вернулась без return).
  • Равно (=) — уже показанный вариант, когда обработчик родителя перекрывается целиком и у новой функции есть выбор — вызывать его или нет, с какими аргументами, в каком контексте и что делать с результатом. Оригинальная функция передаётся в виде первого параметра, для краткости вызываемая как sup(context, argArray).

Во всех случаях параметры события передаются каждому обработчику.
Первый тип покрывает 50% причины для перекрытия методов, второй и третий — ещё 40%.

var Child = Base.extend({
  events: {
    show: function (msg) {
      // Действие совершилось - нужно обновить что-либо, почистить кэш,
      // разослать оповещания или что-то ещё.
      this.render()
    },

    '-show': function (msg) {
      // Сделать что-то до того, как произойдёт действие - выполнить проверку
      // и выбросить исключение, сохранить старое значение и прочее.
      if (msg.length < 3) {
        throw 'Сообщение для show() должно иметь хотя бы 3 символа.'
      }
    },

    '+show': function (res) {
      // Проверить результат, возможно сохранить или изменить его и вернуть новый.
      return '(' + res + ')'
    },

    '=show': function (sup, msg) {
      // Новая логика, которая требует целиком новой функции. В дикой природе
      // встречается редко.
      return '(' + sup(this, [msg + ' foo!']) + ')'
    },
  },
})

Кроме того, обработчики могут быть строками — если нужно просто вызвать метод с этим именем с оригинальными параметрами. Это сильно сокращает код и делает его понятнее:

var Child = Base.extend({
  events: {
    // Вызывает render() с аргументами, переданными show. Результат отбрасывает.
    show: 'render',
  },
})


Наследование на лету или прототипирование-2.0


Итак, у нас есть наследование через события… А обязательно ли его проводить во время объявлении класса через extend?

Как понятно из названия — нет. События — они динамические, их кашей не корми, дай только возбудиться. Да, главное, побольше обработчиков!

var Base = Sqimitive.Sqimitive.extend({
  property: 'Hello',

  show: function (right) {
    alert(this.property + right)
  },
})

var base = new Base
base.on('=show', function (sup) {
  sup(this, [' - I alert'])
})

Результат (JSFiddle) аналогичен тому, как если бы мы унаследовали от Base новый класс и перекрыли там метод. Здесь же мы сделали это на «живом» объекте, единственном в своём роде. В добавок — как сделали, точно так же можем и убрать (JSFIddle):

base.off('show')

Но будьте осторожны: это уберёт все обработчики события show, кроме «припаянных» — fused (наследованные, к примеру, одни из таких). Если мы хотим убрать именно наш — используем его идентификатор (JSFiddle):

var handlerID = base.on('=show', function (sup) {
  sup(this, [' - I alert'])
})

base.off(handlerID)

А что произойдёт с методом, который мы перекрыли — Base.show? Как видно в JSFiddle, он восстановится, как только его =show-обработчик будет снят. Всё, как у людей.

Естественно, другие префиксы можно использовать точно так же, как они используются в блоке events.

Кроме on и off в нашем распоряжении есть и once — полностью аналогичен on, но отменяет обработчик после того, как он был вызван ровно один раз.


О важных связях с общественностью


До поры до времени объектов мало, приложение простое, памяти много и вообще полный мир и идиллия. Но так бывает не всегда.

Для приложений средней руки классов становятся десятки и сотни, а объектов за тысячи. Они постоянно заменяют друг друга и борются за место под солнцем в тесной песочнице DOM. В такой ситуации оставлять их все висеть в фоне — не гуманно. И в то же время не понятно, как управлять их связями — когда именно объект создаётся и «подключается» к матрице, а когда — удаляется, и как отключить его обработчики при переходе из бренного мира к праотцам?

В Backbone появились методы listenTo и stopListening (изначально их не было), которые позволяют запоминать связанные объекты и избавляться от связей с ними. Однако сам Backbone не содержит логики вкладывания этих объектов. Модели в коллекциях не считаем — основная проблема именно в постоянной циркуляции представлений (или видов, View).

В Sqimitive есть и аналог listenTo, и вложенность объектов. О последней подробно поговорим дальше в статье, а пока простой пример:

var Bindable = Sqimitive.Sqimitive.extend({
  // opt (option) в терминах Sqimitive аналогичен attribute в Backbone: он точно так же
  // возбуждает событие при изменении значения и имеет пару-тройку других особенностей.
  _opt: {
    // Этот флаг будет нам говорить, были ли инициализированы обработчики или нет.
    wasBound: false,
  },

  events: {
    // postInit вызывается после того, как объект был создан. Можно заменить
    // на owned - после того, как объект был вложен в другой.
    postInit: 'bindAll',
    // unnest вызывается для удаления объекта из списка родителя.
    '-unnest': 'unbindAll',
  },

  bindAll: function () {
    // ifSet возвращает true, если новое значение опции было отличным от старого.
    this.ifSet('wasBound', true) && this.bind(this)
    // sink вызывает указанный метод на всех вложенных объектах, рекурсивно.
    return this.sink('bindALl')
  },

  unbindAll: function () {
    if (this._parent && this.ifSet('wasBound', false)) {
      this.unbind(this)
    }
    return this.sink('unbindAll')
  },

  // Здесь наследованные классы уже указывают свою логику - регистрируют
  // обработчики, связываются с другими объектами и прочее. Гарантированно
  // вызывается один раз, если не был вызван unbind.
  bind: function () { },

  // Отменяет действия bind - удаляет обработчики. Вызывается только один раз,
  // если не был вызван bind.
  unbind: function (self) {
    // autoOff без параметров - аналог stopListening. Удаляет обработчики с
    // объектов, которые были зарегистрированы через autoOff('event') - см. ниже.
    this.autoOff()
  },
})

Теперь мы можем наследовать Bindable, наполнив его своей логикой. В большинстве случаев выглядит это так:

var MyObject = Bindable.extend({
  _opt: {
    someObject: null,   // некий объект Sqimitive, который мы "слушаем".
  },

  events: {
    bind: function () {
      this.autoOff(this.get('someObject'), {
        event1: ...,
        event2: ...,
      })
    },
  },
})

new MyObject({someObject: new X})

Здесь MyObject создаётся с опцией (параметром) someObject, к которому затем добавляются обработчики двух событий: event1 и event2. Делается это через autoOff, который аналогичен on, но добавляет данный объект в список зависимостей и затем, когда вызывается unbind, autoOff() без параметров удаляет все обработчики своего объекта (MyObject) со всех объектов, для которых он ранее был вызван (someObject).

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

Третий параметр к autoOff — необязательный контекст, который изначально устанавливается в зависимый объект (а не тот, к которому добавляется обработчик). В связке с именами методов-обработчиков вместо замыканий это даёт довольно компактный синтаксис:

this.autoOff(someObject, {
  // Вызвать render() на this при событии change в someObject.
  change: 'render',
  nest: 'render',
})

// Аналогично следующему:
someObject.on('change', function () { this.render.apply(this, arguments) }, this)
someObject.on('nest',   function () { this.nest.apply(this, arguments) },   this)

У этих методов есть и другие особенности — подробности см. в документации.


Вложенность — наше всё


В Backbone, на мой взгляд, очень мало внимания (читай — никакого) уделено вкладыванию объектов друг в друга. А ведь это крайне важная их особенность. Проекты наподобие Marionette.js пытаются компенсировать этот недостаток, но это как раз тот случай, когда библиотека зиждется на библиотеке, всё это как-то собирается и даже работает, но потребляет столько космической энергии, что лучше бы все сидели по домам. А в случае ошибки — не понятно, кого ругать — авторов Backbone за отсутствие штатных средств, авторов Marionette за их логику, себя — за несовместимое с ними мировоззрение, или JavaScript — просто потому, что он «не такой, как все».

Кроме того, Marionette — это ещё 4 000 строк кода в добавок к существующим зависимостям. А ведь каждая строчка — потенциальная ошибка, каждый метод — новая статья в документации (Marionette, впрочем, таковой просто не имеет).

В Sqimitive концепция родитель-потомок заложена на втором уровне. Сама библиотека разбита на две части в виде двух классов: Sqimitive.Core и Sqimitive.Sqimitive. Core — событийное ядро, всё то, что я уже описал выше. Sqimitive — его наследник, добавляющий опции и вложенность.

Именно Sqimitive даёт тот конечный функционал, который нужен в приложениях. Core можно наследовать, если вы хотите внедрить в свой класс только событийный (и наследующий) механизм.

В библиотеке Sqimitive нет разделения на модель, коллекцию и представление (M-C-V). Единый класс обладает как атрибутами (присущи моделям в Backbone) — их зовут «опциями», так как они передаются в конструктор, а также может содержать вложенные объекты определённого класса, над которыми можно проводить фильтрацию (доступен весь набор методов Underscore.js), автоматически перенаправлять их события родителю и вообще трактовать как некую совокупность, над которой можно работать как с чем-то единым, безликим, а не с каждым объектом в отдельности.

Для индивидуальной работы как раз подходит _opt, где каждый элемент — нечто особенное, и каждое движение (доступ, замена) можно отследить, перекрыв ifSet и get, добавив normalize_OPT, реагируя на change_OPT и change — об этом будет ниже.

В противоположность этой дзенской простоте Marionette, Ember и другие — сложны. В Ember есть разные типы свойств (computer, observers, bindings), в Marionette — разные типы представлений (раскладки, регионы, представления элементов и коллекций, составные). Конечно, это всё полезно — для определённого уровня приложений и команд. Но для многих других это всё равно что стрельба из пушки по воробьям. Дыма и шума много, публика довольна, но само действо не эффективно и трудозатратно. К тому же нужно для начала изучить, как летают пушки воробьи и ядра.

Интересно и то, что даже наличие таких готовых средств не гарантирует, что вам не будет проще написать какую-то их часть заново конкретно под вашу задачу.

Ниже — пример объявления вкладываемых классов в Sqimitive:

var MyItem = Sqimitive.Sqimitive.extend({
  // Какой-то набор атрибутов данной модели.
  _opt: {
    complete: false,
    something: 'bar',
  },
})

var MyCollection = Sqimitive.Sqimitive.extend({
  _childClass: MyItem,
  _childEvents: ['change', 'foobar'],

  _opt: {
    // Сделаем так, чтобы коллекция не допускала объекты с !complete.
    allowIncomplete: false,
  },

  events: {
    // Опция изменилась - перепроверим всё, что вложено.
    change_allowIncomplete: function (newValue) {
      newValue || this.each(this._checkComplete, this)
    },

    // Вложенный объект изменился - перепроверим его.
    '.change': '_checkComplete',

    // Добавили новый объект - проверим, что с ним.
    '+nest': '_checkComplete',
  },

  _checkComplete: function (sqim) {
    if (!this.get('allowIncomplete') && !sqim.get('complete')) {
      throw 'This collection only allows complete items!'
    }
  },
})

Мы объявили два класса: MyItem, который имеет опцию (атрибут) complete, и MyCollection, который:

  • Содержит экземпляры MyItem, на что указывает свойство _childClass
  • Автоматически возбуждает события .change и .foobar (с лидирующей точкой), если change и foobar (без точки) возникли в одном из объектов, которые он содержит
  • Имеет опцию allowIncomplete, которую использует для проверки всех вложенных объектов (их complete должно не быть false, если allowIncomplete не установлен)
  • При изменении allowIncomplete в false автоматически происходит проверка всех вложенных объектов
  • При изменении вложенного объекта (благодаря событию .change) происходит проверка этого объекта
  • При добавлении (nest) нового объекта также происходит его проверка

Вот пример использования, когда коллекция изначально не допускает не-complete объекты (JSFiddle):

var col = new MyCollection

var item1 = new MyItem
col.nest(item1)
  // exception

var item2 = new MyItem({complete: true})
col.nest(item2)
  // okay

item2.set('complete', false)
  // exception

А вот — когда флаг allowIncomplete меняется на ходу (JSFiddle):

var col = new MyCollection({allowIncomplete: true})

var item1 = new MyItem
col.nest(item1)
  // okay

col.set('allowIncomplete', false)
  // exception

Внимание: в случае не прохождения проверки исключение будет выброшено, но объект останется частью коллекции. В реальной жизни изменение либо нужно блокировать (слушая .-change и -nest), либо удалять неугодный объект (при change_allowIncomplete).


UnderGoodness


Sqimitive внутренне использует Underscore.js — библиотеку с функциями общего назначения, во многом перекрывающую функционал новых версий ECMAScript. Особенно много удобных функций имеется для работы с наборами данных — массивами и объектами.

Большую часть этих функций (около 40) можно использовать и на объекте Sqimitive для работы с его вложенными объектами.

Ниже — пример использования наиболее полезных методов на примере MyCollection, описанного выше. Полный список с описаниями приведён в документации.

var col = new MyCollection
col.nest( new MyItem({complete: true}) )
col.nest( new MyItem({something: 'item2'}) )
col.nest( new MyItem({complete: true, something: 'item3'}) )

var completeCounts = col.countBy(function (sqim) {
  return sqim.get('complete') ? 'done' : 'undone'
})
  // completeCounts = {done: 2, undone: 1}

var isEveryComplete = col.every(function (sqim) {
  return sqim.get('complete')
})
  // isEveryComplete = false, так как не все элементы имеют complete == true.

var allComplete = col.filter( Sqimitive.Sqimitive.picker('get', 'complete') )
  // Итератор, сгенерированный picker() - идентичен тому, что выше.
  // allComplete = [MyItem item1, MyItem item3] - два объекта с complete == true.

var firstComplete = col.find( Sqimitive.Sqimitive.picker('get', 'complete') )
  // firstComplete = MyItem item1 (её complete == true). Либо undefined,

var doneUndone = col.partition( Sqimitive.Sqimitive.picker('get', 'complete') )
  // doneUndone = [[item1, item3], [item2]] - фильтрует объекты, помещая
  // прошедшие условия в первый массив, а не прошедшие - во второй.

var firstChild = col.first()
var lastChild = col.last()
var parentKeys = col.keys()
var three = col.length
var item2 = col.at(1)
var item2_3 = col.slice(1, 1)

var somethings = col.invoke('get', 'something')
  // somethings = ['bar', 'item2', 'item3'] - вызывает метод с параметрами
  // и возвращает массив результатов, по результату для каждого объекта в col.

var sorted = col.sortBy( Sqimitive.Sqimitive.picker('get', 'something') )
  // sorted = [item1, item2, item3] - массив вложенных объектов, отсортированных
  // по значению, которое вернул итератор.

var serialized = col.invoke('get')
  // Аналог Backbone.Collection.toJSON(), который делает shallow copy.

col.invoke('render')
  // Вызывает render() на всех вложенных объектах. Часто используется.

var cids = col.map(function (sqim) { return sqim._cid })
  // cids = ['p11', 'p12', 'p13'] - почти как invoke(), только использует
  // результат вызова замыкания. _cid - уникальный идентификатор объекта.

col.each(function (sqim, key) {
  alert(key + ': ' + sqim._cid)
}, col)
  // Вызывает итератор 3 раза в контексте col (this).


Что в опциях тебе моём?


Опции или атрибуты — необычайно полезная вещь для любого типа класса, а не только моделей, как это сделано в Backbone. Это основа для state-based programming, когда ваш код реагирует на изменения сразу, а не проверяет их в местах, где от их состояния зависит какой-то результат (тем более обычно их много и из всевозможных вызовов _updateSize и _checkInput получаются отличные макароны).

Самый простой пример — изменение параметров представления. Сейчас очень модно говорить о шаблонах, самообновляющихся при изменении свойств модели — этим славятся Angular, Knockout и, конечно, React. В Sqimitive можно делать нечто подобное, только здесь нет зависимостей от шаблонизатора (вы можете вообще весь HTML писать вручную), моделей (все данные могут быть в самом представлении или разбросаны по разным объектам), события нужно расставлять самому, а изменять при их срабатывании можно всё что угодно.

var MyView = Sqimitive.Sqimitive.extend({
  _opt: {
    name: 'Иван',
    surname: 'Петрович',
    age: 900,
  },

  events: {
    change: function (opt, value) {
      this.$('.' + opt).text(value)
    },

    render: function () {
      this.el.empty()
        .append( $('<p class=name>').text(this.get('name')) )
        .append( $('<p class=surname>').text(this.get('surname')) )
        .append( $('<p class=age>').text(this.get('age')) )
    },
  },
})

Это очень простой пример (JSFiddle) и у него есть очевидные недостатки:

  • Данные хранятся в самом объекте-представлении. Для простейших приложений (или простых классов в сложных приложениях) это — оптимально, но всё же желательно держать их в отдельном объекте, которым можно обмениваться, события которого можно слушать, который можно добавлять в коллекции и прочее.
  • HTML задан прямо в коде класса. Пуристы не оценят, да и вообще это не очень удобно — к тому же страдает подсветка синтаксиса. Мне нравится использовать Handlebars, но он объёмный и для простых случаев вполне подойдёт встроенный в Underscore шаблонизатор template().
  • Вариант с change — короткий, но опасный, так как мы не проверяем opt и она вполне может отличаться от name, surname и age, которые мы хотим обновлять

var MyModel = Sqimitive.Sqimitive.extend({
  _opt: {
    name: 'Иван',
    surname: 'Петрович',
    age: 900,
  },
})

var MyView = Sqimitive.Sqimitive.extend({
  _opt: {
    model: null,
  },

  // Естественно, код шаблонов лучше всего выносить прямо в код самой страницы
  // как <script id="MyView" type="text/template"> или новомодный <template>.
  // По соглашению, в Sqimitive свойства и опции, начинающиеся с подчёркивания,
  // предназначены для использования внутри этого класса и его потомков.
  _template: _.template(
    '<p class="name"><%- name %></p>' +
    '<p class="surname"><%- surname %></p>' +
    '<p class="age"><%- age %></p>'),

  events: {
    // При передаче начальных опций в конструктор, change и другие события
    // вызываются, как положено (камень в огород Backbone).
    change_model: function (newModel, oldModel) {
      // Отключимся от старой модели, чтобы её изменения нас более не беспокоили.
      oldModel && oldModel.off(this)
      newModel.on('change', '_modelChanged', this)
    },

    render: function () {
      // get() без параметров аналогичен toJSON() в Backbone, только
      // возвращает поверхностную копию всех опций (shallow copy).
      this.el.html( this._template(this.get('model').get()) )
    },
  },

  _modelChanged: function (opt, value) {
    if (/^(name|surname|age)$/.test(opt)) {
      this.$('.' + opt).text(value)
    }
  },
})

Использование (JSFiddle):

var view = new MyView({
  el: $('#foo'),
  model: new MyModel,
})

// Начальная отрисовка. Где, кем и когда именно она происходит сильно зависит
// от вашего приложения. Можно делать в postInit, но это не всегда оптимально.
view.render()

view.get('model').set('name', 'Василий')
  // вызывается _modelChanged('name', 'Василий', 'Иван')

Код примера можно улучшать и дальше — но нам важно не это, а то, что Sqimitive позволяет масштабировать этот код именно так, как вам хочется, причём не в рамках выбора идеологии для всего проекта и на всю его жизнь (Ember? Knockout? Backbone? Angular?), а для каждого отдельного класса.

Например, традиционная для Backbone прослойка View < Collection < Model в Sqimitive иногда может быть сокращена до View < Model (когда модели добавляются в представление каким-то внешним кодом из своего источника), что часто делает код проще и менее запутанным. Но вы вольны выбирать сами, оставаясь в рамках Sqimitive.


Нерадивые родители: деревья и списки


Описанный функционал покрывает представления, но коллекции — лишь частично. На языке Sqimitive первые можно условно назвать владеющими (owning), а вторые — невладеющими (non-owning). Их отличия в следующем:

  • Владеющий объект гарантирует, что все вложенные в него объекты не содержатся в других владеющих объектах. Как пример: список из элементов на экране. Каждый элемент вложен в родителя, но стоит его вложить в другой список — как из первого он удаляется. И наоборот: коллекция моделей, где каждая модель может быть зачислена в несколько коллекций, так как по сути это просто улучшенный массив.
  • Как следствие, для любого вложенного объекта можно определить его владеющего родителя и ключ, под которым он значится. Родитель может быть лишь один. Невладеющие объекты никак не сообщают о себе вложенным объектам, поэтому узнать о них нельзя.
  • На владеемых объектах можно вызывать методы вроде remove и bubble, которые обращаются к своему родителю — как противоположность методам родителя, ищущим объекты для воздействия. Для первого достаточно иметь ссылку на сам объект, для второго — ссылку на родителя и некий идентификатор объекта в нём.
  • Оба типа родителей могут использовать почти все стандартные методы Sqimitive: фильтрацию, поиск, перенаправление событий и т.п.

Иными словами, владеющие объекты (это их состояние по умолчанию) создают двухсторонние деревья, а невладеющие — однонаправленные списки.

Пример невладеющего списка (JSFiddle):

var MyList = Sqimitive.Sqimitive.extend({
	// Отключаем режим владения.
  _owning: false,
  _childClass: MyItem,
  _childEvents: ['change'],
})

var item = new MyItem

var list1 = new MyList
list1.nest(item)

var list2 = new MyList
list2.nest(item)

alert(list1.length + ' ' + list2.length)
  // alert('1 1')

И его противоположность (JSFiddle):

var MyList = Sqimitive.Sqimitive.extend({
  // true можно не указывать - это значение по умолчанию.
  _owning: true,
  _childClass: MyItem,
  _childEvents: ['change'],
})

var item = new MyItem

var list1 = new MyList
list1.nest(item)

var list2 = new MyList
list2.nest(item)

alert(list1.length + ' ' + list2.length)
  // alert('0 1')

alert(item._parent === list2)
  // alert('TRUE')


Представляем виды


Всё описанное выше — штука полезна, но, по сути, является лишь сферическим конём в вакууме, потому как это голая логика без какого-либо взаимодействия с пользователем. А ведь вся наша работа делается именно ради него, родимого.

На сцену выходят представления — Views.

Здесь Sqimitive очень похож на Backbone — по моему мнению, эту часть MVC авторы библиотеки ухватили правильно. Однако из-за отсутствия механизма вложенности есть острые углы — например, при удалении вложенных объектов из DOM при перезаписи содержимого HTML в render элементы DOM этих объектов не восстанавливаются, а их события — не регистрируются заново. (Да, я понимаю, что представления-коллекции не должны просто так стирать своё содержимое, но все мы знаем, зачем существуют правила.)

Пример простого представления (JSFiddle):

var MyView = Sqimitive.Sqimitive.extend({
  el: {tag: 'aside', className: 'info'},

  _opt: {
    // Придумаем какой-нибудь флаг.
    loaded: false,
  },

  elEvents: {
    'click .something': '_somethingClicked',
  },

  events: {
    // Обычно флаг обновляется прямо в render. Но мы можем сделать это точечно.
    change_loaded: function (value) {
      this.el.toggleClass('loaded', value)
    },

    render: function () {
      this.el.html('Click <u class="something">here</u>?')
    },
  },

  _somethingClicked: function (e) {
    alert(e.target.tagName + ' clicked!')
  },
})

var view = new MyView({el: $('body')})
view
	.render()		// начальная отрисовка
	.attach()		// привязываем обработчики событий DOM (elEvents)
	.set('loaded', true)
		// <aside class="info loaded">

А как работают вложенные представления мы уже знаем — ведь это тот же Sqimitive:

var MyList = Sqimitive.Sqimitive.extend({
  // el задавать не обязательно, по умолчанию это простой <div>.
  // В классах, которым элемент DOM ни к чему, его лучше отключить, как здесь,
  // чтобы не гонять зря циклы процессора.
  el: false,

  // Некая абстрактная модель, в этом примере детали нам не важны.
  _childClass: MyItem,
})

var MyViewItem = Sqimitive.Sqimitive.extend({
  el: {tag: 'li'},

  _opt: {
    model: null,    // MyItem.
    // Точка привязки this.el к родительскому элементу. Описание ниже.
    attachPath: '.',
  },

  events: {
    change_model: function (newModel, oldModel) {
      oldModel && oldModel.off(this)

      // Предполагается, что модель содержится в некоем владеющем списке - в этом случае
      // при удалении её из него удаляем и её представление. MyList именно такой список.
      // Если же список не владеющий - нужно слушать его событие unnested.
      newModel.on({
        change: 'render',

        // remove - стандартный метод, удаляет объект из своего родителя вместе
        // с его el (unnest делает то же самое, но оставляет el где он есть).
        // Так как on вызывается с контекстом this (3-й параметр), то и remove
        // в ответ на -unnest модели будет вызван на объекте MyViewItem.
        '-unnest': 'remove',
      }, this)
    },

    unnest: function () {
      // Теоретически это делать не обязательно - можно обновлять MyViewItem и
      // после его ухода со сцены (удаления из родителя). Но можем сэкономить
      // память и такты, отключив его явно, когда он больше не нужен.
      this.get('model') && this.get('model').off(this)
    },

    render: function () {
      this.el.html(...)
    },
  },
})

var MyViewList = Sqimitive.Sqimitive.extend({
  el: {tag: 'ul'},
  _childClass: MyViewItem,

  _opt: {
    list: null,   // MyList.
  },

  events: {
    change_list: function (newList, oldList) {
      oldList && oldList.off(this)
      newList.on('+nest', '_modelAdded', this)

      // Добавим уже существующие элементы в newList.
      this.invoke('remove')
      newList.each(this._modelAdded, this)
    },
  },

  _modelAdded: function (sqim) {
    this.nest( new MyViewItem({model: sqim}) )
			.render()
  },
})

Мы объявили класс MyViewList, который служит мостиком между набором моделей (коллекцией) MyList и индивидуальным представлением каждой модели в этом наборе — MyViewItem. При этом он отражает все изменения в списке моментально — как добавление/удаление моделей, так и изменение свойств самих моделей (за это отвечает MyViewItem).

Добавление el в дерево документа делается несколькими способами:

  • Вручную как el.appendTo(...) — при этом события elEvents зарегистрированы не будут без вызова attach()
  • Через item.attach($('...')), тогда и el перемещается, и elEvents регистрируются
  • Автоматически при выполнении render на родителе или вызове attach() без аргументов — задав вложенному объекту опцию attachPath, значение которой может быть селектором, выполняемом на элементе родителя. Точка, как в MyViewItem, означает сам родительский элемент.

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

Использование (JSFiddle):

var list = new MyList
list.nest(new MyItem)
  // list.length == 1

var view = new MyViewList({list: list})
  // view.length == 1

list.nest(new MyItem)
  // list.length == view.length == 2

list.at(1).remove()
  // list.length == view.length == 1

var list2 = new MyList
view.set('list', list2)
  // list.length == 1
  // list2.length == view.length == 0


О бренности жизни без сохранения данных


Итак, интерфейс у нас нарисовался, пользователю есть, где пощёлкать клавишами. Но доволен ли он?

Конечно, нет (для этого даже не нужно читать вопрос). Как в мире есть небо и земля, так и в веб-программировании есть фронт и тыл… пардон, это из другой области. У нас это завётся frontend и backend, и обычно когда есть одно — где-то рядом бегает и второе.

Говоря по-простому, когда есть интерфейс, нам надо где-то сохранять данные. И побыстрее!

Работа с сервером — тот самый AJAX — сегодня считается чем-то вроде светового меча для джедая. Каждая уважающая себя клиентская библиотека считает своим долгом создать «лучший в мире API», чтобы вам, как программисту, не пришлось думать об этом даже краем мысли — чего доброго.

Слой общения с backend есть везде — от Backbone и jQuery до Knockout и Ember. В Backbone он называется sync и глубоко встроен в её среду. В частности, коллекции и модели имеют набор методов — parse, fetch, create и другие — которые используют интерфейс Backbone.sync для отправки запроса на сервер, обработки результата и присвоения его вызвавшему объекту.

В Sqimitive такого слоя нет. Причина этому следующая: я обнаружил, что в 80% случаев простым sync дело не обходится. Нужно обновить несколько объектов, сделать нестандартную обработку, отреагировать на синхронизацию конкретно в этом месте, либо сделать ещё что-то, ради чего приходится перекрывать стандартный метод (чаще всего fetch и parse), либо городить сомнительный огород из событий.

Оставшиеся же 20% — очень простые случаи и отлично укладываются в $.ajax() на 5 строчках.

Однако это проблема ещё так себе. Другая — более глубока и коварна: строя проект на Backbone мы привязываемся к его слою работы с сервером. Это проявляется как в требованиях к API (строгий REST), так и в отсутствии простых способов делать запросы к серверу напрямую. Доходит до того, что создаются временные модели и коллекции, потому что лишь они могут сделать требуемый запрос, но логически они избыточны и служат доступом к зарытому в недрах sync. Написание же своего слоя для доступа к API грозит дублированием кода, да ещё и не совсем понятно, как увязать его с уже написанными parse в моделях и коллекциях.

А раз свой слой всё равно пишется, то зачем нужен стандартный?

Возможно, мой опыт говорит об ошибках проектирования, но, на мой взгляд, инструменты, которые к ним подталкивают — плохие инструменты. А в Backbone sync — это, действительно, основа всего и с ним возникает куча проблем. Кстати, его логика изначально рассчитана на Rails.

«У нас в России за такое в морду дают...»
Однажды, когда я был ещё совсем маленьким и не решался браться за серьёзные проекты на Backbone, мне пришлось начинать с проекта примерно в 6 000 строк кода. Там мне довелось отлаживать fetch() на коллекции. Backbone почему-то упорно создавал модели умело обходя заданные для них parse.

Спустя энное количество клеток я, закопавшись в исходники, обнаружил, что при присвоении ответа коллекцией она создаёт модели напрямую из данных сервера, минуя их parse. Выходит, parse в моделях нужно дублировать на уровне коллекции. Логика авторов мне не понятна и, по-моему, является просто грубой ошибкой.

Sqimitive предлагает элегантное решение этой проблемы. Вместо слоя работы с сервером, который берёт на себя всё и даже ваши тапочки — она содержит лишь функции для работы с уже готовым ответом, который может быть получен как из API, так и из cookies, Local Storage, location.hash или другого источника. Методов всего два: assignChildren и assignResp.

assignChildren — это аналог Backbone.Collection.parse + set. На вход поступает объект, который вы получили от сервера или какой-то иной системы хранения данных. Обычно это массив, элементы которого — объекты (сериализованные модели). assignChildren преобразует каждый объект в модель (создаёт _childClass) и присваивает ему опции через assignResp. Новые модели — создаются, существующие — обновляются, отсутствующие — удаляются.

assignResp — аналог Model.parse + set. На входе — некий объект с атрибутами в непонятном формате. Метод преобразует его в подходящий этому объекту набор опций и присваивает их, запуская соответствующие события normalize_OPT, change_OPT и change, используя стандартный set, как если бы вы делали все присвоения вручную.

Оба метода служат прослойками между форматом вашего API и форматом вашего интерфейса на JavaScript. Их использование явно говорит о том, что приходят «нечистые» сырые данные, которые нужно тонко встроить в текущий мир приложения.

Оба метода принимают набор параметров, которыми можно настроить их поведение — они описаны в документации. Здесь же хочу показать, как удобно используется assignResp для преобразования ответов сервера в конечные опции для модели.


assignResp и _respToOpt — карта ответа API


Допустим, сервер возвращает такой объект (он так же может быть частью ответа-списка для assignChildren):

{
  'thisID':    123,
  'date':     '2014-10-03T10:26:22+03:00',
  'parents':  '1 2 3',
  'junk':     'ONk49Xo3SxEps8uCV9je8dhez',
  'caption':  'Имя',
}

Наша модель имеет следующие опции:

var MyModel = Sqimitive.Sqimitive.extend({
  // Изначальный набор опций-атрибутов. Аналогичен defaults в Backbone.
  _opt: {
    id: 0,
    date: new Date,
    parents: [],
    caption: '',
  },
})

Мы видим несоответствие:

  • id в ответе сервера называется thisID
  • date нужно распарсить и преобразовать в объект
  • parents — строка, где ID разделены пробелами
  • junk — какое-то «левое» значение, не понятно зачем нужное клиенту
  • caption — единственный соответствующий элемент

Следующий код сделает нужные преобразования:

var MyModel = Sqimitive.Sqimitive.extend({
  _opt: {
    id: 0,
    date: new Date,
    parents: [],
    caption: '',
  },

  // Мы можем и полностью переписать сам assignResp для этого класса, но
  // намного проще настроить его поведение с помощью этого блока.
  _respToOpt: {
    // Простое переименование исходного ключа.
    thisID: 'id',

    // Произвольное сложное преобразование. Функция возвращает массив, где
    // первый элемент - имя конечной опции, а значение - значение опции для присвоения.
    date: function (str) {
      return ['date', new Date(str)]
    },

    // Игнорирование ключа.
    junk: false,

    parents: function (str) {
      return ['parents', str.split(/\s+/)]
    },

    // Принятия ключа/значения как есть. Аналогично caption: 'caption'.
    caption: true,
  },

  // Всё, что ниже - не обязательно, но поможет поддерживать значения в чистоте.
  normalize_id: function (value) {
    var id = parseInt(value)
    if (isNaN(id)) { throw 'What kind of ID is that?' }
    return id
  },

  normalize_parents: function (value) {
    return _.map([].concat(value), this.normalize_id, this)
  },

  normalize_caption: function (value) {
    return value.replace(/^\s+|\s+$/g, '')
  },
})

Использование (JSFiddle):

var model = new MyModel
$.getJSON('api/route', _.bind(model.assignResp, model))

// Либо из POST:
$.ajax({
  url: 'api/route',
  type: 'POST',
  data: {...},
  context: model,
  success: model.assignResp,
})


И это что, всё?


На самом деле, нет. Весь самый важный функционал был описал, но остаются ещё несколько вещей, отличающих Sqimitive от других библиотек. Ниже — кратко о некоторых из них. Остальные вы найдёте при выполнении квеста «Прочитать Zen Book».

_shareProps: клонирование свойств


Let's not talk about languages that suck. Let's talk about Python.

В Python, если вы объявляете объект со сложными (нескалярными) начальными значениями — вы ненароком делаете их общими для всех экземпляров этого объекта, которые не перезаписали эти поля новыми объектами. Например:

class SomeObject:
  list = []

  def push(self, value):
    self.list.append(value)
    return self

print SomeObject().push('123').list
  #=> ['123']

print SomeObject().push('345').list
  #=> ['123', '456']

Если у JavaScript и Python и есть что-то общее — так это автоматизированное выкапывание ям, используя логически обоснованные особенности языка. JSFiddle:

var SomeObject = Backbone.View.extend({
  list: [],

  push: function (value) {
    this.list.push(value)
    return this
  },
})

console.dir( (new SomeObject).push('123').list )
  //=> ['123']

console.dir( (new SomeObject).push('456').list )
  //=> ['123', '456']

Причина, если подумать, понятна: мы объявляем класс, передаём в него начальные значения-объекты, а эти значения затем копируются в новый экземпляр. А ведь, как известно, объекты в обоих языках копируются по ссылке — поэтому в новых объектах мы получаем старые ссылки на те объекты (массив в примере выше), которые мы изначально передали в extend.

Логично? Конечно. Очевидно? Не более, чем номер новой версии Windows (который, кстати, тоже может быть логичен).

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

В Sqimitive все свойства глубоко копируются (deep copy) при создании нового экземпляра. Свойства, которые не нужно копировать, задаются явно в статическом массиве _shareProps. Обычно там указываются поля, где хранится ссылка на класс — такие как _childClass, который здесь уже указан по умолчанию. Однако используется оно редко, так как совсем сложные объекты обычно проще инициализировать в init.

var MySqimitive = Sqimitive.Sqimitive.extend({ ... })
MySqimitive._shareProps.push('notToCloneSomething')


_mergeProps: слияние с родителем


В Backbone, при наследовании, свойства потомка всегда перекрывают базовые. Это правильно, но в отдельно взятых случаях выливается в такой же неудобный костыль, как __super__. К примеру (JSFiddle):

var MyView = Backbone.View.extend({
  events: {
    'click .me': function () { alert('Есть контакт!') },
  },
})

var MyOtherView = MyView.extend({
  events: {
    'keypress .me': function () { alert('Мы сломали кнопку :(') },
  },
})

MyOtherView полностью перекрыл унаследованный от MyView блок events. Решения два: либо events: _.extend(MyView.prototype.events, {...}), либо добавление новых элементов в events в конструкторе. Второе более красиво (или менее сломано), но при большом числе событий получается каша. Здесь бы как раз и пригодился events, изначально призванный её разруливать.

_mergeProps — статическое свойство-массив, где перечисляются поля, которые должны быть объединены при наследовании, а не перезаписаны. Для массивов это base.concat(child), для объектов — _.extend(base, child) (когда одноимённые ключи на верхнем уровне в объекте потомка перекрывают базовые). При таком подходе новые элементы всегда добавляются, а удалить элемент можно только в конструкторе, либо перезаписав значением null/undefined, где это подходит.

Так как изначально в _mergeProps уже перечислены elEvents, events и _opt (а также _shareProps) — пример с Backbone в Sqimitive сработает верно: MyOtherView получит оба обработчика событий.

Аналогичный пример со слиянием опций в потомке (JSFiddle):

var MyBase = Sqimitive.Sqimitive.extend({
  _opt: {
    base: 123,
    base2: 'kept',
    complex: {a: 1},
  },
})

var MyChild = MyBase.extend({
  _opt: {
    // Заменит 123.
    base: 'replaced',
    // Целиком заменит {a: 1} - объекты объединяются только на первом уровне.
    complex: {b: 2},
    // Добавит новый ключ в _opt.
    child: 'new',
    // base2 - останется.
    //base2: 'kept',
  },
})


masker(): передай мне их с хитростью


И напоследок о маленькой, но любопытной фиче Sqimitive. Иногда бывает так, что callback-функция в точности совпадает с уже имеющимся методом, за исключением аргументов. К примеру, у вас есть nest(key, object) и вы хотите вызвать его для каждого ID в некоем списке var list = {11: obj1, 22: obj2, 33: obj3}.

Это можно сделать несколькими способами:

  • $.each(list, _.bind(this.nest, this)) — один из случаев, когда передача jQuery ключей в итератор первым параметром бывает полезна
  • _.each(list, function (o, k) { this.nest(k, o) }, this)
  • _.each(Sqimitive.Sqimitive.masker('21'), this.nest, this)

Третий вариант кажется длиннее и поэтому есть смысл объявить глобальный алиас для этой функции как masker или даже m. Функция может быть вызвана в разных формах, но суть сводится к строке-маске, где каждый символ описывает источник аргумента (номер входного параметра), а позиция символа — номер выходного (для «замаскированной» функции).

Другой пример: нужно присвоить ответ API с сервера через $.ajax. jQuery кроме самого ответа передаёт и другие аргументы функции в success, а assignResp принимает как сам ответ, так и опции. Если пропустить аргументы от первого к последнему, то может получиться ошибка — последний попробует трактовать объект jQuery как набор опций. Здесь можно использовать маску:

$.getJSON('api/route', masker(model.assignResp, '.', model))

Точки в маске заменяются на порядковый номер символа, а всё что после последнего символа — не передаётся. Таким образом, здесь к assignResp будет передан только первый аргумент.

masker неявно используется в events, elEvents, on и once в форме имяМетода[маска] (см. документацию по expandFunc). Ниже — слегка надуманный, но наглядный пример (JSFiddle):

var MyView = Sqimitive.Sqimitive.extend({
  // Показывает/скрывает элемент. Без аргументов меняет текущее состояние
  // на противоположное.
  toggle: function (state) {
    arguments.length || (state = !this.$('.contents').is(':visible'))
    this.$('.contents').toggle(!!state)
    return this
  },

  elEvents: {
    // Дефисы в маске отбрасывают исходные аргументы. Дефис в конце - удаляется.
    // В результате остаётся пустая маска, которая отбрасывает все аргументы.
    'click .toggle': 'toggle-',

    // jQuery первым параметром передаёт event object, который == true.
    'click .show': 'toggle',

    // 9 параметра jQuery точно не передаёт, поэтому передаётся нечто != true.
    // Дефис отделяет имя метода от маски - без него был бы просто метод toggle9.
    'click .hide': 'toggle-9',
  },
})

А вот аналогичный блок без масок — безусловно, кому-то он покажется более наглядным, и вам не обязательно их использовать

  elEvents: {
    'click .toggle': function () {
      this.toggle()
    },

    'click .show': function () {
      this.toggle(true)
    },

    'click .hide': function () {
      this.toggle(false)
    },
  },


Он улетел… но обещал вернуться!


Введение в нирвану подошло к концу. Признаться, когда я вёл курсор к большой зелёной кнопке «Опубликовать», мои пальцы слегка дрожали. Оценят ли элегантность простых решений? Много ли осталось тех из нас, кто считает, что шквал веб-технологий ведёт разработчиков по лезвию ножа?

Как бы то ни было, теперь ваша очередь. Проект доступен на GitHub, исчерпывающая документация — на squizzle.me, пример простого приложения — тут. Буду рад вашим вопросам в комментариях, а исправлениям опечаток — в личке.

Увидимся по ту сторону баррикад.

Squizzle  it
Tags:
Hubs:
+44
Comments 33
Comments Comments 33

Articles