Модульный подход довольно распространённая техника программирования в JavaScript. Обычно его понимают довольно хорошо, но продвинутые техники описаны недостаточно. В этой статье я рассмотрю основы и затрону некоторые сложные приёмы, включая один, по моему мнению, оригинальный.
Мы начнём с несложного обзора модульного подхода, хорошо известного с тех пор, как Эрик Миралья (Eric Miraglia) из YUI впервые об этом написал. Если вам уже знаком модульный подход, переходите сразу к «Продвинутым техникам».
Эта основопологающая конструкция лежит в основе всего, и реально лучшее, что есть в жаваскрипте. Мы просто создаём анонимную функцию, и немедленно её исполняем. Весь исполняемый код живёт внутри замыкания, обеспечивающего приватность и сохранение состояния на протяжении жизни всего приложения.
(function () {
// ... все var-ы и функции только внутри этого контекста
// по-прежнему имеется доступ к глобальным переменным
}());
Обратите внимание на () вокруг анонимной функции. Этого требует язык, поскольку операторы, начинающиеся со слова function, всегда интерпретируются как объявления функций. Добавление () создаёт вместо этого функциональное выражение.
JavaScript поддерживает так называемые умолчательные глобальные. Встретив имя переменной, интерпретатор проходит по цепочке контекстов назад в поисках оператора var для этого имени. Если таковой не находится, переменная полагается глобальной. Если она используется в присваивании, создаётся глобальная, если её ещё не было. Это означает, что использовать или создавать глобальные переменные в анонимных замыканиях очень просто. К сожалению, это ведёт к плохо поддерживаемому коду, так так (людям) не очевидно, какие переменные глобальны в данном файле
К счастью, наша анонимная функция предлагает простую альтернативу. Передавая глобальные в качестве параметров анонимной функции, мы импортируем их в наш код, что и чётче, и быстрее, чем умолчательные глобальные. Например:
(function ($, YAHOO) {
// теперь в коде есть доступ к переменным jQuery (как $) и YAHOO
}(jQuery, YAHOO));
Иногда вы хотите не просто использовать глобальные, вы хотите их объявить. Мы можем это легко сделать, экспортируя их через возвращаемое значение анонимной функции. Этот приём завершает основной модульный подход, вот полный пример:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
Обратите внимание, что мы объявили глобальный модуль под названием MODULE с двумя публичными членами: метод по имени MODULE.moduleMethod и переменная по имени MODULE.moduleProperty. Кроме того, он хранит отдельное внутреннее состояние, используя замыкание анонимной функции, плюс мы легко можем импортировать глобальные переменные, используя предыдущий подход.
Несмотря на то, что во многих случаях хватит вышеизложенных приёмов, мы можем их улучшить и создать весьма мощные, расширяемые конструкции. Рассмотрим их по очереди, начиная с нашего модуля по имени MODULE.
Одно из ограничений модульного подхода в том, что весь модуль должен содержаться в одном файле. Любой, кто работал с большими программами понимает значение разбиения кода на несколько файлов. К счастью, имеется элегантное решение для пополнения модулей. Сначала мы импортируем модуль, потом добавляем члены, а потом его экспортируем. Вот пример с пополнением нашего модуля MODULE:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
Мы здесь снова используем var для единообразия, хотя это и не обязательно. После того, как это код выполнится, наш модуль будет иметь новый публичный метод под названием MODULE.anotherMethod. Файл пополнения также будет хранить своё собственное состояние и импортированные переменные.
Предыдущие примеры полагались на модуль, созданный заранее, и пополняемый потом, но можно сделать и иначе. Лучшее, что может сделать приложение на JavaScript для повышения производительности, это загрузить скрипты асинхронно. Мы можем сделать гибкие модули, разбитые на куски, которые загружают себя в любом порядке при помощи свободного пополнения. Каждый файл должен иметь такую структуру:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
В этой схеме оператор var нужен всегда. Заметьте, что импорт создаст модуль, если его ещё не было. Это означает, что вы можете использовать утилиты наподобие LABjs и загружать все свои файлы с модулями параллельно, без блокирования.
Свободное пополнение это хорошо, но оно накладывает ограничения, главное из которых в том, что вы не можете безопасно переопределить члены модуля. Кроме того, вы не можете использовать члены модуля из других файлов во время инициализации (но можете после её) завершения. Ограниченное пополнение задаёт порядок загрузки, но позволяет переопределение. Вот простой пример (пополнение нашего старого MODULE):
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
Здесь мы переопределили MODULE.moduleMethod, но сохранили ссылку на исходный метод, если она нужна.
var MODULE_TWO = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var super_moduleMethod = old.moduleMethod;
my.moduleMethod = function () {
// override method on the clone, access to super through super_moduleMethod
};
return my;
}(MODULE));
Такой подход вносит некоторую элегантность, но за счёт гибкости. Члены, являющиеся объектами или функциями, не дублируются, они продолжают существовать как один объект с двумя именами. Изменение одного меняет и второе. Для объектов это можно исправить через рекурсивное клонирование, но функциям, похоже, ничем не помочь, разве что через eval. Так или иначе, я включил это для полноты картины.
Серьёзное ограничение разбиения модуля по файлам состоит в том, что каждый хранит своё собственное состояние, и не видит состояния других файлов. Вот пример свободно-дополненного модуля, который хранит состояние, несмотря на все пополнения:
var MODULE = (function (my) {
var _private = my._private = my._private || {},
_seal = my._seal = my._seal || function () {
delete my._private;
delete my._seal;
delete my._unseal;
},
_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE || {}));
Любой файл может задать члены на локальной переменной _private, и они будут немедленно доступны снаружи. Как только это модуль полностью загрузится, приложение должно вызввать MODULE.seal(), что предотвратит доступ извне к внутренней _private. Если мы хотим пополнить модуль ещё за время жизни приложения, один из внутренних методов может вызвать _unseal() перед загрузкой нового файла, а потом _seal() после его выполнения.
Это пришло мне в голову сегодня на работе, я такого раньше не видел. Думаю, что это очень полезный подход, и стоит отдельного описания.
Наш последний продвинутый подход самый простой. Для создания подмодулей есть много причин. Это как создать обычный модуль:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
Сколь бы очевидно это ни было, мне показалось это стоящим упоминания. Подмодули имеют все свойства обычных модулей, включая пополнение и сохранения состояния.
Большинство продвинутых подходов могут сочетаться. При создании сложного приложения я бы лично избрал свободное пополнение, приватные состояния и подмодули.
Я совсем не затронул вопросы производительности, но коротко могу сказать: модульный подход производителен. Он хорошо минифицируется, что ускоряет загрузку. Свободное пополнение позволяет примениять неблокирующую параллельную загрузку, что также ускоряет запуск кода. Время инициализации, скорее всего, побольше, чем в других подходах, но это того стоит. Производительность выполнения не должна страдать, если глобальные переменные корректно импортированы, и видимо будет ещё лучше в подмодулях из-за сокращения цепочки переменных при помощи локальных.
В завершение, вот примет подмодуля, который динамически загружается своим родителем (и при необходимости, создаётся). Я пропустил сохранение состояния для краткости, но добавить его несложно. Этот подход позволяет загружать сложный иерархический код полностью параллельно, с подмодулями и прочим.
var UTIL = (function (parent, $) {
var my = parent.ajax = parent.ajax || {};
my.get = function (url, params, callback) {
// ok, so I'm cheating a bit :)
return $.getJSON(url, params, callback);
};
// etc...
return parent;
}(UTIL || {}, jQuery));
Надеюсь, что вам понравилось, поделитесь своими мыслями. А теперь вперёд, писать на JavaScript модульно!
Основы
Мы начнём с несложного обзора модульного подхода, хорошо известного с тех пор, как Эрик Миралья (Eric Miraglia) из YUI впервые об этом написал. Если вам уже знаком модульный подход, переходите сразу к «Продвинутым техникам».
Анонимные замыкания
Эта основопологающая конструкция лежит в основе всего, и реально лучшее, что есть в жаваскрипте. Мы просто создаём анонимную функцию, и немедленно её исполняем. Весь исполняемый код живёт внутри замыкания, обеспечивающего приватность и сохранение состояния на протяжении жизни всего приложения.
(function () {
// ... все var-ы и функции только внутри этого контекста
// по-прежнему имеется доступ к глобальным переменным
}());
Обратите внимание на () вокруг анонимной функции. Этого требует язык, поскольку операторы, начинающиеся со слова function, всегда интерпретируются как объявления функций. Добавление () создаёт вместо этого функциональное выражение.
Глобальный импорт
JavaScript поддерживает так называемые умолчательные глобальные. Встретив имя переменной, интерпретатор проходит по цепочке контекстов назад в поисках оператора var для этого имени. Если таковой не находится, переменная полагается глобальной. Если она используется в присваивании, создаётся глобальная, если её ещё не было. Это означает, что использовать или создавать глобальные переменные в анонимных замыканиях очень просто. К сожалению, это ведёт к плохо поддерживаемому коду, так так (людям) не очевидно, какие переменные глобальны в данном файле
К счастью, наша анонимная функция предлагает простую альтернативу. Передавая глобальные в качестве параметров анонимной функции, мы импортируем их в наш код, что и чётче, и быстрее, чем умолчательные глобальные. Например:
(function ($, YAHOO) {
// теперь в коде есть доступ к переменным jQuery (как $) и YAHOO
}(jQuery, YAHOO));
Экспорт модуля
Иногда вы хотите не просто использовать глобальные, вы хотите их объявить. Мы можем это легко сделать, экспортируя их через возвращаемое значение анонимной функции. Этот приём завершает основной модульный подход, вот полный пример:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
Обратите внимание, что мы объявили глобальный модуль под названием MODULE с двумя публичными членами: метод по имени MODULE.moduleMethod и переменная по имени MODULE.moduleProperty. Кроме того, он хранит отдельное внутреннее состояние, используя замыкание анонимной функции, плюс мы легко можем импортировать глобальные переменные, используя предыдущий подход.
Продвинутые подходы
Несмотря на то, что во многих случаях хватит вышеизложенных приёмов, мы можем их улучшить и создать весьма мощные, расширяемые конструкции. Рассмотрим их по очереди, начиная с нашего модуля по имени MODULE.
Пополнение
Одно из ограничений модульного подхода в том, что весь модуль должен содержаться в одном файле. Любой, кто работал с большими программами понимает значение разбиения кода на несколько файлов. К счастью, имеется элегантное решение для пополнения модулей. Сначала мы импортируем модуль, потом добавляем члены, а потом его экспортируем. Вот пример с пополнением нашего модуля MODULE:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
Мы здесь снова используем var для единообразия, хотя это и не обязательно. После того, как это код выполнится, наш модуль будет иметь новый публичный метод под названием MODULE.anotherMethod. Файл пополнения также будет хранить своё собственное состояние и импортированные переменные.
Свободное пополнение
Предыдущие примеры полагались на модуль, созданный заранее, и пополняемый потом, но можно сделать и иначе. Лучшее, что может сделать приложение на JavaScript для повышения производительности, это загрузить скрипты асинхронно. Мы можем сделать гибкие модули, разбитые на куски, которые загружают себя в любом порядке при помощи свободного пополнения. Каждый файл должен иметь такую структуру:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
В этой схеме оператор var нужен всегда. Заметьте, что импорт создаст модуль, если его ещё не было. Это означает, что вы можете использовать утилиты наподобие LABjs и загружать все свои файлы с модулями параллельно, без блокирования.
Ограниченное пополнение
Свободное пополнение это хорошо, но оно накладывает ограничения, главное из которых в том, что вы не можете безопасно переопределить члены модуля. Кроме того, вы не можете использовать члены модуля из других файлов во время инициализации (но можете после её) завершения. Ограниченное пополнение задаёт порядок загрузки, но позволяет переопределение. Вот простой пример (пополнение нашего старого MODULE):
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
Здесь мы переопределили MODULE.moduleMethod, но сохранили ссылку на исходный метод, если она нужна.
Клонирование и наследование
var MODULE_TWO = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var super_moduleMethod = old.moduleMethod;
my.moduleMethod = function () {
// override method on the clone, access to super through super_moduleMethod
};
return my;
}(MODULE));
Такой подход вносит некоторую элегантность, но за счёт гибкости. Члены, являющиеся объектами или функциями, не дублируются, они продолжают существовать как один объект с двумя именами. Изменение одного меняет и второе. Для объектов это можно исправить через рекурсивное клонирование, но функциям, похоже, ничем не помочь, разве что через eval. Так или иначе, я включил это для полноты картины.
Кросс-файловое состояние
Серьёзное ограничение разбиения модуля по файлам состоит в том, что каждый хранит своё собственное состояние, и не видит состояния других файлов. Вот пример свободно-дополненного модуля, который хранит состояние, несмотря на все пополнения:
var MODULE = (function (my) {
var _private = my._private = my._private || {},
_seal = my._seal = my._seal || function () {
delete my._private;
delete my._seal;
delete my._unseal;
},
_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE || {}));
Любой файл может задать члены на локальной переменной _private, и они будут немедленно доступны снаружи. Как только это модуль полностью загрузится, приложение должно вызввать MODULE.seal(), что предотвратит доступ извне к внутренней _private. Если мы хотим пополнить модуль ещё за время жизни приложения, один из внутренних методов может вызвать _unseal() перед загрузкой нового файла, а потом _seal() после его выполнения.
Это пришло мне в голову сегодня на работе, я такого раньше не видел. Думаю, что это очень полезный подход, и стоит отдельного описания.
Подмодули
Наш последний продвинутый подход самый простой. Для создания подмодулей есть много причин. Это как создать обычный модуль:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
Сколь бы очевидно это ни было, мне показалось это стоящим упоминания. Подмодули имеют все свойства обычных модулей, включая пополнение и сохранения состояния.
Выводы
Большинство продвинутых подходов могут сочетаться. При создании сложного приложения я бы лично избрал свободное пополнение, приватные состояния и подмодули.
Я совсем не затронул вопросы производительности, но коротко могу сказать: модульный подход производителен. Он хорошо минифицируется, что ускоряет загрузку. Свободное пополнение позволяет примениять неблокирующую параллельную загрузку, что также ускоряет запуск кода. Время инициализации, скорее всего, побольше, чем в других подходах, но это того стоит. Производительность выполнения не должна страдать, если глобальные переменные корректно импортированы, и видимо будет ещё лучше в подмодулях из-за сокращения цепочки переменных при помощи локальных.
В завершение, вот примет подмодуля, который динамически загружается своим родителем (и при необходимости, создаётся). Я пропустил сохранение состояния для краткости, но добавить его несложно. Этот подход позволяет загружать сложный иерархический код полностью параллельно, с подмодулями и прочим.
var UTIL = (function (parent, $) {
var my = parent.ajax = parent.ajax || {};
my.get = function (url, params, callback) {
// ok, so I'm cheating a bit :)
return $.getJSON(url, params, callback);
};
// etc...
return parent;
}(UTIL || {}, jQuery));
Надеюсь, что вам понравилось, поделитесь своими мыслями. А теперь вперёд, писать на JavaScript модульно!