Модули

Концепция модулей как способа организации JavaScript-кода существовала давно.

Когда приложение сложное и кода много – мы пытаемся разбить его на файлы. В каждом файле описываем какую-то часть, а в дальнейшем – собираем эти части воедино.

Модули в стандарте ECMAScript предоставляют удобные средства для этого.

Такие средства предлагались сообществом и ранее, например:

  • AMD – одна из самых древних систем организации модулей, требует лишь наличия клиентской библиотеки, к примеру, require.js, но поддерживается и серверными средствами.
  • CommonJS – система модулей, встроенная в сервер Node.JS. Требует поддержки на клиентской и серверной стороне.
  • UMD – система модулей, которая предложена в качестве универсальной. UMD-модули будут работать и в системе AMD и в CommonJS.

Все перечисленные выше системы требуют различных библиотек или систем сборки для использования.

Новый стандарт отличается от них прежде всего тем, что это – стандарт. А значит, со временем, будет поддерживаться браузерами без дополнительных утилит.

Однако, сейчас браузерной поддержки почти нет. Поэтому ES-модули используются в сочетании с системами сборки, такими как webpack, brunch и другими, при подключённом Babel.JS. Мы рассмотрим это далее.

Что такое модуль?

Модулем считается файл с кодом.

В этом файле ключевым словом export помечаются переменные и функции, которые могут быть использованы снаружи.

Другие модули могут подключать их через вызов import.

export

Ключевое слово export можно ставить:

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

Например, так экспортируется переменная one:

// экспорт прямо перед объявлением
export let one = 1;

Можно написать export и отдельно от объявления:

let two = 2;

export {two};

При этом в фигурных скобках указываются одна или несколько экспортируемых переменных.

Для двух переменных будет так:

export {one, two};

При помощи ключевого слова as можно указать, что переменная one будет доступна снаружи (экспортирована) под именем once, а two – под именем twice:

export {one as once, two as twice};

Экспорт функций и классов выглядит так же:

export class User {
  constructor(name) {
    this.name = name;
  }
};

export function sayHi() {
  alert("Hello!");
};

// отдельно от объявлений было бы так:
// export {User, sayHi}
Для экспорта обязательно нужно имя

Заметим, что и у функции и у класса при таком экспорте должно быть имя.

Так будет ошибка:

// функция без имени
export function() { alert("Error"); };

В экспорте указываются именно имена, а не произвольные выражения.

import

Другие модули могут подключать экспортированные значения при помощи ключевого слова import.

Синтаксис:

import {one, two} from "./nums";

Здесь:

  • "./nums" – модуль, как правило это путь к файлу модуля.
  • one, two – импортируемые переменные, которые должны быть обозначены в nums словом export.

В результате импорта появятся локальные переменные one, two, которые будут содержать значения соответствующих экспортов.

Например, при таком файле nums.js:

export let one = 1;
export let two = 2;

Модуль ниже выведет «1 and 2»:

import {one, two} from "./nums";

alert( `${one} and ${two}` ); // 1 and 2

Импортировать можно и под другим именем, указав его в «as»:

// импорт one под именем item1, а two – под именем item2
import {one as item1, two as item2} from "./nums";

alert( `${item1} and ${item2}` ); // 1 and 2
Импорт всех значений в виде объекта

Можно импортировать все значения сразу в виде объекта вызовом import * as obj, например:

import * as numbers from "./nums";

// теперь экспортированные переменные - свойства numbers
alert( `${numbers.one} and ${numbers.two}` ); // 1 and 2

export default

Выше мы видели, что модуль может экспортировать выбранные переменные при помощи export.

Однако, как правило, код стараются организовать так, чтобы каждый модуль делал одну вещь. Иначе говоря, «один файл – одна сущность, которую он описывает». Например, файл user.js содержит class User, файл login.js – функцию login() для авторизации, и т.п.

При этом модули, разумеется, будут использовать друг друга. Например, login.js, скорее всего, будет импортировать класс User из модуля user.js.

Для такой ситуации, когда один модуль экспортирует одно значение, предусмотрено особое ключевое сочетание export default.

Если поставить после export слово default, то значение станет «экспортом по умолчанию».

Такое значение можно импортировать без фигурных скобок.

Например, файл user.js:

export default class User {
  constructor(name) {
    this.name = name;
  }
};

…А в файле login.js:

import User from './user';

new User("Вася");

«Экспорт по умолчанию» – своего рода «синтаксический сахар». Можно было бы и без него, импортировать значение обычным образом через фигурные скобки {…}. Если бы в user.js не было default, то в login.js необходимо было бы указать фигурные скобки:

// если бы user.js содержал
// export class User { ... }

// …то при импорте User понадобились бы фигурные скобки:
import {User} from './user';

new User("Вася");

На практике этот «сахар» весьма приятен, так как позволяет легко видеть, какое именно значение экспортирует модуль, а также обойтись без лишних символов при импорте.

CommonJS

Если вы раньше работали с Node.JS или использовали систему сборки в синтаксисе CommonJS, то вот соответствия.

Для экспорта по умолчанию вместо:

module.exports = VARIABLE;

Пишем:

export default VARIABLE;

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

let VARIABLE = require('./file');

Пишем:

import VARIABLE from './file';

Для экспорта нескольких значений из модуля, вместо:

exports.NAME = VARIABLE;

Пишем в фигурных скобках, что надо экспортировать и под каким именем (без as, если имя совпадает):

export {VARIABLE as NAME};

При импорте – также фигурные скобки:

import {NAME} from './file';

Использование

Современный стандарт ECMAScript описывает, как импортировать и экспортировать значения из модулей, но он ничего не говорит о том, как эти модули искать, загружать и т.п.

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

Сейчас используются системы сборки, как правило, в сочетании с Babel.JS.

Система сборки обрабатывает скрипты, находит в них import/export и заменяет их на свои внутренние JavaScript-вызовы. При этом, как правило, много файлов-модулей объединяются в один или несколько скриптов, смотря как указано в конфигурации сборки.

Ниже вы можете увидеть полный пример использования модулей с системой сборки webpack.

В нём есть:

  • nums.js – модуль, экспортирующий one и two, как описано выше.
  • main.js – модуль, который импортирует one, two из nums и выводит их сумму.
  • webpack.config.js – конфигурация для системы сборки.
  • bundle.js – файл, который создала система сборки из main.js и nums.js.
  • index.html – простой HTML-файл для демонстрации.
Результат
bundle.js
main.js
nums.js
webpack.config.js
index.html
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};

/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {

/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId])
/******/ 			return installedModules[moduleId].exports;

/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			exports: {},
/******/ 			id: moduleId,
/******/ 			loaded: false
/******/ 		};

/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/ 		// Flag the module as loaded
/******/ 		module.loaded = true;

/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}


/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;

/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;

/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";

/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

	'use strict';

	var _nums = __webpack_require__(1);

	document.write('Сумма импортов: ' + (_nums.one + _nums.two));

/***/ },
/* 1 */
/***/ function(module, exports) {

	"use strict";

	Object.defineProperty(exports, "__esModule", {
	  value: true
	});
	var one = 1;

	exports.one = one;
	var two = 2;

	exports.two = two;

/***/ }
/******/ ]);
import {one, two} from './nums';

document.write(`Сумма импортов: ${one + two}`);
export let one = 1;

let two = 2;

export {two};
// Для использования нужен Node.JS
// Поставьте его:
//   npm i -g webpack
// Поставьте babel-loader:
//   npm i babel-loader
// Запустите его в директории с файлами:
//   webpack

module.exports = {
  entry: './main',

  output: {
    filename: 'bundle.js'
  },

  module: {
    loaders: [
      { test: /\.js$/, loader: "babel" }
    ]
  }
};
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>

  <script src="bundle.js"></script>

</body>
</html>

Итого

Современный стандарт описывает, как организовать код в модули, экспортировать и импортировать значения.

Экспорт:

  • export можно поставить прямо перед объявлением функции, класса, переменной.
  • Если export стоит отдельно от объявления, то значения в нём указываются в фигурных скобках: export {…}.
  • Также можно экспортировать «значение по умолчанию» при помощи export default.

Импорт:

  • В фигурных скобках указываются значения, а затем – модуль, откуда их брать: import {a, b, c as d} from "module".
  • Можно импортировать все значения в виде объекта при помощи import * as obj from "module".
  • Без фигурных скобок будет импортировано «значение по умолчанию»: import User from "user".

На текущий момент модули требуют системы сборки на сервере. Автор этого текста преимущественно использует webpack, но есть и другие варианты.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.