Сегодня ни один большой web-проект не обходится без JavaScript. Этот язык удобен для создания интерактивных интерфейсов, к тому же имеет низкий порог входа. Но начинающие разработчики часто пишут код «простыней», не задумываясь об архитектуре проекта. Это вызывает трудности с расширением и поддержкой, особенно, если ведется совместная работа. В этой статье поговорим об одном из способов организовать код в виде модулей, и о преимуществах этого подхода.
Для примера реализуем на странице слайд-шоу. Будем менять атрибут srс у нужного изображения по интервалу:
var imgs = ['img/first.jpg', 'img/second.jpg', 'img/third.jpg'],
imgElement = document.getElementById('slideshow'),
currentImg = 0;imgElement.src = imgs[currentImg];
var next = function() {
currentImg = (currentImg + 1) % imgs.length;
imgElement.src = imgs[currentImg];
};
setInterval(next, 3000);
В таком коде все переменные находятся в глобальной области видимости, то есть являются свойствами объекта window. В большом приложении будет непонятно к какой функциональной части относятся переменные, а также могут возникнуть конфликты в названиях.
Модульность
В такой ситуации было бы неплохо организовать код в виде отдельного модуля, при этом оставить в глобальном объекте window только необходимые публичные методы, а все остальное спрятать «под капот». Такой код можно написать в отдельном файле и без опаски подключать к большому проекту. Рассмотрим один из возможных способов организации модулей в JavaScript.
Теоретическая основа
Для создания модулей мы будем использовать замыкания функции. Если коротко, то замыканием называются все переменные, находящиеся в области видимости функции.
var name = 'Вася';
var sayHello = function() {
var text = 'Привет';
alert(text + ', ' + name)
}
Замыкание функции sayHello — переменные name и text.
Также мы будем использовать конструкторы. Конструктор — это функция, вызванная через оператор new.
var obj = new myFunc();
При таком вызове в переменную obj запишется новый объект, свойства которого можно определять в myFunc через this.
var myFunc = function() {
this.property = 'newProp';
}
var obj = new myFunc();
console.log(obj.property); //выведет newProp
Функция-конструктор может возвращать объект, в таком случае в obj запишется он.
var myFunc = function() {
return {
newProperty: 'myNewProp'
}
}
var obj = new myFunc();
console.log(obj.newProperty); //выведет myNewProp
Первые шаги
Основная деталь при разработке модуля — архитектура. С самого начала создания нужно четко разделить приватные и публичные методы, а также продумать связь между ними. Методы, отвечающие за одну и ту же сущность стоит объединять в объекты. Перед реализацией модуля, удобно представить его в виде схемы.
Оформим слайд-шоу в виде модуля. Чтобы спрятать переменные, которые не должны быть видны в глобальном объекте, «завернем» весь код в тело функции:
var slideshow = function() {
var imgs = ['img/first.jpg', 'img/second.jpg', 'img/third.jpg'],
imgElement = document.getElementById('slideshow'),
currentImg = 0;
imgElement.src = imgs[currentImg];
var next = function() {
currentImg = (currentImg + 1) % imgs.length;
imgElement.src = imgs[currentImg];
};
};
Теперь в глобальном объекте только одна переменная — slideshow. А все внутренние переменные недоступны, то есть они стали приватными.
Пока в переменную slideshow записана функция. Такое объявление функции называется Expression Declaration. Чтобы вернуть в slideshow объект, вызовем функцию сразу при объявлении.
var slideshow = function(){...}();
Или можно просто вызвать slideshow как конструктор, и записать результат в новую переменную:
var mainSlideshow = new slideshow();
Сейчас значением slideshow будет undefined. А в mainSlideshow будет записан пустой объект.
Создание публичных методов
Теперь, когда основа для модуля готова, схематично опишем его структуру:
Красным обозначены приватные методы и переменные модуля. Точкой входа будет служить публичный метод init, в котором установим начальные значения локальных переменных. Затем из init будет запущен интервал, по которому срабатывает метод next.
var slideshow = function() {
var imgs,
imgElement,
currentImg;
var init = function(imgElementId, imgsPaths, time) {
imgElement = document.getElementById(imgElementId);
imgs = imgsPaths;
currentImg = 0;
next(); //показываем первую картинку
setInterval(next, time);
}
var next = function() {
imgElement.src = imgs[currentImg];
currentImg = (currentImg + 1) % imgs.length;
};
return {
init: init
};
}();
Чтобы сделать метод публичным, запишем его в свойства объекта, который вернет функция-обертка.
В init можно передавать аргументы, которые будут задавать начальное состояние модуля. В данном случае, в качестве аргументов init принимает идентификатор imageElementId тега img, в который будем подгружать слайды, массив путей до картинок и время между слайдами.
Теперь вызовем slideshow в консоли браузера и убедимся, что это объект с единственным свойством init:
Переменные imgs, imgElement и currentImg входят в замыкание функций next и init. Поэтому, после выполнения функции-обертки, эти переменные будут доступны для чтения и записи из методов модуля.
Простейший модуль готов. Добавим на страницу тег img и загрузим картинки.
Убедившись в работоспособности модуля, можем задуматься о его расширении.
Расширение модулей
Модульная разработка позволяет изменять структуру приложения, сохраняя внешний интерфейс. Таким образом, у сторонних разработчиков будет возможность использовать модуль, не разбираясь в коде, а также добавлять новые методы или расширять существующие.
Расширение необходимо, если хочется добавить функциональности уже готовому приложению, не меняя исходный код. Используя приведенный в статье подход, нельзя получить доступ к приватным свойства. Зато можно создать функцию, которая будет обрабатывать входные данные перед передачей их в модуль. Назовем это «надстройкой».
Для примера реализуем «надстройку», которая позволит передавать в slideshow.init время на каждый слайд не в миллисекундах, а в секундах.
var addon = function(slideshow) {
var init = function(imgElementId, imgsPaths, timeS) {
var timeMs = timeS * 1000;
slideshow.init(imgElementId, imgsPaths, timeMs);
};
return {
init: init
};
}(slideshow);
Как видно из примера, модуль addon имеет публичный метод init, который принимает время в секундах, а затем вызывает метод init модуля slideshow с уже обработанными входными данными. Заметим, что slideshow передается в качестве аргумента addon. То есть внутри addon slideshow — это абстрактный модуль.Таким образом можно написать всего одну такую «надстройку», каждый раз передавая туда нужный модуль. Главное, чтобы их публичные методы были совместимы с «надстройкой».
Сборка модулей в один проект
После того, как каждый компонент проекта оформлен в виде отдельного модуля, нужно подключить их на странице сайта. Самый простой способ — загрузить js-файлы c помощью тега. Но в таком случае нужно следить за порядком загрузки скриптов, особенно, если модули как-то общаются между собой. Эта задача заслуживает отдельной статьи, но если в вашем проекте модули независимы между собой, то этот способ вам подойдет.
Остальные способы используют сторонние библиотеки. Например, можно собрать все скрипты в один файл с помощью одной из систем сборки: webpack, gulp, grunt и т.д. Или подключить библиотеку, использующую описание модулей в формате CommonJS (например, RequireJS ). Синтаксис у всех таких библиотек похож и весьма удобен.
Коротко о главном
Модули — удобный и гибкий подход к созданию архитектуры в большом проекте:
- с их помощью можно очистить глобальный объект от лишних переменных и функций;
- появляется логическая организация кода;
- уменьшается вероятность конфликтов в именах переменных, за счет чего модуль можно подключить в любом месте;
- удобно модифицировать и расширять функциональность модуля.
Модули можно хранить в отдельных файлах, поэтому приложение легко поддерживать. К тому же, благодаря хорошей структуре, увеличивается понятность кода. Так что, если вы еще не использовали модули, то самое время начать!