Многих людей это вдохновляет. Да что там — 95% моих знакомых в один голос твердят, как подключив всего пару-тройку библиотек с особой, уличной магией можно забабахать сайт на over-9000 зелёных австралийских долларов — и всего за один вечер, с перерывом на кофе и бублики.
А я — странный человек. Не люблю смешения языков, технологий, библиотек. Angular, Knockout, React — они все хороши, но каждая — по-своему сложна. А ведь есть и «гибриды», где сходится сразу несколько миров — как Ember и Knockout.Bootstrap. Вдобавок, многие построены на jQuery — впрочем, к ней даже у меня претензий нет; наверное, таким и должен был быть JavaScript.
Как бы то ни было, реальность беззастенчиво входит в контакт с мечтами и расставляет точки над «i». Мне так же приходится писать на «new & popular» — а когда пишешь, душа томится и просится создать очередной велосипед… а ей разве откажешь? Она ведь как дитя малое.
Велосипед был создан. Велосипед без фантиков. Такой же простой, как автомат Калашникова, и многогранный, как швейцарский нож, где вместо наследования — события, вместо моделей, коллекций и представлений — один класс, с неограниченной вложенностью и полной свободой действий, почти в два раза меньший Backbone.js, использующий Underscore.js и, необязательно, jQuery/Zepto.
Как всё начиналось
Я — фрилансер. «Фри» в данном случае обозначает прямо противоположное, поэтому по долгу службы я работаю над многими проектами, со многими технологиями и иногда подолгу. Последние два года было много работы именно с 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 вам покажется очень знакомым, тёплым и почти ламповым.
(Здесь и далее — моя субъективная точка зрения, которая может не совпадать с вашей, Хабра, президента или Космической коалиции. Читайте на свой страх и риск, не забывайте комментировать и помните о своей карме!)
Оглавление:
- Наследование как фактор выживания
- Превращение методов в события
- Наследование на лету или прототипирование-2.0
- О важных связях с общественностью
- Вложенность — наше всё
- UnderGoodness
- Что в опциях тебе моём?
- Нерадивые родители: деревья и списки
- Представляем виды
- О бренности жизни без сохранения данных
- assignResp и _respToOpt — карта ответа API
- И это что, всё? _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.
Спустя энное количество клеток я, закопавшись в исходники, обнаружил, что при присвоении ответа коллекцией она создаёт модели напрямую из данных сервера, минуя их 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, пример простого приложения — тут. Буду рад вашим вопросам в комментариях, а исправлениям опечаток — в личке.
Увидимся по ту сторону баррикад.
ссылка на оригинал статьи http://habrahabr.ru/post/239149/
Добавить комментарий