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

Модули JS

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

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