
Сначала я недооценил document.currentScript
, но оказалось, что он отлично подходит для передачи параметров конфигурации прямо в теги <script>
— и это далеко не все.
Порой я натыкаюсь на давно существующие браузерные API в JavaScript, о которых, по идее, я должен был узнать гораздо раньше. Например, window.screen
или метод CSS.supports()
. К счастью, я понял, что не один такой. Помню, как однажды упомянул window.screen
в посте и получил неожиданно много комментариев от людей, которые тоже впервые о нем слышали. Это меня немного приободрило — я почувствовал себя не таким уж глупым.
Видимо, дело не в том, как давно существует API, а в том, насколько он полезен в реальных задачах. Если window.screen
почти нигде не используется, о нем легко забыть.
Но иногда все же появляется неожиданный шанс применить одну из этих малоизвестных возможностей. Похоже, я как раз нашел такой случай для document.currentScript
— и намерен использовать его по максимуму.
❯ Зачем он нужен
Достаточно просто взглянуть на его API, чтобы понять: он возвращает ссылку на тот элемент <script>
, внутри которого выполняется текущий код:
<script>
console.log("название тега:", document.currentScript.tagName);
// название тега: SCRIPT
console.log(
"элемент script?",
document.currentScript instanceof HTMLScriptElement
);
// элемент script? true
</script>
Поскольку возвращается сам элемент, к его свойствам можно обращаться так же, как и к любому другому DOM-узлу:
<script data-external-key="123urmom" defer>
console.log("внешний ключ:", document.currentScript.dataset.externalKey);
// внешний ключ: 123urmom
if (document.currentScript.defer) {
console.log("скрипт выполняется отложено");
}
// скрипт выполняется отложено
</script>
Все довольно просто. И, что вполне очевидно — поддержка браузеров вообще не проблема. document.currentScript
существует во всех основных браузерах уже больше десяти лет. По меркам веба — это целая геологическая эпоха, за которую успевают образоваться натуральные алмазы.
Для модулей недоступен
Интересная особенность document.currentScript
— он недоступен внутри модулей. Но что любопытно: при попытке обратиться к нему мы получим не undefined
, а null
.
<script type="module">
console.log(document.currentScript);
// null
console.log(document.doesNotExist);
// undefined
</script>
Это предусмотрено спецификацией. Как только создается document
, currentScript
инициализируется значением null
:
АтрибутcurrentScript
при доступе должен возвращать последнее установленное значение. При создании documentcurrentScript
должен быть инициализирован значениемnull
.
Поскольку после синхронного выполнения скрипта значение возвращается к исходному, то при выполнении асинхронного кода также возвращается null
:
<script>
console.log(document.currentScript);
// <script> tag
setTimeout(() => {
console.log(document.currentScript);
// null
}, 1000);
</script>
Исходя из этого, внутри <script type="module">
нет возможности получить текущий тег <script>
. Единственное, что можно сделать — определить, выполняется ли скрипт как модуль, и для этого лучше всего проверять значение на null
(проверка должна выполняться вне асинхронного кода):
function isInModule() {
return document.currentScript === null;
}
Кстати, не стоит проверять import.meta
, даже если делать это внутри try/catch
. Само наличие этого выражения в теге <script>
вызывает ошибку SyntaxError
. Скрипт даже не нужно запускать — ошибка возникает при первом разборе содержимого браузером:
<script>
// При первом парсинге будет выброшена `SyntaxError`
function isInModule() {
try {
return !!import.meta;
} catch (e) {
return false;
}
};
// Также вызывает ошибку
console.log(typeof import?.meta);
</script>
Поскольку у модулей пока нет такой возможности, будет интересно посмотреть, как эту проблему решат в будущем. В спецификации отмечается, что обсуждение все еще ведется:
Этот API утратил популярность среди разработчиков и участников сообщества стандартизации, поскольку он предоставляет глобальный доступ к элементам script и SVG script. Поэтому он недоступен в новых контекстах, таких как выполнение модульных скриптов или скриптов в теневом DOM. В настоящее время ведется работа над новым решением, которое позволит идентифицировать выполняющийся скрипт в таких контекстах без глобального доступа — см. issue #1013.
Кстати, это обсуждение ведется уже давно — с 2016 года, и в нем участвует очень много людей. Пока окончательного решения нет, лучше всего просто получать нужный элемент напрямую:
<script type="module" id="moduleScript">
const scriptTag = document.getElementById("moduleScript");
// Работаем с элементом
</script>
❯ Передача параметров конфигурации
На сайте PicPerf я использую таблицу цен Stripe, которую можно встроить с помощью нативного веб-компонента. Нужно загрузить скрипт, вставить элемент в HTML и задать пару атрибутов:
<script
async
src="https://js.stripe.com/v3/pricing-table.js">
</script>
<stripe-pricing-table
pricing-table-id='prctbl_blahblahblah'
publishable-key="pk_test_blahblahblah"
>
</stripe-pricing-table>
Это работает хорошо, когда имеется доступ к переменным окружения во время рендеринга HTML, но мне хотелось встроить таблицу прямо в Markdown-файл. Markdown отлично поддерживает чистый HTML, но получить доступ к этим значениям не так просто, как использовать import.meta.env
или process.env
. Вместо этого пришлось бы динамически подставлять значения отдельно от разметки страницы.
К сожалению, нельзя отделить процесс рендеринга таблицы от установки ее параметров — нужные значения должны быть доступны при инициализации элемента.
Поэтому мне пришлось вставлять весь элемент целиком (со всеми настройками) с помощью клиентского скрипта. В Markdown я добавил специальный плейсхолдер, а потом подставил туда готовую разметку таблицы:
## My Pricing Table
<div data-pricing-table></div>
<script>
document.querySelectorAll('[data-pricing-table]').forEach(table => {
table.innerHTML = `
<stripe-pricing-table
pricing-table-id="STAY_TUNED"
publishable-key="STANY_TUNED"
client-reference-id="picperf"
></stripe-pricing-table>
`;
})
</script>
На этом этапе мне не хватало только значений атрибутов. Один из вариантов — получить их на сервере и добавить в объект window
, но такой подход выглядит не слишком аккуратным. Мне совсем не по душе разбрасываться глобальными переменными.
А можно было просто…
Если быть откровенным, я мог бы решить эту задачу за каких-то 14 секунд. Сайт PicPerf.io построен на Astro, который предоставляет директиву define:vars
. С ее помощью передать серверные переменные в клиентский скрипт — проще простого:
---
const truth = "Taxation is theft.";
---
<style define:vars={{ truth }}>
console.log(truth);
// Taxation is theft.
</style>
Однако в решении, которое занимает считанные секунды, нет ни веселья, ни материала для статьи :D
К тому же, define:vars
— это довольно специфичный способ решения задачи, который не используется другими платформами и системами управления контентом (с которыми мне приходилось работать).
Задача, которая встречается чаще, чем кажется
В системах управления контентом ограничения зачастую намеренно довольно жесткие. Редактор позволяет настраивать отдельные элементы разметки, но крайне редко — содержимое тегов <script>
. И на это есть веские причины: здесь таится множество потенциальных угроз безопасности.
Кроме того, такие скрипты часто ссылаются на внешние пакеты, используемые другими командами, но при этом требуют настройки. В таких случаях рендеринг значений на сервере прямо внутри скрипта становится невозможным.
<!-- Сторонняя библиотека, но требуется настройка -->
<script src="path/to/shared/signup-form.js"></script>
В подобных ситуациях я часто встречал, что значения настроек передаются через атрибуты data
, сгенерированные на сервере. Мы задаем нужные значения атрибутов, а скрипт самостоятельно их считывает. Такой подход особенно распространен в модульных одностраничных приложениях, где настройки задаются на корневом элементе:
<div
id="app"
data-recaptcha-site-key="{{ siteKey }}"
></div>
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const appNode = document.getElementById('app');
const root = ReactDOM.createRoot(appNode);
root.render(
// Извлекаем значение из атрибута корневого элемента
<App recaptchaSiteKey={appNode.dataset.recaptchaSiteKey} />
);
Догадались, к чему я клоню? Атрибуты данных — это удобный и аккуратный способ передачи значений с сервера на клиент. В примере с SPA единственным, хотя и незначительным, неудобством остается необходимость предварительного получения элемента для доступа к его атрибутам.
Однако в моем случае использовался именно тег <script />
, а не какой-либо другой элемент, и это позволило легко решить данную проблему. Свойство document.currentScript
делает это за нас автоматически:
<script
data-stripe-pricing-table="{{pricingTableId}}"
data-stripe-publishable-key="{{publishableKey}}"
>
const scriptData = document.currentScript.dataset;
document.querySelectorAll('[data-pricing-table]').forEach(table => {
table.innerHTML = `
<stripe-pricing-table
pricing-table-id="${scriptData.stripePricingTable}"
publishable-key="${scriptData.stripePublishableKey}"
client-reference-id="picperf"
></stripe-pricing-table>
`;
})
</script>
Настоящее удовольствие. Никакой магии и проприетарных решений, никаких данных, засоряющих глобальную область видимости. И при этом можно с гордостью заявить в Х (Твиттере), что я "использую нативные возможности". Выигрыш по всем фронтам.
❯ Другие кейсы
Рассмотрим парочку других вариантов использования document.currentScript
.
Рекомендации по установке
Предположим, мы разрабатываем JavaScript-библиотеку, которую необходимо загружать асинхронно. document.currentScript
позволяет легко дать четкую и понятную обратную связь:
<script defer src="./script.js"></script>
// script.js
if (!document.currentScript.async) {
throw new Error("Скрипт должен загружаться асинхронно");
}
// Остальная часть библиотеки
Можно даже установить конкретное правило для размещения тега <script>
на странице — например, чтобы он загружался сразу после открывающего тега <body>
:
const isFirstBodyChild =
document.body.firstElementChild === document.currentScript;
if (!isFirstBodyChild) {
throw new Error(
"Этот скрипт ДОЛЖЕН загружаться сразу после открывающего тега <body>."
);
}
Такая ошибка однозначна и легко воспринимается:

В целом это дает понятную и наглядную обратную связь — отличное дополнение к хорошей документации.
Локальность поведения
Эту идею подсказал пользователь ShotgunPayDay на Reddit. Принцип локальности поведения (Locality of Behavior) гласит: поведение каждого блока кода должно быть очевидным при его проверке (об этом хорошо написал Карсон Гросс). В голову сразу приходят фреймворки с поддержкой однофайловых компонентов — все находится в одном месте и легко читается.
В контексте document.currentScript
это означает, что можно создавать автономные и переносимые части интерфейса просто за счет их совместного расположения. Например, можно сделать так, чтобы любая форма отправлялась асинхронно, просто добавив тег <script>
сразу после нее. Скрипт сможет определить, что ему нужно работать с элементом, находящимся прямо перед тегом <script>
.
// form-submitter.js
const form = document.currentScript.previousElementSibling;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const method = form.method || "POST";
const submitGet = () => fetch(`${form.action}?${params}`, {
method: "GET",
});
const submitPost = () => fetch(form.action, {
method: method,
body: formData,
});
const submit = method === "GET" ? submitGet : submitPost;
const response = await submit();
form.reset();
alert(response.ok ? "Успех" : "Ошибка");
});
Все сводится к тому, чтобы правильно разместить скрипт:
<form action="/endpoint-one" method="POST">
<input type="text" name="firstName"/>
<input type="text" name="lastName"/>
<input type="submit" value="Submit" />
</form>
<script src="form-submitter.js"></script>
<form action="/endpoint-two" method="POST">
<input type="email" name="emailAddress" />
<input type="submit" value="Submit" />
</form>
<script src="form-submitter.js" ></script>
Сомневаюсь, что буду часто использовать такой подход, но хорошо знать, что он существует.
❯ Приятное ощущение
Очень приятно наконец-то понять, зачем нужны некоторые из этих давно существующих, но малоизвестных возможностей веба. Это вызывает у меня уважение к создателям API раннего Интернета — особенно учитывая, как часто им приходится иметь дело с претензиями современных разработчиков. Интересно, что еще я смогу открыть для себя. Возможно, искусственный интеллект уже встроен в спецификацию HTML, а мы просто его еще не обнаружили :D
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩