$ sudo teach IT
МОДУЛЬ 12 · УРОК 1

Модули: import / export

Любой большой проект — это не один огромный файл на 10 000 строк, а сотни маленьких, хорошо организованных файлов. Модули — это механизм, который позволяет разделить код на части и связать их между собой. Разбираемся, как это работает в JavaScript.

⏱ ~25 минут 🎓 Для начинающих 📦 JavaScript ESM

📁 Зачем вообще нужны модули?

Представьте: вы пишете приложение. Сначала — пара функций, несколько переменных, всё умещается в один файл. Жизнь прекрасна. Но через пару месяцев в этом файле уже 2000 строк, а через год — 8000. Вы уже не помните, где что находится. Функция с именем helper определена трижды в разных местах. Переменная data используется везде и непонятно, где она была изменена последний раз.

Это классическая проблема «спагетти-кода» — когда всё перемешано и невозможно разобраться. Именно для её решения были придуманы модули.

📦 Модуль — это просто файл с кодом на JavaScript. Но файл особый: он явно объявляет, что он отдаёт другим (export) и что он берёт от других (import). Это создаёт чёткие границы между частями кода.

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

😱
Без модулей

Один файл app.js на 5000 строк. Все переменные глобальные. Имена конфликтуют. Никто не понимает, что происходит.

😌
С модулями

utils.js, api.js, auth.js, main.js — каждый файл делает одно. Чёткие зависимости. Легко найти нужный код.

🏷️ Named exports — именованный экспорт

Самый распространённый тип экспорта — именованный. Это когда вы явно «публикуете» какие-то переменные, функции или классы из файла, давая им конкретные имена.

Представьте, что вы создаёте файл utils.js с набором вспомогательных функций. Вы хотите, чтобы другие файлы могли их использовать. Добавьте ключевое слово export перед каждой функцией или переменной, которую хотите «открыть»:

// utils.js

// Эти функции доступны для других файлов
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export function formatName(firstName, lastName) {
  return `${firstName} ${lastName}`.trim();
}

// Эта переменная — ПРИВАТНАЯ, снаружи не видна
const secretKey = "abc123";

function helperInternal() {
  // внутренняя утилита — не экспортируем
  return "только для внутреннего использования";
}

Теперь в другом файле — main.js — мы можем импортировать именно то, что нам нужно, указав имена в фигурных скобках:

// main.js

// Импортируем только то, что нужно
import { add, multiply, PI } from './utils.js';

console.log(add(2, 3));        // 5
console.log(multiply(4, 5));   // 20
console.log(PI);               // 3.14159

// Попытка использовать secretKey — ошибка!
// console.log(secretKey); // ReferenceError

💡 Путь к файлу всегда начинается с ./ (текущая папка) или ../ (родительская). Без точки — это встроенный или установленный пакет, например import { useState } from 'react'.

Есть ещё один вариант: экспортировать всё сразу в конце файла. Это удобно, когда хочется видеть в одном месте, что именно «публичное» в этом файле:

// math.js

const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;

// Экспортируем всё в конце — удобно для просмотра "публичного API" файла
export { add, subtract, multiply };

При импорте можно переименовать что-то через ключевое слово as, если имя занято или нужно сделать его короче:

// Переименование при импорте
import { formatName as format, add as sum } from './utils.js';

console.log(format("Иван", "Петров")); // Иван Петров
console.log(sum(10, 20));              // 30

⭐ Default export — экспорт по умолчанию

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

// user.js

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

  greet() {
    return `Привет, меня зовут ${this.name}!`;
  }
}

// Экспорт по умолчанию — только один на файл
export default User;

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

// main.js

// Имя выбираем произвольно — не обязательно User
import User from './user.js';
import MyUser from './user.js'; // Тоже сработает

const alice = new User("Алиса", "alice@example.com");
console.log(alice.greet()); // Привет, меня зовут Алиса!
Named export

Несколько экспортов из одного файла. При импорте — фигурные скобки { name }. Имя должно совпадать (или использовать as).

Default export

Один главный экспорт из файла. При импорте — без скобок. Имя выбираете сами. Удобно для компонентов и классов.

Можно смешивать оба типа в одном файле — это вполне нормальная практика:

// api.js

// Default export — главная функция файла
export default function fetchUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

// Named exports — вспомогательные константы
export const BASE_URL = "https://api.example.com";
export const TIMEOUT = 5000;
// main.js
import fetchUser, { BASE_URL, TIMEOUT } from './api.js';
// Сначала default, потом в скобках named

🔀 Re-export — переэкспорт

В больших проектах часто создают файл-«точку входа» — обычно это index.js — который собирает экспорты из разных файлов и предоставляет их как единый фасад. Это называется переэкспортом (re-export).

Представьте, что у вас есть папка utils/ с несколькими файлами:

// utils/math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;

// utils/string.js
export const capitalize = str => str[0].toUpperCase() + str.slice(1);
export const trim = str => str.trim();

// utils/index.js — точка входа, переэкспортирует всё
export { add, multiply } from './math.js';
export { capitalize, trim } from './string.js';

Теперь пользователи могут импортировать всё из одного места:

// main.js — удобно! Один импорт вместо двух
import { add, capitalize } from './utils/index.js';
// или ещё короче, если папка называется utils:
import { add, capitalize } from './utils';

✅ Практика: файл index.js в папке — стандартный паттерн JavaScript. Именно поэтому можно писать import X from './components/Button' вместо import X from './components/Button/Button.js'.

🔧 CommonJS vs ESM: две системы модулей

Исторически в JavaScript сложилось так, что существуют две разные системы модулей. Это важно знать, потому что вы столкнётесь с обеими в реальных проектах.

📦 CommonJS (старая система, Node.js)

CommonJS — исторически первая система модулей в Node.js. Использует require() для импорта и module.exports для экспорта. По умолчанию используется в Node.js, если не указано иначе.

// math.js — CommonJS экспорт
function add(a, b) { return a + b; }
module.exports = { add };

// main.js — CommonJS импорт
const { add } = require('./math.js');
console.log(add(2, 3)); // 5

✨ ESM — ECMAScript Modules (современная система)

ESM — стандартная система модулей JavaScript (ES6, 2015). Использует import/export. Это то, что мы изучали выше. Сегодня это предпочтительный вариант — он работает и в браузере, и в Node.js.

// math.js — ESM экспорт
export function add(a, b) { return a + b; }

// main.js — ESM импорт
import { add } from './math.js';
console.log(add(2, 3)); // 5

🔑 Чтобы использовать ESM в Node.js, нужно либо назвать файл .mjs, либо добавить "type": "module" в package.json. Большинство современных проектов делают именно так.

🌐 Модули в браузере

Браузеры поддерживают ESM нативно начиная с 2017 года. Чтобы подключить модульный JavaScript к HTML-странице, нужно добавить атрибут type="module" к тегу <script>:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Мой проект</title>
</head>
<body>
  <!-- type="module" — ключевой атрибут! -->
  <script type="module" src="./main.js"></script>
</body>
</html>

Когда браузер видит type="module", он включает режим модулей: переменные не попадают в глобальную область видимости, выполнение строгое (use strict по умолчанию), и можно использовать import/export.

⚠️ Важно: модули в браузере работают только при загрузке через HTTP/HTTPS сервер. Если просто открыть HTML-файл двойным кликом (протокол file://), браузер заблокирует импорты из соображений безопасности. Используйте расширение Live Server в VS Code.

🛠️ Практика: разбиваем программу на модули

Давайте посмотрим на реальный пример. Представьте небольшое приложение — менеджер задач. Без модулей всё было бы в одном файле. С модулями мы разобьём это на три логических части:

// storage.js — работа с localStorage
export function saveTasks(tasks) {
  localStorage.setItem('tasks', JSON.stringify(tasks));
}

export function loadTasks() {
  const raw = localStorage.getItem('tasks');
  return raw ? JSON.parse(raw) : [];
}

export function clearTasks() {
  localStorage.removeItem('tasks');
}
// task.js — логика задач
export function createTask(text) {
  return {
    id: Date.now(),
    text,
    done: false,
    createdAt: new Date().toISOString()
  };
}

export function completeTask(task) {
  return { ...task, done: true };
}

export function filterActive(tasks) {
  return tasks.filter(t => !t.done);
}
// main.js — точка входа, связывает всё вместе
import { saveTasks, loadTasks } from './storage.js';
import { createTask, completeTask, filterActive } from './task.js';

let tasks = loadTasks();

// Добавить задачу
const newTask = createTask("Выучить модули JS");
tasks.push(newTask);
saveTasks(tasks);

// Найти активные задачи
const active = filterActive(tasks);
console.log(`Активных задач: ${active.length}`);

Заметьте, насколько это чище: каждый файл отвечает за одну вещь. Хотите изменить способ хранения данных? Правите только storage.js. Хотите изменить структуру задачи? Только task.js. Это называется принципом единственной ответственности.

✅ Итоги урока

  • Модули позволяют разбить код на файлы с чёткими границами — что видно снаружи, а что нет
  • Named export: export const name = ... / export function ... — можно несколько на файл
  • Named import: import { name, func } from './file.js' — фигурные скобки обязательны
  • Default export: export default ... — один на файл, без фигурных скобок при импорте
  • Переименование при импорте: import { name as alias } from './file.js'
  • Re-export: export { X } from './other.js' — удобно для index.js-фасадов
  • CommonJS (require/module.exports) — старая Node.js система; ESM (import/export) — современный стандарт
  • В браузере: <script type="module"> включает поддержку ESM

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

Тест: Модули JavaScript

7 вопросов

Named и default импорт

Дан файл utils.js с экспортами. Напишите, какой синтаксис импорта используется для получения именованного экспорта 'add' и для получения default-экспорта из того же файла. Укажите оба варианта одной строкой кода.

Сравнение CommonJS и ESM

Напишите, как выглядит экспорт функции sum в CommonJS (через module.exports) и как выглядит тот же экспорт в ESM (через export). Укажите оба варианта через точку с запятой в одной строке.