О модулях JavaScript, форматах, загрузчиках и сборщиках модулей за 10 минут
https://t.me/we_use_js![](/file/26df1a60e70689e66399c.png)
Несмотря на то, что новые языки программирования появляются каждый год, JavaScript остаётся одним из самых распространённых и любимых программистами. И как и любой современный язык, он стремительно развивается, что делает изучение его с нуля очень непростой задачей. В этом материале простым языком рассказывается о том, как в JavaScript устроена работа с модулями. Статья может не только помочь новичкам понять, что происходит, но и освежить знания основ у тех, кто уже работает с JS.
Разработка с использованием современных технологий JavaScript может ошеломлять. Работая над проектом, ты легко можешь задаться вопросом, для чего же нужны новые механизмы и инструменты.
Для чего предназначены Webpack и SystemJS? Что значит AMD, UMD или CommonJS? Какое отношение они имеют друг к другу и зачем вообще их использовать?
В этой статье речь пойдёт о различии между модулями JavaScript, а также о форматах, загрузчиках и сборщиках модулей. Это не подробное руководство по использованию каких-либо инструментов или паттернов, а краткий обзор, призванный дать общее понятие о концепции современного JavaScript.
Итак, приступим.
Что такое модуль?
Модуль — это переиспользуемая часть кода, содержащая в себе детали реализации и предоставляющая открытое API, что позволяет легко загрузить её и использовать в другом коде.
Зачем нужны модули?
Технически код можно написать и без использования модулей. Модули — это паттерн, который в разных формах и на разных языках используется разработчиками с 60-х и 70-х годов.
В идеале, модули JavaScript позволяют нам:
- абстрагировать код, передавая функциональные возможности сторонним библиотекам, так что нам не придётся разбираться во всех сложностях их реализации;
- инкапсулировать код, скрывая его внутри модуля, если не хотим, чтобы его изменяли;
- переиспользовать код, избавляясь от необходимости писать одно и то же снова и снова;
- управлять зависимостями, легко изменяя зависимости без необходимости переписывать наш код.
Паттерны модулей в ES5
ECMAScript 5 и более ранние версии не были спроектированы с учётом модулей. Со временем разработчики нашли различные возможности симулировать модульную архитектуру на JavaScript.
Прим. перев. Senior-разработчики компании Noveo говорят, что помнят, как это было: как они проходили путь от работы без модулей к первым попыткам написать их самостоятельно, потом использовать чужие наработки… Ну а все системы, перечисленные ниже, они знают не понаслышке. Эх, были времена!
Чтобы дать представление о том, как выглядят такие паттерны, давайте взглянем на два из них: мгновенно вызываемая функция (Immediately Invoked Function Expressions) и выявление модуля (Revealing Module).
Немедленно вызываемая функция (Immediately Invoked Function Expression или IIFE)
(function(){ // ... })()
Немедленно вызываемая функция (IIFE) — анонимная функция, которая вызывается сразу после объявления. Обратите внимание: функция окружена скобками. В JavaScript строка, начинающаяся со слова function, воспринимается как объявление функции:
// Объявление функции function(){ console.log('test'); }
Мгновенный вызов объявления функции выдаёт ошибку:
// Мгновенное объявление функции function(){ console.log('test'); }() // Uncaught SyntaxError: Unexpected token )
Помещение функции в скобки делает это функциональным выражением:
// Функциональное выражение (function(){ console.log('test'); }) // returns function test(){ console.log('test') }
Функциональное выражение возвращает нам функцию, так что мы можем тут же к ней обратиться:
// Мгновенно вызываемая функция (function(){ console.log('test'); })() // пишет 'test' в консоли и возвращает undefined
Мгновенно вызываемые функции позволяют нам:
- полностью инкапсулировать код в IIFE, так что нам не придётся разбираться, как работает код IIFE;
- определять переменные внутри IIFE, чтобы они не засоряли глобальную область видимости (переменные, объявленные внутри IIFE, остаются в рамках замкнутого выражения).
Однако они не дают нам механизма управления зависимостями.
Паттерн выявления модуля (Revealing Module)
Паттерн выявления модуля схож с IIFE, но здесь мы присваиваем возвращённое значение переменной:
// Объявление модуля как глобальной переменной var singleton = function(){ // Внутренняя логика function sayHello(){ console.log('Hello'); } // Внешнее API return { sayHello: sayHello } }()
Заметьте, что здесь нет необходимости в скобках, так как слово function находится не в начале строки.
Теперь мы можем обратиться к API модуля через переменную:
// Access module functionality singleton.sayHello(); // Hello
Вместо синглтона модуль может выступать и как функция-конструктор:
// Объявление модуля как глобальной переменной var Module = function(){ // Внутренняя логика function sayHello(){ console.log('Hello'); } // Внешнее API return { sayHello: sayHello } }
Обратите внимание: мы не запускаем функцию при её объявлении, вместо этого мы инициализируем модуль при помощи функции-конструктора Module
var module = new Module();
для доступа к внешнему API
module.sayHello(); // Hello
Паттерн выявления модуля предоставляет те же преимущества, что и IIFE, но опять же не даёт возможности управлять зависимостями.
С развитием JavaScript появлялись разные синтаксические возможности определения модулей, и у каждого были свои сильные и слабые стороны.
Форматы модулей
Формат модуля — это синтаксис, который используется для его определения.
До создания ECMAScript 6, или ES2015, в JavaScript не было официального синтаксиса для определения модулей. А значит, опытные разработчики предлагали разные форматы определения.
Вот несколько наиболее известных и широко используемых:
- асинхронное определение модуля (Asynchronous Module Definition или AMD);
- CommonJS;
- универсальное определение модуля (Universal Module Definition или UMD);
- System.register;
- формат модуля ES6.
Давайте рассмотрим каждый из них, чтобы вы смогли распознать их по синтаксису.
Асинхронное определение модуля (AMD)
Формат AMD используется в браузерах и применяет для определения модулей функцию define:
//Вызов функции define с массивом зависимостей и фабричной функцией define(['dep1', 'dep2'], function (dep1, dep2) { //Определение модуля с помощью возвращаемого значения return function () {}; });
Формат CommonJS
Формат CommonJS применяется в Node.js и использует для определения зависимостей и модулей require и module.exports:
var dep1 = require('./dep1'); var dep2 = require('./dep2'); module.exports = function(){ // ... }
Универсальное определение модуля (UMD)
Формат UMD может быть использован как в браузере, так и в Node.js.
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Подключение анонимного модуля define(['b'], factory); } else if (typeof module === 'object' && module.exports) { // Node. Не работает с CommonJS напрямую, // только CommonJS-образными средами, которые поддерживают // module.exports, как Node. module.exports = factory(require('b')); } else { // Глобальные переменные браузера (root это window) root.returnExports = factory(root.b); } }(this, function (b) { //как-нибудь использовать b. // Просто возвращаем значение для определения модуля. // Этот пример возвращает объект, но модуль // может вернуть и функцию как экспортируемое значение. return {}; }));
System.registerА
Формат System.register был разработан для поддержки синтаксиса модулей ES6 в ES5:
import { p as q } from './dep'; var s = 'local'; export function func() { return q; } export class C { }
Формат модулей ES6
В ES6 JavaScript уже поддерживает нативный формат модулей.
Он использует токен export для экспорта публичного API модуля:
// lib.js // Экспорт функции export function sayHello(){ console.log('Hello'); } // Не экспоруемая функция function somePrivateFunction(){ // ... }
и токен import для импорта частей, которые модуль экспортирует:
import { sayHello } from './lib'; sayHello(); // Hello
Мы можем даже присваивать импорту алиас, используя as:
import { sayHello as say } from './lib'; say(); // Hello
или загружать сразу весь модуль:
import * as lib from './lib'; lib.sayHello(); // Hello
Формат также поддерживает экспорт по умолчанию:
// lib.js // Экспорт дефолтной функции export default function sayHello(){ console.log('Hello'); } // Экспорт недефолтной функции export function sayGoodbye(){ console.log('Goodbye'); }
который можно импортировать, например, так:
import sayHello, { sayGoodbye } from './lib'; sayHello(); // Hello sayGoodbye(); // Goodbye
Вы можете экспортировать не только функции, но и всё, что пожелаете:
// lib.js // Экспорт дефолтной функции export default function sayHello(){ console.log('Hello'); } // Экспорт недефолтной функции export function sayGoodbye(){ console.log('Goodbye'); } // Экспорт простого значения export const apiUrl = '...'; // Экспорт объекта export const settings = { debug: true }
К сожалению, нативный формат модулей пока поддерживают не все браузеры.
Мы можем использовать формат модулей ES6 уже сегодня, но для этого потребуется компилятор наподобие Babel, который будет переводить наш код в формат ES5, такой, как AMD или CommonJS, перед тем, как код будет запущен в браузере.
![](/file/9368f5692cefca5572335.png)
Загрузчики модулей
Загрузчик модулей интерпретирует и загружает модуль, написанный в определённом формате.
Загрузчик модуля запускается в среде исполнения:
- вы загружаете загрузчик модуля в браузере;
- вы сообщаете загрузчику, какой главный файл приложения запустить;
- модуль скачивает и интерпретирует главный файл приложения;
- загрузчик модулей скачивает файлы по мере необходимости.
Если вы откроете вкладку «Сеть» в консоли разработчика на своём браузере, вы увидите, что многие файлы были загружены по запросу загрузчика модулей.
Вот несколько популярных загрузчиков:
- RequireJS: загрузчик модулей в формате AMD ;
- SystemJS: загрузчик модулей в форматах AMD, CommonJS, UMD и System.register format.
Сборщики модулей
![](/file/740f095e38d3a8e878ab6.png)
Сборщик модулей заменяет собой загрузчик модулей. Однако в отличие от загрузчика модулей, сборщик модулей запускается при сборке:
- вы запускаете сборщик модулей для создания файла пакета во время сборки (например, bundle.js);
- и загружаете пакет в браузер.
Если вы откроете вкладку «Сеть» в консоли разработчика на своём браузере, вы увидите, что загружен только один файл. Таким образом, отпадает необходимость в загрузчике модулей: весь код включён в один пакет.
Пара популярных сборщиков:
- Browserify: сборщик для модулей CommonJS;
- Webpack: сборщик для модулей AMD, CommonJS, ES6.
Подводим итоги
Чтобы лучше разобраться в инструментах современной среды разработки на JavaScript, важно понимать разницу между модулями, форматами, загрузчиками и сборщиками модулей.
Модуль — это переиспользуемая часть кода, содержащая в себе детали реализации и предоставляющая открытое API, что позволяет легко загрузить её и использовать в другом коде.
Формат модуля — это синтаксис, который используется для определения модулей. В прошлом возникали разные форматы: AMD, CommonJS, UMD и System.register нативный формат модулей появился в ES6.
Загрузчик модуля интерпретирует и загружает модуль, написанный в определённом формате, в время выполнения (в браузере). Распространённые — RequireJS и SystemJS.
Сборщик модуля заменяет загрузчик модулей и создаёт пакет, содержащий весь код, во время сборки. Популярные примеры — Browserify и Webpack.
Вот и всё — теперь у вас достаточно знаний для понимания современной разработки на JavaScript. В следующий раз, когда ваш компилятор TypeScript спросит, какой формат модулей вы хотите использовать, вы уже не будете гадать, что бы это значило. А если такое произойдет — просто перечитайте эту статью.
Отличного дня и программируйте с удовольствием!