Простая минималистская реализация сложных JavaScript приложений

  • Tutorial
Я хочу описать простой минималистский подход к разработке сложных JavaScript приложений. Из внешних библиотек будут использоваться только jQuery и мой js-шаблонизатор, причём из jQuery используются только $.ready(), $.ajax() и $.proxy() — т.е. суть не в библиотеках (их тривиально заменить на предпочитаемые вами), а в самом подходе.

В основе подхода лежат две идеи:
  1. JavaScript виджеты — небольшие модули, каждый из которых «владеет» определённой частью веб-странички (т.е. всё управление этой частью странички происходит исключительно через методы этого модуля, а не через прямую модификацию DOM — инкапсуляция). Виджет отвечает исключительно за функциональность, но не за внешний вид; поэтому прямая модификация части DOM, которым «владеет» виджет, снаружи виджета допускается — но только для чисто дизайнерских задач (для архитектуры и общей сложности приложения нет принципиальной разницы между коррекцией внешнего вида через CSS или jQuery).
  2. Глобальный диспетчер событий. Взаимодействие между виджетами осуществляется путём посылки сообщений глобальному диспетчеру (слабая связанность, паттерн 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 приложения.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 42

    +10
    Мне кажется что ваш notice очень быстро превратится в непонятную кашу.

    Недавно впервые столкнулся с большим количеством js в проекте и разделил его на 3 части:
    — модули
    — обработчики
    — вспомогательные функции

    Дальше написал метод автозагрузки обработчиков, который может выполнить набор функций в объекте.

    Обработчики хранятся в отдельном объекте и делятся на группы, при загрузке страницы выполняется autoLoad, набор функций для всего сайта, далее при опр. действиях, либо на опр. страницах загружаются дополнительные обработчики.

    Вот код который выполняет все функции в объекте.

    Project.load = function (obj) {
      for (var handler in obj) {
        var load = obj[handler]();
    
        if (!load) continue;
    
        if (load.init) {
          load.init();
        } else {
          Project.init(load);
        }
      }
    }
    


    Скоро напишу статью об этом.
      +2
      notice() в кашу не превращается, с чего бы? Обработка любого события в среднем занимает 3 строчки. Он может быть очень большим, это да. Но во-первых большой notice() != сложный notice(), его сложность практически не увеличивается от добавления новых событий. А во-вторых этот, даже большой, notice() — является почти идеальным изложением высокоуровневой логики всего приложения!

      В этом его громадное преимущество перед динамической подпиской на события внутри модулей — либо вы чётко и явно видите что в какие моменты происходит в системе, либо всё это происходит неявно, под капотом. Явное всегда лучше неявного. К сожалению, иногда без динамической подписки на события обойтись нельзя, но эти ситуации бывают намного реже, чем принято думать.
        +2
        А для чего это нужно? Приведите пожалуйста пример проблемы которую удается решить этим, я сейчас не говорю о проблеме структурирования кода, это есть во всех ОО подходах к JS и у каждого он свой.

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

        Для отладки ведь можно добавлять console.log, а в готовом файле их просто убрать, тут же базируется вся архитектура на этом.
          0
          Проблемы? При чём тут конкретные проблемы, это простая разница между явным и не явным, и явное всегда лучше не явного (явное не всегда возможно, но когда возможно — всегда лучше).

          Динамическая подписка на события автоматически означает несколько вещей, которые могут капитально осложнить отладку и понимание работы системы в целом:
          • Нет полного списка всех возможных событий. Любой модуль может в любой момент начать генерировать новый вид событий, а другой модуль может начать их слушать. В результате без изучения всего кода всех модулей вы никогда не выясните, какие же вообще события бывают в вашей системе.
          • Т.к. модули могут подписываться и отписываться от событий, то вы не можете быть уверены, что модуль гарантированно получит нужное событие. И если он не делает то, что должен был сделать после отправки этого события, то отладка может быть… интересной.
          • Иногда возникают проблемы из-за того, чтобы несколько динамически зарегистрированных обработчиков события выполняются не в том порядке.
          • и т.д.
          Фактически, динамические обработчики моментально превращают вашу систему в неконтролируемый набор хаотично общающихся объектов. Если эти объекты пишутся очень аккуратно, с учётом всех возможных ошибок и нюансов, то всё отлично работает. Ты просто добавляешь новый модуль, и оно само его на лету подхватывает и как-то колбасится. Что и как там внутри происходит — никто не знает, но всё работает. Если аккуратно написано. А вот если не работает — отладка будет весёлой.

          Я знаю о чём говорю — моя текущая система написана именно в таком стиле — только масштабы другие, не одностраничный js, а куча сетевых сервисов в кластере, которые динамически друг друга находят и общаются. Так вот, несмотря на то, что эта моя система отлично работает, я предпочитаю видеть в коде статический диспетчер событий до тех пор, пока это возможно — ибо это намного проще.
            0
            Вот например модуль всплывающего окна, почему он был бы лучше используя вашу технологию?

            Не подумайте пожалуйста что я тут пытаюсь вас потролить, просто мне действительно не понятно какие плюсы мне это дает на практике.

            MyProject.module.popUp = function (data) {
              var $pp = $('<div class="project-popup"><a href="#" class="project-popup_close">Закрыть</a>'),
            		  $bg = $('<div class="project-popup_backbg">');
            
              function getBgDimenstions() {
                return {
                  x: $(document).width(),
                  y: $(document).height()
                }
              }
            
              function getPpPosition() {
                return {
                  left: $(window).width() / 2 - $pp.width() / 2,
                  top: $(window).scrollTop() + ($(window).height() / 2) - $pp.height() / 2
                }
              }
            
              function setPositions() {
                var ppPos = getPpPosition(),
            			  bgDim = getBgDimenstions();
            
                $pp.css({ 'left': ppPos.left, 'top': ppPos.top });
                $bg.width(bgDim.x).height(bgDim.y);
              }
            
            
              return {
                isBuild: false,
            
                el: {
                  pp: $pp,
                  bg: $bg
                },
            
                build: function () {
                  $('body').append($bg).append($pp.append(data));
            
                  var SELF = this;
            
                  $bg.click(function () {
                    SELF.hide();
                  });
            
                  $pp.find('.project-popup_close').click(function () {
                    SELF.hide();
                    return false;
                  });
                },
            
                show: function () {
                  if (!this.isBuild) {
                    this.build();
                    this.isBuild = true;
                  }
            
                  $('body').addClass('project-popup-overflow');
            
                  setPositions();
            
                  $pp.fadeIn();
                  $bg.fadeTo(300, 0.7);
            
                  $(window).bind('resize.popUp', function () {
                    setPositions();
                  });
                },
            
                hide: function () {
                  $pp.fadeOut();
            
                  $bg.fadeOut('normal', function () {
                    $('body').removeClass('project-popup-overflow');
                  });
            
                  $(window).unbind('resize.popUp');
                },
            
                destroy: function () {
                  $pp.remove();
                  $bg.remove();
                }
              }
            }
            
              +1
              Статья об архитектуре. Т.е. о том, как разделить функциональность приложения между модулями, и как потом их объединить чтобы они совместно реализовали функциональность полного приложения. В каком стиле написаны эти модули, какими библиотеками пользуются, и как они делают свою часть работы — с точки зрения описанной в статье архитектуры не имеет значения, делайте как Вам удобно.

              Поэтому Ваш вопрос не имеет смысла — имея один модуль его никак нельзя «улучшить» описанным в статье подходом. Улучшить (потенциально, конечно) можно архитектуру приложения, для чего нужно видеть не один модуль, а все, плюс код «ядра», которых их объединяет и обеспечивает взаимодействие между этими модулями. При этом реализация самих модулей может практически не измениться, смотря насколько хорошо функциональность приложения разделена между модулями и насколько они изолированы друг от друга.
                –2
                А вы можете поделится живым примером, где используются тысячи строк js?
          0
          А если есть несколько реализаций одного и того же виджета, и надо в зависимости от ситуации подгружать определенную реализацию. Вы предлагаете все эти реализации впихнуть в notice?
          Так же при каждом чихе надо лезть в этот самый notice и там что-то делать, это очень неудобно.
            0
            notice() занимается обработкой событий, а не подгрузкой конкретных реализаций. notice() знает, что при возникновении события A нужно создать новый объект класса B и добавить его в определённое место на страничке. Конкретная реализация класса B выбирается в других местах — либо на этапе подгрузки скриптов с сервера, либо класс B это на самом деле фабрика, и он сам выбирает подходящую реализацию в момент создания объекта.
              +1
              Я не говорю что у вас плохая или неправильная архитектура, я с ней не работал, но с первого взгляда мне это кажется неудобным, и как писали выше приведет к каше, когда количество модулей будет приблежаться к тысяче (а вернее в несколько раз больше, потому как согласно вашей архитектуре каждый модуле будет разделен на несколько более простых виджетов).

              Вот тут идет вполне конкретная реализация.
              case 'spellcheck: success':
              w.sum.add(data.spellerrors);
              spellerrors[data.phrase] = data.spellerrors;
              break;


              Я не совсем понимаю почему обработчик события должен выноситься из модуля (виджета), в какой-то общий компонент. На мой взгляд, все что должен сделать notice: поймать событие и оповестить всех кто на него подписан.
              Ведь по сути на тот же case 'spellcheck: success' могут реагировать несколько виджетов, получается, что придобавлении нового модуля который работает с этим кейсом надо лезть в notice и что-то допедаливать. Изменилась концепция модуля — лезем в notice, переписываем.

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

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

                0
                Я с трудом представляю тысячу разных модулей (не тысячу объектов одного класса, а именно тысячу разных классов) на одной страничке. Фактически это означает тысячу разных элементов UI, с которыми должен взаимодействовать пользователь. Вам этого пользователя не жалко? :) Я не уверен, что можно выделить тысячу таких виджетов даже на страничке GMail. Для таких больших проектов, как я и писал в статье, нужна полноценная архитектура предложенная Nicholas Zakas. Суть статьи в том, что большинство сложных приложений всё-таки не до такой степени сложны как GMail, и для них будет проще и нагляднее использовать описанный минималистский вариант этой архитектуры.
                  0
                  Тысяча модулей — легко в SAAS проектах.
                  Кстати GMail в принципе не особо большой проект и достаточно простой.
                  Если рассматривать как подход для более простых проектов, тогда это перетекает больше в вопрос идеологии, кому что больше по душе. Подойдет практически любая нормально структурированная архитектура.
        +2
        Чем не устроил backbone.js?
          0
          Backbone.js не было 2 года назад, когда автор разработал свой фреймворк. И кстати у backbone нет глобального диспетчера событий (а очень не помешал бы).
            0
            что вы подразумеваете под «глобальным» диспетчером событий?
              0
              Диспетчер через который проходят все события приложения, неважно из какого объекта они запущены (triggered).
                0
                $(window).trigger('my_custom_event', { foo: 'bar' });
                
                $(window).bind('my_custom_event', myCustomEventHandler);
                
                function myCustomEventHandler(){ doStuff(); }


                Подобное реализуется любой библиотекой уже много лет.
                  0
                  Это не отлавливает нативные события backbone.
              +1
              Зачем? backbone.view легко расширяется до обработки любых событий в документе, например генерируемых jquery.ui.
              Вообще не понимаю, зачем реализовывать свою систему событий вместо предоставляемой jquery.
                +1
                Я выше в комментариях объяснял, зачем. Если в какой-то момент понадобятся динамические обработчики событий, то, разумеется, нет смысла что-то своё изобретать, вполне подойдут события jQuery. Но статический диспетчер всё сильно упрощает, поэтому (пока его возможностей хватает) нет смысла усложнять систему без нужды.
                  0
                  Скажите, а насколько легко вы в процессе принимаете решения о способе реализации тех или иных фич? Не возникает ли сложностей с выбором из пяти альтернативных вариантов реализации?
                  И правильно ли я понял, что предлагаемая вами концепция виджетов чем-то похожа на jquery.ui, когда на один элемент можно навесить и draggable, и resizable и пр.?
                    0
                    Принимать решения всегда непросто. :) Я стараюсь писать код как можно проще, что в свою очередь обычно означает что не будет универсальным и типовым. Т.е. из N вариантов приходится выбирать каждый раз разные, потому что ситуации/приложения тоже каждый раз разные, и самым простым решением в одной конкретной ситуации оказывается не то же решение, что в другой ситуации.

                    Могу привести пример такой ситуации даже с ключевыми элементами архитектуры: в некоторых приложениях проще оказывается держать в модулях/виджетах только логику, а все дизайнерские/оформительские действия выполнять в notice(); а в других приложениях проще писать модули с жёстким ограничением что в их часть DOM никто снаружи не лазит, и все дизайнерские/оформительские операции над модулем реализуются у него внутри (в существующих и/или специально созданных новых методах).

                    Насчёт jQuery.UI — врядли. Виджеты о которых писал я не предназначены для совместного навешивания на один и тот же элемент DOM. Виджет является единственным владельцем какого-то блока DOM, и максимум может делегировать части своего блока вложенным виджетам — но в этом случае он сам уже в эти части не лазит (если нужно — дёргает методы вложенного виждета которому он делегировал эту часть своего блока).
                  0
                  Подскажите как через jQuery отловить например встроенное событие backbone «change:attribute» не дублируя его методами jQuery?
                    0
                    События backbone не являются событиями документа, если я не ошибаюсь.
                      0
                      Возможно, а отлавливать их нужно. И то как сейчас backbone предлагает сделать (например передавать линки на вью в модели и/или обратно через конструкторы) — не очень удобно. А был бы родной бекбоновский глобальный диспетчер событий — ловить model.save() во вью было бы можно простой однострочной декларацией в events вью без лишних выкрутасов.
                        +1
                        Во-первых, не надо путать Backbone.Events и события DOMа, это разные вещи. А что касается линков на модели, в чем проблема? View служит для формирования отображения модели, так что иметь ссылку на модель просто обязана, и все легко делается как раз простой однострочной декларацией this.model.bind('change', this.render, this); в методе initialize.
                          0
                          Не понял почему мне адресовано пожелание не путать события backbone и DOM, если я говорю только о нативных событиях backbone, но это неважно. Проблемы с передачей линков нет — костыль как костыль. Но еще раз — если бы был глобальный диспетчер backbone, линки можно было бы не передавать вообще, что сделало бы код лаконичнее и без костылей.
                  0
                  var eventEmitter = _.extend({}, Backbone.Events);
                  eventEmitter.bind('msg', function(msg){ console.log(msg) });
                  eventEmitter.trigger('msg', 'hello world');
                  

                  не катит?
                    0
                    это не отловит например нативное событие «change», который триггерится backbone скажем в момент model.fetch() и которое хотелось бы отлавливать во вью простой декларацией во View.events
                0
                Хочется уточнить по поводу заворачивания статуса запроса в — разве replaceWith не решает описанной в тексте проблемы?
                  0
                  Нет, не решает. Я, безусловно, в магию верю. Но здесь довольно безнадёжная ситуация: есть объект (с тэгами) на который есть две ссылки (одна в DOM, вторая в this.$). Заменить этот объект другим (доступным по обоим ссылкам) используя только одну из ссылок невозможно — нужен дополнительный indirection level (ссылка на ссылку). Как писал Спольски, все программисты делятся на две группы — одни понимают ссылки, вторые нет. :-/ В данном случае я бы хотел их временно не понимать, и чтобы всё как-нить магически заработало. ;)
                    0
                    Мне почему то казалось что в this.$ как объект jQuery должна быть ссылка на DOM.
                    Разве проблема не в том что используется this.$.html(«далее идет строка с HTML кодом») вместо this.$.replaceWith? Руки чешутся проверить, но время то позднее :)
                      0
                      (ниже — псевдокод)
                      Нет, так не получится. У нас есть ссылка на "div#foo" в this.element и он же в DOM. Делаем так: $('div#foo').replaceWith('div#bar'), в DOM объект заменяется, а вот в this.element ссылка ведёт на "div#foo", которого даже нету на странице.
                        0
                        Допустим так, тогда поясните как у автора работает смена html кода после заворачивания его в span.
                          0
                          Видимо, меняется содержимое тега, на который у нас ссылка.
                            0
                            Этот span добавляет тот самый необходимый indirection level. С ним в DOM ссылка на span, а уже внутри span ссылка на отрендерённый шаблон. В this.$ тоже ссылка на span.

                            this.$.html() заменяет ссылку внутри span, после него span указывает на совершенно другой отрендерённый шаблон. При этом сам span не заменялся, так что ссылки на него и в DOM и в this.$ по-прежнему корректны.
                    –1
                    Вот так вот берешь проект, открываешь такой маленький js файлик, чтобы просто поменять, например, какое-нить название поля…
                    А потом сидишь два дня разбираешся откуда гребанная ошибка выскакивает.
                      0
                      А как в вашей архитектуре может быть реализована функция wysiwyg-редактора «выделение текста» -> «удаление выделенного текста» (удаление с помощью кнопки на панели либо нажатием клавиши Del)?
                        0
                        • За textarea с текстом отвечает виджет Text, у которого есть метод Text.del_selection().
                        • За кнопку(и) на панели отвечает виджет Button, который по нажатию на кнопку вызывает notice('delete text').
                        • Функция notice в обработчике события 'delete text' вызывает Text.del_selection().
                        • UFO just landed and posted this here
                        +1
                        Cерия статей в тему на knockout.js. Правда, на английском.
                          0
                          Имхо ваш notice — это Контроллер, а «события» — это его методы. Если ваше приложение — это одна страница (именно как контекст, а не с точки зрения загрузки с сервера одного html), то все логично. Одна страница — один контроллер. А вот если в приложении появится несколько контекстов, то едининый контроллер смотрится странно. Думаю это и имели в виду предыдущие комментаторы, когда говорили про «будет каша».

                          Only users with full accounts can post comments. Log in, please.