Модульная разработка в JavaScript

10 min read

Сегодня ни один большой 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:

Результат вызова slideshow в консоли

Переменные 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 ). Синтаксис у всех таких библиотек похож и весьма удобен.

Коротко о главном

Модули — удобный и гибкий подход к созданию архитектуры в большом проекте:

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