Сначала я недооценил 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>
Однако в решении, которое занимает считанные секунды, нет ни веселья, ни материала для статьи 😀
К тому же, 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, а мы просто его еще не обнаружили 😀
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
ссылка на оригинал статьи https://habr.com/ru/articles/918092/

Добавить комментарий