Если вы новичок в JavaScript, то разговоры на тему «module bundlres vs module loaders», «Webpack vs Browserify» и «AMD vs CommonJS», могут быть пугающими.
Понимание системы модулей JavaScript жизненно важно для web разработчика. В этом посте попробую простыми словами (и на примерах кода) объяснить эти модные понятия.
Хорошие авторы делят свои книги на главы и разделы; Хорошие программисты делят свои программы на модули.
Как главы в книге, модули являются просто набором слов (или кода, в зависимости от обстоятельств).
Хорошие модули, однако, максимально автономны в своей функциональности, что позволяет их перетасовывать, удалять или добавлять по мере необходимости, не нарушая систему в целом.
Использование модулей дает много преимуществ в пользу расширения и независимости кода. Наиболее важным, по моему мнению, является:
Шаблон модуль иммитирует концепцию классов (поскольку JavaScript не поддерживает классы нативно). Мы можем хрранить как публичные, так и приватные методы и переменные внутри одного объекта - подобно тому как классы используются в других языках, таких как Java или Python. Это позволяет создавать общедоступный API для публичных методов, в то же время инкапсулируя приватные методы и переменные внутри области замыкания.
уществует несколько способов, чтобы написать паттерн Модуль. В первом примере я использую анонимное замыкание. Это поможет нам достич цели, обернув весь код в анонимную функцию. (В JavaScript функции это единственный способ создать новую область видимости).
(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.’
С помощью этой конструкции, наша анонимная функция имеет свою собственную область видимости или замыкание, функция сразу же выполняется. Это позволяет нам скрыть переменные из глобальной области видимости.
Плюсы этого подхода в том, что вы можете использовать локальные переменные внутри функции без опасения, что они перезапишут глобальные переменные.
Обратите внимание на необходимость круглых скобок вокруг анонимной функции, так как они позволяют вызвать функцию сразу после ее объявления.
Другой популярный подход, используемый библиотекой 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));
Преимущество данного подхода по сравнению с анонимным замыканием в том, что мы заранее объявляем глобальные переменные, делая их совершенно понятными для людей читающих наш код.
Еще один подход - создание модуля как автономный интерфейс объекта.
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).
Этот паттерн похож на предыдущие подходы, за исключением того, что все переменные и методы изначально создаются приватными, пока явно не назначаются публичными:
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.'
Подходы выше имеют одну общую черту: используют одну глобальную переменную для обертки кода в функцию, создавая таким образом пространство имен через замыкание.
Хотя каждый подход эффективен по своему, они имеют свои недостатки.
Во-первых, вы должны знать правильный порядок подключения зависимостей.
Допустим, вы подключаете Backbone.js
, который имеет зависимостью underscore.js
, поэтому теги подключения скриптов должны идти в соответствующем порядке.
Еще одним недостатком является то, что они могут занимать одно пространство имен. Например, два ваших модуля могут иметь одинаковые имена или вам нужно подключить две версии вашего модуля.
Можем ли мы подключить интерфейсы модулей не проходя через глобальную область вилимости? Ответ - да.
Есть два популярных и хорошо реализованных подхода: CommonJS и AMD
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!'
Мы получаем два очевидных преимущества, по сравнению с шаблонами, рассмотренными ранее:
Более того, синтаксис становится более компактный.
Следует также отметить, что CommonJS использует серверный подход и синхронно загружает модули. Это важно, потому что если у нас есть три других модуля, которые нам нужны, он будет загружать их один за другим.
Это прекрасно работает на сервере, но, к сожалению, затрудняет его использование при написании JavaScript для браузера. Чтение модуля из Интернета занимает намного больше времени, чем чтение с диска. Пока скрипт загружает модуль, браузер ждет пока он не завершит загрузку.
Что если мы хотим загружать модули асинхронно? Ответ - 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 объединяет преимущества 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
}
}));
Вы все еще здесь? Отлично! У нас есть еще один тип модулей.
Как вы возможно заметили, ни один из модулей выше не является нативным для 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