Как вы структурируете ваш JavaScript код? Модульный вариант шаблона.

Перевод статьи:  How Do You Structure JavaScript? The Module Pattern Edition
Автор:  Chris Coyier.

Язык JavaScript интересен тем, что он сам по себе не обязывает вас к соблюдению определенной структуры кода. Как говорится — «хозяин – барин». Чем больше я занимаюсь созданием веб-приложений на JavaScript, тем больше это захватывает. То, как вы структурируете ваш JavaScript код, имеет огромное значение, и вот почему:

  1. Четкая структура повышает читабельность кода, его доступность для других, а также и для вас при повторном обращении к нему.
  2. Грамотное планирование структуры позволяет поддерживать чистоту кода в будущем и является подтверждением уровня вашего мастерства.
  3. Это дает возможность тестирования вашего кода.

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

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

Затем я разместил статью «Изготовление рекламного блока Treehouse», в рамках которой также применил модульный подход для JavaScript. Не так давно Луис Лазарис подключился к обсуждению темы, сообщив, что тоже предпочитает такой способ создания JavaScript кода и поделился своим вариантом шаблона. Позже Луис продолжил свою мысль в этой статье, остановившись более подробно на структуре своего шаблона.

В чем же идея?

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

Модуль.

Начнем с простейшего:

var NewsWidget = {

};

Обычный объект. При написании своего кода, имена переменных я предпочитаю начинать со строчного символа, а модулей – с прописного. Простая условность, повышающая читабельность кода.

Начальные установки.

Создаваемому нами виджету, скорее всего, понадобится несколько начальных установок — значений (к примеру, количество статей, содержащихся в нем). К тому же, нам необходимо определить несколько важных элементов (используемых виджетом DOM узлов), к которым мы будем постоянно обращаться.

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

var s,
NewsWidget = {

settings: {
numArticles: 5,
articleList: $("#article-list")
}

};

Через мгновение мы займемся доступом к этим значениям со стороны вспомогательных функций.

Функция инициализации Init.

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

var s,
NewsWidget = {

settings: {
numArticles: 5,
articleList: $("#article-list"),
moreButton: $("#more-button")
},

init: function() {
// обеспечиваем доступ к настройкам извне
s = this.settings;
}

};

Первое, что делает функция init — определяет переменную s (которая объявляется на одном уровне с модулем, объектом) для того, чтобы с ее помощью ссылаться на значения настроек. Именно благодаря тому, что переменная s была объявлена в этом месте, все вспомогательные функции нашего модуля будут иметь доступ к настройкам (*Локальный объект settings.). Неплохо, да?

Привязка к пользовательскому интерфейсу.

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

var s,
NewsWidget = {

settings: {
numArticles: 5,
articleList: $("#article-list"),
moreButton: $("#more-button")
},

init: function() {
s = this.settings;
this.bindUIActions();
},

bindUIActions: function() {
s.moreButton.on("click", function() {
NewsWidget.getMoreArticles(s.numArticles);
});
},

getMoreArticles: function(numToGet) {
// загрузка дополнительных статей
// с использованием параметра numToGet
}

};

Подключение файлов.

Скорее всего, виджет новостей будет не единственным построенным на JavaScript компонентом вашей страницы. Помимо него вам необходимо будет загрузить множество других модулей. Допустим, что создаваемый нами виджет должен размещаться на каждой странице вашего сайта, тогда соответствующий ему скрипт будет входить в состав файла global.js. В результате всего данный файл будет представлять собой множество взаимосвязанных отдельных модулей, и содержать внушительный фрагмент кода, производящий необходимые действия инициализации при запуске.

Существуют различные методы создания связей между соответствующими компонентами. Для этого можно воспользоваться таким инструментом для разработчика как CodeKit, предусматривающим возможность добавления скриптов и создания зависимостей appends/prepends. Или более экстравагантный способ работы со скриптами – система сборки Grunt.js

На ресурсе CodePen для этих целей мы используем Ruby on Rails и его фреймворк asset pipeline. Поэтому применительно к нашему проекту файл global.js мог бы выглядеть примерно вот так:

//= require common/library.js

//= require module/news-widget.js
//= require module/some-other-widget.js

(function() {

NewsWidget.init();

SomeOtherModule.init();

})();

Вот, собственно, и все, друзья.

Чертовски благодарный способ написания JavaScript, на мой взгляд. Он позволяет решить три вопроса, упомянутые мной при рассмотрении важности структуры кода в начале статьи.

Что же касается последнего вопроса – тестирования, то, насколько мне известно, он не обсуждался должным образом. Но не сомневаюсь, что вы можете себе представить, насколько легче составлять процедуры утверждения (assertions) имея небольшие специфические функции, выполняющие отдельные задачи. Именно такой принцип используется в большинстве инструментов тестирования JavaScript кода (таких как Jasmine). К примеру, в рамках той или иной структуры кода, «я утверждаю, что получая в качестве аргумента определенное значение, требуемая функция приведет к выполнению необходимых изменений и в результате вернет соответствующее значение».

Все это лишь верхушка айсберга. И я лишь слегка коснулся ее, но книга Эдди Омани (Addy Omani) «Learning JavaScript Design Patterns» («Изучаем проектирование JavaScript шаблонов»), которая доступна для свободного чтения онлайн, более детально и глубоко касается этого вопроса.

* Примечание переводчика.

Обзор комментариев

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

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

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

Идеальное в данном случае решение предлагает на самом деле смышленый парень. Приведем здесь предлагаемый им модульный шаблон.

var NewsWidget = (function () {
var s; // private alias to settings
function somePrivateFunction() {
alert("There are " + s.NumArticles + " articles");
}

return {
settings: {
numArticles: 5,
articleList: $("#article-list"),
moreButton: $("#more-button")
},

init: function() {
s = this.settings;
this.bindUIActions();
somePrivateFunction();
},

bindUIActions: function() {
s.moreButton.on("click", function() {
NewsWidget.getMoreArticles(s.numArticles);
});
},

getMoreArticles: function(numToGet) {
// $.ajax or something
// using numToGet as param
}

};
})();

Этот вариант тоже использует самовыполняющуюся функцию, которая возвращает сам объект и соответствующие ему методы и свойства. «Проблемная» переменная быстрого доступа к настройкам модуля в данном случае локальна и не приводит к каким-либо конфликтам имен. Соблюдена сама концепция модульного шаблона, т.е. переменная s не выходит за рамки объекта. Модуль имеет две области — локальную (до оператора return) и глобальную (после оператора return), которая присваивается глобальному объекту NewsWidget. Таким образом, в локальной области мы объявляем нашу переменную и определяем необходимые локальные процедуры, а в глобальной определяем сам объект с соответствующими ему методами и свойствами.

Проблема решена:

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

Комментариев: 3 на Как вы структурируете ваш JavaScript код? Модульный вариант шаблона.

  1. Хороший пример. Спасибо за перевод. Один вопрос только возник — зачем глобальная переменная s?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *