JavaScript Модули

Если вы новичок в JavaScript, то разговоры на тему «module bundlres vs module loaders», «Webpack vs Browserify» и «AMD vs CommonJS», могут быть пугающими.

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

Что такое модуль в JavaScript?

Хорошие авторы делят свои книги на главы и разделы; Хорошие программисты делят свои программы на модули.

Как главы в книге, модули являются просто набором слов (или кода, в зависимости от обстоятельств).

Хорошие модули, однако, максимально автономны в своей функциональности, что позволяет их перетасовывать, удалять или добавлять по мере необходимости, не нарушая систему в целом.

Почему нужно использовать модули в JavaScript?

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

  1. Ремонтопригодность: По определению модуль автономен. Целью хорошо спроектированного модуля является максимально уменьшенить зависимости между частями кода, таким образом модуль может расти и улучшаться независимо от остального кода. Обновление модуля легче когда модуль отделен от других частей кода.
  2. Пространства имен: В JavaScript переменные, объявленные вне функции являются глобальными (доступными всему коду). Из-за этого нередко возникают проблемы, когда несвязанный код разделяет глобальные переменные. Это большая проблема в разработке. Как мы увидим дальше, модули помогают разделить пространства имен созданием приватных областей вилимости.
  3. Повторное использование: Давайте будем честны, мы все копируем код из предыдущих проектов в новые. Но представьте, что мы нашли лучший способ, чтобы написать часть кода, тогда нам придется вернуться и переписать все предыдущие куски кода. Но не тогда, когда мы используем модули.

Как мы можем включить модули?

Шаблон Модуль

Шаблон модуль иммитирует концепцию классов (поскольку JavaScript не поддерживает классы нативно). Мы можем хрранить как публичные, так и приватные методы и переменные внутри одного объекта - подобно тому как классы используются в других языках, таких как Java или Python. Это позволяет создавать общедоступный API для публичных методов, в то же время инкапсулируя приватные методы и переменные внутри области замыкания.

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

Пример 1: Анонимное замыкание

(function () {
  // We keep these variables private inside this closure scope
  
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item }, 0);
    
      return 'Your average grade is ' + total / myGrades.length + '.';
  }

  var failing = function(){
    var failingGrades = myGrades.filter(function(item) {
      return item < 70;});
      
    return 'You failed ' + failingGrades.length + ' times.';
  }

  console.log( failing() );

} () );

// ‘You failed 2 times.’

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

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

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

Пример 2: Глобальный импорт

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

(function (globalVariable) {

  // Keep this variables private inside this closure scope
  var privateFunction = function() {
    console.log('Shhhh, this is private!');
  }

  // Expose the below methods via the globalVariable interface while
  // hiding the implementation of the method within the 
  // function() block

  globalVariable.each = function(collection, iterator) {
    if (Array.isArray(collection)) {
      for (var i = 0; i < collection.length; i++) {
        iterator(collection[i], i, collection);
      }
    } else {
      for (var key in collection) {
        iterator(collection[key], key, collection);
      }
    }
  };

  globalVariable.filter = function(collection, test) {
    var filtered = [];
    globalVariable.each(collection, function(item) {
      if (test(item)) {
        filtered.push(item);
      }
    });
    return filtered;
  };

  globalVariable.map = function(collection, iterator) {
    var mapped = [];
    globalUtils.each(collection, function(value, key, collection) {
      mapped.push(iterator(value));
    });
    return mapped;
  };

  globalVariable.reduce = function(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    globalVariable.each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;

  };

 }(globalVariable));

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

Пример 3: Интерфейс объекта

Еще один подход - создание модуля как автономный интерфейс объекта.

var myGradesCalculate = (function () {
    
  // Keep this variable private inside this closure scope
  var myGrades = [93, 95, 88, 0, 55, 91];

  // Expose these functions via an interface while hiding
  // the implementation of the module within the function() block

  return {
    average: function() {
      var total = myGrades.reduce(function(accumulator, item) {
        return accumulator + item;
        }, 0);
        
      return'Your average grade is ' + total / myGrades.length + '.';
    },

    failing: function() {
      var failingGrades = myGrades.filter(function(item) {
          return item < 70;
        });

      return 'You failed ' + failingGrades.length + ' times.';
    }
  }
})();

myGradesCalculate.failing(); // 'You failed 2 times.' 
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

Как вы можете видеть, этот подход дает нам возможность решить какие переменные/методы мы хотим оставить приватными (такие как myGrades) и какие мы хотим сделать доступными извне через выражение return (такие как average и failing).

Пример 4: Паттерн «раскрывающий модуль»

Этот паттерн похож на предыдущие подходы, за исключением того, что все переменные и методы изначально создаются приватными, пока явно не назначаются публичными:

var myGradesCalculate = (function () {
    
  // Keep this variable private inside this closure scope
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item;
      }, 0);
      
    return'Your average grade is ' + total / myGrades.length + '.';
  };

  var failing = function() {
    var failingGrades = myGrades.filter(function(item) {
        return item < 70;
      });

    return 'You failed ' + failingGrades.length + ' times.';
  };

  // Explicitly reveal public pointers to the private functions 
  // that we want to reveal publicly

  return {
    average: average,
    failing: failing
  }
})();

myGradesCalculate.failing(); // 'You failed 2 times.' 
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

CommonJS и AMD

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

Хотя каждый подход эффективен по своему, они имеют свои недостатки.

Во-первых, вы должны знать правильный порядок подключения зависимостей.

Допустим, вы подключаете Backbone.js, который имеет зависимостью underscore.js, поэтому теги подключения скриптов должны идти в соответствующем порядке.

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

Можем ли мы подключить интерфейсы модулей не проходя через глобальную область вилимости? Ответ - да.

Есть два популярных и хорошо реализованных подхода: CommonJS и AMD

CommonJS

CommonJS - это группа энтузиастов, которая разрабатывает и реализует JavaScript API для объявления модулей.

Модуль CommonJS - это, по сути, повторно используемый кусок кода JavaScript который экспортирует определенные объекты, делая их доступными для других модулей. Если вы сталкивались с Node.js, то вы хорошо знакомы с этим форматом.

С CommonJS каждый js файл хранит модули в своем уникальном контектсе (подобно обертке в замыкании). В этой области видимости мы используем объект module.exports для того, чтобы сделать модуль доступным и require для его импорта.

Когда вы определяете CommonJS модуль, это выглядит примерно так:

function myModule() {
  this.hello = function() {
    return 'hello!';
  }

  this.goodbye = function() {
    return 'goodbye!';
  }
}

module.exports = myModule;

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

Теперь когда кто-то захочет использовать myModule, то он может вызвать его в своем файле следующим способом:

var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

Мы получаем два очевидных преимущества, по сравнению с шаблонами, рассмотренными ранее:

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

Более того, синтаксис становится более компактный.

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

Это прекрасно работает на сервере, но, к сожалению, затрудняет его использование при написании JavaScript для браузера. Чтение модуля из Интернета занимает намного больше времени, чем чтение с диска. Пока скрипт загружает модуль, браузер ждет пока он не завершит загрузку.

AMD (Asynchronous Module Definition)

Что если мы хотим загружать модули асинхронно? Ответ - Asynchronous Module Definition (сокращенно AMD).

Загрузка модулей будет выглядеть следующим образом:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

Функция define принимает в качестве первого аргумента массив каждой из зависимостей модуля. Эти зависимости загружаются в фоновом режиме (неблокирующим образом), и после загрузки define вызывает колбэк.

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

Например, myModule может выглядеть так:

define([], function() {

  return {
    hello: function() {
      console.log('hello');
    },
    goodbye: function() {
      console.log('goodbye');
    }
  };
});

В отличие от CommonJS, AMD использует браузерный подход (асинхронное поведение).

Помимо асинхронности, еще одним преимуществом AMD является то, что ваши модули могут быть объектами, функциями, конструкторами, строками, JSON и многими другими типами, в то время как CommonJS поддерживает объекты только как модули.

При этом AMD не совместима с файловой системой io и другими серверно-ориентированными функциями, доступными через CommonJS, а синтаксис переноса функций является несколько более подробным по сравнению с простым оператором require.

UMD (Universal Module Definition)

UMD объединяет преимущества AMD и CommonJS. Создает способ использования любого из двух. Как результат, UMD модули могут работать как на сервере, так и на клиенте.

Вот как это работает:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
    define(['myModule', 'myOtherModule'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
  } else {
    // Browser globals (Note: root is window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
  }
}(this, function (myModule, myOtherModule) {
  // Methods
  function notHelloOrGoodbye(){}; // A private method
  function hello(){}; // A public method because it's returned (see below)
  function goodbye(){}; // A public method because it's returned (see below)

  // Exposed public methods
  return {
      hello: hello,
      goodbye: goodbye
  }
}));

Native JS

Вы все еще здесь? Отлично! У нас есть еще один тип модулей.

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

К счастью, умные люди представили встроенные модули с ECMAScript 6 (ES6).

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

Пример того как это работает

// lib/counter.js

var counter = 1;

function increment() {
  counter++;
}

function decrement() {
  counter--;
}

module.exports = {
  counter: counter,
  increment: increment,
  decrement: decrement
};


// src/main.js

var counter = require('../../lib/counter');

counter.increment();
console.log(counter.counter); // 1

В этом примере мы в основном делаем две копии модуля: одну, когда мы ее экспортируем, и одну, когда нам это требуется.

Более того, копия в main.js теперь отключена от исходного модуля. Вот почему даже когда мы увеличиваем наш счетчик, он все равно возвращает 1 - потому что импортированная нами переменная счетчика является отключенной копией переменной счетчика из модуля.

Таким образом, увеличение счетчика будет увеличивать его в модуле, но не будет увеличивать вашу скопированную версию. Единственный способ изменить скопированную версию переменной counter - это сделать это вручную:

counter.counter++;
console.log(counter.counter); // 2
Перейти к верхней панели