Знак доллара $

Qwik разбивает ваше приложение на множество небольших фрагментов, которые мы называем символами. Компонент может быть разбит на множество символов, символ меньше компонента. Это разбиение выполняется с помощью Оптимизатора Qwik.

Окончание $ используется для того, чтобы сигнализировать как оптимизатору, так и разработчику, когда происходит это преобразование. Как разработчик, вы должны понимать, что особые правила проеобразования применяются всякий раз, когда вы видите $ (но не весь допустимый JavaScript является допустимым преобразованием Оптимизатора Qwik).

Последствия для этапа компиляции

Оптимизатор запускается как плагин Vite в процессе сборки бандла. Цель оптимизатора - разбить приложение на множество небольших, лениво-загружаемых фрагментов. Оптимизатор перемещает выражения (обычно функции) в новые файлы и оставляет на их месте ссылку, указывающую на место, куда было перемещено выражение.

$ указывает оптимизатору, какие функции извлечь в отдельный файл, а какие оставить нетронутыми. Оптимизатор не ведёт внутренний список магических функций, вместо этого он полагается исключительно на суффикс $, чтобы определить, какие функции преобразовать. Эта система расширяема, и разработчики могут создавать свои собственные $-функции, например myCustomFunction$().

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  console.log('рендер');
  return <button onClick$={() => console.log('привет')}>Привет, Qwik</button>;
});

Компонент выше разделён на несколько чанков благодаря синтаксису $:

app.js
import { componentQrl, qrl } from '@builder.io/qwik';
 
const App = /*#__PURE__*/ componentQrl(
  qrl(() => import('./app_component_akbu84a8zes.js'), 'App_component_AkbU84a8zes')
);
 
export { App };
app_component_akbu84a8zes.js
import { jsx as _jsx } from '@builder.io/qwik/jsx-runtime';
import { qrl } from '@builder.io/qwik';
export const App_component_AkbU84a8zes = () => {
  console.log('рендер');
  return /*#__PURE__*/ _jsx('p', {
    onClick$: qrl(
      () => import('./app_component_p_onclick_01pegc10cpw'),
      'App_component_p_onClick_01pEgC10cpw'
    ),
    children: 'Привет, Qwik',
  });
};
app_component_p_onclick_01pegc10cpw.js
export const App_component_p_onClick_01pEgC10cpw = () => console.log('привет');

Правила

Оптимизатор использует $ в качестве сигнала для извлечения кода. Разработчик должен понимать, что извлечение связано с некоторыми ограничениями, и поэтому всякий раз, когда встречается $, применяются специальные правила (не весь допустимый код JavaScript является допустимым кодом для оптимизатора).

Худший вид магии кода - тот, который не виден для разработчика.

Разрешённые выражения

Первый аргумент любой функции, заканчивающейся на $, имеет определённые ограничения:

Литералы без локальных идентификаторов

const bar = 'bar';
const foo = 'foo';
 
// Недопустимые выражения
foo$({ value: bar }); // содержит локальный идентификатор "bar"
foo$(`Hello, ${bar}`); // содержит локальный идентификатор "bar"
foo$(count + 1); // содержит локальный идентификатор "count"
foo$(foo); // foo не экспортируется, поэтому его нельзя импортировать
 
// Допустимые выражения
foo$(`Hello, bar`); // строковый литерал без локальных идентификаторов
foo$({ value: 'stuff' }); // литерал объекта без локальных идентификаторов
foo$(1 + 3); // выражение без локальных идентификаторов

Импортируемые идентификаторы

// Неправильно
const foo = 'foo';
foo$(foo); // foo не экспортируется, поэтому его нельзя импортировать
 
// Правильно
export const bar = 'bar';
foo$(bar);
 
// Правильно
import { bar } from './bar';
foo$(bar);

Замыкания

Для замыканий правила мягче, и можно ссылаться на локальные идентификаторы и захватывать их.

ПРАВИЛО: Если функция лексически захватывает переменную (или параметр), эта переменная должна быть:

  1. const и
  2. значение должно быть сериализуемым
Захваченные переменные должны быть объявлены как const.

Неправильно

component$(() => {
  let foo = 'value'; // переменная не объявлена как `const`
  return <div onClick$={() => console.log(foo)}/>
});

Правильно

component$(() => {
  const foo = 'значение';
  return <div onClick$={() => console.log(foo)}/>
});
Локальные захваченные переменные должны быть сериализуемыми
// Неправильно
component$(() => {
  const foo = new MyCustomClass(12); // MyCustomClass не может быть сериализован
  return <div onClick$={() => console.log(foo)}/>
});
 
// Правильно
component$(() => {
  const foo = { data: 12 };
  return <div onClick$={() => console.log(foo)}/>
});
Объявленные переменные модуля должны быть импортируемыми

Если функция, извлекаемая оптимизатором, ссылается на символ верхнего уровня, этот символ должен быть либо импортирован, либо экспортирован.

// Неправильно
const foo = new MyCustomClass(12);
component$(() => {
  // Foo объявлен на уровне модуля, но он не экспортирован
  console.log(foo);
});
 
// Правильно
export const foo = new MyCustomClass(12);
component$(() => {
  console.log(foo);
});
 
// Правильно
import { foo } from './foo';
component$(() => {
  console.log(foo);
});

Погружаемся глубже

Давайте рассмотрим гипотетическую проблему действия на событие прокрутки. У вас может возникнуть соблазн написать код следующим образом:

function onScroll(fn: () => void) {
  document.addEventListener('scroll', fn);
}
onScroll(() => alert('scroll'));

Проблема этого подхода заключается в том, что обработчик события загружается сразу, даже если событие прокрутки никогда не произойдёт. Необходим способ ссылаться на код с возможностью ленивой загрузки.

Разработчик может написать:

export scrollHandler = () => alert('scroll');
 
onScroll(() => (await import('./some-chunk')).scrollHandler());

Это работает, но требует больших усилий. Разработчик отвечает за размещение кода в другом файле и жёсткое кодирование имени чанка. Вместо этого мы используем Оптимизатор, который выполняет эту работу за нас автоматически. Но нам нужен способ сообщить Оптимизатору, что мы хотим выполнить такой рефакторинг. Для этого мы используем $() в качестве функции-маркера.

function onScroll(fnQrl: QRL<() => void>) {
  document.addEventListener('scroll', async () => {
    const fn = await fnQrl.resolve();
    fn();
  });
}
 
onScroll($(() => alert('scroll')));

Оптимизатор сгенерирует:

onScroll(qrl('./chunk-a.js', 'onScroll_1'));
chunk-a.js
export const onScroll_1 = () => alert('scroll');
  1. Всё, что нужно было сделать разработчику, это обернуть функцию в $() и этим подать сигнал оптимизатору, что функция должна быть перемещена в новый файл и, следовательно, лениво загружена.
  2. Функция onScroll должна быть реализована несколько иначе, поскольку она должна учитывать тот факт, что QRL функции должен быть загружен перед её использованием. На практике использование qImport редко встречается в приложениях Qwik, поскольку фреймворк Qwik предоставляет API более высокого уровня, которые редко ожидают, что разработчик будет работать с qImport напрямую.

Однако обёртывание кода в $() несколько неудобно. По этой причине Оптимизатор неявно обёртывает первый аргумент любого вызова функции, который заканчивается на $ (кроме того, можно использовать implicit$FirstArg() для автоматического выполнения обёртывания и согласования типов функции, принимающей QRL).

const onScroll$ = implicit$FirstArg(onScrollQrl);
 
onScroll$(() => alert('scroll'));

Теперь у разработчика есть очень простой синтаксис для выражения того, что определённая функция должна быть лениво загружена.

Извлечение символов

Предположим, что у вас есть такой код:

export const MyComp = component$(() => {
  /* определение компонента */
});

Оптимизатор разбивает код на два файла:

Оригинальный файл:

const MyComp = component(qrl('./chunk-a.js', 'MyComp_onMount'));
chunk-a.js
export const MyComp_onMount = () => {
  /* определение компонента */
};

В результате работы Оптимизатора метод onMount компонента MyComp был извлечён в новый файл. Это приносит несколько преимуществ:

  • Родительский компонент может ссылаться на MyComp не вдаваясь в детали реализации MyComp;
  • Теперь у приложения больше точек входа, что даёт сборщику больше способов разбить кодовую базу на части.

Захват лексическй области

Оптимизатор извлекает выражения (обычно функции) в новые файлы и оставляет на их месте QRL, указывающий на место ленивой загрузки.

Давайте рассмотрим простой случай:

export const Greeter = component$(() => {
  return <div>Привет, мир!</div>;
});

это приведёт к:

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
chunk-a.js
const Greeter_onMount = () => {
  return qrl('./chunk-b.js', 'Greeter_onRender');
};
chunk-b.js
const Greeter_onRender = () => <span>Привет, мир!</span>;

Вышесказанное относится к простым случаям, когда извлечённое замыкание функции не захватывает никаких переменных. Рассмотрим более сложный случай, когда извлечённое замыкание лексически захватывает переменные.

export const Greeter = component$((props: { name: string }) => {
  const salutation = 'Привет';
 
  return (
    <div>
      {salutation} {props.name}!
    </div>
  );
});

Обычный способ извлечения функций не сработает.

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
chunk-a.js
const Greeter_onMount = (props) => {
  const salutation = 'Привет';
  return qrl('./chunk-b.js', 'Greeter_onRender');
};
chunk-b.js
const Greeter_onRender = () => (
  <div>
    {salutation} {props.name}!
  </div>
);

Проблему можно увидеть в файле chunk-b.js. Извлечённая функция ссылается на salutation и props, которые больше не находятся в лексической области видимости функции. По этой причине сгенерированный код должен быть немного другим.

chunk-a.js
const Greeter_onMount = (props) => {
  const salutation = 'Привет';
  return qrl('./chunk-b.js', 'Greeter_onRender', [salutation, props]);
};
chunk-b.js
const Greeter_onRender = () => {
  const [salutation, props] = useLexicalScope();
 
  return (
    <div>
      {salutation} {props.name}!
    </div>
  );
};

Обратите внимание на два изменения:

  1. В QRL функции Greeter_onMount теперь хранится salutation и props. Это сохранение выполняет роль захвата констант внутри замыканий.
  2. Сгенерированное замыкание Greeter_onRender теперь имеет преамбулу, которая восстанавливает salutation и props (const [salutation, props] = useLexicalScope()).

Способность оптимизатора (и среды выполнения Qwik) захватывать константы лексической области значительно увеличивает количество функций, которые могут быть извлечены в лениво загружаемые ресурсы. Это мощный инструмент для разбиения сложных приложений на более мелкие, легко загружаемые фрагменты.

Участники

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

  • the-r3aper7
  • manucorporat
  • adamdbradley
  • saikatdas0790
  • anthonycaron
  • ubmit
  • literalpie
  • forresst
  • mhevery
  • AnthonyPAlicea
  • zanettin
  • mrhoodz
  • thejackshelton
  • hamatoyogi