QRL

QRL (Qwik URL) - это особая форма URL, которую Qwik использует для ленивой загрузки содержимого.

QRL-ы:

  • Это форматированные специальным образом URL-адреса, которые вставляются в HTML в виде атрибутов, чтобы указать Qwik, откуда должны быть загружены обработчики кода;
  • Указывают на фрагменты JavaScript-кода для ленивой загрузки;
  • Содержат имя символа, который необходимо извлечь из фрагмента;
  • Могут содержать ссылки на объекты с лексической областью (переменные, захваченные из замыканий);
  • Если QRL относительный, то для разрешения полного пути используйте q:base.

QRL-кодирование

./path/to/chunk.js#SymbolName

В своей простейшей форме QRL содержит URL-адрес (вроде ./path/to/chunk.js), который браузер может использовать для ленивой загрузки ресурса, и SymbolName для извлечения из лениво загруженного чанка.

Qwik использует q:base для преобразования QRL в абсолютный URL-адрес, если URL-адрес является относительным (если атрибут q:base отсутствует, то в качестве базы используется document.baseURI).

Кодирование захваченных переменных с лексической областью видимости

QRL-адреса также могут восстанавливать переменные с лексической областью видимости. В этом случае переменные кодируются в конце QRL в виде массива индексов атрибута q:obj.

./path/to/chunk.js#SymbolName[0,1]

Массив используется функцией useLexicalScope() для восстановления значений переменных.

Пример

Давайте рассмотрим пример того, как части QRL связаны друг с другом.

Разработчик пишет код простого компонента.

export const Counter = component$((props: { step: number }) => {
  const count = useSignal(0);
 
  return <button onClick$={() => (count.value += props.step || 1)}>{count.value}</button>;
});

Оптимизатор разбивает приведенный выше код на части следующим образом:

const Counter = component(qrl('./chunk-a.js', 'Counter_onMount'));
chunk-a.js
export const Counter_onMount = (props) => {
  const count = useSignal(0);
  return qrl('./chunk-b.js', 'Counter_onRender', [count, props]);
};
chunk-b.js
const Counter_onRender = () => {
  const [count, props] = useLexicalScope();
  return (
    <button onClick$={qrl('./chunk-c.js', 'Counter_onClick', [count, props])}>{count.value}</button>
  );
};
chunk-c.js
const Counter_onClick = () => {
  const [count, props] = useLexicalScope();
  return (count.value += props.step || 1);
};

Рендер HTML

Предположим, текущий маршрут - http://localhost/index.html

После выполнения приведённого выше кода получается такой HTML:

<html>
  <body q:base="/build/">
    <button q:obj="456, 123" on:click="./chunk-c.js#Counter_onClick[0,1]">0</button>
    <script>
      /*Qwikloader script*/
    </script>
    <script type="qwik/json">
      {...json...}
    </script>
  </body>
</html>

Главное, на что следует обратить внимание, это атрибут on:click. Этот атрибут считывается Qwikloader-ом, когда пользователь нажимает на кнопку.

  1. HTML загружается в браузер, и Qwikloader регистрирует глобальный слушатель click. Никакой другой JavaScript не загружается/выполняется в этот момент.
  2. Пользователь кликает на <button>. Это вызывает событие click, которое всплывает и обрабатывается Qwikloader-ом.
  3. Qwikloader прослеживает путь всплытия события и ищет атрибут on:click, который он находит на <button>.
  4. Теперь Qwikloader пытается загрузить соответствующий чанк. Для этого Qwikloader должен разрешить относительный путь ./chunk-c.js. Он использует эти значения для построения абсолютного пути, начинающегося в <button> и идущего к документу.
    • on:click="./chunk-c.js#Counter_onClick[0,1]"
    • <body q:base="/build/">
    • document.baseURI = "http://localhost/index.html"
    • В результате получается абсолютный URL http://localhost/build/chunk-c.js, по которому Qwikloader забирает данные.
  5. Qwikloader теперь получает ссылку на функцию Counter_onClick из http://localhost/build/chunk-c.js и вызывает её.
    const Counter_onClick = () => {
      const [count, props] = useLexicalScope();
      return (count.value += props.step || 1);
    };
  6. В этот момент выполнение передается из Qwikloader-а в лениво загруженный чанк. Это делается для того, чтобы Qwikloader был как можно меньше, поскольку он встраивается в HTML.
  7. useLexicalScope импортируется из @builder.io/qwik и отвечает за получение count и props. const [count, props] = useLexicalScope();
  8. Происходит парсинг JSON <script type="qwik/json">{...json...}</script> и распределение десериализованных объектов по атрибуту q:obj. В нашем случае:
    • <div q:id="123" q:obj="456" q:host> получает объект с идентификатором 123. Это будет count, созданный в функции Counter_onMount.
    • <button q:obj="456, 123" получает count, а также ссылку на <div q:id="457">.
  9. После десериализации qwik/json, useLexicalScope может использовать массив QRL [0,1] для поиска в q:obj="456, 123", откуда получает объекты с id 456 и 123, являющиеся параметрами <div q:id="123" q:obj="456" q:host>, а также store из функции Counter_onMount.

ПРИМЕЧАНИЕ: По соображениям производительности q:obj и <script type="qwik/json"> обновляются только при десериализации приложения в HTML. Когда приложение работает, эти атрибуты могут иметь устаревшие значения.

Почему просто не динамический import()?

Браузеры уже имеют механизм динамического импорта, доступный через import(). Почему бы не использовать его вместо того, чтобы изобретать новый формат QRL?

На это есть несколько причин:

  1. Для работы Qwik необходимо, чтобы QRL были сериализованы в HTML. Это проблематично для динамического import(), поскольку нет простого способа получить относительный URL из выражения import('./some-path.js'), чтобы его можно было вставить в HTML.
  2. Динамический импорт имеет дело с чанками, но у него нет механизма для ссылки на конкретный фрагмент в чанке.
  3. Динамический импорт имеет относительный путь к файлу, который импортируется. Это проблема, потому что когда относительный путь помещается в HTML, он теряет контекст, определяющий, что относительно чего. Когда фреймворк считывает путь из HTML и пытается импортировать его как import(element.getAttribute('on:click')), фреймворк становится контекстом для разрешения относительного пути. Это проблема, поскольку исходный контекст перед сериализацией в HTML был другим.
  4. QRL кодируют информацию о переменных с лексической областью действия, которые были захвачены в замыкании и должны быть восстановлены.
  5. Динамический импорт требует, чтобы разработчик написал import('./file-a.js'), что означает, что разработчик отвечает за определение границ отложенной загрузки. Это ограничивает возможности инструментария для автоматизированного перемещения кода.

Из-за вышеуказанных различий Qwik вводит QRL-адреса как механизм для ленивой загрузки замыканий в приложение Qwik.

Дополнительно

Участники

Спасибо всем участникам, которые помогли сделать эту документацию лучше!

  • ahhshm
  • manucorporat
  • adamdbradley
  • literalpie
  • the-r3aper7
  • mhevery
  • wtlin1228
  • bebraw
  • mrhoodz