Организация js кода для джуниоров

С недавних пор я стал работать в сфере web разработки, и еще нахожусь в стадии падавана. Однако недавно я открыл для себя способ организации клиентского javascript кода, который может быть легко интегрирован в любой существующий проект и который легко освоить.

Этот подход называют «Модульный javascript», и под катом мы научимся его применять.

image

Статья названа так, потому что люди на уровне джедая уже используют более совершенные методики и думаю в комментариях поделятся ими.

Задачу я ставил себе следующую:
«Организовать весь клиентский js код удобным способом, что бы его было легко поддерживать, искать ошибки и дополнять».

Мотивацией этому стала работа с чужим сайтом, где весь js был в одном, огромном файле и попытка дополнить что-то вызывала приступ апатии.

Вся суть методики сводится к разбиению нашего приложения на модули. Я так же называю их «виджетами», потому что так проще воспринимать их суть.

Каждый виджет является обособленной сущностью. Он не знает о других виджетах и не обращается к ним на прямую. Виджет может работать только сам с собой и генерировать события, на которые могут подписываться другие виджеты.

Схематически виджет — это некая часть нашего сайта, у которой есть специфическая функциональность.

image

Наш тестовый сайт мы можем мысленно разбить на 3 виджета.
1. Глобальный модуль — будет отвечать за инициализацию других модулей.
2. Профиль — отобразит аватарку пользователя (привет Ричард :) ), и меню с направлениями деятельности.
3. Портфолио — отобразит примеры работ по выбранному направлению у этого юзера

А теперь создадим наши модули.
Каждый модуль будет находится в отдельном js файле.
Html разметку и CSS стили мы рассматривать не будем. Отмечу лишь, что для отображения я обычно использую шаблонизатор входящий в состав underscore.js. А стили, в основном, используются из основного css файла.

Глобальный модуль App.js

// Модуль представляет из себя переменную, которой присвоено значение самовызывающейся анонимной функции
// Функция возвращает объект, предоставляющий публичный API для работы с модулем

var App = (function(){
	//Тут можно определить приватные переменные и методы
	//Например
	var someArray = []; //Не будет доступен по ссылке App.someArray, не как либо еще вне объекта

	//Объект, содержащий публичное API
	return {
		init: function(){
			// Инициализация модуля. В ней мы инициализируем все остальные модули на странице
			Profile.init();
			Portfolio.init();
		}
	}
})();

//И инициализируем наш глобальный модуль
App.init();


Модуль профиля Profile.js

var Profile = (function(){
	//Приватная переменная хранящая путь до сервера, предоставляющего информацию для модуля
	var url = 'http://someweb.com';
	//Приватная переменная хранящая корневой html элемент, в котором отрисовывается модуль
	var el = '.div-profile';

	return {
		//Инициализация модуля
		init: function(){
			// Получим список пунктов меню и аватарку с сервера
			var profileData = this.getData(url);
		},
		getData: function(url){
			/*
			* Тут будет код ajax запроса на сервер, который в случае успеха сохранит результат в переменную res
			*/

			//Отрисуем наши данные
			this.render(res);
		},
		render: function(){
			/*
			* Тут будет код создания html разметки, с использованием вашего любимого шаблонизатора.
			* Допустим результирующая строка будет сохранена в переменную html
			*/

			//Добавим полученную разметку в корневой элемент модуля. 
//Для простоты представим что на проекте используется jQuery
			$(el).html(html);

			//И привяжем DOM события к нужным элементам модуля
			this.event();
		},
		event: function(){
			//Пусть пункты меню имеют класс .menu-item
			//И содержат атрибут data-list-id
			$('.menu-item').click(function(){
				var id = $(this).data('list-id');

				//Теперь самое важное. Генерируем событие, что пользователь кликнул пункт. 
				//На это событие и будут подписываться другие модули
				//В триггере передадим id выбранного пункта
				$(window).trigger('clickItem', {id: id});
			});
		}
	}
})();


Модуль портфолио Portfolio.js

var Portfolio = (function(){

	//Ссылка на текущий объект
	$this = this;
        var el = '.portfolio'

	return {
		init: function(){

			//Повесим слушатель нашего кастомного события. В функцию обработчик передадим пришедшие данные
			$(window).on('clickItem', function(e, data){
				$this.getData(data.id)
			});
		},
		getData: function(id){
			/*
			* Тут сделаем запрос на сервер и получим наши работы в портфолио. Пусть они так же сохраняются в res
			*/
			this.render(res);
		},
		render: function(data){
			/*
			* И снова отрисовываем данные удобным вам способом
			*/
		},
		event: function(){
			/*
			* Навесим нужные события
			*/
		}
	}
})();


Что это нам дает

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


Например мы захотим добавить новый модуль, который что то делает после того, как пользователь выбрал пункт в профиле. Нам достаточно подписать этот модуль на событие 'clickItem' и выполнить нужные действия.

Мы хотим добавить всплывающее окно, появляющееся при клике на работе в портфолио? Не вопрос. В методе event модуля Portfolio добавим нечто вроде
//'.portfolio-item' - класс-обертка, для каждой работы
$('.portfolio-item').click(function(){
	$(window).trigger('showModal');
});


Теперь нам нужно подписать модуль, генерирующий всплывающие окна, по всему нашему приложению — на событие 'showModal' и все.

Надеюсь этот материал будет вам полезен.

По теме так же советую почитать largescalejs.ru.

Для загрузки файлов с модулями я использую yepnope.js.

Спасибо за внимание.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 41

    +3
    С почином! Только, пожалуйста, спрячьте под Хабракат после первой картинки, а то неаккуратно получилось.
      0
      Спасибо. Все поправил
      +41
        +10
        largescalejs.ru — чисто убить время на чтение. полезной информации 0, только вода. Ибо не рассмотрены реальные проблемы, не показали реальных ситуаций. На словах и без примеров — всегда хорошо, на деле будет ахтунг (мне кажется вы прямо таки от туда и взяли свою идею с модулями, только правда вы сильно упростили подход, тем самым усложным поддержку дальнейшую)

        Для загрузки моделй надо не забыть упомянуть requirejs.org

        Модуль профиля Profile.js
        Я предпочитаю никогда не давать мозможность модулю общаться с сервером на прямую ибо лучше общаться с сервером через единое место.

        P.S.
        начинающим я советую идти поучить backbone ибо дает очень ясное понимание базовых вещей. так как сам чрезвычайно прост, после нег оможно посмотреть ангулар или нокаут, а уже потом фантазировать на тему своих модулей если таковая потребность будет
          +1
          Про общение с сервером из самого модуля — я подсмотрел в одном из докладов от Яндекса. Основной мыслью в докладе было то, что модуль должен быть «вещью в себе». Сам себя рендерить и сам себе тянуть данные.

          Backbone — это ближе к MVC разделению. Для тех проектов что я сейчас делаю — это излишнее усложнение.
          Подход из статьи помог мне лучше структурировать код, вот и решил поделиться с другими :)
            0
            Про общение с сервером из самого модуля — я подсмотрел в одном из докладов от Яндекса. Основной мыслью в докладе было то, что модуль должен быть «вещью в себе». Сам себя рендерить и сам себе тянуть данные.

            Это спорно ибо это принесет гигантский оверхед модуль. где он должен будет хендлить ошибки, обрабатывать запросы и прочие ужасные вещи. Наверное если вы это слышали в идеологии БЭМ то там надо договаривать все до конца, БЭМ предполагает разбиение на модули. которые независимы ибо эти модули у них юзаются в десятках проектов. А вот для конретных приложений типо яндекс карт (имнно работа с самими картами), оформлять каждый модуль как полностю зкарытую коробочку — вредно ибо там должно быть динство. В общем то они сами признали что текущий БЭМ плох для этого и переписали полностю ядро сделав другие подходы.
              0
              Нет про БЭМ я ни слова не говорил.
              Обработка запросов необходимых для модуля — происходит внутри самого модуля. Если есть ошибки — модуль генерирует событие «Я сломался» (условно) — которое отлавливается глобальным модулем. Глобальный модуль уже решает что делать. Например можно переинициализировать отвалившийся модуль
                0
                Это вы цитируете largescalejs.ru в реале все сложнее немного.
                БЭМ — а в яндексе не он? Вы же прояндекс как бы
                  0
                  Ну у Яндекс не только о БЭМ'е говорит)) Конкретно в том докладе, которым я вдохновлялся — про БЭМ не было ни слова.
                  В реале на больших проектах я больше чем уверен, что все будет сложнее. Приведенный выше способ я опробовал на двух небольших проектах, и он себя оправдал. На что то большее конечно лучше смотреть в сторону MV* фреймворков :)
                    0
                    Есть большие проекты со слабой связанностью и с сильной, о подходе лучше принимать решения только после тщательного анализа проекта
                  0
                  К слову, полностью самостоятельные модули, которые могут использоваться в десятке различных проектов — это уже компонента
                0
                Если уж зашла речь про модули и Backbone — то лучше сразу вслед за Backbone смотреть Bacbone.Marionette — это надстройка над Backbone, которая дает в том числе модульность. Т.е. то, о чем написано в статье, только из коробки и с кучей прочих плюшек.
              0
              Modernizr может тоже что и yepnope modernizr.com/docs/#load плюс фичи самого modernizr
                0
                Yepnope интегрирован в Modernizr.
                Но не всегда на проекте нужен Modernizr, поэтому был предложен вариант использовать его отдельно
                0
                И все же у вас появляются глобальные переменные, которые засоряют пространство имен. Лучше всего делать модули через Module Pattern и Mixin Pattern. Размещать это все в одном глобальном объекте, например App, и с помощью Facade Pattern сделать хороший доступ к нужным функциям :)
                  0
                  Тоже отличный подход. В largescalejs.ru этот объект называется песочница, как я понял. Но в конце книги есть сноска, о том что добавление песочницы вносит усложнение в код. В моем случае требовался максимально простой способ организации кода.
                  Но следующем проекте надо попробовать все в глобальный объект собрать :)
                    0
                    Пожалуйста, не могли бы Вы дать литературу к прочтению на эту тему и, по возможности вкратце рассказать об английских названиях в Вашем комментарии?
                    Спасибо )
                      +1
                      Литературы на такие вещи нету, только с опытом придет, что и куда лучше располагать.

                      Английские названия — это паттерны проектирования :)
                      Очень хорошая книга, рекомендую к прочтению — Клац

                      Module Pattern — это паттерн, с помощью которого можно реализовать модульную структуру на JavaScript с инкапсуляцией и девушками :)
                      Mixin Pattern — это паттерн, следуя которому, можно легко расширять существующий функционал модуля без «влезания» в сам модуль.
                      Facade Pattern — это чисто абстрактивный паттерн, который абстрагирует вашу систему и модули на более высокий уровень, предоставляя очень гибкое и удобное API к вашей системе.
                        0
                        Stoyan Stefanov — Javascript Patterns.
                        Подробно и понятно описаны основные паттерны JS.
                        Вроде бы есть издание на русском.
                          +1
                          На русском ее лучше не читать. Не буду остро судить, не читал русское издание.
                          Английское издание само то, выше привел ссылку как раз на это издание.
                            0
                            Про русское издание я упомянул для информации. Сам предпочитаю читать такие книги на языке оригинала.
                            0
                            Отличная книга на books.ru на русском есть, всем читать не задумываясь!
                            0
                            Если вы в поиске github'a наберете «javascript design patterns», то найдете пару-тройку неплохих репозиториев на эту тему.
                            0
                            Никто не мешает все модули обернуть в 1 пространство. Где каждый модуль будет свойством глобального объекта
                              0
                              Я написал абсолютно то же. Или ваш вариант чем-то отличается?
                                0
                                ghaiklor, да, я невнимателен :)
                            +1
                            А ещё можно сразу начать писать на es6 modules. И не придумывать велосипед.
                            Хотя тяга к структурированному коду это хорошо, плюсик вам за это (ну и за картинку — порадовали :) )
                              +1
                              Верно, уже давно можно начать пользоваться traceur, ведь кроме модулей в es6 других очень много плюшек. Так что, кто не хочет стоять на одном месте — пора переходить.
                                0
                                Да, на es6 можно уже смело писать. Правда сам traceur не использую — по разным причинам, и на первом месте потому, что он генерит плохой код на выходе.
                                  0
                                  К сожалению под «браузер» ничего другого толкового нет (насколько мне известно). Но скажу, что если не использовать фичи скрытые за флагами, как `let ` например, то очень он с остальным хорошо справляется. Хотя признаюсь, генераторы ещё не использую, как то страшновата мне их «стэйт-машина» в плане производительности — никак не находится время поближе взглянуть на реализацию и тесты.
                                    +1
                                    TypeScript, например. Правда там нету let, зато есть многие другие фичи из es6
                              +1
                              Крайне рекомендую посмотреть github.com/ai/evil-blocks
                                0
                                Добавьте в 'use strict' и внутренние методы, которые присваиваются свойствам возвращаемого в public модуля перестанут работать.

                                Весь этот подход называется шаблон проектирования «модуль». Знаете как применить модуль с использованием 'use strict'?
                                  +1
                                  Не совсем понял, что вы имели в виду.
                                  Специально после работы попробовал сломать модуль, используя use strict — не получилось
                                  var some = (function(){
                                      "use strict";
                                      var arr = ['Проверка', 'на', 'вшивость'];
                                      
                                      return {
                                          getFirst: function(){
                                              alert(arr[0]);
                                          }
                                      };
                                  })();
                                  
                                  some.getFirst();
                                  

                                  Как и
                                  "use strict";
                                  
                                  var some = (function(){
                                      
                                      var arr = ['Проверка', 'на', 'вшивость'];
                                      
                                      return {
                                          getFirst: function(){
                                              alert(arr[0]);
                                          }
                                      };
                                  })();
                                  
                                  some.getFirst();
                                  

                                  Продолжает работать.
                                  Можете пояснить ваш комментарий подробнее?
                                    0
                                    Нашел косяк в коде, все работает! Спасибо за пост, пока готовил подробный ответ с проблемой нашел ошибку в своем коде :)
                                  0
                                  Автор, Portfolio не вернет объект, поправь самовызов в коде, чтобы в заблуждение не вводить
                                    0
                                    Какая грубая ошибка. Спасибо что заметили :)
                                    Исправил код
                                    0
                                    а может, лучше commonJS+browserify/AMD+requireJS?
                                      0
                                      Лучше, но для тех, кто только начинает заботиться о структурировании кода — подход из статьи проще воспринять, чем сразу кидаться на AMD или еще что либо. По крайней мере мне было проще начинать именно с того что в статье, хотя я на тот момент был знаком с Require js.
                                      0
                                      Мне кажется, что в слове «Глобаный» фразы «Глобаный модуль App.js» опечатка, Вот только не могу понять, какая — «ль» пропущено, или «рё» заменено? Часто в работе хочется упомянуть вторую версию, но тут вроде бы код аккуратно выглядит.
                                        –1
                                        Спасибо что заметили, но лучше о таких вещах в личку сообщать

                                      Only users with full accounts can post comments. Log in, please.