Если вы новичок в JavaScript, жаргон типа "модульные упаковщики против загрузчиков модулей”, “Webpack против Browserify” и" AMD против CommonJS " может быстро стать подавляющим.
Система модулей JavaScript может быть пугающей, но понимание этого жизненно важно для веб-разработчиков.
В этом посте я распакую для вас эти модные слова на простом английском языке (и несколько примеров кода). Я надеюсь, что вы найдете это полезным!
Примечание: Для простоты это будет разделено на два раздела: Часть 1 будет посвящена объяснению того, что такое модули и почему мы их используем. Часть 2 (опубликованная на следующей неделе) расскажет о том, что значит связывать модули и различные способы сделать это.
Часть 1: Может ли кто-нибудь еще раз объяснить, что такое модули?
Хорошие авторы делят свои книги на Главы и разделы; хорошие программисты делят свои программы на модули.
Как и в книжной главе, модули - это просто скопления слов (или кода, в зависимости от ситуации).
Хорошие модули, однако, очень самодостаточны с четкой функциональностью, что позволяет их перетасовывать, удалять или добавлять по мере необходимости, не нарушая работу системы в целом.
Зачем использовать модули?
Есть много преимуществ в использовании модулей в пользу растянутой, взаимозависимой кодовой базы. Наиболее важными из них, на мой взгляд, являются:
1)ремонтопригодность: по определению, модуль является автономным. Хорошо спроектированный модуль стремится максимально уменьшить зависимость от частей кодовой базы, чтобы она могла расти и совершенствоваться независимо. Обновление одного модуля намного проще, когда модуль отделен от других частей кода.
Возвращаясь к нашему примеру с книгой, если вы хотите обновить главу в своей книге, было бы кошмаром, если бы небольшое изменение в одной главе потребовало от вас также изменить каждую другую главу. Вместо этого вы хотели бы написать каждую главу таким образом, чтобы улучшения могли быть сделаны, не затрагивая другие главы.
2) пространство имен: в JavaScript переменные, выходящие за рамки функции верхнего уровня, являются глобальными (то есть каждый может получить к ним доступ). Из-за этого часто возникает “загрязнение пространства имен”, когда совершенно несвязанный код разделяет глобальные переменные.
Совместное использование глобальных переменных между несвязанным кодом - это большое "нет-нет" в разработке .
Как мы увидим позже в этом посте, модули позволяют нам избежать загрязнения пространства имен, создавая личное пространство для наших переменных.
3) возможность повторного использования: давайте будем честны здесь: мы все скопировали код, который ранее писали в новые проекты в тот или иной момент. Например, давайте представим, что вы скопировали некоторые служебные методы, написанные вами из предыдущего проекта, в ваш текущий проект.
Это все хорошо, но если вы найдете лучший способ написать какую-то часть этого кода, вам придется вернуться и не забыть обновить его везде, где вы его написали.
Это, очевидно, огромная трата времени. Разве не было бы намного проще, если бы существовал — подождите — ка-модуль, который мы могли бы использовать снова и снова?
Как вы можете включить модули?
Существует множество способов включения модулей в ваши программы. Давайте пройдемся по нескольким из них:
Модульный шаблон
Шаблон модуля используется для имитации концепции классов (поскольку JavaScript изначально не поддерживает классы), так что мы можем хранить как публичные, так и частные методы и переменные внутри одного объекта — подобно тому, как классы используются в других языках программирования, таких как Java или Python. Это позволяет нам создать открытый API для методов, которые мы хотим предоставить миру, в то же время инкапсулируя частные переменные и методы в области закрытия.
Существует несколько способов выполнения шаблона модуля. В этом первом примере я буду использовать анонимное закрытие. Это поможет нам достичь нашей цели, поместив весь наш код в анонимную функцию. (Помните: в JavaScript функции-это единственный способ создать новую область видимости.)
Пример 1: анонимное закрытие
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.’
С помощью этой конструкции наша анонимная функция имеет свою собственную среду оценки или "закрытие", и тогда мы немедленно оцениваем ее. Это позволяет нам скрывать переменные из родительского (глобального) пространства имен.
Что хорошо в этом подходе, так это то, что вы можете использовать локальные переменные внутри этой функции без случайной перезаписи существующих глобальных переменных, но все же получить доступ к глобальным переменным, например:
JavaScript:
var global = 'Hello, I am a global variable :)';
(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());
console.log(global);
}());
// 'You failed 2 times.'
// 'Hello, I am a global variable :)'
Обратите внимание, что скобки вокруг анонимной функции обязательны, потому что операторы, начинающиеся с ключевого слова function, всегда считаются объявлениями функций (помните, что вы не можете иметь неназванные объявления функций в JavaScript.) Следовательно, окружающие скобки вместо этого создают выражение функции .
Пример 2: глобальный импорт
еще одним популярным подходом, используемым такими библиотеками, как jQuery, является глобальный импорт. Это похоже на анонимное закрытие, которое мы только что видели, за исключением того, что теперь мы передаем глобалы в качестве параметров:
JavaScript:
(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));
В этом примере globalVariable-единственная переменная, которая является глобальной. Преимущество этого подхода перед анонимными закрытиями заключается в том, что вы объявляете глобальные переменные заранее, делая их кристально ясными для людей, читающих ваш код.
Пример 3: объектный интерфейс
еще один подход заключается в создании модулей с использованием автономного объектного интерфейса, например:
JavaScript:
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: раскрытие шаблона модуля
этот подход очень похож на описанный выше, за исключением того, что он гарантирует, что все методы и переменные остаются закрытыми до тех пор, пока они явно не будут раскрыты:
JavaScript:
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 в своем проекте, поэтому вы включаете тег сценария для исходного кода Backbone в свой файл.
Однако, поскольку Backbone имеет жесткую зависимость от подчеркивания.js, тег сценария для основного файла не может быть помещен перед подчеркиванием.файл js.
Как разработчик, управление зависимостями и получение этих вещей правильно иногда может быть головной болью.
Еще одним недостатком является то, что они все еще могут привести к столкновениям пространств имен. Например, что делать, если два ваших модуля имеют одинаковое имя? Или что делать, если у вас есть две версии модуля, и вам нужны обе?
Поэтому вы, вероятно, задаетесь вопросом: Можем ли мы разработать способ запроса интерфейса модуля, не проходя через глобальную область видимости?
К счастью, ответ-да.
Есть два популярных и хорошо реализованных подхода: CommonJS и AMD.
CommonJS
CommonJS-это добровольческая рабочая группа, которая разрабатывает и реализует API JavaScript для объявления модулей.
Модуль CommonJS-это, по сути, многоразовая часть JavaScript, которая экспортирует определенные объекты, делая их доступными для других модулей, необходимых в их программах. Если вы запрограммированы в node.js, вы будете очень хорошо знакомы с этим форматом.
С CommonJS каждый файл JavaScript хранит модули в своем собственном уникальном контексте модуля (точно так же, как обертывание его в закрытие). В этой области мы используем модуль.экспортирует объект для предоставления модулей и требует их импорта.
Когда вы определяете модуль CommonJS, он может выглядеть примерно так:
JavaScript:
function myModule() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModule;
Мы используем специальный модуль объекта и помещаем ссылку на нашу функцию в модуль.экспорт . Это позволяет системе CommonJS module знать, что мы хотим выставить, чтобы другие файлы могли его использовать.
Затем, когда кто-то хочет использовать myModule , он может потребовать его в своем файле, например:
JavaScript:
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'
Есть два очевидных преимущества этого подхода по сравнению с моделями модулей, которые мы обсуждали ранее:
1. Предотвращение глобального загрязнения пространства
имен 2. Делая наши зависимости явными
Более того, синтаксис очень компактный, что лично мне очень нравится.
Еще одна вещь, которую следует отметить, заключается в том, что CommonJS использует подход, основанный на сервере, и синхронно загружает модули. Это важно, потому что если у нас есть три других модуля , которые нам нужны, он загрузит их один за другим.
Теперь это отлично работает на сервере, но, к сожалению, затрудняет его использование при написании JavaScript для браузера. Достаточно сказать, что чтение модуля из интернета занимает гораздо больше времени, чем чтение с диска. Пока работает скрипт для загрузки модуля, он блокирует браузер от запуска чего-либо еще, пока он не завершит загрузку. Он ведет себя таким образом, потому что поток JavaScript останавливается до тех пор, пока код не будет загружен. (Я расскажу, как мы можем обойти эту проблему в части 2, Когда мы обсудим пакетирование модулей. На данный момент это все, что нам нужно знать).
AMD
CommonJS-это все хорошо и хорошо, но что делать, если мы хотим загружать модули асинхронно? Ответ называется асинхронным определением модуля, или сокращенно AMD.
Загрузка модулей с помощью AMD выглядит примерно так:
JavaScript:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
Здесь происходит то, что функция define принимает в качестве своего первого аргумента массив зависимостей каждого модуля. Эти зависимости загружаются в фоновом режиме (неблокирующим образом), и после загрузки define вызывает заданную функцию обратного вызова.
Затем функция обратного вызова принимает в качестве аргументов загруженные зависимости — в нашем случае myModule и myOtherModule-позволяя функции использовать эти зависимости. Наконец, сами зависимости также должны быть определены с помощью ключевого слова define.
Например, myModule может выглядеть следующим образом:
JavaScript:
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, существует еще один формат: Universal Module Definition (UMD).
UMD по существу создает способ использовать любой из этих двух вариантов, а также поддерживает определение глобальной переменной. В результате модули UMD способны работать как на клиенте, так и на сервере.
Вот краткий пример того, как UMD занимается своим бизнесом:
JavaScript:
(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
}
}));
Для получения дополнительных примеров форматов UMD ознакомьтесь с этим поучительным РЕПО на GitHub.
Native JS
Фу! Ты все еще здесь? Я не потерял тебя здесь в лесу? Хорошо! Потому что у нас есть еще один тип модуля, который нужно определить, прежде чем мы закончим.
Как вы, вероятно, заметили, ни один из вышеперечисленных модулей не был встроен в JavaScript. Вместо этого мы создали способы эмуляции системы модулей с помощью шаблона модуля, CommonJS или AMD.
К счастью, умные люди в TC39 (орган стандартов, определяющий синтаксис и семантику ECMAScript) внедрили встроенные модули с ECMAScript 6 (ES6).
ES6 предлагает множество возможностей для импорта и экспорта модулей, которые другие проделали большую работу, объясняя — вот некоторые из этих ресурсов:
Что замечательно в модулях ES6 по сравнению с CommonJS или AMD, так это то, как им удается предлагать лучшее из обоих миров: компактный и декларативный синтаксис и асинхронную загрузку, а также дополнительные преимущества, такие как лучшая поддержка циклических зависимостей.
Вероятно, моя любимая особенность модулей ES6 заключается в том, что импорт-это живое представление экспорта только для чтения. (Сравните это с CommonJS, где импорт является копией экспорта и, следовательно, не является живым).
Вот пример того, как это работает:
JavaScript:
// 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
В этом примере мы в основном делаем две копии модуля: одну при экспорте и одну при необходимости.
Более того, копия в основном.js теперь отключен от исходного модуля. Вот почему даже когда мы увеличиваем наш счетчик, он все равно возвращает 1 — потому что переменная счетчика, которую мы импортировали, является отключенной копией переменной счетчика из модуля.
Таким образом, увеличение счетчика увеличит его в модуле, но не увеличит вашу скопированную версию. Единственный способ изменить скопированную версию переменной счетчика-это сделать это вручную:
JavaScript:
counter.counter++;
console.log(counter.counter); // 2
С другой стороны, ES6 создает живое представление модулей, которые мы импортируем, только для чтения:
JavaScript:
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2
Классная штука, а? Что я нахожу действительно убедительным в живых представлениях только для чтения, так это то, как они позволяют вам разбивать ваши модули на более мелкие части, не теряя функциональности.
Затем вы можете повернуться и снова объединить их, без проблем. Это просто " работает.”
Глядя вперед: комплектация модулей
Ух ты! Куда уходит время? Это была дикая поездка, но я искренне надеюсь, что она дала вам лучшее понимание модулей в JavaScript.
В следующем разделе я пройдусь по комплектованию модулей, охватывая основные темы, включая:
- Почему мы связываем модули
- Различные подходы к комплектации
- API загрузчика модулей ECMAScript
- …и еще.