Модули, npm и сборка

Зачем:Один длинный скрипт быстро превращается в кашу. После урока вы разбиваете код на файлы с помощью модулей ES, читаете package.json, понимаете, что делает сборщик и зачем беречь зависимости.

25 минут

Озвучено синтезом речи (Qwen3-TTS), голос не принадлежит человеку.

Модуль ES — файл JavaScript, который отдаёт свои имена через export и берёт чужие через import. npm ставит пакеты, а сборщик собирает модули для страницы.

Когда скрипт перестаёт помещаться в голове

На вашей странице уже четыре скрипта: сводка, быстрый переход, меню с поиском, загрузка данных. Они лежат подряд внутри одного HTML-файла, и это ещё терпимо. Теперь представьте сайт на двадцати страницах и сорока скриптах. Какой из них объявил переменную items? Какой от какого зависит? Что сломается, если поменять порядок тегов <script>? В начале 2000-х сайты так и жили: один огромный файл или десяток файлов, подключённых в строгом порядке, а у всех скриптов общее пространство имён. Достаточно было двум файлам назвать переменную одинаково, чтобы один тихо затёр другой.

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

Как к этому шли

Решений было несколько. Сначала код заворачивали в функцию, которая тут же вызывала сама себя, и получали личную область видимости (приём называли IIFE). В 2009 году в серверном JavaScript, который называется Node.js, появилась система модулей CommonJS с командой require, а для браузеров придумали AMD и другие. А чтобы всё это работало в браузере, появились сборщики: Browserify и webpack (2011–2012), Rollup, Parcel, а потом esbuild и Vite. В 2015 году сам язык получил собственные модули, модули ES, и к 2018 году их понимали все основные браузеры. С тех пор для маленьких сайтов сборщик нужен не всегда.

Модули ES

Файл-модуль выставляет наружу то, что помечено словом export, а остальное остаётся внутри:

// data.js
export const items = [ /* … */ ];

Другой файл берёт нужное через import:

// render.js
import { items } from './data.js';

export function renderCards(container) { /* … */ }

Подключают главный файл одной строкой, и браузер сам пойдёт по цепочке импортов:

<script type="module" src="main.js"></script>

Правила, о которые спотыкаются в первый раз:

  • путь начинается с ./ или ../, и расширение .js пишется обязательно: браузер не угадывает, что вы имели в виду;
  • каждый модуль выполняется один раз, сколько бы файлов его ни импортировало, и у него своя область видимости, так что items из одного модуля не конфликтует с items из другого;
  • модуль выполняется после разбора страницы, как со скриптом type="module" в наших уроках;
  • нужен сервер: если открыть страницу двойным щелчком (адрес file://), браузер не позволит модулям загрузить друг друга. Любой локальный сервер подойдёт, например npx serve . или python3 -m http.server.

Есть и динамический импорт, import('./big.js') с промисом на выходе: он загружает модуль, только когда он понадобился, например код редактора, открывающегося по кнопке.

Непривычно выглядит другое: запись import x from 'lodash', без ./, в браузере сама по себе не работает. Браузер не знает, где искать пакет по имени. Для этого нужен либо сборщик, который найдёт пакет в node_modules и положит его в бандл, либо карта импортов (importmap), где вы сами пропишете адрес.

Проверьте себя: почему страница с модулями не работает, если открыть её двойным щелчком?

Браузер не отдаёт модулям файлы по адресу file://. Страницу надо открыть через локальный сервер.

npm: чужой код за одну команду

Чтобы не писать всё самому, разработчики делятся кодом через пакеты. Главный склад пакетов называется npm (его же так называют и программа для установки). Любой проект начинается с файла package.json, паспорта проекта:

{
  "name": "moy-sait",
  "private": true,
  "type": "module",
  "scripts": { "start": "npx serve ." },
  "dependencies": { "dates-lib": "^1.4.2" }
}

В нём имя проекта, пометка "type": "module" (файлы считаются модулями), команды в scripts (запускаются как npm run start) и список зависимостей. Команда npm install скачает их в папку node_modules, а файл package-lock.json запишет точные версии, чтобы у вас и у коллеги всё совпало.

Версии записывают по договорённости semver: три числа мажор.минор.исправление. Исправление чинит ошибки, минор добавляет возможности без поломок, а мажор ломает совместимость. Значок ^ в ^1.4.2 разрешает обновляться внутри мажорной версии: 1.5.0 подойдёт, а 2.0.0 нет.

Сборщик

Сборщик (bundler) идёт по цепочке import, собирает всё, что нужно странице, в один или несколько файлов и делает с ними полезные вещи: уменьшает (минификация: убирает пробелы и сокращает имена), выбрасывает то, что никто не использовал (tree shaking, «встряхивание дерева»), переводит TypeScript, JSX и другие форматы в обычный JavaScript и держит локальный сервер с обновлением страницы при каждой правке. Сегодня самый популярный в новых проектах Vite, а под капотом многих сборщиков лежит esbuild. Попробуйте на схеме: подключите большой пакет и посмотрите, что делает с ним каждая из двух операций:

Из файлов в бандл
  • main.js 1.2 кБ импортирует: render.js, data.js, form.js
  • render.js 2.1 кБ импортирует: data.js
  • form.js 1.8 кБ импортирует: dates-lib
  • data.js 1.5 кБ ничего не импортирует
  • dates-lib 72.0 кБ внешний пакет: 40 функций, а форме нужна одна

Исходники: 78.6 кБ в 5 файлах. Браузер загружал бы их по одному, каждый со своими импортами.

Цифры условные: они показывают порядок величин, а не измерение. Настоящий размер вам покажет сборщик и вкладка «Сеть».

Нужен ли сборщик вашему сайту? Для маленького сайта на чистых модулях без внешних пакетов нет: браузер справится сам, а HTTP/2 давно снял прежнюю проблему «много маленьких файлов». Сборщик становится нужен, когда вы подключаете пакеты из npm, пишете на TypeScript или берёте фреймворк (об этом в следующих уроках).

У каждой зависимости есть цена

Пакет подключается одной строкой, и поэтому подключают много. Но каждый пакет это:

  • вес на странице, который скачает посетитель, в том числе на слабом телефоне;
  • чужой код, который надо обновлять и в котором находят уязвимости (команда npm audit сообщает об известных);
  • зависимость от чужих решений. В марте 2016 года автор крошечного пакета left-pad (одиннадцать строк, дополняющих строку пробелами) удалил его из реестра, и по всему миру сломались тысячи сборок, потому что на него опирались другие пакеты. А в 2018 году в популярный пакет event-stream через новую зависимость попал вредоносный код. Это не повод бояться npm, а повод спросить себя перед каждой установкой: сколько этот пакет экономит и сколько стоит?

Хорошее правило: пакет оправдан, если заменяет много работы и его нет смысла писать самим (например, разбор дат с часовыми поясами). Три строки, которые легко написать самому, пакетом не подключают.

Практика

Вы на сайте эпохи 2019 года. Скрипты разрослись, пора раскладывать по файлам. Песочница не умеет подключать несколько файлов, поэтому модули вы напишете текстом, как будто они лежат в файлах: в блоках pre с названиями data.js, render.js, main.js и package.json. Внутри pre знак < пишется как <, остальной код без изменений.

Потом сделайте то, что в песочнице нельзя, и проверьте у себя. Создайте на компьютере папку, положите в неё index.html с тегом <script type="module" src="main.js"></script> и три файла из блоков. Запустите в этой папке npx serve . (понадобится Node.js) или python3 -m http.server и откройте показанный адрес. Если в консоли красная ошибка, прочитайте её: чаще всего это забытое .js в пути или file:// вместо адреса сервера.

В конце напишите, какие внешние пакеты нужны проекту и сколько они весят. Для вашего сайта, вероятно, никаких: всё сделано на чистом JavaScript. Это тоже ответ, и хороший.

Что было не так раньше

Скрипты склеивали вручную или командами сборки (Grunt, Gulp), общие имена хранили в одном глобальном объекте (window.MyApp = {}), порядок тегов <script> был частью программы, и никто не знал точно, какой файл от какого зависит. Для каждого плагина jQuery надо было помнить, что он требует jQuery раньше себя. Сегодня эти зависимости записаны в самом коде, в строках import, и их видят и люди, и сборщик, и редактор.

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

Практика

Готовлю вашу страницу…

Тест

Вопрос 1 из 6

Вы сохранили index.html с модулем на компьютер и открыли его двойным щелчком (адрес file://…). Скрипт не работает. Почему?

Один ответ

Карточки

Эти карточки попадут в повторение, когда вы завершите урок.

  • Как устроены модули ES?

    Ответ

    Файл экспортирует значения (export), другой берёт их (import { a } from "./x.js"). У модуля своя область видимости, он выполняется один раз и после разбора страницы. Подключается через <script type="module">.

  • Что такое npm и package.json?

    Ответ

    npm: менеджер пакетов и реестр чужого кода. package.json: паспорт проекта с именем, скриптами и списком зависимостей; сами пакеты скачиваются в node_modules.

  • Что значат числа версии 2.5.1 и значок ^?

    Ответ

    Мажор.минор.исправление. Мажор меняют при поломке совместимости, минор при новых возможностях, исправление при починке. ^2.5.1 разрешает обновления до 3.0.0, не включая его.

  • Что делает сборщик и нужен ли он всегда?

    Ответ

    Собирает граф import в файлы, минифицирует, выбрасывает лишнее, переводит TypeScript и JSX. Для маленького сайта на чистых модулях он не обязателен; нужен, когда появляются пакеты, TypeScript или фреймворк.

  • Какую цену имеет каждая зависимость?

    Ответ

    Вес для посетителя, обновления и уязвимости, зависимость от чужих решений (левый пакет может исчезнуть или измениться). Подключайте, если пакет экономит больше, чем стоит.

Источники

Дальше

Теперь вы знаете: модули ES, npm и сборка в общих чертах.