HTML / DOM и события
Как подключить JavaScript к HTML и когда выполняется код
Подключение JavaScript-модулей, defer и async, работа с DOM, события и безопасная организация клиентского кода.
Содержание статьи
HTML создаёт структуру страницы, CSS оформляет её, а JavaScript добавляет поведение. Браузер разбирает разметку и строит DOM — объектное представление документа. Скрипт находит элементы DOM, подписывается на события и меняет состояние интерфейса.
Для проектного кода предпочтителен внешний ES-модуль:
<script type="module" src="/assets/js/main.js"></script>Модуль выполняется после разбора HTML, работает в строгом режиме и поддерживает import/export.
Первый интерактивный пример
<button type="button" data-counter>Нажатий: <span>0</span></button><script type="module" src="/assets/js/counter.js"></script>const button = document.querySelector('[data-counter]');if (button) { const value = button.querySelector('span'); let count = 0; button.addEventListener('click', () => { count += 1; value.textContent = String(count); });}Кнопка остаётся настоящей кнопкой, поэтому работает с мышью, клавиатурой и вспомогательными технологиями. JavaScript отвечает только за изменение счётчика.
Почему внешний файл удобнее
Скрипт внутри HTML допустим для короткой демонстрации, но в проекте внешний модуль:
- не смешивает разметку и поведение;
- переиспользуется на нескольких страницах;
- кешируется браузером;
- разбивается на небольшие модули;
- лучше сочетается со строгой Content Security Policy.
Не используйте inline-обработчики:
<!-- Не рекомендуется --><button onclick="openMenu()">Меню</button>Подписка через addEventListener() позволяет нескольким модулям реагировать на событие и не создаёт глобальные функции.
Обычный script, defer и async
Скрипт без атрибутов
<script src="/assets/js/legacy.js"></script>Когда парсер встречает такой тег, загрузка и выполнение могут остановить дальнейший разбор документа. Если скрипт находится в <head> и обращается к ещё не созданным элементам, querySelector() вернёт null.
defer
<script defer src="/assets/js/legacy.js"></script>Файл загружается параллельно и выполняется после разбора HTML. Несколько deferred-скриптов сохраняют порядок подключения. Атрибут предназначен для классических внешних скриптов.
async
<script async src="https://example.com/analytics.js"></script>Скрипт выполняется сразу после загрузки, порядок относительно других файлов не гарантирован. Это подходит для независимого кода, который не ждёт DOM и не зависит от соседних скриптов, например некоторых аналитических интеграций.
type="module"
Модули по умолчанию ведут себя как deferred-скрипты. Для них не нужно добавлять defer.
<script type="module" src="/assets/js/main.js"></script>У модулей собственная область видимости. Переменная верхнего уровня не становится свойством window.
Разделение кода на модули
// format-price.jsexport function formatPrice(value) { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', }).format(value);}// main.jsimport { formatPrice } from './format-price.js';const price = document.querySelector('[data-price]');if (price) { const value = Number(price.dataset.price); if (Number.isFinite(value)) { price.textContent = formatPrice(value); }}В браузере локальные импорты требуют точного относительного пути и обычно расширения .js. Открывайте проект через HTTP-сервер, а не двойным кликом по файлу: политика модулей и запросов для file:// отличается.
Нужно ли ждать DOMContentLoaded
Классический скрипт в <head> без defer может выполниться слишком рано. ES-модуль и deferred-скрипт запускаются после построения документа, поэтому дополнительный обработчик обычно не нужен.
Если код загружается другим способом и момент выполнения неизвестен, можно проверить состояние документа:
function init() { // Поиск элементов и подписка на события.}if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true });} else { init();}События и поведение по умолчанию
Событие передаёт объект с информацией об источнике. Не отменяйте поведение браузера без причины.
const form = document.querySelector('[data-search-form]');form?.addEventListener('submit', (event) => { const query = new FormData(form).get('q'); if (typeof query !== 'string' || query.trim() === '') { event.preventDefault(); form.querySelector('[name="q"]')?.focus(); }});Если JavaScript не загрузится, обычная HTML-форма всё равно должна выполнить базовый сценарий. Такой подход называется progressive enhancement.
Асинхронные операции
Сетевой запрос не блокирует весь интерфейс. await приостанавливает только текущую async-функцию:
async function loadProfile() { const response = await fetch('/api/profile', { headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json();}try { const profile = await loadProfile(); console.log(profile);} catch (error) { console.error('Не удалось загрузить профиль', error);}Даже при клиентской проверке сервер остаётся источником истины для прав, валидации и сохранения данных.
Частые ошибки
- скрипт запускается до появления нужного элемента;
- селектор может вернуть
null, но код сразу обращается к свойству; - обработчики записаны в
onclickи зависят от глобальных функций; asyncиспользуют для файлов, которые зависят друг от друга;- пользовательский текст вставляют через
innerHTMLвместоtextContent; - интерфейс полностью перестаёт работать без JavaScript;
- ошибки
fetch()не обрабатываются и пользователь не получает обратную связь.
Что запомнить
Подключайте проектный JavaScript как внешний ES-модуль, работайте с нативными элементами и событиями через addEventListener(). Учитывайте момент выполнения, проверяйте результат поиска DOM-элементов, безопасно вставляйте текст и сохраняйте базовый сценарий работоспособным без скрипта.