Это первая часть серии статей о написании модульных приложений JavaScript. Во части 2 мы фактически вместе создадим простое модульное приложение. Наконец, в части 3 мы будем использовать Gulp, средство выполнения задач Node, чтобы подготовить наши файлы к развертыванию.

Вступление

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

Возможно, вы захотите использовать какую-нибудь ранее написанную утилиту в своем новом приложении - вместо того, чтобы переписывать все это целиком, вы можете просто скопировать свой файл в новый проект и связать его с помощью тега <script>. Одна только экономия времени делает это стоящим начинанием.

Но есть еще одно, возможно, большее преимущество модульной конструкции: она значительно упрощает сотрудничество с другими людьми. Когда вы работаете с командой разработчиков, где каждый из вас создает часть более крупного приложения, вам в конечном итоге потребуется объединить все эти отдельные части вместе. Если вы заранее приняли модульную структуру дизайна, объединить все в конце будет тривиально просто. Развивайте модульные привычки сегодня, и завтра вы станете героем своей команды.

Наконец, модульные шаблоны проектирования значительно упрощают чтение кода. Вы должны лично заботиться об этом, потому что вам (или другим - см. Предыдущий абзац) в конечном итоге придется разбираться в коде, который вы написали ранее. Чем меньше строк кода, тем быстрее вы и другие сможете понять его. Модуль - это по определению меньшая сфокусированная часть некоторого большего целого.

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

Я предполагаю, что у вас есть базовые знания о клиентском / интерфейсном JavaScript. В моих примерах используется небольшое количество jQuery, но никаких других библиотек или фреймворков для начала работы не требуется. Я упомяну IIFE и замыкания, но не буду вдаваться в подробности о них здесь.

Структура приложения

Модульное приложение начинается с прочной структуры файлов / папок. Есть много способов организовать файлы проекта. Я обычно использую один из двух способов: либо по типу файла (CSS, js, ресурсы и т. Д.), Либо по функции приложения (заголовок, посадка, вход, о , нижний колонтитул и т. д.). В больших проектах организация по функциям почти обязательна. Но мы собираемся использовать более простую систему по типу для этого сообщения.

|— /dist
|    |
|    |— /js
|    |— /css
|    |— /assets
|
|— /src
|    |
|    |— /js
|    |   |
|    |   | app.js
|    |   | module1.js
|    |   | module2.js
|    |   | module3.js
|    |
|    |— /css
|    |   |
|    |   | style.css
|    |
|    |— /assets
|         |
|         | image.jpg
|         | etc.jpg
|
|— /node_modules
|
| .gitignore
| gulpfile.js
| package.json
| index.html

Вкратце:

  • /dist - в конечном итоге здесь будут храниться наши готовые к производству файлы. Gulp создаст их позже - здесь нам не нужно ничего делать.
  • /src - здесь мы напишем весь наш код в стадии разработки (css, JavaScript и т. Д.). Позже Gulp обработает эти файлы для создания файлов в каталоге / dist. Имена файлов модулей, использованные выше, являются просто примерами - вам определенно следует использовать более значимые имена файлов!
  • /node_modules - Узел для интерфейсных приложений? Да! Мы будем использовать Node и NPM для установки и запуска задач Gulp.
  • .gitignore - файлы и папки, указанные в этом файле, будут игнорироваться (не отслеживаться) Git. Они также не будут отправлены на GitHub. Мы будем игнорировать папку node_modules/ - нет причин для отслеживания или отправки всех этих сторонних пакетов.
  • gulpfile.js - здесь мы напишем наши задачи Gulp конкатенации, транспиляции и минификации.
  • index.html - в процессе разработки мы разберем каждый скрипт отдельно. Позже мы просто сделаем ссылку на один готовый к производству файл JavaScript.
  • package.json - Создано NPM. Помимо прочего, в этом файле перечислены все пакеты, от которых зависит наше приложение. Наши будут включать зависимости для Gulp и различных плагинов Gulp.

Грязный и песчаный

Давайте начнем с рассмотрения структуры нашего index.html. Во время разработки мы просто создадим ссылку на каждый из файлов JavaScript в нашей папке /src/js. Итак, наш index.html может выглядеть так:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Nice Boilerplate</title>
</head>
<body>
  <!-- =============== vendor javascript ================ -->
  <script src="https://example.com/jquery.min.js"></script>
  <!-- ================ our javascript ================== -->
  <script src="src/js/module1.js"></script>
  <script src="src/js/module2.js"></script>
  <script src="src/js/module3.js"></script>
  <script src="src/js/app.js"    ></script>
</body>
</html>

Порядок важен! Наши модули должны загружаться первыми, чтобы их общедоступные методы были доступны для вызова. Это утверждение будет иметь больше смысла по мере нашего прогресса. А пока знайте, что app.js должен загружаться последним, потому что мы собираемся использовать его для вызова общедоступных методов всех других наших модулей. В остальном это обычный index.html файл. Создайте каркас своего приложения, как обычно.

Все начинает выглядеть немного иначе, когда мы переходим к нашему app.js файлу:

$(document).ready(function () {
  Module1.init();
  Module2.init();
  Module3.init();
);

Вот и все! Если вы привыкли видеть / писать объемы кода внутри $(document).ready(), приведенное выше может показаться немного странным. У нашего app.js есть только одна обязанность: вызывать .init() методы наших отдельных модулей. Вся фактическая логика приложения находится в этих других модулях. Каждый раз, когда вы добавляете новый модуль в свое приложение, вы добавляете здесь вызов его .init() метода. Легко и организованно.

Поскольку мы используем app.js для начальной загрузки других наших модулей, теперь имеет смысл, что index.html должен сначала загрузить эти модули. Если модули были загружены после app.js, вызов их .init() методов завершился бы ошибкой.

Мы запишем каждый модуль в отдельный файл, чтобы способствовать повторному использованию. И мы примем стиль, известный как шаблон модуля раскрытия. Квалификатор раскрытие указывает на внутреннюю структуру модуля: некоторые функции являются частными, другие раскрываются публично.

Вот пример:

var Module1 = (function() {
  'use strict';
  // placeholder for cached DOM elements
  var DOM = {};

  /* =================== private methods ================= */
  // cache DOM elements
  function cacheDom() {
    DOM.$someElement = $('#some-element');
  }
  // bind events
  function bindEvents() {
    DOM.$someElement.click(handleClick);
  }
  // handle click events
  function handleClick(e) {
    render(); // etc
  }
  // render DOM
  function render() {
    DOM.$someElement
      .html('<p>Yeah!</p>');
  }

  /* =================== public methods ================== */
  // main init method
  function init() {
    cacheDom();
    bindEvents();
  }

  /* =============== export public methods =============== */
  return {
    init: init
  };
}());

Начнем с объявления переменной (Module1) и присвоения ей функции. Весь код модуля входит в эту функцию.

Обратите внимание, что это обе функции (заключенные в круглые скобки и заканчивающиеся другим набором скобок) ()):

var Module1 = (function () {
  // all module logic goes here
}());

Скобки () в конце заставляют функцию интерпретироваться и оцениваться как выражение. Это известно как IIFE, или выражение немедленно вызываемой функции. IIFE означает, что мы не вызываем функцию отдельно - функция вызывается, когда движок JavaScript назначает ее нашей переменной. И это происходит, как только модуль загружается. Обертка () технически не требуется, но используется по соглашению, чтобы другие программисты знали, что эта функция является IIFE.

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

В рамках IIFE наш примерный модуль использует /* comments */ для организации своего содержимого по четырем разделам.

В самом начале мы объявляем все необходимые переменные области модуля. Стандартные вещи, которые вы уже делаете - ничего страшного. В этом примере объявляется только одна переменная, пустой объект, который будет кэшировать элемент DOM.

Затем мы определяем несколько частных методов. Примеры методов обычно встречаются во многих интерфейсных приложениях: cacheDom(), bindEvents() и render(). Они делают именно то, что вы от них ожидаете. Дополнительные частные методы могут обрабатывать отправку форм, запросы AJAX, манипулирование ответами JSON и т. Д. Важно отметить, что эти методы являются частными - нам не нужны или мы не хотим, чтобы они были доступны вне модуля. они определены в. И это не так, потому что они инкапсулированы в нашем IIFE. Например, нет причин для того, чтобы какая-либо функция модуля bindEvents() была доступна глобально - она ​​не имеет отношения к любому другому модулю, поэтому мы оставляем ее закрытой.

Также обратите внимание, что несколько модулей могут иметь одни и те же частные функции. Они никогда не будут сталкиваться друг с другом, потому что они ограничены областью локальной функции каждого модуля. Кроме того, каждый модуль может иметь одни и те же общедоступные методы, поскольку каждый из них пространством имен для своего собственного модуля. Например, у вас может быть три модуля, каждый с одним и тем же общедоступным методом: modals.init(), greeting.init(), login.init(). Эти методы с одинаковыми именами никогда не будут конфликтовать друг с другом, потому что все они вызываются в уникальных модулях. Очень вежливо.

В следующем разделе мы определяем наши общедоступные методы. В данном случае у нас только один: init(). Но их могло быть сколько угодно. Обратите внимание, что init() делает только одно: выполняет вызовы наших закрытых методов. При вызове init() закрывает частные переменные и функции в области видимости модуля, делая их доступными за пределами области, в которой они были объявлены (ура, закрытие).

Вы можете спросить: «Что делает функцию init() общедоступной? - он выглядит так же, как и другие частные функции над ним ». Хороший вопрос - переходите к следующему абзацу!

Последний раздел возвращает литерал объекта. Это то, что делает паттерн раскрывающего модуля таким крутым. По сути, мы возвращаем набор «ссылок» на любые частные методы, которые мы хотим сделать общедоступными за пределами нашего модуля. Это позволяет нам разделять частную и общедоступную части наших модулей, и - чтобы ответить на вопрос предыдущего абзаца - это то, что делает init() общедоступным методом.

Помните, что свойства объекта - это { key : value } пар:

return {
  init : init
};

В приведенном выше примере «ключ» слева - это имя метода, который мы будем вызывать вне нашего модуля. Правое «значение» относится к имени функции, определенной внутри модуля. Хотя это выглядит странным, видеть init : init, это обычная практика - использовать то же имя для внешнего использования, что и для внутреннего использования. Кроме того, вам нужно запомнить на одно имя меньше.

Объединяя все вместе

Возвращая общедоступные методы через литерал объекта, мы «прикрепляем» их к переменной, которую мы объявили в начале: Module1 получает метод init(). А поскольку Module1 был объявлен глобально, он и его общедоступные методы доступны в глобальном пространстве имен. Когда модуль глобально предоставляет свои общедоступные методы, любой другой модуль может вызывать эти методы. Таким образом, мы можем вызывать Module1.init() где угодно - именно это мы и делали ранее в app.js. Module1 могут иметь дополнительные общедоступные методы - их также можно вызывать в любом месте приложения.

Видите, как все это начинает сочетаться? И как в условиях группы каждый член команды может работать независимо, а затем публиковать общедоступные методы своего модуля, добавляя любые необходимые вызовы к общедоступным методам своего модуля в app.js?

Обычно я ограничиваю каждый модуль одной уникальной функцией приложения или одной уникальной служебной программой. Функция приложения может иметь множество частных методов и не нуждаться ни в чем, кроме одного init() метода, открытого для всех. Напротив, служебный модуль может иметь несколько (или ноль) частных методов и вместо этого предоставлять все свои методы публично. А служебным модулям могут вообще не понадобиться init() методы.

Подводя итоги

Мы разбиваем логику нашего приложения на блоки на основе одной функции или утилиты. Каждый член команды составляет свой модуль (модули) как IIFE, состоящий из всей бизнес-логики, необходимой для модуля. Каждый модуль раскрывает определенные общедоступные методы, которые можно вызывать в любом месте приложения. Наш основной app.js загружает эти модули, вызывая каждый из их общедоступных init() методов. Другие члены проектной группы признают ваш талант и купят вам мятные джулепы.

Вверх Далее: давайте создадим модульное приложение!

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