Для демонстрации как работает идеология Jiant — реализовал ToDoMVC, проект, созданный для оценки различных MVC фреймворков. Jiant не MVC фреймворк, а скорее подход к разработке с набором вспомогательных инструментов. На разработку у меня ушло порядка 8 часов с учетом чтения и понимания спецификации, изучения референтной реализации, localStorage, с которым не имел дела (очень простая штука) ну и всего прочего. Не знаю много это или мало, но вот столько. Результаты лежат по адресу: github.com/vecnas/todomvc-jiant. В Chrome и Firefox работает прямо с файловой системы, в IE — с сервера.
Спецификация
github.com/addyosmani/todomvc/wiki/App-Specification — спецификация на английском. Кратко изложу ключевые пункты здесь.
Нужно разработать инструмент для управления задачами (ToDo), следующие пункты должны быть реализованы:
- Хранение. Все задачи и их состояние должны сохраняться в локальном хранилище (HTML5 localStorage) и восстанавливаться при например перезапуске браузера.
- Навигация. Приложение должно поддерживать хэш-навигацию в браузере, более детально см. оригинал. Там задан формат навигации, но у Jiant-а немного отличный от этого формат, связано это с возможность задавать навигационные узлы и затем возвращаться к ним. Управляется кнопками на панели навигации. Есть три состояния: показать все, показать активные и показать завершенные
- Новые задачи. Вводятся заполнением поля ввода наверху страницы и нажатием клавиши Enter
- Массовые манипуляции. Специальная кнопка отмечает все задачи как выполненные или не выполненные. Состояние кнопки также синхронизируется с текущим состоянием всех задач. То есть если мы добавим новую когда все уже выполнены — индикатор перестанет показывать что все сделано
- Задача. Представляет три интерактивных элемента — переключатель сделано-не сделано работает по щелчку, кнопку удаления задачи, и по двойному щелчку на названии — дает возможность редактировать текст задачи
- Редактирование. Открывается поле ввода, остальные элементы прячутся, по нажатию Enter или потере фокуса — сохраняет текст, по нажатию Escape — отменяет все изменения, если при сохранении текст пустой — удаляет задачу
- Счетчик активных задач. Показывает количество текущих активных задач, синхронизируется на любые изменения, текст должен быть грамотным («1 задача», но «2 задачи»)
- Очистить завершенные. Показывает количество завершенных задач, удаляет их по щелчку и показывается только если есть хотя бы одна завершенная задача
- Панель со счетчиками и кнопками навигации показывается только если есть хотя бы одна задача
На входе выдан шаблон проекта, содержащий html код и некоторые интерактивные скрипты, а также все стили.
Структура Jiant проекта
На текущий момент идеальная структура выглядит следующим образом:
- Файл определения приложения — объявление json переменной, описывающей API приложения
- «Зажигание» — это вызов bindUi где-либо где удобно пользователю
- Набор «плагинов» — логики приложения, каждая из которых инкапсулирована в своей песочнице и работает через API приложения
Начальное проектирование
Первая стадия в подходе Jiant это максимально абстрактное проектирование. Самое важное и возможно непривычное (по-крайней мере для меня самого) — это полностью абстрагироваться от того как все будет реализовываться. Создаю первую версию описания приложения, глядя на описание заказчика. Здесь я излагаю первые соображения, после чтения спецификации, а не подготовленные по результатам проекта идеальные рассуждения. То есть все жизненно, как оно происходит.
Состояния
Раз нужна хэш навигация, значит у приложения будут состояния. Перечисляем их, прямо берем список из спецификации, не надо думать:
states: { "": { go: function () {}, start: function(cb) {}, end: function(cb) {} }, active: { go: function () {}, start: function(cb) {}, end: function(cb) {} }, completed: { go: function () {}, start: function(cb) {}, end: function(cb) {} } }
Пояснение: согласно формату, если переменная описания приложения содержит секцию states — Jiant загружает перечисленные в ней состояния и реализует для каждого три метода: go, start, end. go служит для перехода в состояние, start и end для реакции на начало или конец состояния. Наиболее краткая запись выглядит так:
states: { "": {}, active: {}, completed: {} }
Но в этом случае у нас не будет автозавершения в IDE, так что для собственного удобства и лучшего документирования я использую первую нотацию. Пустое состояние соответствует «неопределенным» ситуациям — например, когда просто загружено окно браузера без любых хэшей.
Все, это все что нужно для работы хэш-навигации. Весь функционал будет добавлен при инициализации приложения Jiant’ом.
Чтобы полностью закрыть тему проектирования состояний — можно сделать одно состояние с двумя параметрами, например:
states: { "": { go: function(showActive, showCompleted) {} } }
И это тоже будет работать. Но, так как у нас по контролу на состояние, то из чисто утилитарных соображений — завести три состояния кажется удобней. Опять же это первое интуитивное решение. Кстати, в итоговой версии так и остались эти состояния.
События
Теперь определяем события уровня приложения, снова абстрактно. Интуитивно кажется что следующий список правилен:
events: { todoAdded: { fire: function(todo) {}, on: function(cb) {} }, todoRemoved: { fire: function(todo) {}, on: function(cb) {} }, todoStateChanged: { fire: function(todo) {}, on: function(cb) {} } }
Единственное размышление вызвало последнее событие — надо ли разбить его на два — на завершение или ре-активацию todo. Снова весь код поддержки событий работает внутри Jiant, пользователю нужно только определить абстрактный список событий и воспользоваться им. Каждое событие имеет два метода — достаточно очевидных. Функция cb (callback, параметр метода on) принимает в точности те же аргументы что вызов fire.
Интерфейс
Теперь нужно определить визуальную часть приложения. Список элементов как обычно просто копируем из описания заказчика, как удобно. После того как написал так:
views: { main: { batchToggleStateCtl: ctl, newTodoTitleInput: input, todoList: container }, controls: { activeCountLabel: label, clearCompletedCtl: ctl, completedCountLabel: label, showAllCtl: ctl, showActiveCtl: ctl, showCompletedCtl: ctl } },
— полез в html и обнаружил что идентификаторы на элементы уже прописаны, структура задана, поэтому логичней будет просто ее применить. Итог:
views: { header: { newTodoTitleInput: input }, main: { batchToggleStateCtl: ctl, todoList: container }, footer: { activeCountLabel: label, clearCompletedCtl: ctl, completedCountLabel: label, showAllCtl: ctl, showActiveCtl: ctl, showCompletedCtl: ctl } }
и эта структура, созданная на 10й минуте проектирования, уже не менялась до конца проекта.
Шаблоны
Исходя из спецификации видим один крайне динамичный элемент — визуальное представление задачи. Количество их разное, они добавляются и убираются, значит для этого согласно идеологии Jiant просто необходимо использовать шаблон, определим его:
templates: { tmTodo: { deleteCtl: ctl, editCtl: ctl, stateMarker: label, toggleStateCtl: ctl, titleInput: input, hiddenInEditMode: collection, titleLabel: label } }
Снова дабы не утруждать лишний раз мозг — просто переносим все из описания заказчика, по принципу «кнопка удаления задачи» — deleteCtl.
Здесь следует кое-что сказать о шаблонах. Первое, можно увидеть что для шаблона определены поля — они во-первых проверяются на валидность при запуске приложения, во-вторых привязываются после создания элемента из шаблона. Каждый шаблон имеет два метода: parseTemplate, parseTemplate2Text, принимающие параметры для подстановки. Шаблон не содержит никакой логики, только подстановку значений, и это намеренно — место логики в javascript коде. Позднее в приложении появился еще один шаблон, введенный больше для того чтобы показать как подставляются значения:
templates: { .... itemsLeft: {}, itemsLeft1: {} }
реализация:
<div id="itemsLeft1" style="display: none;"> <strong>!!count!!</strong> item left </div> <div id="itemsLeft" style="display: none;"> <strong>!!count!!</strong> items left </div>
и использование:
tm.parseTemplate2Text({count: count})
Модель данных пока не проектируем, об этом ниже. Хочется поскорей запустить и увидеть что ничего не ломается, для этого нужен стартер.
Стартер
Исходя из идеальной структуры, стартер приложения поместим в app.js файл (имеющийся в базовом шаблоне «от заказчика») и его код следующий:
jQuery(function() { jiant.bindUi("", todomvcJiant, true); });
Так как код html уже задан заказчиком (и менять не хочется, чтобы не менять css), то префикс здесь используется пустой (первый параметр), переменную todomvcJiant определяем в файле определения приложения (она содержит views, templates, states, events) и включаем режим разработки.
Реализация в HTML
Ну все, у нас есть только файл определения и стартер, никакой логики, но уж больно хочется запустить приложение. Запускаю html в Chrome и вижу:
- Алерт с сообщением от Jiant, в котором написано «No history plugin and states configured. Don’t use states or add $.History plugin»
- Еще один алерт с сообщением о нереализованных элементах интерфейса
- И повтор текста второго алерта в консоли: non existing object referred by class under object id, non existing object referred by id, check stack trace for details, expected obj id:
Добавляю history — идет в комплекте. Теперь остается реализовать абстрактное определение на уровне html, например так:
<section id="main"> <input id="toggle-all" class="batchToggleStateCtl" type="checkbox" style="display: none;"> <label for="toggle-all">Mark all as complete</label> <ul id="todo-list" class="todoList"> </ul> </section>
— идентификаторы уже расставлены, остается добавить классы на нужные элементы. Реализация шаблона:
<div id="tmTodo" style="display: none;"> <li class="stateMarker"> <div class="view hiddenInEditMode"> <input class="toggle toggleStateCtl" type="checkbox"> <label class="editCtl titleLabel"></label> <button class="destroy deleteCtl"></button> </div> <input class="edit titleInput" value=""> </li> </div>
Следует заметить что в отличие от общей практики помещать шаблоны в <script type=«someUnreadableTypeToFoolBrowser»>, Jiant использует правильную html структуру, это связано с тем что внутри script тэга нет DOM модели и невозможно провести валидацию привязки полей шаблона к реализации.
Наконец, Jiant перестал сообщать о несвязанных элементах, абстрактная модель приложения привязана к реализации и готова к использованию.
Плагины
Следуя идеологии Jiant — любая логика добавляется плагинами. Если появляется новая вьюшка или состояние-событие, мы добавляем ее/его к файлу описания приложения и реализацию к html. То есть процесс расширения приложения всегда постоянный и контролируемый, что крайне важно для крупных разработок, начинающихся с малого.
Модель
Встает вопрос — нужна ли данному приложению централизованная модель данных? Теоретически каждый плагин может содержать свою модель и синхронизировать ее на основе происходящих в приложении событий. На практике я для сравнения написал такой вариант и как и ожидалось — получилось много ненужного и повторяющегося кода. Поэтому реализуем модель (практически можно было бы поместить реализацию прямо в json файл описания приложения, но тогда его станет сложнее читать, поэтому вынесем отдельным плагином, следуя идеологии), model.js:
todomvcJiant.model.todo = (function($, app) { var todos = []; return { add: function(title, completed) { var todo = {title: title, completed: completed ? true : false}; todos.push(todo); app.events.todoAdded.fire(todo); return todo; }, remove: function(todo) { todos = $.grep(todos, function(value) {return value != todo;}); app.events.todoRemoved.fire(todo); return todo; }, getAll: function() { return todos; }, getCompleted: function() { return $.grep(todos, function(value) {return value.completed}); }, getActive: function() { return $.grep(todos, function(value) {return !value.completed}); } } })($, todomvcJiant);
Простейшая реализация на основе массива. Стоит заметить только что здесь мы запускаем некоторые события приложения. В данном случае добавление модели в json описание проекта носит косметический характер (например, там нет метода getActive()), Jiant пока никак не занимается моделями данных.
Реализация плагинов
Дальше, для каждого элемента логики из спецификации — пишем свой плагин и добавляем его. В любой момент времени у нас все работает, функционал наращивается, не задевая остальное. Ниже пара примеров, комментарий прямо в коде
stateCtls.js
jiant.onUiBound(function($, app) { // плагин регистрируется на событие когда API приложения проинициализировано var ctlsView = app.views.footer, ctls = { "showActiveCtl": app.states.active, // просто используем ссылки на состояния "showCompletedCtl": app.states.completed, "showAllCtl": app.states[""] }; $.each(ctls, function(key, state) { ctlsView[key].click(function() { state.go(); }); state.start(function() { // состояние может включаться при переходе на него или при начальной загрузке страницы ctlsView[key].addClass("selected"); }); state.end(function() { ctlsView[key].removeClass("selected"); }); }); });
footerVisibility.js
jiant.onUiBound(function($, app) { app.events.todoAdded.on(updateView); // подписываемся на событие app.events.todoRemoved.on(updateView); function updateView() { // конкретно добавленное todo не интересует, так как все время проверяем текущее состояние модели app.model.todo.getAll().length > 0 ? app.views.footer.show() : app.views.footer.hide(); } });
Аналогично добавляются другие плагины, используя onUiBound. Когда плагину требуется новый функционал API — создаем абстрактное объявление и реализацию. В данном случае в большинстве плагинов получилось что-то вроде глобального события smthChanged в ответ на которое плагины обновляют свой статус, но это частное совпадение.
Стоит заметить что html используется только как html, никаких кастом атрибутов или тэгов. Вся логика работы с UI написана на jQuery функциях.
Забавный факт — не понадобилось вводить идентификатор объекта. Вся работа ведется через прямые ссылки на объекты. Ссылка на UI реализацию объекта todo сохраняется как поле объекта todo, внутри todoRenderer.js и никем за его пределами не используется, порядок сохраняется внутри модели.
PS
Уже когда все написал на последней строчке понял что после изменения текста задачи — новый текст не сохраняется в localStorage, пока не произойдет какое-нибудь из уже имеющихся событий. Чтобы исправить, добавил новое событие todoTitleModified, кидается в редакторе после установки нового текста (todoRenderer.js) и подписка на событие в модуле сохранения (persistence.js).
ссылка на оригинал статьи http://habrahabr.ru/post/176617/
Добавить комментарий