JavaScript: Полное руководство по модулям в браузере и Node.js

Источник: «The Complete Guide To Modules In Browsers And Node.»
Большинство языков программирования имеют концепцию модулей: способ определить функции в одном файле и использовать их в другом.

В большинстве языков программирования есть концепция модулей: способ определить функции в одном файле, а использовать в другом. Разработчики могут создавать библиотеки кода, отвечающие за связанные задачи.

К преимуществам модулей относятся:

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

Разработчики клиентской части должны были:

Использование модулей ES2015 (ESM)

Модули ES (ESM) появились в ECMAScript 2015 (ES6). ESM предлагал следующие возможности:

Следующий код определяет модуль mathlib.js экспортирующий три публичные функции в конце:

// mathlib.js

// сложение значений
function sum(...args) {
log('sum', args);
return args.reduce((num, tot) => tot + num);
}

// умножение значений
function multiply(...args) {
log('multiply', args);
return args.reduce((num, tot) => tot * num);
}

// factorial: умножить все значения от 1 до значения
function factorial(arg) {
log('factorial', arg);
if (arg < 0) throw new RangeError('Invalid value');
if (arg <= 1) return 1;
return arg * factorial(arg - 1);
}

// приватная функция вывода лога
function log(...msg) {
console.log(...msg);
}

export { sum, multiply, factorial };

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

// сложение значений
export function sum(...args) {
log('sum', args);
return args.reduce((num, tot) => tot + num);
}

Оператор import включает модуль ES, ссылаясь на его URL путь с использованием относительной записи (./mathlib.js, ../mathlib.js) или полной записи (file:///home/path/mathlib.js, https://mysite.com/mathlib.js).

Вы можете ссылаться на модули ES, добавленные с помощью Node.js npm install используя "name" определённое в package.json.

Современные браузеры, Deno и Bun могут загружать модули с URL-адреса в интернете (https://mysite.com/mathlib.js). Это изначально не поддерживалось в Node.js, но должно появиться в следующем выпуске.

Вы можете импортировать определённые именованные элементы:

import { sum, multiply } from './mathlib.js';

console.log( sum(1,2,3) ); // 6
console.log( multiply(1,2,3) ); // 6

Вы можете использовать псевдонимы для импорта, чтобы разрешить любые конфликты:

import { sum as addAll, mult as multiplyAll } from './mathlib.js';

console.log( addAll(1,2,3) ); // 6
console.log( multiplyAll(1,2,3) ); // 6

Или импортировать все общедоступные значения, используя имя объекта в качестве пространства имён:

import * as lib from './mathlib.js';

console.log( lib.sum(1,2,3) ); // 6
console.log( lib.multiply(1,2,3) ); // 6
console.log( lib.factorial(3) ); // 6

Модуль экспортирующий один элемент, может быть анонимным по умолчанию (default). Например:

// defaultmodule.js
export default function() { ... };

Импортируя default без фигурных скобок, используйте любое предпочитаемое имя:

import myDefault from './defaultmodule.js';

Это фактически то же самое, что и следующее:

import { default as myDefault } from './defaultmodule.js';

Некоторые разработчики избегают экспорта default, потому что:

Я бы не никогда не стал говорить никогда, но от экспорта по умолчанию мало пользы.

Загрузка ES модулей в браузерах

Браузеры загружают ES модули асинхронно, и откладывают выполнение до готовности DOM. Модули запускаются в порядке, указанным тэгом <script>:

<script type="module" src="./run-first.js"></script>
<script type="module" src="./run-second.js"></script>

И каждым встроенным import:

<script type="module">
import { something } from './run-third.js';
// ...
</script>

Браузеры не поддерживающие ESM не будут загружать и запускать скрипт с атрибутом type="module". Точно так же браузеры с поддержкой ESM не будут загружать скрипты с атрибутом nomodule.

При необходимости можно предоставить два скрипта для современного и старого браузера:

<script type="module" src="./runs-in-modern-browser.js"></script>
<script nomodule src="./runs-in-old-browser.js"></script>

Это может быть практично, когда:

Обратите внимание, что ES модули должны отдаваться сервером с MIME типом application/javascript или text/javascript. Заголовок CORS должен быть установлен для модуля импортируемого из другого домена, например, Access-Control-Allow-Origin: * для разрешения доступа с любого сайта.

Будьте осторожны с импортом стороннего кода из другого домена. Это повлияет на производительность и представляет угрозу безопасности. Если вы сомневаетесь, скопируйте файл на локальный сервер и импортируйте оттуда.

Использование модулей CommonJS в Node.js

CommonJS был выбран в качестве системы модулей для Node.js, потому что в 2009, когда появилась среда выполнения JavaScript, ESM ещё не существовало. Возможно вы сталкивались с CommonJS используя Node.js или npm. Модуль CommonJS делает функцию или значение доступным с помощью module.exports. Перепишем наш ES модуль mathlib.js:

// mathlib.js

// сложение значений
function sum(...args) {
log('sum', args);
return args.reduce((num, tot) => tot + num);
}

// умножение значений
function multiply(...args) {
log('multiply', args);
return args.reduce((num, tot) => tot * num);
}

// factorial: умножить все значения от 1 до значения
function factorial(arg) {
log('factorial', arg);
if (arg < 0) throw new RangeError('Invalid value');
if (arg <= 1) return 1;
return arg * factorial(arg - 1);
}

// приватная функция вывода лога
function log(...msg) {
console.log(...msg);
}

module.exports = { sum, multiply, factorial };

Оператор require включает модуль CommonJS, ссылаясь на путь к его файлу, используя относительную (./mathlib.js, ../mathlib.js) или абсолютную нотацию (/path/mathlib.js). Справочные модули добавляются с помощью npm install с использованием имени, определённого в package.json.

Модуль CommonJS подключается динамически и синхронно загружается в точке, на которую он ссылается во время выполнения скрипта. Вы можете задать при подключении определённые экспортируемые элементы:

const { sum, mult } = require('./mathlib.js');

console.log( sum(1,2,3) ); // 6
console.log( multiply(1,2,3) ); // 6

Или может, при подключении, задать, чтобы каждый экспортируемый элемент использовал имя переменной в качестве пространства имён:

const lib = require('./mathlib.js');

console.log( lib.sum(1,2,3) ); // 6
console.log( lib.multiply(1,2,3) ); // 6
console.log( lib.factorial(3) ); // 6

Вы можете определить модуль с одним экспортируемым элементом по умолчанию:

// mynewclass.js
class MyNewClass {};
module.exports = MyNewClass;

Подключая элемент по умолчанию, можно использовать любое имя:

const
ClassX = require('mynewclass.js'),
myObj = new ClassX();

Различия между ES модулями и CommonJS

ESM и CommonJS внешне похожи, но есть фундаментальные различия.

Динамический импорт модулей ES напрямую не поддерживается и не рекомендуется — этот код не выполнится:

// НЕ  РАБОТАЕТ!
const script = `./lib-${ Math.round(Math.random() * 3) }.js`;
import * as lib from script;

Можно динамически загружать модули ES с помощью асинхронной функции import(), которая возвращает Promise.

const script = `./lib-${ Math.round(Math.random() * 3) }.js`;
const lib = await import(script);

Это влияет на производительность, а проверка кода становится более сложной. Используйте функцию import() когда нет другого варианта, например, скрипт создаётся динамически после запуска приложения.

ESM также может импортировать данные JSON, хотя это (пока) не утверждённый стандарт, и поддержка на разных платформах может различаться:

import data from './data.json' assert { type: 'json' };

Динамический CommonJS и ESM с поднятой (hoisted) загрузкой могут привести к другим логическим несовместимостям. Рассмотрим этот ES модуль:

// ESM two.js
console.log('running two');
export const hello = 'Hello from two';

Этот сценарий импортирует его:

// ESM one.js
console.log('running one');
import { hello } from './two.js';
console.log(hello);

При выполнении one.js выведет следующее:

running two
running one
hello from two

Это происходит из-за того, что two.js импортируется до того, как one.js выполнится, даже если импорт происходит после console.log().

Подобный CommonJS модуль two.js:

// CommonJS two.js
console.log('running two');
module.exports = 'Hello from two';

Вызывается в one.js:

// CommonJS one.js
console.log('running one');
const hello = require('./two.js');
console.log(hello);

Порядок выполнения другой:

running one
running two
hello from two

Браузеры не поддерживают CommonJS напрямую, поэтому вряд ли это повлияет на клиентский код. Node.js поддерживает оба типа модулей, и в одном проекте можно смешивать CommonJS и ESM!

Node.js использует следующий подход для решения проблем совместимости модулей:

Ещё одно преимущество ES модулей заключается в том, что они поддерживают верхний уровень await. Вы можете выполнить асинхронный код в коде входа:

await sleep(1);

Это невозможно в CommonJS, необходимо объявлять внешнее асинхронное Выражения Немедленно Вызываемой Функции (IIFE).

(async () => {
await sleep(1);
})();

Импорт модулей CommonJS в ESM

Node.js может импортировать модуль CommonJS в файл ESM. Например:

import lib from './lib.cjs';

Часто это работает хорошо, и Node.js предлагает варианты синтаксиса при возникновении проблем.

Подключение ES модулей в CommonJS

Невозможно подключить ES модуль, через require в файл CommonJS. При необходимости можно использовать асинхронную функцию import(), показанную выше:

// CommonJS script
(async () => {

const lib = await import('./lib.mjs');

// ... use lib ...

})();

Заключение

На разработку ES Модулей ушло много лет, но наконец-то у нас есть система, которая работает в браузерах и средах выполнения JavaScript на стороне сервера, таких как Node.js, Deno и Bun.

Тем не менее Node.js использует CommonJS половину своей жизни, и он так же поддерживается в Bun. Вы можете столкнуться с библиотеками, которые предназначены только для CommonJS, только для ESM или предоставляют сборки для обеих модульных систем JavaScrip. Я рекомендую использовать ES Модули для новых проектов Node.js, если только вы не столкнётесь с важным (но редким) пакетом CommonJS, который невозможно импортировать, через import. Даже в этом случае вы можете рассмотреть возможность переноса этой функциональности в рабочий поток или дочерний процесс, чтобы остальная часть проекта сохранила ESM.

Преобразование большого устаревшего проекта Node.js из CommonJS в ESM может оказаться сложной задачей, особенно если вы столкнётесь с указанными выше различиями в порядке выполнения. Node.js будет поддерживать CommonJS в течение многих лет — возможно, навсегда — так что, вероятно, это не стоит затраченных усилий. Это может измениться, если клиенты потребуют полной совместимости ESM для ваших публичных библиотек.

Для всего остального: используйте ES модули. Это стандарт JavaScript.

Дополнительная информация: JavaScript: различие между require и import

Дополнительные материалы

Предыдущая Статья

Laravel 9: Ваше первое приложение

Следующая Статья

TypeScript: 11 советов, которые улучшат ваши навыки