Модульность кода

1. Модульность кода

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

Модульный код помогает в организации, обслуживании, тестировании и, самое главное, управлении зависимостями. Наиболее важные преимущества модулей - это поддерживаемость, пространство имен и повторное использование.

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

  • Пространство имен — переменные, не входящие в область видимости функции, являются глобальными. Из-за этого обычно происходит загрязнение пространства имен, где полностью несвязанный код разделяет глобальные переменные. Модули позволяют избежать загрязнения пространства имен, создавая отдельную область видимости для переменных.

  • Повторное использование — все разработчики копировали готовый код в новые проекты, изменяя его под специфику проекта. Это, очевидно, огромная трата времени. Намного лучше когда есть модуль, который можно повторно использовать снова и снова без необходимости знать что-либо об окружении в котором он используется.

1.1. Сборка модулей

Сборка модулей — это процесс конкатенации группы модулей и их зависимостей в один или группу файлов.

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

Наличие отдельных тегов script для каждого файла означает, что браузер будет загружать каждый файл по отдельности, что негативно сказывается на скорости загрузки страницы (HTTP/2 немного помогает с решением этой проблемы). Чтобы обойти эту проблему, файлы объединяются в один или пару файлов, чтобы уменьшить количество запросов. Но остается проблема управления зависимостями между модулями.

Если используются системы модулей, которые браузеры не поддерживают, такие как CommonJS или ESM, необходимо использовать инструмент для их преобразования в правильно упорядоченный, доступный браузеру код. Именно здесь вступают в действие Webpack и другие бандлеры (от английского bundle).

2. CommonJS модули

CommonJS — модульная система, используемая в Node.js. Модули имеют компактный синтаксис, предназначены для синхронной загрузки и, в основном, используются на сервере и не работают в браузере по умолчанию.

CommonJS-модуль - это многократно используемый фрагмент JS-кода, который экспортирует определенные объекты, делая их доступными для других модулей.

Каждый JS-файл хранит код в уникальном контексте модуля. Для того чтобы описать интерфейс модуля, используется объект module.exports, значение которого будет доступно для использования другими модулями при импорте.

Для того чтобы получить интерфейс модуля в коде (импортировать), используется функция require('путь-к-модулю'). Результатом своего выполнения require вернет интерфейс модуля — то что в модуле указано в module.exports.

// В файле greeter.js
const helloMessage = 'hello!';
const goodbyeMessage = 'goodbye!';

const hello = () => helloMessage;
const goodbye = () => goodbyeMessage;

module.exports = {
  hello,
  goodbye,
};

// В файле index.js
const greeter = require('./greeter');

console.log(greeter.hello()); // 'hello!'
console.log(greeter.goodbye()); // 'goodbye!'
Copy

Есть два очевидных преимущества этого подхода:

  • Предотвращение загрязнения глобального пространства имен

  • Явное указание зависимостей

3. ECMAScript Modules (ESM)

До недавнего времени в языке не было встроенной модульной системы. ESM имеют декларативный синтаксис и возможность асинхронной загрузки.

Новая система модулей отличается от CommonJS и других, прежде всего тем, что это стандарт. А значит, со временем, будет поддерживаться браузерами нативно, без дополнительных инструментов. Однако сейчас браузерная поддержка слабая, поэтому ESM используются в сочетании с инструментами сборки модулей, такими как Webpack, Parcel, Rollup и другими.

Самое важное отличие ESM заключается в том, что они разработаны с учетом статического анализа. Это значит, что при импорте модулей, импорт обрабатывается во время компиляции, то есть до запуска скрипта. Это позволяет удалять экспорт, который не используется другими модулями, прежде чем запускать скрипт, что может привести к значительной экономии веса JS-файла, уменьшив нагрузку на браузер.

3.1. Синтаксис

Каждый модуль импортирует необходимые ему зависимости и экспортирует все, что должно быть импортировано другими модулями. Операции экспорта сущностей и импорта интерфейса модуля реализованы конструкциями import from и export.

3.2. Named export

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

Первый способ - это использовать ключевое слово export перед всеми сущностями, которые необходимо экспортировать. Они будут добавлены как свойства в экспортируемый объект. При импорте мы деструктуризируем свойства из импортируемого объекта.

// myModule.js
const sqrt = Math.sqrt;
export const square = x => x * x;
export const diag = (x, y) => sqrt(square(x) + square(y));

// main.js
import { square, diag } from './path/to/myModule';

console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
Copy

Второй способ - это явно указать объект со свойствами для экспорта.

// myModule.js
const sqrt = Math.sqrt;
const square = x => x * x;
const diag = (x, y) => sqrt(square(x) + square(y));

export { square, diag };

// main.js
import { square, diag } from './path/to/myModule';

console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
Copy

Следующий синтаксис импортирует все экспорты модуля как объект с указанным именем.

// main.js
import * as myModule from './path/to/myModule';

console.log(myModule.square(11)); // 121
console.log(myModule.diag(4, 3)); // 5
Copy

3.3. Default export

Часто модуль экспортирует всего одну сущность, такой экспорт удобен для импорта. Экспорт по умолчанию — самое главное экспортируемое значение, которое может быть чем угодно: переменной, функцией, классом и т. д.

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

// myClass.js
export default class MyClass { ... };

// main.js
import myFunc from './path/to/myFunc';
import MyClass from './path/to/myClass';

myFunc();

const inst = new MyClass();
Copy

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

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

Last updated