О модулях JavaScript, форматах, загрузчиках и сборщиках модулей за 10 минут

Модули JS

Несмотря на то, что новые языки программирования появляются каждый год, 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, перед тем, как код будет запущен в браузере.

Модули JS

 

Загрузчики модулей

Загрузчик модулей интерпретирует и загружает модуль, написанный в определённом формате.

Загрузчик модуля запускается в среде исполнения:

  • вы загружаете загрузчик модуля в браузере;
  • вы сообщаете загрузчику, какой главный файл приложения запустить;
  • модуль скачивает и интерпретирует главный файл приложения;
  • загрузчик модулей скачивает файлы по мере необходимости.

Если вы откроете вкладку «Сеть» в консоли разработчика на своём браузере, вы увидите, что многие файлы были загружены по запросу загрузчика модулей.

Вот несколько популярных загрузчиков:

  • RequireJS: загрузчик модулей в формате AMD ;
  • SystemJS: загрузчик модулей в форматах AMD, CommonJS, UMD и System.register format.

Сборщики модулей

Webpack JavaScript

 

Сборщик модулей заменяет собой загрузчик модулей. Однако в отличие от загрузчика модулей, сборщик модулей запускается при сборке:

  • вы запускаете сборщик модулей для создания файла пакета во время сборки (например, bundle.js);
  • и загружаете пакет в браузер.

Если вы откроете вкладку «Сеть» в консоли разработчика на своём браузере, вы увидите, что загружен только один файл. Таким образом, отпадает необходимость в загрузчике модулей: весь код включён в один пакет.

Пара популярных сборщиков:

  • Browserify: сборщик для модулей CommonJS;
  • Webpack: сборщик для модулей AMD, CommonJS, ES6.

Подводим итоги

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

Модуль — это переиспользуемая часть кода, содержащая в себе детали реализации и предоставляющая открытое API, что позволяет легко загрузить её и использовать в другом коде.

Формат модуля — это синтаксис, который используется для определения модулей. В прошлом возникали разные форматы: AMD, CommonJS, UMD и System.register нативный формат модулей появился в ES6.

Загрузчик модуля интерпретирует и загружает модуль, написанный в определённом формате, в время выполнения (в браузере). Распространённые — RequireJS и SystemJS.

Сборщик модуля заменяет загрузчик модулей и создаёт пакет, содержащий весь код, во время сборки.  Популярные примеры — Browserify и Webpack.

Вот и всё — теперь у вас достаточно знаний для понимания современной разработки на JavaScript. В следующий раз, когда ваш компилятор TypeScript спросит, какой формат модулей вы хотите использовать, вы уже не будете гадать, что бы это значило. А если такое произойдет — просто перечитайте эту статью.

Отличного дня и программируйте с удовольствием!


За перевод материала выражаем благодарность международной IT-компании Noveo.

Перевод статьи «A 10 minute primer to JavaScript modules, module formats, module loaders and module bundlers»