Уже довольно давно большинство сайтов перестало быть набором 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 вам покажется очень знакомым, тёплым и почти ламповым.
(Здесь и далее — моя субъективная точка зрения, которая может не совпадать с вашей, Хабра, президента или Космической коалиции. Читайте на свой страх и риск, не забывайте комментировать и помните о своей карме!)
Оглавление:
Если бы JavaScript был Haskel, то у него бы не было этой проблемы (отчасти потому, что на нем бы никто не писал). Если бы JavaScript был Си, то у него были бы другие проблемы — может, много проблем, но точно не таких.
Но JavaScript — это и не Haskel, и не Си. У него было тяжёлое детство с непостоянными родителями, которые навсегда повредили егоэто this. Так что теперь это непостоянство расхлёбывают программисты.
Функции в JavaScript — это обычные значения вроде чисел и строк (т.н. first class citizen). Функции можно присваивать, удалять, копировать, даже преобразовывать в строку. Плюс к этому функция имеет неявный параметр — this — который по задумке авторов языка должен указывать на объект, который вызвал срабатывание этой функции — а никак не на объект, к которому эта функция была привязана.
Таким образом, объекты в JavaScript как бы есть, но получить контекст объекта — невозможно. Объекты в этом смысле — это те же ассоциативные массивы.Видимо, в этом суть концепции Java (ООП) + Script (ФП). Шутка.
(Прошу не принимать мой сарказм близко к сердцу людям с плохим чувством юмора. Я люблю и JavaScript, и Haskel, но я точно так же осведомлён об их… особенностях и стараюсь их чётко обозначить, чтобы найти им хорошее решение. А вообще, пора бы уже Ruby захватить мир.)
Классический пример (JSFiddle):
Причина — в том, что мы считали значение-функцию, которое записано в свойстве
Впрочем, проблема с непостоянным this худо-бедно, но решается — и даже товарищи из ECMAScript в конце концов сдались и спустя каких-то 16 лет (в 2011 вместе с 5.1) к
Другая особенность JavaScript — отсутствие в языке ссылки на базовый класс и вообще понятия «базового класса». JavaScript использует прототипное наследование, что в общем означает следующее: каждая функция (она же конструктор, который вы вызываете как new Func) имеет так называемый «прототип». При new Func происходит копирование полей этого прототипа в новый экземпляр объекта. «Наследование» в понятиях традиционного ООП — отсутствует, вместо этого прототип копируется в другой прототип, то есть все его поля копируются в другой объект: переменные и методы-функции — которые, как уже сказано, обычные значения, которыми можно манипулировать. Затем на новом прототипе делаются все изменения, которые предписывает «наследование» (перекрываются методы, добавляются поля и т.п.).
Фактически же мы получаем два независимых класса-прототипа.
Эта техника призвана бороться с некоторыми недостатками классического ООП — хрупким базовым классом, коллизиях при множественном наследовании и другими нюансами. Важную проблему в JavaScript решали, в целом, правильными методами, но плавающий this, функции-значения (невозможность определить своё имя в рамках объекта без прямого перебора всех полей) и отсутствие простых штатных связей между прототипами в сумме вызывают кровь и ярость.
В традиционном ООП наследование — это когда один объект копирует другой (возможно, с изменениями), но между ними сохраняется связь родитель-потомок. Теперь посмотрим на ООП в JavaScript (JSFiddle):
Как видно, здесь функция show, которая привязана к Child, не знает ни своего имени (она может быть привязана под разными именами, много раз, к разным прототипам), ни имени базового класса, ни даже this, если мы сделаем что-то вроде
Нам остаётся только вшить (hardcode) эти значения в код самой функции. Понятно, что это — плохой путь:
Это не говоря о том, что писать
Тем не менее, именно такой принцип используется в Backbone (в Angular, Knockout, React вы фактически пишите на «полу-ООП», где явно указываете предков при вызове, что не многим лучше). Хорошее решение — у Ember, с его автоматическим this._super:
Но Ember — это 48 000 строк чистого JavaScript. Неужели нельзя проще?..
Можно. Sqimitive решает эту проблему так (JSFiddle):
Кроме того, вы можете использовать стандартный вариант с __super__ — оставлен для любителей стрелять себе по ногам и для поддержки legacy-кода (JSFiddle):
Блок events в Sqimitive определяет новые обработчики, которые вызываются для событий объектов этого класса. Когда имя события совпадает с именем уже существующего метода — этот метод (в примере — show) заменяется на firer('show') — функцию, которая при вызове инициирует одноимённое событие. Заменённый метод (например, унаследованный) ставится в начало цепочки обработки, а заменяемый — после него. Таким образом, сохраняется логика выполнения. Нет нужды изменять что-либо при изменении структуры базового или наследующего класса.
Если же метода не было — новый метод становится единственным обработчиком. Если же под данным именем значится не метод, то это свойство перезаписано не будет и событие можно будет вызвать только явно, через fire().
Таким образом, любой метод класса — возможная точка привязки события, а сами события можно возбуждать как явно через fire('event'), так и вызывая метод на самом объекте — если это событие, то оно будет инициировано благодаря firer(), а если нет — функция будет вызвана напрямую (фактически это единственный обработчик события). Трансформация метода в событие делается прозрачно и на лету.
При этом стоит отметить, что для пуристов, которые борются за наносекунды — всё чисто. Если вам важна производительность конкретного метода или класса — просто определите его как обычно, без события (см. пример выше с __super__) — тогда он будет вызываться напрямую, минуя fire(). Причём сделать это можно и в базовом классе, и в потомках, и уже имея перекрытые методы-события. Нужно только следить, чтобы в последствии никто не создал из этого метода событие, иначе наносекунды потекут не в ту сторону.
Как показал мой опыт, полная замена метода, как в примере выше — штука довольно редкая. В Sqimitive есть ещё три типа добавления обработчика, которые различаются префиксом (знак равно выше — один из них):
Во всех случаях параметры события передаются каждому обработчику.
Первый тип покрывает 50% причины для перекрытия методов, второй и третий — ещё 40%.
Кроме того, обработчики могут быть строками — если нужно просто вызвать метод с этим именем с оригинальными параметрами. Это сильно сокращает код и делает его понятнее:
Итак, у нас есть наследование через события… А обязательно ли его проводить во время объявлении класса через extend?
Как понятно из названия — нет. События — они динамические, их кашей не корми, дай только возбудиться. Да, главное, побольше обработчиков!
Результат (JSFiddle) аналогичен тому, как если бы мы унаследовали от Base новый класс и перекрыли там метод. Здесь же мы сделали это на «живом» объекте, единственном в своём роде. В добавок — как сделали, точно так же можем и убрать (JSFIddle):
Но будьте осторожны: это уберёт все обработчики события show, кроме «припаянных» — fused (наследованные, к примеру, одни из таких). Если мы хотим убрать именно наш — используем его идентификатор (JSFiddle):
А что произойдёт с методом, который мы перекрыли — Base.show? Как видно в JSFiddle, он восстановится, как только его =show-обработчик будет снят. Всё, как у людей.
Естественно, другие префиксы можно использовать точно так же, как они используются в блоке events.
Кроме on и off в нашем распоряжении есть и once — полностью аналогичен on, но отменяет обработчик после того, как он был вызван ровно один раз.
До поры до времени объектов мало, приложение простое, памяти много и вообще полный мир и идиллия. Но так бывает не всегда.
Для приложений средней руки классов становятся десятки и сотни, а объектов за тысячи. Они постоянно заменяют друг друга и борются за место под солнцем в тесной песочнице DOM. В такой ситуации оставлять их все висеть в фоне — не гуманно. И в то же время не понятно, как управлять их связями — когда именно объект создаётся и «подключается» к матрице, а когда — удаляется, и как отключить его обработчики при переходе из бренного мира к праотцам?
В Backbone появились методы listenTo и stopListening (изначально их не было), которые позволяют запоминать связанные объекты и избавляться от связей с ними. Однако сам Backbone не содержит логики вкладывания этих объектов. Модели в коллекциях не считаем — основная проблема именно в постоянной циркуляции представлений (или видов, View).
В Sqimitive есть и аналог listenTo, и вложенность объектов. О последней подробно поговорим дальше в статье, а пока простой пример:
Теперь мы можем наследовать Bindable, наполнив его своей логикой. В большинстве случаев выглядит это так:
Здесь
Заметьте, что это не стандартное поведение Sqimitive, это уже наш собственный код, который можно заложить в базовый для вашего приложения класс
Третий параметр к autoOff — необязательный контекст, который изначально устанавливается в зависимый объект (а не тот, к которому добавляется обработчик). В связке с именами методов-обработчиков вместо замыканий это даёт довольно компактный синтаксис:
У этих методов есть и другие особенности — подробности см. в документации.
В Backbone, на мой взгляд, очень мало внимания (читай — никакого) уделено вкладыванию объектов друг в друга. А ведь это крайне важная их особенность. Проекты наподобие Marionette.js пытаются компенсировать этот недостаток, но это как раз тот случай, когда библиотека зиждется на библиотеке, всё это как-то собирается и даже работает, но потребляет столько космической энергии, что лучше бы все сидели по домам. А в случае ошибки — не понятно, кого ругать — авторов Backbone за отсутствие штатных средств, авторов Marionette за их логику, себя — за несовместимое с ними мировоззрение, или JavaScript — просто потому, что он «не такой, как все».
Кроме того, Marionette — это ещё 4 000 строк кода в добавок к существующим зависимостям. А ведь каждая строчка — потенциальная ошибка, каждый метод — новая статья в документации (Marionette, впрочем, таковой просто не имеет).
В Sqimitive концепция родитель-потомок заложена на втором уровне. Сама библиотека разбита на две части в виде двух классов:
Именно Sqimitive даёт тот конечный функционал, который нужен в приложениях. Core можно наследовать, если вы хотите внедрить в свой класс только событийный (и наследующий) механизм.
В библиотеке Sqimitive нет разделения на модель, коллекцию и представление (M-C-V). Единый класс обладает как атрибутами (присущи моделям в Backbone) — их зовут «опциями», так как они передаются в конструктор, а также может содержать вложенные объекты определённого класса, над которыми можно проводить фильтрацию (доступен весь набор методов Underscore.js), автоматически перенаправлять их события родителю и вообще трактовать как некую совокупность, над которой можно работать как с чем-то единым, безликим, а не с каждым объектом в отдельности.
Для индивидуальной работы как раз подходит _opt, где каждый элемент — нечто особенное, и каждое движение (доступ, замена) можно отследить, перекрыв ifSet и get, добавив
В противоположность этой дзенской простоте Marionette, Ember и другие — сложны. В Ember есть разные типы свойств (computer, observers, bindings), в Marionette — разные типы представлений (раскладки, регионы, представления элементов и коллекций, составные). Конечно, это всё полезно — для определённого уровня приложений и команд. Но для многих других это всё равно что стрельба из пушки по воробьям. Дыма и шума много, публика довольна, но само действо не эффективно и трудозатратно. К тому же нужно для начала изучить, как летаютпушки воробьи и ядра.
Интересно и то, что даже наличие таких готовых средств не гарантирует, что вам не будет проще написать какую-то их часть заново конкретно под вашу задачу.
Ниже — пример объявления вкладываемых классов в Sqimitive:
Мы объявили два класса: MyItem, который имеет опцию (атрибут) complete, и MyCollection, который:
Вот пример использования, когда коллекция изначально не допускает не-complete объекты (JSFiddle):
А вот — когда флаг allowIncomplete меняется на ходу (JSFiddle):
Внимание: в случае не прохождения проверки исключение будет выброшено, но объект останется частью коллекции. В реальной жизни изменение либо нужно блокировать (слушая .-change и -nest), либо удалять неугодный объект (при
Sqimitive внутренне использует Underscore.js — библиотеку с функциями общего назначения, во многом перекрывающую функционал новых версий ECMAScript. Особенно много удобных функций имеется для работы с наборами данных — массивами и объектами.
Большую часть этих функций (около 40) можно использовать и на объекте Sqimitive для работы с его вложенными объектами.
Ниже — пример использования наиболее полезных методов на примере MyCollection, описанного выше. Полный список с описаниями приведён в документации.
Опции или атрибуты — необычайно полезная вещь для любого типа класса, а не только моделей, как это сделано в Backbone. Это основа для state-based programming, когда ваш код реагирует на изменения сразу, а не проверяет их в местах, где от их состояния зависит какой-то результат (тем более обычно их много и из всевозможных вызовов
Самый простой пример — изменение параметров представления. Сейчас очень модно говорить о шаблонах, самообновляющихся при изменении свойств модели — этим славятся Angular, Knockout и, конечно, React. В Sqimitive можно делать нечто подобное, только здесь нет зависимостей от шаблонизатора (вы можете вообще весь HTML писать вручную), моделей (все данные могут быть в самом представлении или разбросаны по разным объектам), события нужно расставлять самому, а изменять при их срабатывании можно всё что угодно.
Это очень простой пример (JSFiddle) и у него есть очевидные недостатки:
Использование (JSFiddle):
Код примера можно улучшать и дальше — но нам важно не это, а то, что Sqimitive позволяет масштабировать этот код именно так, как вам хочется, причём не в рамках выбора идеологии для всего проекта и на всю его жизнь (Ember? Knockout? Backbone? Angular?), а для каждого отдельного класса.
Например, традиционная для Backbone прослойка View < Collection < Model в Sqimitive иногда может быть сокращена до View < Model (когда модели добавляются в представление каким-то внешним кодом из своего источника), что часто делает код проще и менее запутанным. Но вы вольны выбирать сами, оставаясь в рамках Sqimitive.
Описанный функционал покрывает представления, но коллекции — лишь частично. На языке Sqimitive первые можно условно назвать владеющими (owning), а вторые — невладеющими (non-owning). Их отличия в следующем:
Иными словами, владеющие объекты (это их состояние по умолчанию) создают двухсторонние деревья, а невладеющие — однонаправленные списки.
Пример невладеющего списка (JSFiddle):
И его противоположность (JSFiddle):
Всё описанное выше — штука полезна, но, по сути, является лишь сферическим конём в вакууме, потому как это голая логика без какого-либо взаимодействия с пользователем. А ведь вся наша работа делается именно ради него, родимого.
На сцену выходят представления — Views.
Здесь Sqimitive очень похож на Backbone — по моему мнению, эту часть MVC авторы библиотеки ухватили правильно. Однако из-за отсутствия механизма вложенности есть острые углы — например, при удалении вложенных объектов из DOM при перезаписи содержимого HTML в render элементы DOM этих объектов не восстанавливаются, а их события — не регистрируются заново. (Да, я понимаю, что представления-коллекции не должны просто так стирать своё содержимое, но все мы знаем, зачем существуют правила.)
Пример простого представления (JSFiddle):
А как работают вложенные представления мы уже знаем — ведь это тот же Sqimitive:
Мы объявили класс MyViewList, который служит мостиком между набором моделей (коллекцией) MyList и индивидуальным представлением каждой модели в этом наборе — MyViewItem. При этом он отражает все изменения в списке моментально — как добавление/удаление моделей, так и изменение свойств самих моделей (за это отвечает MyViewItem).
Добавление el в дерево документа делается несколькими способами:
В примере выше в качестве бонуса мы также можем на лету присваивать новую коллекцию для MyViewList и последний тут же обновит и свои связи, и содержимое.
Использование (JSFiddle):
Итак, интерфейс у нас нарисовался, пользователю есть, где пощёлкать клавишами. Но доволен ли он?
Конечно, нет (для этого даже не нужно читать вопрос). Как в мире есть небо и земля, так и в веб-программировании есть фронт и тыл… пардон, это из другой области. У нас это завётся frontend и backend, и обычно когда есть одно — где-то рядом бегает и второе.
Говоря по-простому, когда есть интерфейс, нам надо где-то сохранять данные. И побыстрее!
Работа с сервером — тот самый AJAX — сегодня считается чем-то вроде светового меча для джедая. Каждая уважающая себя клиентская библиотека считает своим долгом создать «лучший в мире API», чтобы вам, как программисту, не пришлось думать об этом даже краем мысли — чего доброго.
Слой общения с backend есть везде — от Backbone и jQuery до Knockout и Ember. В Backbone он называется sync и глубоко встроен в её среду. В частности, коллекции и модели имеют набор методов — parse, fetch, create и другие — которые используют интерфейс
В Sqimitive такого слоя нет. Причина этому следующая: я обнаружил, что в 80% случаев простым sync дело не обходится. Нужно обновить несколько объектов, сделать нестандартную обработку, отреагировать на синхронизацию конкретно в этом месте, либо сделать ещё что-то, ради чего приходится перекрывать стандартный метод (чаще всего fetch и parse), либо городить сомнительный огород из событий.
Оставшиеся же 20% — очень простые случаи и отлично укладываются в
Однако это проблема ещё так себе. Другая — более глубока и коварна: строя проект на Backbone мы привязываемся к его слою работы с сервером. Это проявляется как в требованиях к API (строгий REST), так и в отсутствии простых способов делать запросы к серверу напрямую. Доходит до того, что создаются временные модели и коллекции, потому что лишь они могут сделать требуемый запрос, но логически они избыточны и служат доступом к зарытому в недрах sync. Написание же своего слоя для доступа к API грозит дублированием кода, да ещё и не совсем понятно, как увязать его с уже написанными parse в моделях и коллекциях.
А раз свой слой всё равно пишется, то зачем нужен стандартный?
Возможно, мой опыт говорит об ошибках проектирования, но, на мой взгляд, инструменты, которые к ним подталкивают — плохие инструменты. А в Backbone sync — это, действительно, основа всего и с ним возникает куча проблем. Кстати, его логика изначально рассчитана на Rails.
Sqimitive предлагает элегантное решение этой проблемы. Вместо слоя работы с сервером, который берёт на себя всё и даже ваши тапочки — она содержит лишь функции для работы с уже готовым ответом, который может быть получен как из API, так и из cookies,
assignChildren — это аналог Backbone.Collection.parse + set. На вход поступает объект, который вы получили от сервера или какой-то иной системы хранения данных. Обычно это массив, элементы которого — объекты (сериализованные модели). assignChildren преобразует каждый объект в модель (создаёт _childClass) и присваивает ему опции через assignResp. Новые модели — создаются, существующие — обновляются, отсутствующие — удаляются.
assignResp — аналог Model.parse + set. На входе — некий объект с атрибутами в непонятном формате. Метод преобразует его в подходящий этому объекту набор опций и присваивает их, запуская соответствующие события
Оба метода служат прослойками между форматом вашего API и форматом вашего интерфейса на JavaScript. Их использование явно говорит о том, что приходят «нечистые» сырые данные, которые нужно тонко встроить в текущий мир приложения.
Оба метода принимают набор параметров, которыми можно настроить их поведение — они описаны в документации. Здесь же хочу показать, как удобно используется assignResp для преобразования ответов сервера в конечные опции для модели.
Допустим, сервер возвращает такой объект (он так же может быть частью ответа-списка для assignChildren):
Наша модель имеет следующие опции:
Мы видим несоответствие:
Следующий код сделает нужные преобразования:
Использование (JSFiddle):
На самом деле, нет. Весь самый важный функционал был описал, но остаются ещё несколько вещей, отличающих Sqimitive от других библиотек. Ниже — кратко о некоторых из них. Остальные вы найдёте при выполнении квеста «Прочитать Zen Book».
Let's not talk about languages that suck. Let's talk about Python.
В Python, если вы объявляете объект со сложными (нескалярными) начальными значениями — вы ненароком делаете их общими для всех экземпляров этого объекта, которые не перезаписали эти поля новыми объектами. Например:
Если у JavaScript и Python и есть что-то общее — так это автоматизированное выкапывание ям, используя логически обоснованные особенности языка. JSFiddle:
Причина, если подумать, понятна: мы объявляем класс, передаём в него начальные значения-объекты, а эти значения затем копируются в новый экземпляр. А ведь, как известно, объекты в обоих языках копируются по ссылке — поэтому в новых объектах мы получаем старые ссылки на те объекты (массив в примере выше), которые мы изначально передали в extend.
Логично? Конечно. Очевидно? Не более, чем номер новой версии Windows (который, кстати, тоже может быть логичен).
Решением проблемы является присвоение сложных значений в конструкторе, но решение это неудобное — слишком часто приходится инициализировать свойства пустыми объектами и массивами.
В Sqimitive все свойства глубоко копируются (deep copy) при создании нового экземпляра. Свойства, которые не нужно копировать, задаются явно в статическом массиве _shareProps. Обычно там указываются поля, где хранится ссылка на класс — такие как _childClass, который здесь уже указан по умолчанию. Однако используется оно редко, так как совсем сложные объекты обычно проще инициализировать в init.
В Backbone, при наследовании, свойства потомка всегда перекрывают базовые. Это правильно, но в отдельно взятых случаях выливается в такой же неудобный костыль, как __super__. К примеру (JSFiddle):
MyOtherView полностью перекрыл унаследованный от MyView блок events. Решения два: либо
_mergeProps — статическое свойство-массив, где перечисляются поля, которые должны быть объединены при наследовании, а не перезаписаны. Для массивов это
Так как изначально в _mergeProps уже перечислены
Аналогичный пример со слиянием опций в потомке (JSFiddle):
И напоследок о маленькой, но любопытной фиче Sqimitive. Иногда бывает так, что callback-функция в точности совпадает с уже имеющимся методом, за исключением аргументов. К примеру, у вас есть
Это можно сделать несколькими способами:
Третий вариант кажется длиннее и поэтому есть смысл объявить глобальный алиас для этой функции как masker или даже m. Функция может быть вызвана в разных формах, но суть сводится к строке-маске, где каждый символ описывает источник аргумента (номер входного параметра), а позиция символа — номер выходного (для «замаскированной» функции).
Другой пример: нужно присвоить ответ API с сервера через
Точки в маске заменяются на порядковый номер символа, а всё что после последнего символа — не передаётся. Таким образом, здесь к assignResp будет передан только первый аргумент.
masker неявно используется в events, elEvents, on и once в форме
А вот аналогичный блок без масок — безусловно, кому-то он покажется более наглядным, и вам не обязательно их использовать
Введение в нирвану подошло к концу. Признаться, когда я вёл курсор к большой зелёной кнопке «Опубликовать», мои пальцы слегка дрожали. Оценят ли элегантность простых решений? Много ли осталось тех из нас, кто считает, что шквал веб-технологий ведёт разработчиков по лезвию ножа?
Как бы то ни было, теперь ваша очередь. Проект доступен на GitHub, исчерпывающая документация — на squizzle.me, пример простого приложения — тут. Буду рад вашим вопросам в комментариях, а исправлениям опечаток — в личке.
Увидимся по ту сторону баррикад.
Многих людей это вдохновляет. Да что там — 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 вам покажется очень знакомым, тёплым и почти ламповым.
(Здесь и далее — моя субъективная точка зрения, которая может не совпадать с вашей, Хабра, президента или Космической коалиции. Читайте на свой страх и риск, не забывайте комментировать и помните о своей карме!)
Оглавление:
- Наследование как фактор выживания
- Превращение методов в события
- Наследование на лету или прототипирование-2.0
- О важных связях с общественностью
- Вложенность — наше всё
- UnderGoodness
- Что в опциях тебе моём?
- Нерадивые родители: деревья и списки
- Представляем виды
- О бренности жизни без сохранения данных
- assignResp и _respToOpt — карта ответа API
- И это что, всё? _shareProps, _mergeProps, masker()
Наследование как фактор выживания
Если бы JavaScript был Haskel, то у него бы не было этой проблемы (
Но JavaScript — это и не Haskel, и не Си. У него было тяжёлое детство с непостоянными родителями, которые навсегда повредили его
Функции в JavaScript — это обычные значения вроде чисел и строк (т.н. first class citizen). Функции можно присваивать, удалять, копировать, даже преобразовывать в строку. Плюс к этому функция имеет неявный параметр — this — который по задумке авторов языка должен указывать на объект, который вызвал срабатывание этой функции — а никак не на объект, к которому эта функция была привязана.
Таким образом, объекты в JavaScript как бы есть, но получить контекст объекта — невозможно. Объекты в этом смысле — это те же ассоциативные массивы.
(Прошу не принимать мой сарказм близко к сердцу людям с плохим чувством юмора. Я люблю и 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
О бренности жизни без сохранения данных
Итак, интерфейс у нас нарисовался, пользователю есть, где пощёлкать клавишами. Но доволен ли он?
Конечно, нет (
Говоря по-простому, когда есть интерфейс, нам надо где-то сохранять данные. И побыстрее!
Работа с сервером — тот самый 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 в моделях нужно дублировать на уровне коллекции. Логика авторов мне не понятна и, по-моему, является просто грубой ошибкой.
Спустя энное количество клеток я, закопавшись в исходники, обнаружил, что при присвоении ответа коллекцией она создаёт модели напрямую из данных сервера, минуя их 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, пример простого приложения — тут. Буду рад вашим вопросам в комментариях, а исправлениям опечаток — в личке.
Увидимся по ту сторону баррикад.