Пользователь
0,0
рейтинг
7 июля 2011 в 19:01

Разработка → Масштабируемые JavaScript приложения

Более месяца назад в статье FAQ по JavaScript: задавайте вопросы был задан вопрос «Подскажите примеры хорошего подхода организации JS кода к сайту на достаточно высоком уровне. Как можно узнать подробнее практики реализации например gmail?».

Пришло время ответить на данный вопрос. Я немного затянул т.к. хотел рассказать доклад на одноименную тему на Я.Субботнике. Доклад был очень коротким многие важные моменты пришлось выкинуть. Статья — более-менее полная версия.

Эта статья о том, как сделать крупное веб-приложение расширяемым и поддерживаемым: архитектура, подходы, правила.

Если вы работаете над веб-приложением, то большую часть времени вы тратите на дописывание кода, исправлении ошибок. Только малая часть уходит на дописывания нового функционала. Любое сложное веб-приложение постоянно меняется. Сегодня — 1 строка, завтра — 20, послезавтра — 300.

Давайте посмотрим какой же код есть на сайтах:
$(function () { // Типичный код для сайта
    $('#button').click(function (event) {
        alert(this.innerHTML);
    });

    $('#list').uberScrollerPluginStart({
        "theme": "red"
    });

    $('#lazy_thing').click(function () {
        $.get('/lazy/thing/body.html', doLazyLoad.bind(this));
    });

    /* Ещё десяток разных стилей
       и плагинов */
});

Чаще это jQeury, мы навешиваем события, подключаем плагины, выполняем ajax-запросы. Если какой-то плагин удалить, то все сломается. Мы получаем своеобразный клубок кода в который намешаны фреймворки, плагины, наш код. Такой клубок не очень большой (строк 100) и, как правило, создается и поддерживается одним человеком. Большинство сайтов создается 1 раз и не поддерживается вообще, поэтому для них что-то большее может быть вредно и может увеличить себестоимость всего сайта в целом.



Если применить такую архитектуру к GMail, Yandex.Mail, Портал Yahoo!, Twitter, то мы получим огромный клубок кода(10000+ строк), который создает несколько человек. Огромный клубок очень сложно распутать, а запутать ещё сожнее, чтобы ничего не сломать. Веб-приложения постоянно развиваются, поэтому такой клубок приходиться постоянно распутывать и запутывать.
Код сайта не структурирован, а его архитектура имеет сильную связанность. Для веб-приложений такую архитектуру использовать невозможно.

Архитектура


Рассмотрим одну из нескольких архитектур, которая позволяет без проблем создавать масштабируемые приложения. Архитектура взята у N.C. Zakas, она мне очень нравится (будет здорово если вы смотрели его презентацию и вспомните о чем идет речь), в ходе я её немного изменю, а конечный результат вы уведите в примерах.

Фреймворк

В любое приложение, как правило, входит фреймворк. Все клиентские фреймворки будь jQuery, Mootools, YUI, dojo — это всего лишь коробка с инструментами. Инструменты помогают вам забивать гвозди, пилить доски. Какие-то инструменты очень нужны, есть и которые пылятся. Если основных инструментов мало, то подключаются более тяжелые, например, Backbone.js + Underscore.js
В жизни заменить коробку от jQuery на Mootools не составит труда. Подумайте, что вам будет стоить отказ от jQuery или вашей любимой библиотеки сейчас? Чтобы замена была легкой необходимо добавить обертку над функциями библиотек — им может быть Ядро приложения.

Ядро

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

Модули

Приложение состоит из модулей — это независимые части приложения, управляемые Ядром, но не имеющие прямых связей с самим Ядром. Тяжелое JavaScript приложение такое же сложное как и космическая станция. МКС имеет расширяемую архитектуру в ней десятки модулей, каждый выполняет свою собственную роль. Модули станции делали в различных странах, доставляли в различное время и их очень много, но все они работают как единое целое.

Модули веб-приложений состоят из HTML + CSS + JavaScript + Ресурсы

Ресурсы модуля — локализация, дескрипторы и другие приватные данные модуля.

Каждый модуль должен жить отдельно от всего приложения. Задача каждого модуля — выполнять узконаправленную функцию. Модули должны быть обособлены и чем меньше модуль знает о других модулях тем будет проще поддерживать всё приложение. Когда вы создаете новый код и навешиваете зависимости, подумайте о том, что завтра нужно будет выкинуть его.

Для обеспечения слабой связанности и для ограничения свободы модуля его необходимо огородить специальным объектом-медиумом — песочницей. Каждый модуль обязан находиться внутри своей песочницы и общаться только с ней. Это единственный объект о котором знает модуль. Роль песочницы проста. Она выступает в роли охранника — знает что может делать модуль, знает с кем может общаться модуль. Песочница обеспечивает связь модуля с ядром.

Модуль может вызывать свои методы и песочницы, использовать свой HTML элемент. Модуль должен спрашивать разрешения перед выполнением каких-то действий. Модулю запрещено создавать глобалы, использовать нестандартные глобалы, общаться с другими модулями напрямую.

Субмодули

Каждый модуль выступают в роли микроприложения, которое может иметь свои правила, правила не должны противоречить глобальным. Каждый модуль может делегировать свою песочницу своим частям, подключать другие модули (во время сборки проекта).

Ядру не важно как модуль будет управлять своими частями — ему важно, чтобы соблюдались глобальные правила для каждого модуля.

Подчиненность


Для уменьшения числа связей внутри системы нам нужна жесткая подчиненность

— Только библиотека знает о браузере и имеющемся АПИ
— Только ядро знает о библиотеке
— Только песочница знает о ядре
— Каждый модуль знает только о своей песочнице

Ни один из объектов не должен знать о всем приложении.



Наше приложение должно постоянно изменяться. Пользователи хотят новый функцинал — мы его добавляем. Браузеры получают новую фичу — мы её прикручиваем.

Поэтому каждый объект должен изменяться
— Библиотека расширяется за счет плагинов — Браузер получает новый API — добавляем плагин
— Ядро обновляется за счет расширений — Заменили протокол с XML на JSON, поменяли формат отправляемых данных, изменили AJAX транспорт — добавили расширение
— Все приложение расширяется за счет модулей — Пользователи желают новую функцию — мы добавляем какой-либо модуль

Коммуникация


Все знают, что HTML DOM изобилует событиями. События есть везде — есть у элементов, есть в API (XHR, Workers). События в DOM позволяет объектной модели документа бесконечно расширяться и создавать расширяемые приложения. Я считаю, что события — это лучшая база для веб-приложений.

Рассмотрим пример:
var Module1 = {
   "someAction": function () {
       Module2.getSomeValue();
   }
};
  
var Module2 = {
   "getSomeValue": function () {
       return 'data';     
   }
};

При обычной схеме модули общаются друг с другом напрямую. Модуль 1 зависит от модуля 2 и от его метода getSomeValue(). Если мы уберем модуль 2, то все сломается.

Если вызов метода заменить событием, то модули станут независимыми (будут слабо связаны).
// Слабая связанность
var Module1 = {
   "init": function ($) {
      $.on('event', function (e) { // $ - не jQuery, это экземпляр sandbox
         console.log(e.data);
      });
   }
};
    
var Module2 = {
   "someAction": function ($) { // $ - не jQuery
      $.trigger('event', 'data');  
   }
};

В модуле 1 мы слушаем событие event, модуль 2 вызывает событие event с какими-либо данными и хэндлер события отрисовывает данные в консоль

Да, архитектура сильно меняется, но мы избавляемся от сильной связанности.

Асинхронные функции


События неминуемо влекут за собой асинхронность, думаю все знаю, что HTML DOM изобилует асинхронными методами. XHR, JSONP, отправка данных из фрейма в фрейм, пересылка данные в воркер и обратно. Асинхронное программирование сложнее и его использование не всегда оправдано. Но в нашем случае асинхронные функции могут быть невероятно полезными.

Посмотрим пример:
// Синхронный код
var Storage = {
    "read": function (key) {
        return localStorage[key];
    }
};

var data = Storage.read('key'),
    pData = process(data);
$.trigger('data', pData);

Предположим, что мы храним какие-то пользовательские данные в localStorage, работаем с ним синхронно и все хорошо. Хорошо до тех пор пока мы не захотели хранить эти же данные на сервере, а вероятность, что мы решим что-то менять очень велика. Перевод синхронного кода на асинхронный может быть большой проблемой, даже с событийной моделью.

Переделаем наш код с заменой localStorage:
// Асинхронный код
var Storage = {
    "read": function (key, cb) {
        $.get('/read/' + key, cb);
    }
};

Storage.read('key',function(data) {
    var pData = processData(data);
    $.trigger('data', pData);
}.bind(this));

У нас была простая функция получения данных и 10 строк исходного кода. Мы добавили 1 строку и изменили 4 строки. Получили практически 50% изменений (и то я учитывал скобки). Если бы не наши события, то пришлось менять и код, использующий Storage. Используя асинхронный подход в функциях получения и сохранения данных мы избавляем себя от части проблем в будущем.

Как правило, все асинхронные методы связаны с отправкой и получением данных (XHR, Location API, File API), поэтому все функции сохранения и получения данных лучше сделать асинхронными либо подключить события.

Плюсы такой архитектуры
  • На основе полученного фреймворка, набора модулей и плагинов мы можем создать несколько приложений
  • Мы можем протестировать каждый модуль, каждый плагин отдельно. Это невероятно выгодно по сравнению с тестированием всего приложения
  • Мы можем гибко масштабировать наше приложение — менять, добавлять модули не боясь, что изменится какая-то важная часть приложения
  • События позволяют уменьшить связанность приложения
  • Асинхронные функции, работающие с данными, позволяют подготовиться к глобальным изменениям в будущем

Это выдержки из доклада c Я.Субботника. На субботнике я пообещал проиллюстрировать теорию практикой. В теории все красиво, но теория без практики имеет малую ценность, поэтому давайте на основе нашей архитектуры создадим приложение.

Пример: масштабируемое JavaScript приложение, которое легко поддерживать


Будем создавать приложение, точнее огрызок приложения (статья получается очень большой, поэтому сокращу эту часть), демонстрирующий модульность. В состав приложения входит:
  • Ядро,
  • Песочница,
  • Модули: модуль, отображающий текст; модуль, генерирующий события по таймеру; модуль, фильтрующий это событие; модуль, логирующий некоторые события в консоль.
Часть примеров связанных со сборкой проекта, автоматической генерацией юнит-тестов и модульным тестированием я покажу на пальцах, чтобы не перегружать статью.

Библиотеки и технологии

Как водится, будет jQuery. Для систематизации верстки будем использовать упрощенную технологию БЭМ: Блок, Элемент, Модификатор. Для динамической загрузки скриптов мы будем использовать $script.js. Шаблоны — модифицированный шаблонизатор от Резига.

Структура проекта

    /app
        /css                             - верстка и стили блоков по упрощенному БЭМ
            /blocks
                /b-module-name
                    /b-module-name.css
                    ...
                ...
            /pages
                /index.css
                ...
        /descriptors                     - дескрипторы модулей
            /ModuleName.json
            ...
        /locales                         - локализация модулей
            /ModuleName.json
            ...
        /modules                         - логика модулей
            /ModuleName.js
            ...
        /templates                       - шаблоны модулей
            /ModuleName.html
            ...
        /views                           - семплы верстки
            /ModuleName.html
            ...
    /build                               - скрипты для сборки
    /lib                                 - наши скрипты
        /Core.js
    /test                                - тесты
        /lib
            /qunit.css
            /qunit.js
        /ModuleName                      - тест модуля ModuleName
            /index.html
            /index.js
        /TestData.js                     - семплы данных для теста
    /vendors                             - внешние библиотеки
        /Script.js
        /jQuery.js
        ...
    /index.html                          - базовая верстка
    /index.js                            - основной js файл, использующийся для сборки
    /index.json                          - дескриптор приложения
    ...

Модули

Начнем сперва с модулей. Каждая часть модуля должна подключаться динамически и статически(при сборке в один файл), либо и так и так. Все должно быть максимально прозрачно для разработчика: есть модуль — используем, нет — загружаем и используем.

Наши модули будут состоять из нескольких частей:
  1. Семпл верстки: HTML файл с подключенными стилями. Этот файл будет использоваться в юнит-тесте, он показывает как выглядит модуль из него могут быть взяты семплы для создания шаблона.
  2. Шаблон: Блоки HTML, которые использует модуль.
  3. Стили и изображения: простой CSS файл или файлы, изображения
  4. JavaScript: код модуля
  5. Дескриптор: JSON файл, содержащий имя модуля, его настройки и список тех событий, которые может слушать и порождать. Он используется для автоматической генерации Юнит-теста, его использует песочница для разграничени прав.
  6. Локализация: JSON файл, содержащий тексты на разных языках
Каждый модуль имеет адекватное название, каждая часть модуля расположена в отдельной директории и имеет такое же имя как и у модуля. Мы максимально отделили все логические части модуля (Разметка/Шаблон, Вид, Логика, Описание и конфигурация, Тексты). Каждый модуль экспортирует только 2 метода: init и destroy:

Пример модуля (модуль DataGenerator)

(function(global){
    "use strict";
    var intervalId;
        
    var DataGenerator = {
        init: function (sandbox) {
            intervalId = setInterval(function () {
                sandbox.trigger('newData', Math.random());
            }, sandbox.getResource('interval'));
        },
        destroy: function () {
            clearInterval(intervalId);    
        }
    };
    
// Экспортируем    
    if (!global) {
        return DataGenerator;
    }
    if (!global.exports) {
        global.exports = {};
    }
    global.exports.DataGenerator = DataGenerator;
}(this)) // ; не ставим!

Согласен, что много мусора в коде. У нас именно такой формат из-за требований к модулю: Ядро авторитарно оно само подключает модули, можуль должен подключаться как статически так и динамически. В JavaScript нет модулей как таковых, поэтому каждый создает свой вид. Есть какие-то "стандартные", но чаще — это велосипед под конкретные задачи.

Пример дескриптора (модуль DataGenerator)

{
    "name": "DataGenerator",
    "acl": {
        "trigger:newData": true // модуль может пораждать событие newData
    },
    "resources": {
        "interval": 1000
    }
}

Пример локали (модуль MessageView)

{
    "text_label": {
        "ru": "Он сказал: ",
        "en": "He said: "
    }
}

Пример шаблона (модуль MessageView)

<div class="b-message-view">
    <span class="b-message-view__label">{%=label%}</span>
    <span class="b-message-view__value">{%=value%}</span>
</div>


Плюсы такого формата
Каждая логическая часть отделена. Мы можем использовать каждую часть неоднократно. Например, дескриптор мы можем использовать для автоматический генерации скелета юнит-тестов.

Ядро

Нам необходимо загружать и регистрировать модули. Мы должны это уметь делать как при сборке так и динамически. jQuery Deffered нам в этом очень сильно помогут. Т.к процесс загрузки одной части модуля практически ни чем не отличается от другой, а частей у нас много, то нам необходимо выделить фабрику по производству функций загрузки:
    var loaderFactory = function (cacheObject, method, format, methodOwner, type) {
        return function (name) {
            var dfd = $.Deferred(),
                self = this;

            if (cacheObject[name]) {
                dfd.resolve();
                return dfd.promise();
            }

            function successOrFail(object) {
                var camelCasedType = type.slice(0, 1).toUpperCase() + type.slice(1);
                self['push' + camelCasedType](name, object);

                dfd.resolve();
                if (object) { // if fail
                    EventManager.trigger(type + ':loaded', {name: name});
                    EventManager.trigger(type + ':' + name + ':loaded');
                }
            }

            var path = Core.descriptor.path[type] + format.replace('$0', name);

            if (type === 'module') {
                method.call(methodOwner, path, successOrFail);
            } else if (type === 'template') {
                method.call(methodOwner, path, successOrFail, 'html').error(successOrFail);
            } else {
                method.call(methodOwner, path, successOrFail).error(successOrFail);
            }
            return dfd.promise();
        }
    };


ModuleManager

Менеджер модулей просто загружает части модулей и кэширует их. У него есть ряд методов для регистрации модуля без загрузки(статическая сборка).
    var ModuleManager = {
        modules: {},
        descriptors: {},
        locales: {},
        templates: {},
        pushModule: function (name, module) {},
        pushDescriptor: function (name, descriptor) {},
        pushLocale: function (name, locale) {},
        pushTemplate: function (name, template) {},
        load: function (name) {}
    };

    ModuleManager.getModule = 
        loaderFactory(ModuleManager.modules, require, '$0.js', this, 'module');
        
    ModuleManager.getDescriptor = 
        loaderFactory(ModuleManager.descriptors, $.getJSON, '$0.json', $,          
                      'descriptor');
                      
    ModuleManager.getLocale = 
        loaderFactory(ModuleManager.locales, $.getJSON, '$0.json', $, 'locale');
        
    ModuleManager.getTemplate = 
        loaderFactory(ModuleManager.templates, $.get, '$0.html', $, 'template');

Я оставил скелет объекта, чтобы много место не занимал. В любом случае ни кто не читает. Полная версия в исходниках.

Шаблонизатор

Будем использовать простой шаблонизатор от John Resig
var templateFactory = function(str, data) {}

EventManager

Менеджер событий регистрирует события, удаляет, вызывает. Все глобальные события в приложении идут через него. Не будем изобретать велосипед. EventManager будет использовать jQuery.bind jQuery.trigger jQuery.unbind кроме стандартных методов у него будет интересный метод — hook, который навешивает хук-функцию на событие. Хук-функция может менять содержимое параметров события, также оно может предотвращать вызов события.
    var EventManager = {
        $: $('<div/>'),
        hooks: {},
        trigger: function (event, data) {
            if (this.hooks[event]) {
                // Update event data
                var result = this.hooks[event](data);
                // Don't trigger event
                if (result === false) {
                    return this;
                }
                // Trigger with new data
                data = result || data;
            }
            this.$.trigger.apply(this.$, [event, data]);
            return this;
        },
        bind: function () {},
        unbind: function () {},
        hook: function (event, hookFunction) {
            // One hook for example
            this.hooks[event] = hookFunction;
            return this;
        },
        unhook: function (event) {
            delete this.hooks[event];
            return this;
        }
    };

Использование глобального менеджера событий имеет один важный плюс: Мы можем записать лог событий, а потом из лога восстановить ход событий (удобно для отлова багов на стороне пользователя).

Core

    var Core = {
        descriptor: {},
        runningModules: {},

        // Основной метод, инициализирующий ядро
        init: function (descriptorOrFileName, callback) {},

        // Загружает все модули
        _initModules: function (callback) {},
        
        // Загружает один модуль по имени
        initModule: function (name, callback) {},
        
        // Уничтожает модуль
        destroyModule: function (name) {},
        
        // Получает HTMLElement модуля по имени
        getBox: function (name) {},

        // Получает шаблон по имени подуля
        getTemplateFunction: function (moduleName, templateSelector) {}
    };

Удалил тело функций и JSDoc блоки.

Извне нам нужны только некоторые методы из всех наших модулей ядра. Будем экспортировать только их:
    var CorePublic = {
        trigger:         $.proxy(EventManager.trigger, EventManager),
        bind:            $.proxy(EventManager.bind, EventManager),
        unbind:          $.proxy(EventManager.trigger, EventManager),
        on:              $.proxy(EventManager.bind, EventManager),

        getModule:       $.proxy(ModuleManager.getModule, ModuleManager),
        getDescriptor:   $.proxy(ModuleManager.getDescriptor, ModuleManager),
        getLocale:       $.proxy(ModuleManager.getLocale, ModuleManager),
        getTemplate:     $.proxy(ModuleManager.getTemplate, ModuleManager),

        pushModule:      $.proxy(ModuleManager.pushModule, ModuleManager),
        pushDescriptor:  $.proxy(ModuleManager.pushDescriptor, ModuleManager),
        pushLocale:      $.proxy(ModuleManager.pushLocale, ModuleManager),
        pushTemplate:    $.proxy(ModuleManager.pushTemplate, ModuleManager),

        init:            $.proxy(Core.init, Core),
        destroyModule:   $.proxy(Core.destroyModule, Core),
        initModule:      $.proxy(Core.initModule, Core),

        getTemplateFunction:  $.proxy(Core.getTemplateFunction, Core)
    }; 

Песочница

Каждый модуль имеет свою собственную песочницу, поэтому мы создадим конструктор Sandbox, порождающий песочницы. Песочница получает в качестве аргумента дескриптор модуля. Все методы песочницы может использовать модуль.
    var Sandbox = function (descriptor) {
        this.descriptor = descriptor || {};
    };  
    
    Sandbox.prototype.getBox = function () {};
    // Проверяет может ли модуль сделать такое-то действие
    Sandbox.prototype.is = function (role) {};    
    Sandbox.prototype.bind = function (event, callback) {};
    Sandbox.prototype.unbind = function (event, callback) {};
    Sandbox.prototype.trigger = function (event, data) {};
    Sandbox.prototype.hook = function (event, hookFunction) {};
    Sandbox.prototype.unhook = function (event) {};
    Sandbox.prototype.getText = function (message) {};
    Sandbox.prototype.getResource = function (resource) {};
    Sandbox.prototype.getTemplate = function (templateSelector) {};


В тех функциях (bind, trigger, hook, ...), которые могут повлиять на другие объекты песочница проверяет возможность выполнить данную функцию у данного модуля (используя дескриптор модуля).

Сборка ядра

Каждая часть ядра должна лежать в отдельном файле и собираться воедино препроцессором, в примере я не использую препроцессор(он сферический в вакууме), поэтому скинул все вместе.
(function(global, $, require, undefined){
    "use strict";

    var templateFactory = function(str, data){};

    var loaderFactory = function (cacheObject, method, format, self, type) {};

    var ModuleManager = {};

    var Sandbox = function (descriptor) {};
    
    var EventManager = {};

    var Core = {};

    var CorePublic = {};           

    if (!global) {
        return CorePublic;
    }
    if (!global.exports) {
        global.exports = {};
    }
    global.exports.Core = CorePublic;    
}(this, jQuery, $script));

Дескриптор приложения

Приложение тоже имеет дескриптор, который описывает какие модули входят в приложение, где находятся части модулей, определяет текущую локаль, описывает базовую разметку. Дескрипторов может быть несколько под разные сборки.
{
    "modules": ["MessageView", "DataGenerator", "Logger", "Hook"],
    "layout": {
        "MessageView": ".b-message-view"
    },
    "locale": "ru",
    "path": {
        "descriptor": "./app/descriptors/",
        "module": "./app/modules/",
        "locale": "./app/locales/",
        "template": "./app/templates/"
    }
}

Приложение: index.js

Как вы поняли из дескриптора приложения, у нас будет 4 модуля. Приложение получилось простейшим без каких-либо взаимодействий с сервером. Код модулей я не прикладываю в статье — он есть в репозитории.

MessageView — отображает сообщение по событию newData
DataGenerator — раз в секунду генерирует событие newData с данными Math.random()
Logger — слушает событие newData и записывает в консоль то, что пришло
Hook — навешивает хук на событие newData. Если в событие приходит строка, то хук прерывает событие. Если приходит число меньше 0.5, то оно умножается на 100.

Сборка


Для каждой стадии (dev, test, prod) нужен свой сборщик (или своя конфигурация). Для разработки необходимо собирать документацию, а сжимать файлы не нужно. Для тестинга и продакшена могут быть разные пути до файлов, необходимо сжимать и собирать файлы, проверять код на наличие ошибок(статическая проверка). Может существовать много стратегий и инструментов сборки. Я опишу простейший. При сборке мы будем использовать дескриптор приложения index.json.

Сборка index.js

Для сборки index.js мы будем использовать сферический в вакууме препроцессор, который имеет функции: require, buildFrom. Функции препроцессора обрамлены в блочные комментарии, поэтому они не мешают работе всего приложения (и подходят как для JavaScript так и для CSS). index.js подается на вход препроцессору, который сканирует файл и собирает проект.
/*$require: ./lib/Core.js */
(function (Core) {
"use strict";

/*$buildFrom ./index.json */

Core.on('ready', function () {
    Core.trigger('newData', 'Pewpew');
});

Core.init(/*$require*/'./index.json'/*$*/);
}(this.exports.Core))

После сборки файл может выглядеть как-то вот так:
// Тут подключили Core.js

(function (Core) {
"use strict";

// + descriptors/Logger.json
Core.pushDescriptor("Logger", {
    "name": "Logger",
    "acl": {
        "listen:newData": true,
        "listen:ready": true
    }
});
// - descriptors/Logger.json

// + modules/Logger.js
Core.pushModule("Logger", (function(global){
    // ...
}(this)));
// - modules/Logger.js

// + locales/Logger.js
Core.pushLocale("Logger", {});
// - locales/Logger.js

// ... Ещё какие-то модули ....


Core.on('ready', function () {
    Core.trigger('newData', 'Pewpew');
});

Core.init({
    "modules": ["MessageView", "DataGenerator", "Logger", "Hook"],
    "layout": {
        "MessageView": ".b-message-view"
    },
    "locale": "ru",
    "path": {
        "descriptor": "./app/descriptors/",
        "module": "./app/modules/",
        "locale": "./app/locales/",
        "template": "./app/templates/"
    }
});
}(this.exports.Core))


C require все понятно, вот с buildFrom немного сложнее. Эта функция использует дескриптор приложения для подключения определенных файлов. В нашем случае я собрал модуль Logger (остальные тоже как-бы подключены). Логика buildFrom может быть немного сложнее она может чистить локализацию (в нашем случае удалит "en": "He said: "), выполнять предварительную проверку и тп.
Для среды dev приложение может не собираться — модули динамически загружаются ядром.

Сборка index.css

В примере у нас простая структура модулей: каждый модуль имеет 1 блок, поэтому мы ограничимся тем же сферическим в вакууме сборщиком:
/*$buildFrom: ../../index.json */

/* app/css/blocks/b-message-view/b-message-view.css */
.b-message-view {
    color: green;
    font-family: monospace;
}

Логика buildFrom в контексте css следующая: он смотрит конфиг приложение на наличие layout и для каждого лейаута, подключает соответствующий блок у нас это b-message-view — все просто. Это предельно упрощенный вариант сборки, который может не подойти для более сложных приложений, но для примера этого достаточно. Например, сборщик БЭМ использует json файл с описанием блоков приложения.

Сборка index.html

У нас есть index.css и index.js. Будем использовать тот же препроцессор для сборки index.html
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <title></title>
        <!--$require: index.css-->
        <link rel="stylesheet" href="app/css/pages/index.css" />
        <!--$-->
    </head>
    <body>
        <!--$buildFrom: index.json-->
        <div class="b-message-view"></div>
        <!--$-->
        <script type="text/javascript" src="http://yandex.st/jquery/1.6.1/jquery.js"></script>
        <!--$require: index.js-->
        <script type="text/javascript" src="./vendors/Script.js"></script>
        <script type="text/javascript" src="./lib/Core.js"></script>
        <script type="text/javascript" src="./index.js"></script>
        <!--$-->
    </body>
</html>

Логика buildFrom в контексте html следующая (похожа на css): он смотрит конфиг приложения на наличие layout и для каждого лейаута создает соответствующий div у нас это b-message-view. Ещё раз скажу, что это простейший вариант сборки. Вы же можете применять xslt, более умные сборщики.

На выходе мы получим:
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <title></title>
        <link rel="stylesheet" href="/index.css" />
    </head>
    <body>
        <div class="b-message-view"></div>
        <script type="text/javascript" src="http://yandex.st/jquery/1.6.1/jquery.js"></script>
        <script type="text/javascript" src="/index.js"></script>
    </body>
</html>

Makefile — Общая сборка

Что делает сборщик:
  1. Выполняет запуск препроцессора для сборки index.css
  2. Оптимизирует index.css: data/uri и т.п.
  3. Ужимает index.css (gz опционально)
  4. Выполняет сборку ресурсов index.css: картинки
  5. Запускает автоматизированные юниттесты (ниже)
  6. Выполняет запуск препроцессора для сборки index.js
  7. Проводит валидацию index.js
  8. Ужимает index.js (gz опционально)
  9. Собирает пакет под вашу OS .deb .rpm
  10. Кладет пакет в репозиторий
  11. Устанавливает пакет

Юнит-тесты


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

Используя наш дескриптор приложения и дескриптор модуля, мы можем применять автоматическое создание юнит-тестов. Генератор (он тоже сферический в вакууме) соберет необходимые ресурсы модуля и, используя acl в дескрипторе модуля, создаст скелеты тестов для событий, которые генерирует и слушает модуль. Все остальное ручками.

Как правило, каждое событие имеет свой собственный формат, чтобы упростить задачу тестирования мы создадим семплер событий:
var TestData = {
    "newData": function () {
        var data = [NaN, Infinity, window, Error, 'pewpewpew', 
        '<b>Pewpew</b>', '"', '\'', new Date, Date, Math, 42, 
        8, -1, 0, false, true];

        return data;
    }
};

Для тестирование мы будем использовать QUnit. Рассмотрим на примере MessageView. Генератор юнит-теста создает (автоматически) тест из одного модуля MessageView, создает скелеты тестов для проверки интерфейса модуля — те события, которые он слушает и генерирует. Это минимум того, что мы должны проверить.

Код теста: index.js

// MessageView test
(function (Core, $, TestData, ok, test, module, equals, expect, asyncTest, start, stop) {
"use strict";

// Текущая среда приложения
var ApplicationEnvironment =
{
    "modules": ["MessageView"],
    "layout": {
        "MessageView": ".b-message-view"
    },
    "locale": "ru",
    "path": {
        "descriptor": "../../app/descriptors/",
        "module": "../../app/modules/",
        "locale": "../../app/locales/",
        "template": "../../app/templates/"
    }
};

Core.on('ready', function () {
    module("MessageView");

    // Тест 1
    test("listen:newData", function() {
        var testItems = TestData["newData"](),
            $MessageView = Core.getBox("MessageView"),
            template = Core.getTemplateFunction("MessageView", '.b-message-view'),
            label = Core.getText("MessageView", "text_label");

        expect(testItems.length);

        // >>> put your code

        $.each(testItems, function (index, text) {
            Core.trigger("newData", [text]);

            // >>> put your code
            var expected = template({label: label, value: text}); // <<<
            equals(expected, $MessageView.html(), 'Should be "text_label: value"'); // <<<
        });
    });
    
    // Тест 2
    test("trigger:newData:display", function() {
        var testItems = TestData["newData"](),
            $MessageView = Core.getBox("MessageView"),
            template = Core.getTemplateFunction("MessageView", '.b-message-view');

        expect(testItems.length);

        // >>> put your code
        Core.on("newData:display", function () { // <<<
            ok(true); // <<<
        }); // <<<

        $.each(testItems, function (index, item) {
            Core.trigger("newData", [item]);
            // >>> put your code
        });
    });

});

Core.init(ApplicationEnvironment);

}(this.exports.Core, jQuery, TestData, ok, test, module, equals, expect, asyncTest, start, stop))

Из всей этой массы кода разработчик должен дописать всего 5 строк я их отметил "<<<".

Код теста index.html очевиден — не прикладываю.

Автоматизация тестирования и покрытие кода тестами


Если вы не используете автоматизированное тестирование, то полезность ваших тестов сокращается процентов на 60 (вы не сможете постоянно запускать тесты на всех браузерах, да и это неблагодарная работа). Если вы не проверяете покрытие кода тестами, то польза от таких тестов тоже сокращается (есть шанс пропустить важный момент). Есть несколько фреймворков, которые сильно упрощают эту задачу (это не тема данной статьи, поэтому упомяну вскользь):
js-test-driver — дружит с QUnit, имеет встроенный модуль покрытия кода.
TestSwarm (вики) — модуль от Резига и Mozilla Labs
JSCoverage

Делайте тесты, чтобы они реально работали и выполняли свои задачи. Не делайте тесты «что бы было». Если вы не желаете вводить автоматизицию и покрытие кода, то лучше подумать о необходимости юнит-тестов.

Валидация кода и сборка документации


Тоже не тема данной статьи, но упомянуть стоит. Если вам нужны доки, то по заданным конфигам сборщик проекта может собрать документацию, используя Dox, jsdoc-toolkit и т.п. см. Написание документации
Перед сборкой проекта в тестинг и перед коммитом в репозиторий необходимой проверять код валидатором. Если не проверять код во время прекоммита (проверять перед сборкой или когда вздумается), то может быть слишком поздно — может накопиться большой объем кода, а большой объем кода сложнее исправлять и рано или поздно вы можете забить на валидацию. Для предотвращения ошибок сборки необходимо проверять код (не так строго) после сборки проекта.

Общие моменты


Оформлю данную часть в виде тезисов. Они справедливы не только для веб-приложений.
  1. С вами работают другие люди — уважайте их труд и цените их время
  2. Необходимо красиво оформлять код (отступы и необходимые пробелы). JSLint, JSHint в помощь!
  3. Обязательны хорошие комментарии как для функции вцелом так и для важных моментов кода.
  4. Оставляйте ссылки на тикеты внутри кода, чтобы читающий код знал почему это сделано так, а не по другому! Чтобы не удивлялись конструкциям $textarea.val($textarea.val())
  5. Нейминг: Давайте внятные имена. Длинное имя функции — хорошо! Переменные — существительные. Функции — глаголы или глагольные выражения. Избегайте бесполезных имен: foo, temp. Часто бывает, что имя не приходит в голову (так и хочется назвать tmp), подумайте, окиньте взглядом окружение кода — красота кода превыше всего!
  6. Держите JavaScript, HTML и CSS порознь (у них разные задачи). HTML — каркас, разметка. CSS — представление разметки. JavaScript — логика приложения.
  7. Избегайте объявления обработчиков событий в атрибутах. Избегайте Element.style. Избегайте функций, генерирующих html (шаблоны FTW!). Избегайте CSS Expressions!
  8. Разгружайте обработчиков событий. 2-3 строки кода им за глаза!
  9. Не пропатчивайте чужие объекты, если они это не разрешают явно. Если объект не ваш — не трогайте его! (Array.prototype, Function.prototype)
  10. Избегайте создание глобальных объектов
  11. Если код может выбросить ошибку, то лучше если это будет ваша ошибка — ошибка, сгенерированная вами!
  12. Проверяйте тип данные через instanceof, typeof, Object.prototype.toString magic
  13. Отделите конфигурацию от кода! URL Пути; тексты; константы, в частности, строковые
  14. Автоматизируйте процесс разработки: Используйте автоматизированные инструменты для тестирования(js-test-driver), проверки кода(JSLint, JSHint), сборки кода и построения документации(JSDocToolkit, Dox). Если тестовый сервер находится удаленно, то настройте автоматический аплоад файлов по Ctrl+S. Используйте генераторы кода: тесты, скелеты модулей.

Код приложения из примера


Исходники лежат на GitHub scalable-js-app (полные тексты модулей, комментарии)

Чего нет в приложении: автоматической сборки, автоматической генерации юнит-тестов, всех тестов (кроме MessageView), автоматической сборки документации.

Почитать/Посмотреть

  1. Andrew Dupont (Gowalla, Prototype.js, S2) — Maintainable JavaScript
  2. Nicholas Zakas (Yahoo!, YUI, YUI Test) — Writing Maintainable JavaScript. Слайды новые, старые
  3. Nicholas Zakas — Scalable JavaScript Application Architecture
  4. Моё видео "Масштабируемые JavaScript приложения" с Я.Субботника слайды
PS И вот, вы решили переписать весь код, почистить код с помощью JSHint/JSLint. Остановитесь! У вас уйдет много времени на переделку, ещё больше на тестирование и переделку переделанного т.к. обязательно хоть что-нибудь да сломается (если у вас нет Юнит-Тестов, то это наверняка случится). Создавайте новый код по новому стандарту, а старый модифицируйте по необходимости, тогда, когда вы меняете его часть.

Надеюсь, было интересно. Предложения, пожелания, критика приветствуются! В будущих статьях я хотел бы подробнее описать процесс сборки проекта и автоматизированного тестирования. Если у вас есть вопросы — самое время их задать.
Mikhail Davydov @azproduction
карма
449,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (108)

  • +13
    Простите, не удержался :) azproduction с этим докладом:
    image
    • +7
      Lamo code? Не согласен. Дебажить Pro code намного сложнее, потому что… до сих пор нет нормального способа получить список навешенных через addEventListener обработчиков.
      • 0
        Кадр вырван из контекста доклада. Используя обработчиков событий в атрибутах элементов, вы переходите на сторону зла! Смешивать представление и логику ради сомнительных бонусов при отладке…

        Нормального нет(вообще это реально нужно?), но способы есть: сделать хук HTMLElement.prototype.addEventListener, FireQuery, Visual Event
        • +15
          Почему я перехожу на сторону зла? Не понимаю…

          Представление и логику никто не смешивает, если переложить логику на представление.

          Приведу простой пример. Вы придумываете идентификаторы, имена классов, которые нигде в CSS не используются, вводите новые, бесполезные с точки зрения «представления» сущности, только для того, чтобы работала ваша «логика». Таким образом «логика» влияет на «представление» даже если вы использовали ненавязчивый JS. Зачастую «ненавязчивые» скрипты требуют для работы вполне жестко описанную структуру DOM-дерева. Изменив структуру HTML все приходит в негодность и перестает работать. Чтобы скрипт снова начал работать, ему нужно или рассказывать постоянно про структуру, что откуда и по какому принципу брать, или же… переложить эту работу на само представление. Представление лучше знает, как оно устроено, и нужно всего лишь научить его попутно выполнять действия.

          В свое время я извратил свое мышление в сторону, обратно-пропорциональное ненавязчивому JS. И как итог получил реально ненавязчивый JS.
          • +4
            Зависит от того, что у вас меняется чаще: логика или представление.

            У вас приведён хороший пример, когда изменения в структуре HTML заставляют переколбашивать JS. Но бывает и наоборот: меняется структура и логика скриптов, и тогда вместо того, чтобы поправить список навешенных обработчиков в одном месте, придётся скакать по всему HTML и вносить изменения во многих местах. Что неизбежно повышает вероятность ошибки.
            • 0
              Пример можно?

              Обычно представление доминирует в WEB. Генерировать динамически контент на основании данных и шаблонов очень затратный способ, хотя именно в нем гораздо удобнее использовать ненавязчивый JS
              • +1
                Я не про динамическую генерацию, я про изменения, вносимые в процессе разработки. Потвикал какой-нибудь модуль, изменил пару функций — и скачи по всему HTML-у, правь вызовы.
                • +3
                  Да, такое есть. Вот только на практике получается так, что это дешевле все равно. Ты точно знаешь что ожидается от этого элемента, а не лазишь по файлам в надежде найти реальное место вызова.
                  • +1
                    Доля правды в этом есть, но на моей практике получается так: если верстальщики не дураки, то изменение представления делается через css, разметка меняется не сильно. А правильные классы позволяют также понять что это за элемент, и не трогать js.
          • +1
            >Чтобы скрипт снова начал работать, ему нужно или рассказывать постоянно про структуру, что откуда и >по какому принципу брать, или же

            Эмм, даже в простейшем варианте без любых фреймворков и библиотек, разве document.getElement[s]By… не для этого то и создан был?

            >Представление лучше знает, как оно устроено, и нужно всего лишь научить его попутно выполнять >действия.

            Наглая ложь. Статическая часть представления после отрисовки тихо висит и молится, пока из-за пользовательский действий не наступит её удаление. А если в том представлении было что то на манер Lamo Code, то логика, которая якобы отлично смотрелась там, тупо исчезает, и в результате можно часами курить свой собственный код.
            • 0
              >> Эмм, даже в простейшем варианте без любых фреймворков и библиотек, разве document.getElement[s]By… не для этого то и создан был?

              Таблица из 50 элементов, по 5 управляющих кнопок в каждой строке. Предлагаете по идентификатору к каждому элементу обращаться?

              >> Наглая ложь. Статическая часть представления после отрисовки тихо висит и молится, пока из-за пользовательский действий не наступит её удаление. А если в том представлении было что то на манер Lamo Code, то логика, которая якобы отлично смотрелась там, тупо исчезает, и в результате можно часами курить свой собственный код.

              Пример можно? Возможно мы про разные вещи говорим.
              • +2
                >>Таблица из 50 элементов, по 5 управляющих кнопок в каждой строке. Предлагаете по идентификатору к каждому элементу обращаться?

                И так что мы имеем? 50 строчек копипаста вида
                <a onclick='u touch my talala'>TOUCH NOW!</a>

                каждой по 5 раз.
                Или строку вида
                 $('table > tr ').each(function(){}) 

                и 5 строк селекторов с
                 bind('click', callback)

                для управляющих элементов.
                >>Пример можно? Возможно мы про разные вещи говорим.
                Да пожалуйста, я только за. Было у нас короче всё тоже самое
                 $('table > tr ').each(function(){}) 

                Мы когда то js'ом выбирали 1ю таблицу из документа при условии что её родитель не был DIV с ID=TABLE и убивали в ней все ссылки. Таблица в диве раньше была первой, но из-за дива, мы выбирали 2ю, потому что её родитель боди к примеру.
                Спустя некоторое время мы убрали у первой таблицы DIV скрипт не поменяли, и что вышло.
                Теперь мы убиваем ссылки в нашей первой таблице, хотя надо во второй. А в первой у нас была наша ЛОГИКА без которой мы теперь в просаке. Надо менять скрипты.
                А если бы мы селекторами аккуратно сами всё выбирали и вешали эвенты то такой бы беды и не всплыло.
                • 0
                  >> И так что мы имеем? 50 строчек копипаста вида

                  Данные генерируются скриптом. Поэтому получаем всего одну строчку в исходниках.

                  По второму примеру слабо понял в чем проблема. На аналогичные проблемы можно и с jQuery нарваться. Особенно когда два модуля работают с одним и тем же контентом или DOM-структурой.
                  • 0
                    >>Данные генерируются скриптом. Поэтому получаем всего одну строчку в исходниках.
                    Ну здрасьте приехали. Тут играем, тут не играем, а тут рыбу заворачиваем. Если генерим скриптом, что что сложно сделать усилие и повесить ещё и бинды? или просто нравится ощущение от надписи при наведении на ссылку javascript:doSomeReallyBigShit()??
                    • +1
                      Никогда не использую в href протокол javascript:

                      А я нигде не писал, что это статическая таблица, сделанная руками. Мы же вроде в рамках статьи обсуждение ведем, нет? А там про сложные веб-системы речь идет, а не о простых вебсайтах…
                • 0
                  Напомню, что в данном примере уместнее было бы написать так:

                  $('table > tr ').delegate(, 'click', callback);
        • 0
          HTMLElement.prototype.addEventListener, FireQuery, Visual Event
          Такой способ работает в ие? На сколько я знаю в ие HTML элементы наследуются не от одного, а от разных обьектов, или я ошибаюсь?
          • 0
            В ИЕ8- html элементы берутся как будто из воздуха :D
            >>document.createElement('div').constructor
            //undefined -- даже не null...

            >>document.createElement('div') instanceof Object
            //false -- забавно, правда? хотя магия с toString говорит обратное
            >>Object.prototype.toString.call(document.createElement('div'));
            //"[object Object]"

            Ну и соответственно нет никаких конструкторов (Node → Element → HTMLElement → HTMLDivElement), которые мы могли бы пропатчить без плясок с бубном.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Не языка, объектной модели DOM
          • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Почему это сложнее?
          Никто не мешает из одной большой пожатой свалки модулей выдать браузеру кучу маленький распакованных кусочков.
          //@ sourceURL=
    • +1
      Lamo знает про call? Автор презентации переоценил знания lamo :)
  • +5
    Хороший JS-MVC фреймворк — Backbone ( documentcloud.github.com/backbone/ )
    • +1
      В статье есть упоминание о нем.
      • 0
        Извините, статью просмотрел по диагонали.
    • +1
      или Closure Templates от гугла, есть Java и JavaScript версия
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          пока не пробовал, я стараюсь держать как можно меньше логики в шаблонах и поэтому пока такой надобности не возникало
  • +3
    Один из лучших докладов на субботнике. Спасибо за текстовый вариант.
  • 0
    $.on('event', function (e) {
    console.log(e.data);
    });

    Простите, или я что-то путаю, или вместо on должно быть bind/one.
    Спасибо за интересную статью!
    • 0
      У функции подписки на событие много имен: addEvent, attachEvent, addEventListener, bind, on, subscribe, listen,… кто-нибудь продолжит? :) Как вы его назовете это не важно — я выбрал самый короткий.
      • +2
        Думаю тут опять проблема в том, что человек не понял что бакс в данном случае не jquery.
        • 0
          Да, возможно. Мне подобный вопрос задали на субботнике (по слайду 24). Добавлю примечание.
        • 0
          Да, Вы правы :)
  • +4
    Хороший пример из серии «Масштабируемые JavaScript приложения» это ExtJs 4.
  • +4
    WebOS какой-то получился.
  • +2
    Спасибо большое. После вдумчивого прочтения статьи понял какую каку написал и, надеюсь, что надо исправить.
    • –3
      Я хорошо посмеялся, представив, что эти слава были сказаны человеком с этой аватаркой image
  • +2
    Хотите масштабируемые JavaScript приложения?? Используйте code.google.com/intl/ru-RU/closure/library/
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Активно используем. В данный момент еще и допиливаем, делая для себя плагины к vim/WebStorm.
        Все там просто и удобно — получается полноценное ООП и куча плюшек аля type checking, области видимости и т.п.
      • 0
        Но ни столько library, сколько linter + compiler
        • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        У нас успешный опыт исп. данной библиотеки. Мы отказались от всяких свестелок-перделок типа JQuery (для того, чтобы не было клубка как нарисовано в начале поста). С комьюнити конечно напряг, приходится читать исходники closure-library (благо они очень хорошо документированы). В итоге написать сложное RIA приложение like Gmail просто не возможно с помощью чего-то другого без проблем с утечками памяти, наследовании.
  • 0
    >> Избегайте объявления обработчиков событий в атрибутах.

    Почему? Три основные причины назовете?
    • 0
      1. MVC
      2. Отладка
      3. Поддержка
      • +1
        1. Выше писал, что логика зачастую диктует правила для представления. Поменяйте структуру HTML кода, и ваша логика рухнет в большинстве случаев.
        2. Отладка событий, навешенных через addEventListener еще та боль. Тем более, без открытия трех исходников (а иногда и больше), в том, как работает код очень тяжело разобраться. Отладка проще, говорите?
        3. Что с поддержкой не так? Детальнее можно?
        • 0
          >Поменяйте структуру HTML кода, и ваша логика рухнет в большинстве случаев.

          Только если навешивание обработчика завязано на структуру (а не на идентификатор или класс элемента).
          • +1
            Если вы игнорируете глубину вложенности, то можете напороться на баг. У вас есть некий управляющий элемент с кнопкой «Закрыть», внутри которого есть несколько аналогичных элементов с кнопочками «Закрыть». Вам нужно навесть на одни кнопочки обработчик «сохранить_и_закрыть» а на другие «закрыть».

            Ваши действия:

            Вариант 1
            Идентифицировать дополнительно кнопки, чтобы их типы явно отличались друг от друга. Тогда можно легко найти нужную кнопку по уникальному идентификатору. Как итог — проблема уникальности идентификаторов или ошибки неправильного определения функциональности кнопки.

            Вариант 2
            Привязаться к структуре самого управляющего элемента, ограничить распространение поиска кнопок до определенной глубины. Чревато багами из-за случайного изменения структуры.

            Чем крупнее проект, тем изощреннее баги будут вылазить.
            • +1
              >Как итог — проблема уникальности идентификаторов или ошибки неправильного определения функциональности кнопки.

              Как-то не возникало у меня доселе проблемы уникальности имён переменных.

              Понятно, что если называть элементы button1, button2, то проблема уникальности встанет во весь рост, вот только проблема тут явно не связана с темой обсуждения.

              А если присваивать осмысленные идентификаторы (closePageBtn, closePanelBtn, closeSmthElseBtn), то никакой проблемы не будет. А если таки будет, то тут уже нужно пересматривать весь UI, т.к. такое количество кнопок «Закрыть», да ещё и схожего назначения (в результате чего появляются схожие идентификаторы) — явный провал в дизайне UI.
              • +1
                Проблем уникальности нет, если интерфейс статический. Если добавить подгружаемые модули по мере работы пользователя с интерфейсом, то можно выхватить все прелести дубликатов.

                Давайте рассмотрим следующий пример. У вас есть дерево-раскрывашка (как список директорий в проводнике). Есть замечательный элемент [+], при нажатии на который нужно открыть дочернюю ветку. Логично подравнять структуру HTML под такую, чтобы было легко выполнять данные манипуляции. Действия, которые нужно производить, просты — найти некий родительский элемент и ему присвоить класс, что ты показывай свои дочерние элементы. Но случилась «беда», дерево не просто огромное, а очень огромное, например, список улиц всех городов всех регионов страны. И было принято решение догружать данные по мере поступления. Другими словами, нужно найти некий контейнер, в который догрузить контент. И контейнер с дочерними элементами нужно не просто найти, а тот, который соотвествует элементу, а не все дочерние внутри него. Написать селектор можно, но как только изменится HTML структура, все сломается.

                Мало того, в подгружаемых элементах нужно тоже каким-то способом активировать наши плюсики [+]. А все работает асинхронно… Вроде бы проблем нет. Но они почему-то всегда есть.
                • 0
                  >Если добавить подгружаемые модули по мере работы пользователя с интерфейсом, то можно выхватить все прелести дубликатов.

                  В этом случае отлично работает связка «класс элемента + id с порядковым номером». Класс задаёт общее поведение, id идентифицирует конкретный экземпляр.

                  А в общем и целом, всё это дело вкуса, а о вкусах не спорят. Хотя если разобраться, то в большинстве случаев спорят как раз о вкусах %)
                  • +1
                    На порядковые номера нужно заводить менеджер идентификаторов. Иначе будет myID_1 и подгрузится еще раз myID_1. Я столько уже навыхватывал проблем с дубликаторами идентификаторов, что решил их использовать как можно меньше.

                    Тут спор даже не о вкусах, и даже и не спор. Я описываю типичные проблемы, которые возникают как раз при перегибах палки как в пунктах 6, 7, 10 общих тезисов статьи автора. Хотя, чего я так переживаю, каждый волен выбирать то, что ему ближе.
    • 0
      1. Смешивание логики и представления (JavaScript в шаблоне).
      2. Невозможно навесить несколько обработчиков (но чаще всего бывает 1 обработчик).
      3. Для событий в атрибутах необходимы глобальные объекты (чаще глобальные объекты и так торчат).
      • +2
        Забавно, что к примеру knockoutjs.com не считает п.1 зазорным. MVVM, однако.
        • +3
          [sarcazm]Да то наверное какие-то лошки-студенты писали...[/sarcazm]

          Я похожий принцип уже года 4 в разработке использую. На jQ теперь смотреть не могу без слез. Вот такой я лошара.
      • +1
        По поводу JS в шаблоне. Как в таком случае следует расматривать такую, довольно таки удачную фичу, как _.template в underscore.js? С одной стороны конечно с ним очень частые проблемы, но зато крайне удобно.
    • +1
      Поступая так, вы не сможете использовать в обработчике внутреннее состояние какого-либо объекта, не делая его глобальным. Представьте, что у вас 50 модулей — что, всех их делать глобальными, чтобы обработчики могли с ними работать? Статья как раз про то, что подобные вещи мешают масштабируемости приложения.
      • +1
        У вас ООП мышление. Это не единственно правильное мышление в разработке. JS- скриптовый язык, и позволяет более гибко подходить к созданию приложений.

        Например, я использую принцип активатор-диспетчер-обработчик. Событийная модель. При таком подходе становится банально все равно, сколько модулей, глобальные они или локальные, API диктует условия. И масштабируемость реализуется дешевле чем ядра, песочницы и прочее.
        • +2
          Расскажите подробнее, пожалуйста, про «принцип активатор-диспетчер-обработчик» желательно с небольшим примером. Буду признателен.
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      По поводу локализаций — я лично вас понял… тоже скривил моську когда увидел. А можете подробнее раскрыть насчет модулей, желательно с примерами?
      • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Примерно тоже самое предлагаю и я. Только вместо перечисления всех модулей используется функция препроцессора buildFrom (чтобы не копипастить имена).
          • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Спасибо за статью. До многих вещей доходил самостоятельно. В частности, о пользе событий и слабого связывания. Но вместо событий jQuery стал использовать js-signals — библиотека помощнее, да и работает в разы быстрее. У них же есть сравнение между разными реализациями событий.
    • +1
      кстати, тоже пришел к этой же идеи и этой же библиотеке :)
      • +1
        Это закономерное движение в сторону event-based бибилиотек. Потому что они проще и легче масштабируются. Но требуют подготовки разработчика. С мышлением jQuery, mootools и прочих аналогов переходить на такие библиотеки гораздо сложнее
  • +14
    замена вызова метода на возбуждение события имеет и другую сторону: если запускается одно событие, а ловится другое (опечатка, неудачный рефакторинг, вливание стороннего кода несовместимой версии), то мы не получим в консоли ошибок вменяемого сообщения. вместо этого надо будет плясать с бубном в поисках этого расхождения. с тем же успехом можно написать что-нибудь типа try { Module2.getSomeValue() } catch( e ){ }
    вызов метода — простейший способ доставки сообщения _с уведомлением о доставке_ (точнее с уведомлением о недоставке, но не принципиально). без этой фичи передача сообщений превращается в хаос, где никто ни за что не отвечает. я всё послал, какие претензии?: О а я ничего не получал, работать не буду!: О

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

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

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

    далее, ты пишешь «Модули веб-приложений состоят из HTML + CSS + JavaScript + Ресурсы» но тут же приводишь в пример структуру директорий, где НЕТ МОДУЛЬНОСТИ. я не могу добавить или удалить модуль, без рассовывания всех его ресурсов по разным директориям и ручного контролирования всех зависимостей (а событийная модель лишь маскирует зависимости, но они всё-равно есть — добавлять модуль А может быть совершенно бессмысленно без модулей Б, В и Г). я пробовал писать много мелких модулей (не более нескольких килобайт каждый) с ручным прописыванием зависимостей, но уже при количестве в районе 50 начинается настоящий ад — мало того, что на каждый чих надо лезть в список зависимостей и добавлять нужное, так ещё и надо не забыть удалить ненужную зависимость, иначе она притянет тебе модули, которые будут висеть мёртвым грузом. поэтому первое что должен делать модульный фреймворк — обеспечить автоматическое управление зависимостями.

    код модулей у вас получился монструозным. нельзя просто создать файл и начать писать код — сначала надо скопипастить в него шаблон. а если захочется изменить апи экспорта переменных, то придётся править все файлы. а при сборке — пытаться какими-то эвристиками вычищать эту копипасту. что мешает сделать менее авторитарное ядро вида:

    MySuperPuperCore.defineModule( function( sandbox ){
    sandbox.DataGenerator= {

    }
    })

    и пусть оно само заботится обо всём?

    «sandbox.getResource('interval')» — надеюсь эта волшебная функция позволит мне задавать, чтобы один дата-генератор выдавал данные с одним интервалом, а другой — с другим?

    «Нам необходимо загружать и регистрировать модули. Мы должны это уметь делать как при сборке так и динамически.» — то есть вместо того, чтобы сделать какой-нибудь index.js в который прилинковать сразу все необходимые модули с указанием их версий и быстро загрузить скопом только изменившиеся файлы, вы загружаете 1 модуль, когда он обратится к другому модулю — загружаете его, и так далее? если до сервера всего 5 мегабит через двойной впн, то это превращается в диалап.
    • +1
      О гарантированный доставке сообщения. В среде браузера событие доставляется всегда. Управлением ошибками транспортов занимается ядро — оно может генерировать то или иное событие в зависимости от результата транспортировки данных.
      обеспечить автоматическое управление зависимостями
      Про это я не упомянул, но при разработке чего-то более-менее большого без автоматизации никуда. Безусловно нужен скрипт, который будет создавать все эти мелкие файлы, будет управлять зависимостями и т.п. Вообще все что напрягает нужно автоматизировать.

      О виде модуля. Я писал, что модули имеют не очень удобный формат из-за выбранной архитектуры и моих предпочтений писать максимум руками. Согласен, что можно подумать о более удачном формате при такой архитектуре. Например, мне очень нравится такой вид модуля Modules/Wrappings-Explicit-Dependencies
      module.declare(function(require, exports, module) {
          exports.foo = "bar"; 
      });
      надеюсь эта волшебная функция позволит мне задавать, чтобы один дата-генератор выдавал данные с одним интервалом, а другой — с другим
      Предполагается, что модули существуют в единственном экземпляре.

      то есть вместо того, чтобы сделать какой-нибудь index.js в который прилинковать сразу все необходимые модули с указанием их версий и быстро загрузить скопом только изменившиеся файлы, вы загружаете 1 модуль, когда он обратится к другому модулю — загружаете его, и так далее?
      для продакшен версии все статически собирается (все необходимые модули), подключаются зависимости (в итоге получается один файл). Некоторые опциональные части могут загружаться динамически — каждый модуль состоит из нескольких частей мелкие части мы можем собрать статически, а крупные (логика модуля) могут подключаться динамически. Так может быть, поэтому я заложил такую возможность в архитектуру. Подобное сделано в Я.Почте.
      • +2
        ну разумеется оно всегда доставляется, только не всегда тому, кто его ждёт ибо баг.

        какой смысл автоматически генерировать эти мелкие файлы, чтобы с их помощью генерировать сборки, если можно сразу генерировать сборки?

        ну так чем не угодил commonjs?

        то есть как это в единственном? то есть я не могу вставить на страницу два датапикера чтоли?

        ну разумеется для продакшена модули рассовываются по пакетам (пакетам модулей! модули вообще не должны знать как их раскладывают по пакетам и динамически грузиться должны именно пакеты, а не модули). речь шла о разработке, если это не очевидно.
        • +1
          только не всегда тому, кто его ждёт ибо баг
          Частично этот баг устраняет дескриптор (песочница проверяет имя события перед биндом по дескриптору). Дескриптор же мы можем проверять автоматизированно.
          какой смысл автоматически генерировать эти мелкие файлы
          чтобы разделить соответствующие части модуля и модули друг от друга.
          то есть как это в единственном? то есть я не могу вставить на страницу два датапикера чтоли?
          Модули состоят из блоков/компонентов, определяемых версткой и css и js. Датапикер — компонент.
          речь шла о разработке, если это не очевидно.
          Как правило у разработчика бывает широкий канал, да и сервер часто бывает под боком. Так что загрузить 100 файлов не будет большой проблемой. Зато при возникновении ошибки в каком-либо модуле мы сразу будем знать его имя и строку(хороший бонус). В любом случае все можно гибко переделать. Например, в проекте над которым работаю я все собирается в 1н файл.
          • +2
            что если та же опечатка и в дескрипторе?

            почему не разбить модули по директориям?

            это не отменяет вопрос — если настройки выносить в ресурсы, то нельзя для разных экземпляров компонент задавать разные значения. значит нужно где-то хранить настройки для каждого инстанса.

            а иногда канал не очень широкий и тогда хочется громко материться. у вас в 1 файл при разработке пакуется? ну тогда сами себе злобные буратины. а по поводу бонуса — посмотри как сделано тут habrahabr.ru/blogs/css/96434/ то же самое не сложно сделать и для js
            • +1
              > что если та же опечатка и в дескрипторе?
              Дескриптор можно проверить автоматизированно исходя из имеющегося списка событий(который тоже сферический в вакууме). Да и вообще от ошибок ни кто не застрахован при любом раскладе.

              > почему не разбить модули по директориям?
              Да, это как вариант структуры. Так будет даже лучше. Тот пример из статьи — это попытка обратить внимание разработчиков на правильный подход (я не утверждаю, что мой пример идеален и в нем нет изъянов).

              >у вас в 1 файл при разработке пакуется?
              не минимизируется(ибо глупо), но пакуется gz

              >посмотри как сделано тут
              Посмотрел. Посмотрите и вы clubs.ya.ru/bem/ ну хотя бы видео :)
              • +1
                А почему вы решили, что этот подход правильный?
                • +2
                  Задача ставилась такая: «Показать, что поддерживать крупное веб-приложение с кодом как у типичного сайта невозможно» — причины я объяснил и показал возможный вариант решения. Опять же не утверждаю, что он идеален. Но поддерживать такой код более чем реально.
                  Что вам конкретно не нравится?
              • +3
                ещё раз: у нас есть 2 модуля, один бросает событие, другой ловит. как в дескрипторах указать, что событие которое ловит второй должно быть то же самое, что бросает первый? это не повод предоставлять для ошибок благодатную почву.

                я лично присутствовал и был просто в ужасе от того, что вы наворотили. у вас абстракции лишь увеличивают сложность системы, вместо того, чтобы её уменьшать. яваскрипт, который парсится яваскриптом, чтобы сформировать яваскрипт, который формирует хтмл с инлайновым яваскриптом. собственный примитивный язык запросов с яваскриптовыми вставками, для которого даже подсветки синтаксиса нигде нет, что уж говорить о чём-то большем. в погоне за байтиками (которая началась слишком поздно — широкополосный инет уже стал массовым) вы наворотили настолько сложную систему с кучей условностей, что вливание нового человека в разработку без многодневных семинаров просто невозможна, а разработка возможна исключительно на сервере, ибо на локалхосте запаришься создавать всю необходимую инфраструктуру. казалось бы, зачем сервер для разработки яваскриптового модуля, который исполняться будет на клиенте?
                • +1
                  trigger:newData
                  listen:newData
                  яваскрипт, который парсится яваскриптом, чтобы сформировать яваскрипт...
                  Сейчас вы говорите о препроцессоре, участвующий в сборке проекта, которому подсветка синтаксиса и тп не нужно. Он вставляется 1 раз в код и на века.
                  зачем сервер для разработки яваскриптового модуля, который исполняться будет на клиенте
                  сборка

                  • 0
                    trigger:newData
                    listen:mewData

                    и никакие автогенерированные тесты это не отследят
                    • 0
                      От ошибок, опечаток мы не застрахованы, все это знают. Но такую проблему мы можем отловить руками, подсчитав все события:
                      newData - 6 слушателей
                      mewData - 1 слушатель (подрзрительно)
                      • 0
                        когда число таких событий достигает сотен — обнаружить такое подозрительное место крайне сложно.
                        • 0
                          Согласен. Но большую часть их можно отловить. Вот другая идея. Есть модули с дескрипторами у некоторых модулей могут быть криво написаны события и дескрипторы.
                          Случай 1: кривой дескриптор, парильный модуль или наоборот
                          — при тестировании мы собираем логи песочниц и в этот лог попадет запись о том, что модуль пытается слушать или вызывать событие, которое не задекларировано.
                          Случай 2: у 1го модуля все кривое, соответственно тесты собраны тоже криво
                          — мы проводим статический анализ дескрипторов и смотрим, что такое-то событие слушает такой-то модуль, но другой модуль не порождает и наоборот. Так же у нас может быть список правильных событий, по которому мы можем сверять их имена.

                          Это не панацея против ошибок, но большую часть таким способом мы можем выловить.
    • 0
      Мне тоже не нравится лишняя сложность, и я каждый раз ищу компромиссы, в зависимости от проекта.
      Да описанный подход имеет минусы, но также дает плюсы, вносит порядок. Я, конечно, не буду оригинальным в своей просьбе, но вы бы показали как правильно надо делать.
  • +1
    Спасибо, интересная статья. Многие рекомендации из нее можно использовать не только в клиентских js-приложениях.
  • +9
    Много слов, много перегруженных примеров, мало внятной и новой информации. Не зная таких простейших вещей, не стоит даже начинать писать «тяжелое приложение», лучше для начала саму дисциплину программирования подучить немного.

    Все сказанное в статье можно описать тремя тезисами: 1) абстракция; 2) абстракция; 3) абстракция.

    Не увеличивайте энтропию.
  • +1
    Мне кажется, что данное решение проблем расширяемости и поддержки приводит к другой проблеме — нарушению Главным Техническим Императивом Разработки ПО — управлением сложностью.

    Совершенное необязательное увеличение сложности приведет к повышению порога вхождения в проект, вы пишете под номером 1: «С вами работают другие люди — уважайте их труд и цените их время»

    Представьте ситуацию — главный архитектор по какой-либо причине недоступен. Приходит новый программист. Насколько легко ему будет влиться в проект?

    Спасибо за статью, но вам не кажется, что описанная архитектура несколько излишняя?
    • +2
      В любом случае есть какой-то порог вхождения. Нельзя знать всего в первый же день.
      описанная архитектура несколько излишняя?
      Единственное, что мне кажется подозрительным — это обретка над базовой библиотекой.
  • +1
    Гм, по моему, это уже перебор (вспоминается статья про расчет факториала на Яве с абстрактными фабриками). Ну, ок, я могу еще понять использование слотов и сигналов для UI-компонент, это может быть полезно для наведения порядка в коде, но предложенный вами фреймворк крайне сложен для понимания. Я смотрю на структуру папок. и понимаю, что с такой структурой только разбираться, с чего начать создавать модуль, надо полчаса.

    Касательно проблемы, упомянутой в начале статьи: связанности модулей и необходимости писать примитивный код инициализации в большом количестве экземплятров: а если переложить это на бекенд? То есть вы в шаблоне сайта (на сервере, не на клиенте), допустим, пишете: {insert tree datasource=someTreeModel} или {insert gallery images=«1.jpg,2.jpg,3.jpg»}, а этот шаблонизатор сам строит нужную HTML-структуру (с graceful degradation) и генерирует нужный JS-код инициализации дерева. Поменялся фреймворк/код для работы с деревом/галерееей — меняете шаблон этого самого дерева/галереи. И вся эта затея с модулями и событиями становится не нужна, так как у нас больше нет дублирования JS-кода.

    Также, можно вспомнить примеры из реальной жизни: vkontakte — крупный сайт, не использует подобного, ограничивается в общем обычным, немодульным JS (и, что приятно, без jQuery! хоть в этом молодцы).

    Ну и насчет кем-то упоминавшегося ExtJS: ExtJS на меня производит впечатление монструозного, тяжелого, тормозящего фреймоврка, который пытается сделать программирование компонент сайта похожим на программирование в десктопном окружении (формы там, layouts, и все такое). Только вот непонятно, зачем.
    • +1
      Для того, чтобы в вебе делать настоящие веб-приложения, а не только сайты-странички
    • +1
      Ваш подход в целом подходит для обычных сайтов (пусть даже и с большим количеством JS), а не веб-приложений. VK — не веб-приложение.
      • 0
        А что тогда? о_О
        А чем VK отличается от админки CMS?
        • 0
          > А чем VK отличается от админки CMS?
          Тем, что никому не придет в голову админить сайт через мобильник с оплатой трафика по 10р/Мб (без учета роуминга)
  • +1
    Слишком слабая связанность и большое количество абстракций — тоже плохо. На мой взгляд, в вашем примере дескрипторы только мешают. Если поменялся один модуль, или поменялось отношение одного модуля от другого нужно руками править дескрипторы. Где гибкость? Большое количество событий только запутывает. Про проблемы с дебагом уже говорили. Нет ничего смертельного в том, что какой-то модуль системы знает о существовании другого и вызывает его методы. Эта та же зависимость, которую вы скрываете с помощью событий и дескрипторов.
    • 0
      Модуль — микроприложение он вправе подключать какие угодно компоненты и работать с ними как ему вздумается. Чем меньше модуль знает о других модулях — тем лучше, тем проще поддерживать все приложение(события этому способствуют).
      Одна из ролей дескриптора — описывать те события, которые слушает и генерирует данный модуль. Верно, что при добавлении события нам придется исправить как в модуле так и в дескрипторе. Но дескриптор это плюс: мы можем построить карту событий — знать сколько модулей зависят от такого-то события(удобно при переименовании), можем автоматизированно генерировать тесты, можем отлавливать ошибки при сборке (подключаем модуль, который слушает событие А, но не подключаем модуль, генерирующий событие А — сборщик вызывает варнинг). Без дескриптора такого не получить(можно, но сложно).
  • 0
    Если бы я был девушкой, то я бы ему дала! :)
  • +2
    Эх зазеличь, зря я тебя в свое время не дотыкал носом в нашу модульную систему.
    У вас не правильный мед(!), который, в том числе, не удовлетворяет заявленным параметрам. А точнее не препятствует доступу модуля к тому коду который ему «не надо».
    • +1
      Обязательно проконсультируюсь у тебя. Начало видел, все хотел спросить к чему вы пришли.
      • 0
        Главное отличие в том что модуль явно, в файле своего определения, говорит кто он, из чего состоит и _кого_ему_нужно_.
        На твоем коде это будет образно говоря значить что только то что он просил попадет ему в global, до остального он вообще никак, никакими хаками добраться не сможет.
        В итоге получаем полное знание того что где используется, ну и какие хреновины один модуль может представить другим.
        Полная чистота и прозрачность связности, и техническая невозможность делать кольцевые зависимости :)
    • 0
      А как насчет пост написать?
      • +1
        Окей, только не забываем что я не так крут как наши главное js-обозреватели :)
        • 0
          А главные js-обозреватели — они где?
          • 0
            вот прямо сейчас — на 5ом и 6ом месте в общем рейтинге :)
  • 0
    мне иногда кажется что одни придумывают как сделать код более запутанным, другие кричат как это круто и правильно и начинают реализовывать тонны кода ради простейшей вещи, а третьи пишут как проще разбираться в искусственно осложненном коде попутно делая еще одно наложение на нативный код, на который уже наложили 100500 крутых модных штук
    в итоге вместо простого и красивого чистого кода получаем кучу библиотек, которые к 2кб реальных нужных данных подгружают еще 1-2 мб фигни… зато «модно выглядит» и «все так делают»
    • 0
      Когда ваше приложение разрабатывает, скажем, 5 человек (вероятно не супер-звезд), то 1-2 мб фигни оправдывают вообще не работающий код в противном случае.
  • +1
    А в фабрике loaderFactory нет ли ошибки? Там self в аргументе функции и переопределение ее в произведенной функции
    • 0
      Исправлено
  • 0
    Выбрал в качестве альтернативы для загрузки модулей require.js. Простая и удобная вещь.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.