Я хочу описать простой минималистский подход к разработке сложных JavaScript приложений. Из внешних библиотек будут использоваться только jQuery и мой js-шаблонизатор, причём из jQuery используются только
В основе подхода лежат две идеи:
Я обычно разрабатываю серверную часть приложения (причём больше даже не столько сам веб-сайт, сколько сетевые сервисы), клиентская часть это не мой основной профиль. Так что когда у меня впервые возникла задача разработки достаточно сложного одностраничного веб-приложения (примерно два года назад), я поискал готовые архитектурные подходы для таких приложений, но, к сожалению, не нашёл (хотя к тому моменту уже прошло пол года с момента доклада Nicholas Zakas «Scalable JavaScript Application Architecture»: видео, слайды).
Разработанную мной тогда архитектуру я обкатывал эти два года на разных проектах, проверял как она работает на реальных задачах. Хорошо работает. :) Поэтому я и решил написать эту статью. В процессе написания ещё раз поискал другие решения, и обнаружил доклад Nicholas Zakas, плюс ещё из прочитанного на эту тему очень понравилась статья Addy Osmani Patterns For Large-Scale JavaScript Application Architecture.
Для тех, кто уже знаком с описанной там архитектурой, кратко опишу отличия моего подхода:
Но, по большому счёту, я тогда разработал минималистский вариант этой же архитектуры.
Создаём скелет странички нашего приложения.
index.html
Добавляем глобальный диспетчер событий. События представляют из себя текстовое имя события и не обязательный хеш с параметрами.
Обычно после загрузки страницы нам необходимо выполнить какие-то действия (как минимум создать и добавить на страницу виджеты), и для единообразия мы это событие тоже обработаем в основном диспетчере.
index.html
Хеш
Универсальная часть готова, самое время определиться что же наше приложение будет делать. Чтобы продемонстрировать сложное динамическое взаимодействие между разными частями странички нам нужно будет нескольк�� активных элементов, действия которых будут отражаться в разных частях странички.
Давайте сделаем приложение, в которое можно будет добавлять/удалять фразы, а оно будет показывать количество каких-нибудь результатов по этим фразам (например количество ошибок найденное сервисом проверки правописания). Плюс суммарное количество результатов по всем фразам.
Разделим необходимую функциональность между виджетами. Чем меньше и проще получатся виджеты, тем проще будет приложение в целом, так что не удивляйтесь их небольшим размерам — это не особенность этого приложения, а рекомендуемый подход к архитектуре любых сложных приложений.
Хотя виджет владеет своей областью, он не контролирует где именно эта область находится на страничке — т.е. он не знает, видна ли она на экране, да и вообще добавлена ли в DOM, или нет. За добавление/удаление контролируемых виджетами областей на страничку отвечает внешний код (т.е. наш глобальный диспетчер событий, либо внешний виджет в случае когда одни виджеты вложены в другие).
Внутри класса виджета ему доступен абсолютный минимум внешних функций:
За редким исключением виджет должен как-то отображаться на экране. Область странички, которой он «владеет» обычно либо уже существует (и тогда в конструктор виджета передаётся её
Все остальное не принципиально. Мой стиль:
Небольшое терминологическое уточнение. Нередко JavaScript-виджетом называют комплект из HTML+CSS+JavaScript. Мои виджеты тоже можно распространять вместе с их кусками HTML и CSS, чтобы облегчить их подключение «по умолчанию» в другие приложения, но писать виджеты я предпочитаю так, чтобы в них был абсолютный минимум информации о дизайне — это упрощает и код и изменение дизайна. Поэтому в описываемых далее виджетах вы не найдёте ни слова о дизайне.
Требования этого виджета к HTML — нужна форма с одним input type=text. Всё остальное дизайнер может оформлять как угодно.
w.addphrase.js
Добавляем создание виджета при загрузке страницы. И немного кода для юзабилити — такой код должен быть именно здесь, а не в виджете, т.к. виджет не знает что в данном приложении именно он — главный элемент интерфейса и с него начинается работа.
index.html
index.html
Собственно, это всё. :) У нас есть универсальный виджет, который можно «повесить» на любые формы, и который исправно генерирует события по мере ввода пользователем фраз (мы их пока в диспетчере игнорируем, т.к. ещё нет виджета, который должен их обрабатывать).
При этом в виджете нет ни одной «капли жира» — строчки, не относящейся к его непосредственной функциональности. В каком стиле бы не реализовалась эта функциональность, все эти строчки будут в любом случае. В диспетчере, конечно, «лишние» строчки есть — если сравнивать с не модульным приложением в стиле N-летней давности, когда весь код писался одним куском. Но на самом деле эти «лишние» строчки — самое ценное в нашем приложении, т.к. именно они наглядно и просто описывают высокоуровневую логику приложения.
На примере этого виджета мы посмотрим на работу с js-шаблонами и вложенными виджетами.
Требования этого виджета к HTML — нужен js-шаблон, в который будет передана переменная
w.phrase.js
С шаблонами думаю, всё понятно. А вот работу с вложенными виджетами лучше пояснить.
Хоть виджет проверки правописания «вложен» визуально в виджет Phrase и виджет Phrase «владеет» виджетом проверки правописания (единственная ссылка на виджет проверки правописания находится в
index.html
index.html
Пара пояснений: во-первых у нас ещё нет виджета W.SpellCheck, поэтому мы пока не вызываем
На примере этого виджета мы посмотрим на работу с ajax.
Кроме того нам нужно будет показывать состояние ajax-запроса (в процессе, получен ответ). Есть два разных подхода: можно изменять DOM в диспетчере (обрабатывая события
Для его реализации приходится использовать один трюк. Дело в том, что если мы сохраним выполненный шаблон в свойство
Требования этого виджета к HTML — нужен js-шаблон, в который будут переданы переменные
w.spellcheck.js
index.html
index.html
w.sum.js
index.html
index.html
В сумме по всем файлам: 200 строк HTML+JavaScript, 5.5KB. Из них почти 50 строк — документация/комментарии.
За кадром остались вопросы обработки ошибок, тестирования, логирования и отладочного режима. Тут я ничего нового добавить не могу, всё стандартно (например смотрите доклад Nicholas Zakas «Enterprise JavaScript Error Handling»: слайды).
Исходники описываемого в статье приложения выложены на bitbucket, с пошаговыми commit-ами соответствующими статье. Так же можно посмотреть на само приложение.
Желающие покритиковать мой подход к реализации модулей могут для начала ознакомиться с моим мнением о других подходах. Плюс когда-то была небольшая дискуссия о моём js-шаблонизаторе.
Другие статьи на хабре на эту тему: Масштабируемые JavaScript приложения.
$.ready(), $.ajax() и $.proxy() — т.е. суть не в библиотеках (их тривиально заменить на предпочитаемые вами), а в самом подходе.В основе подхода лежат две идеи:
- JavaScript виджеты — небольшие модули, каждый из которых «владеет» определённой частью веб-странички (т.е. всё управление этой частью странички происходит исключительно через методы этого модуля, а не через прямую модификацию DOM — инкапсуляция). Виджет отвечает исключительно за функциональность, но не за внешний вид; поэтому прямая модификация части DOM, которым «владеет» виджет, снаружи виджета допускается — но только для чисто дизайнерских задач (для архитектуры и общей сложности приложения нет принципиальной разницы между коррекцией внешнего вида через CSS или jQuery).
- Глобальный диспетчер событий. Взаимодействие между виджетами осуществляется путём посылки сообщений глобальному диспетчеру (слабая связанность, паттерн Mediator/Посредник), а уже он принимает решение что с этим сообщением делать — создать/удалить виджеты, дёрнуть методы других виджет��в, выполнить дизайнерский код, etc. В отличие от динамического подхода к обработке событий (когда обработчики конкретного события добавляются/удаляются в процессе работы) статический диспетчер сильно упрощает понимание и отладку кода. Безусловно, есть задачи, для которых нужны именно динамические обработчики событий, но в большинстве случаев это избыточное усложнение, поэтому всё, что можно, делается статическими обработчиками.
О велосипедах
Я обычно разрабатываю серверную часть приложения (причём больше даже не столько сам веб-сайт, сколько сетевые сервисы), клиентская часть это не мой основной профиль. Так что когда у меня впервые возникла задача разработки достаточно сложного одностраничного веб-приложения (примерно два года назад), я поискал готовые архитектурные подходы для таких приложений, но, к сожалению, не нашёл (хотя к тому моменту уже прошло пол года с момента доклада Nicholas Zakas «Scalable JavaScript Application Architecture»: видео, слайды).
Разработанную мной тогда архитектуру я обкатывал эти два года на разных проектах, проверял как она работает на реальных задачах. Хорошо работает. :) Поэтому я и решил написать эту статью. В процессе написания ещё раз поискал другие решения, и обнаружил доклад Nicholas Zakas, плюс ещё из прочитанного на эту тему очень понравилась статья Addy Osmani Patterns For Large-Scale JavaScript Application Architecture.
Для тех, кто уже знаком с описанной там архитектурой, кратко опишу отличия моего подхода:
- На мой взгляд, та архитектура необходима для приложений масштаба Yahoo!, GMail, etc. но для абсолютного большинства современных достаточно сложных веб-приложений это всё-таки перебор. Избыточная гибкость этой архитектуры имеет свою цену — она увеличивает сложность приложения. Вот пример фич, в которых у меня никогда не возникало реальной необходимости:
- возможность просто заменять базовую библиотеку (напр. jQuery на Dojo)
- проверка и контроль «прав доступа» модулей к функциональности системы
- динамическое добавление/удаление модулей на странице
- по поводу динамических обработчиков событий я уже писал выше
- Это нигде не упоминается явно, но насколько я понял, в их архитектуре «модули» это практически целые большие и сложные приложения (вроде чата на странице GMail или виджета погоды на Yahoo). У меня «модули» реализуют минимально возможную функциональность, которую удаётся выделить в отдельную изолированную сущность. (Впрочем, в скринкасте Andrew Burgess «Writing Modular JavaScript» показаны такие же небольшие модули, как и у меня.) В качестве примера можно привести модуль отвечающий за форму с одним input-ом или модуль отвечающий за вывод одного значения внутри одного тэга. При этом эти модули реализуют именно бизнес-логику приложения, и не являются аналогами плагинов jQuery делающими из обычного <select> очень крутой, красивый и фичастый <select>.
- Как следствие разницы в масштабе функциональности модулей, у них модули состоят из HTML+CSS+JavaScript, а у меня большинство модулей это только JavaScript плюс документация на минимально необходимую этому модулю структуру HTML.
- Хоть я и абсолютно согласен с тем, что необходимо чётко ограничить, к какой внешней функциональности у модулей есть доступ (фактически, я об этом написал в первом абзаце статьи, к тому списку осталось только добавить диспетчер), но я считаю что самый надёжный, эффективный и безопасный код тот, которого нет. Поэтому если есть возможность заменить код, например, соглашениями, я обычно предпочитаю соглашения (хотя, безусловно, здесь тоже надо меру знать). В моём подходе нет кода, который бы ограничивал модули в доступе, но есть соглашение, куда модули могут лазить, а куда нет.
Но, по большому счёту, я тогда разработал минималистский вариант этой же архитектуры.
Начало
Создаём скелет странички нашего приложения.
index.html
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Простой минималистский пример сложного JavaScript приложения</title> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> <script type="text/javascript" src="http://powerman.name/download/js/POWER.js"></script> <!-- Здесь будем подключать виджеты --> </head> <body> <script type="text/javascript"> /* Основной код приложения */ </script> <!-- Здесь будет html-код странички и js-шаблоны --> </body> </html>
Добавляем глобальный диспетчер событий. События представляют из себя текстовое имя события и не обязательный хеш с параметрами.
Обычно после загрузки страницы нам необходимо выполнить какие-то действия (как минимум создать и добавить на страницу виджеты), и для единообразия мы это событие тоже обработаем в основном диспетчере.
index.html
/* Основной код приложения */ notice = (function(){ var w = { }; return function(e,data){ switch (e) { case 'document ready': break; default: alert('notice: unknown event "'+e+'"'); } }; })(); $(document).ready(function(){ notice('document ready') });
Хеш
w будет использоваться для хранения создаваемых объектов-виджетов — надо же их где-то хранить после создания — плюс чтобы у диспетчера (который и будет эти объекты создавать) всегда был к ним доступ.А что мы делаем-то?
Универсальная часть готова, самое время определиться что же наше приложение будет делать. Чтобы продемонстрировать сложное динамическое взаимодействие между разными частями странички нам нужно будет нескольк�� активных элементов, действия которых будут отражаться в разных частях странички.
Давайте сделаем приложение, в которое можно будет добавлять/удалять фразы, а оно будет показывать количество каких-нибудь результатов по этим фразам (например количество ошибок найденное сервисом проверки правописания). Плюс суммарное количество результатов по всем фразам.
Архитектура
Разделим необходимую функциональность между виджетами. Чем меньше и проще получатся виджеты, тем проще будет приложение в целом, так что не удивляйтесь их небольшим размерам — это не особенность этого приложения, а рекомендуемый подход к архитектуре любых сложных приложений.
- AddPhrase — виджет добавления фразы.
- Методы: нет.
- Генерирует события:
'add phrase'.
- Phrase — виджет выводящий добавленную фразу, позволяющий её выбрать (для удаления) и добавить разные обработчики этой фразы (вроде проверки правописания).
- Методы:
add_handler. - Генерирует события:
'select phrase'.
- Методы:
- SpellCheck — виджет выводящий количество ошибок в этой фразе. Так же отвечает за ajax-запрос на сервис.
- Методы: нет.
- Генерирует события
'spellcheck: started'и'spellcheck: success'.
- Sum — виджет выводящий общее количество результатов по всем фразам.
- Методы:
addиsub. - Генерирует события: нет.
- Методы:
Виджеты
Хотя виджет владеет своей областью, он не контролирует где именно эта область находится на страничке — т.е. он не знает, видна ли она на экране, да и вообще добавлена ли в DOM, или нет. За добавление/удаление контролируемых виджетами областей на страничку отвечает внешний код (т.е. наш глобальный диспетчер событий, либо внешний виджет в случае когда одни виджеты вложены в другие).
Внутри класса виджета ему доступен абсолютный минимум внешних функций:
- глобальная функция
notice()для генерации событий; - объект jQuery
$; - функции библиотеки-шаблонизатора.
За редким исключением виджет должен как-то отображаться на экране. Область странички, которой он «владеет» обычно либо уже существует (и тогда в конструктор виджета передаётся её
#id), либо генерируется виджетом на лету из js-шаблона (и тогда в конструктор передаётся #id шаблона).Все остальное не принципиально. Мой стиль:
- Виджеты находятся в отдельных файлах w.widget_name.js.
- Виджеты реализованы как классы в глобальном namespace
W, обычно один виджет — один класс:W.WidgetName. - Классы реализованы в самом простом и естественном для JavaScript виде — обычные функции-конструкторы и прототипное наследование.
- Приватные свойства и методы начинаются на подчёркивание.
- В начале виджета документируется какой ему нужен формат js-шаблона (или блока HTML); конструктор, публичные методы/свойства, генерируемые события.
- По соглашению, контролируемая виджетом область доступна через свойство объекта-виджета
.$(именно его внешний код вставляет в DOM чтобы вывести виджет на страничке).
Небольшое терминологическое уточнение. Нередко JavaScript-виджетом называют комплект из HTML+CSS+JavaScript. Мои виджеты тоже можно распространять вместе с их кусками HTML и CSS, чтобы облегчить их подключение «по умолчанию» в другие приложения, но писать виджеты я предпочитаю так, чтобы в них был абсолютный минимум информации о дизайне — это упрощает и код и изменение дизайна. Поэтому в описываемых далее виджетах вы не найдёте ни слова о дизайне.
AddPhrase
Требования этого виджета к HTML — нужна форма с одним input type=text. Всё остальное дизайнер может оформлять как угодно.
w.addphrase.js
/* * <form id="addphrase"><input type=text></form> * * w = new W.AddPhrase('addphrase'); * * -> 'add phrase', phrase * */ var W; W = W || {}; W.AddPhrase = function(id){ this.$ = $( '#'+id ); this._input = this.$.find('input'); // cache this.$.submit( $.proxy(this,'_onsubmit') ); }; W.AddPhrase.prototype._onsubmit = function(){ notice('add phrase', { phrase: this._input.val() }); this._input.val(''); return false; };
Добавляем создание виджета при загрузке страницы. И немного кода для юзабилити — такой код должен быть именно здесь, а не в виджете, т.к. виджет не знает что в данном приложении именно он — главный элемент интерфейса и с него начинается работа.
index.html
<!-- Здесь будем подключать виджеты --> <script type="text/javascript" src="w.addphrase.js"></script> ... <!-- Здесь будет html-код странички и js-шаблоны --> <form id="addphrase"> Добавить фразу: <input type=text> </form>
index.html
/* Основной код приложения */ notice = (function(){ var w = { addphrase: null }; return function(e,data){ switch (e) { case 'document ready': w.addphrase = new W.AddPhrase('addphrase'); $('#addphrase input').focus(); break; case 'add phrase': break;
Собственно, это всё. :) У нас есть универсальный виджет, который можно «повесить» на любые формы, и который исправно генерирует события по мере ввода пользователем фраз (мы их пока в диспетчере игнорируем, т.к. ещё нет виджета, который должен их обрабатывать).
При этом в виджете нет ни одной «капли жира» — строчки, не относящейся к его непосредственной функциональности. В каком стиле бы не реализовалась эта функциональность, все эти строчки будут в любом случае. В диспетчере, конечно, «лишние» строчки есть — если сравнивать с не модульным приложением в стиле N-летней давности, когда весь код писался одним куском. Но на самом деле эти «лишние» строчки — самое ценное в нашем приложении, т.к. именно они наглядно и просто описывают высокоуровневую логику приложения.
Phrase
На примере этого виджета мы посмотрим на работу с js-шаблонами и вложенными виджетами.
Требования этого виджета к HTML — нужен js-шаблон, в который будет передана переменная
phrase, и в котором должен быть элемент с классом handlers — в него будут добавляться виджеты с результатами.w.phrase.js
/* * <script type="text/x-tmpl" id="tmpl_phrase"> * <div> * <%= phrase %> * <span class="handlers"></span> * </div> * </script> * * w = new W.Phrase('tmpl_phrase', 'some phrase'); * w.add_handler( new W.SomeHandler() ); * * -> 'select phrase', phrase * */ var W; W = W || {}; W.Phrase = function(tmpl, phrase){ this.$ = $( POWER.render(tmpl, { 'phrase': phrase }) ); this._phrase= phrase; this._w = []; this._h = this.$.find('.handlers'); // cache this.$.click( $.proxy(this,'_onclick') ); }; W.Phrase.prototype.add_handler = function(w_handler){ this._w.push( w_handler ); this._h.append( w_handler.$ ); }; W.Phrase.prototype._onclick = function(){ notice('select phrase', { phrase: this._phrase }); };
С шаблонами думаю, всё понятно. А вот работу с вложенными виджетами лучше пояснить.
Хоть виджет проверки правописания «вложен» визуально в виджет Phrase и виджет Phrase «владеет» виджетом проверки правописания (единственная ссылка на виджет проверки правописания находится в
this._w, в то время как все глобальные виджеты удерживаются ссылками в хеше w), тем не менее, как вы видите, Phrase не создаёт объект виджета проверки правописания, а получает его параметром. Это внедрение зависимости (dependency injection) для увеличения гибкости — позволяет избежать зависимости между конкретными виджетами и упрощает создание объектов виджетов: дело в том, что обычно все необходимые данные для вызова конструктора вложенного виджета есть только у диспетчера, и такой подход позволяет избежать прозрачной передачи этих параметров для конструкторов вложенных виджетов через конструкторы внешних виджетов.index.html
<!-- Здесь будем подключать виджеты --> ... <script type="text/javascript" src="w.phrase.js"></script> ... <!-- Здесь будет html-код странички и js-шаблоны --> ... <div id="phrases"> <p> <script type="text/x-tmpl" id="tmpl_phrase"> <div> <%= phrase %> <span class="handlers"></span> </div> </script> </div>
index.html
/* Основной код приложения */ notice = (function(){ var w = { ... phrase: {} }; return function(e,data){ ... case 'add phrase': if(data.phrase in w.phrase) break; w.phrase[data.phrase] = new W.Phrase('tmpl_phrase', data.phrase); $('#phrases').append( w.phrase[data.phrase].$ ); break; case 'select phrase': w.phrase[data.phrase].$.remove(); delete w.phrase[data.phrase]; $('#addphrase input').focus(); break;
Пара пояснений: во-первых у нас ещё нет виджета W.SpellCheck, поэтому мы пока не вызываем
w.phrase[phrase].add_handler( new W.SpellCheck() ); во-вторых после клика на виджете Phrase (для удаления фразы) теряет фокус строка ввода фразы, и его надо вернуть.SpellCheck
На примере этого виджета мы посмотрим на работу с ajax.
Кроме того нам нужно будет показывать состояние ajax-запроса (в процессе, получен ответ). Есть два разных подхода: можно изменять DOM в диспетчере (обрабатывая события
'spellcheck: started' и 'spellcheck: success'), а можно прошить все состояния внутри js-шаблона и многократно выполнять/подменять шаблон. Второй способ немного сложнее, но т.к. у нас пример, то стоит реализовать именно его.Для его реализации приходится использовать один трюк. Дело в том, что если мы сохраним выполненный шаблон в свойство
.$ (как в виджете Phrase), то мы в дальнейшем уже не сможем его подменить — дело в том, что значение .$ будет добавлено куда-то в DOM (мы внутри виджета не знаем куда), и если мы просто заменим значение в .$ новым шаблоном, то в DOM останется ссылка на старое значение .$ и визуально на страничке ничего не изменится (а этот виджет окончательно потеряет доступ к «своей» части странички). Чтобы этого избежать в .$ сохраняется любой пустой тэг-контейнер (напр. span или div), а уже в него помещается результат выполнения шаблона. К сожалению, при этом на страничке появляется «неожиданный» для дизайнера тэг, но как эту проблему решить более элегантно я не придумал.Требования этого виджета к HTML — нужен js-шаблон, в который будут переданы переменные
status (возможные значения "started" и "success") и spellerrors (если значение status=="success").w.spellcheck.js
/* * <script type="text/x-tmpl" id="tmpl_spellcheck"> * <% if (status == 'started') { %> * Проверяю… * <% } else { %> * Найдено <%= spellerrors %> ошибок. * <% } %> * </script> * * w = new W.SpellCheck('tmpl_spellcheck', 'some phrase'); * * -> 'spellcheck: started', phrase * -> 'spellcheck: success', phrase, spellerrors * */ var W; W = W || {}; W.SpellCheck = function(tmpl, phrase){ this.$ = $('<span>'); this._tmpl = tmpl; this._phrase= phrase; this._load(); }; W.SpellCheck.prototype._load = function(){ $.ajax({ url: 'http://speller.yandex.net/services/spellservice.json/checkText', data: { text: this._phrase }, dataType: 'jsonp', success: $.proxy(this,'_load_success') }); this.$.html( POWER.render(this._tmpl, { status: 'started' }) ); notice('spellcheck: started', { phrase: this._phrase }); } W.SpellCheck.prototype._load_success = function(data){ this.$.html( POWER.render(this._tmpl, { status: 'success', spellerrors: data.length }) ); notice('spellcheck: success', { phrase: this._phrase, spellerrors: data.length }); };
index.html
<!-- Здесь будем подключать виджеты --> ... <script type="text/javascript" src="w.spellcheck.js"></script> ... <!-- Здесь будет html-код странички и js-шаблоны --> ... <div id="phrases"> ... <script type="text/x-tmpl" id="tmpl_spellcheck"> <% if (status == 'started') { %> (проверяю…) <% } else { %> (найдено <%= spellerrors %> ошибок) <% } %> </script> </div>
index.html
case 'add phrase': ... w.phrase[data.phrase].add_handler(new W.SpellCheck('tmpl_spellcheck', data.phrase)); ... break; case 'spellcheck: started': break; case 'spellcheck: success': break;
Sum
w.sum.js
/* * <span id="sum"></span> * * w = new W.Sum('sum'); * w.add(5); * w.sub(3); * */ var W; W = W || {}; W.Sum = function(id){ this.$ = $( '#'+id ); this.$.html(0); }; W.Sum.prototype.add = function(n){ this.$.html( parseInt(this.$.html()) + n ); }; W.Sum.prototype.sub = function(n){ this.$.html( parseInt(this.$.html()) - n ); };
index.html
<!-- Здесь будем подключать виджеты --> ... <script type="text/javascript" src="w.sum.js"></script> ... <!-- Здесь будет html-код странички и js-шаблоны --> ... <p><b>Итого: <span id="sum"></span> ошибок.</b></p>
index.html
/* Основной код приложения */ notice = (function(){ var w = { ... sum: null }; var spellerrors = {}; return function(e,data){ ... case 'document ready': ... w.sum = new W.Sum('sum'); break; case 'select phrase': ... if(data.phrase in spellerrors){ w.sum.sub(spellerrors[data.phrase]); delete spellerrors[data.phrase]; } break; case 'spellcheck: success': w.sum.add(data.spellerrors); spellerrors[data.phrase] = data.spellerrors; break;
Итого
В сумме по всем файлам: 200 строк HTML+JavaScript, 5.5KB. Из них почти 50 строк — документация/комментарии.
За кадром остались вопросы обработки ошибок, тестирования, логирования и отладочного режима. Тут я ничего нового добавить не могу, всё стандартно (например смотрите доклад Nicholas Zakas «Enterprise JavaScript Error Handling»: слайды).
Дополнительная информация
Исходники описываемого в статье приложения выложены на bitbucket, с пошаговыми commit-ами соответствующими статье. Так же можно посмотреть на само приложение.
Желающие покритиковать мой подход к реализации модулей могут для начала ознакомиться с моим мнением о других подходах. Плюс когда-то была небольшая дискуссия о моём js-шаблонизаторе.
Другие статьи на хабре на эту тему: Масштабируемые JavaScript приложения.
