document.currentScript: что такое и с чем едят. browser api.. browser api. currentscript.. browser api. currentscript. document.currentscript.. browser api. currentscript. document.currentscript. JavaScript.. browser api. currentscript. document.currentscript. JavaScript. js.. browser api. currentscript. document.currentscript. JavaScript. js. script.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev. Блог компании Timeweb Cloud.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev. Блог компании Timeweb Cloud. Веб-разработка.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev. Блог компании Timeweb Cloud. Веб-разработка. возможности браузера.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev. Блог компании Timeweb Cloud. Веб-разработка. возможности браузера. возможности веба.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev. Блог компании Timeweb Cloud. Веб-разработка. возможности браузера. возможности веба. Программирование.. browser api. currentscript. document.currentscript. JavaScript. js. script. timeweb_статьи_перевод. web api. webdev. Блог компании Timeweb Cloud. Веб-разработка. возможности браузера. возможности веба. Программирование. скрипт.
document.currentScript: что такое и с чем едят - 1

Сначала я недооценил 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 при доступе должен возвращать последнее установленное значение. При создании document currentScript должен быть инициализирован значением 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>."
  );
}

Такая ошибка однозначна и легко воспринимается:

document.currentScript: что такое и с чем едят - 2

В целом это дает понятную и наглядную обратную связь — отличное дополнение к хорошей документации.

Локальность поведения

Эту идею подсказал пользователь 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-канале

document.currentScript: что такое и с чем едят - 3

Автор: aio350

Источник

Rambler's Top100