Мне было очень грустно слышать, что RxJS нельзя сериализовать в Qwik. Я много вложил в изучение RxJS и полюбил писать реактивный код.

Но потом я нырнул в Qwik, чтобы посмотреть, какая реактивность был возможный. Может быть, что-то вроде хуков React? React не нуждается в RxJS из-за хуков, поэтому, возможно, Qwik будет таким же. Дело в этом, оказывается. Вот видео на YouTube того, как я изучаю Qwik и прихожу к такому выводу:

Но Джей Ларки пила мое видео и хотел помочь мне усерднее заставить RxJS работать в Qwik. Мы пришли к лучшему пониманию того, как это может работать в это видеои в этой статье я хочу обобщить и расширить то, что мы узнали.


Сериализация в Qwik

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

export default component$(() => {
  // Code 1

  useWatch$(({ track }) => {
    // Code 2
  });

  return (
    <button onClick$={() => {
      // Code 3
    }}>Set to 1</button>
  );
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Компилятор Qwik разделит его на 3 фрагмента, содержащих код 1, код 2 и код 3. Код 2 и код 3 могут использовать только то, что сериализуемо из кода 1, а код 1 может использовать только то, что находится вне экспортируемого компонента. Это означает, что они будут вызывать ошибки:

const x = 1; // Need to export

export default component$(() => {
  return <div>{x}</div>; // Error
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

export default component$(() => {
  // Need to move out of component, export:
  const getX = () => 5; // Can't be serialized

  useWatch$(({ track }) => {
    const x = getX(); // Error
  });

return (
    <button onClick$={() => {
      const x = getX(); // Error
    }}>Set to 1</button>
  );
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Но это будет работать:

export default component$(() => {
  const getX = () => 5;
  return <div>{getX()}</div>; // No serialization needed, no error
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

export const getX = () => 5; // Exported

export default component$(() => {
  useWatch$(({ track }) => {
    const x = getX(); // No error
  });

return (
    <button onClick$={() => {
      const x = getX(); // No error
    }}>Set to 1</button>
  );
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

export default component$(() => {
  const x = 5; // Can be serialized

  useWatch$(({ track }) => {
    console.log('x', x); // No error
  });

return (
    <button onClick$={() => {
      console.log('x', x); // No error
    }}>Set to 1</button>
  );
});
Войти в полноэкранный режим

Выйти из полноэкранного режима


Асинхронная сериализация

В идеале приложение Qwik можно приостановить в любое время, перенести в другое место, а затем снова возобновить в том же самом состоянии, в котором оно было до приостановки.

Но Qwik может приостановить только сериализуемое поведение, и это исключает асинхронные вещи. Нам нужно будет найти способ представить асинхронное поведение в виде сериализованного состояния, чтобы сохранить его при паузе и возобновлении. Если мы сможем понять это для простого setTimeoutможет быть, мы сможем понять это и для какого-нибудь RxJS.

Скажем, у нас есть шрифт со смехотворно долгим дребезгом в 3 секунды. Если бы Qwik приостановил приложение через 1,5 секунды, ход выполнения внутреннего тайм-аута debounce был бы потерян, поскольку он не сериализуется как состояние.

Если мы устанавливаем тайм-аут в onKeyUp$ обработчик, я не знаю, как восстановить информацию о тайм-ауте. Это означает, что пользователь может ввести что-то в поиске, и если приложение будет приостановлено и сразу после этого возобновлено, результаты поиска никогда не будут получены.

Кроме того, мы можем установить тайм-аут внутри useWatch$ или же useClientEffect$ крюк. Когда пользователь инициирует событие, мы можем поместить данные события в хранилище Qwik, которое будет сериализовано, чтобы данные события никогда не были потеряны. Затем мы можем обрабатывать любое асинхронное поведение ниже по течению в ловушке. Хуки также позволяют нам определить функцию очистки, которая будет вызываться при сериализации приложения, которую мы могли бы использовать для возобновления асинхронного поведения вместо его сброса.

Но даже если мы найдем способ получить доступ к данным тайм-аута во время тайм-аута, Qwik не даст нам возможности их сохранить. Если мы попытаемся установить состояние во время очистки, оно будет пропущено во время сериализации, а вместо этого вызовет повторный рендеринг. Если это произойдет на сервере, это будет зарегистрировано:

QWIK WARN Can not rerender in server platform
Войти в полноэкранный режим

Выйти из полноэкранного режима

Поэтому мы не можем сериализовать простой тайм-аут.

На данный момент попытка сериализовать весь конвейер RxJS кажется невозможной.


Обещания

Обещания обычно не сериализуемы, но, хотите верьте, хотите нет, в Qwik есть способ их сериализовать. Как это работает и можем ли мы использовать это, чтобы помочь с RxJS?

Qwik во многом похож на React, но имеет очень важное отличие: Qwik выполняет асинхронный рендеринг. Это означает, что вы можете бросить промис в любом месте компонента, и компонент в конечном итоге отобразится с разрешенным промисом. Итак, это:

export default component$(() => {
  const message = new Promise(resolve => {
    setTimeout(() => resolve('Hi'), 1000);
  });

  return (
    <div>{message}</div>
  );
}); 
Войти в полноэкранный режим

Выйти из полноэкранного режима

для рендеринга на сервере потребуется 1 секунда, а затем, когда он будет отправлен клиенту, вы увидите разрешенное значение в отображаемом HTML: <div>Hi</div>

Даже если мы используем обещание не в HTML, а в useWatch$ например, Qwik ожидает его разрешения перед его сериализацией. Таким образом, вы можете сказать, что Qwik не сериализует никакие промисы, а специально разрешает промисы. Это может измениться, когда Qwik реализует неупорядоченную потоковую передачу, но я не очень понимаю это. Если кто-то знает, пожалуйста, прокомментируйте.

Итак, вместо сериализации текущего асинхронного поведения, должны ли мы использовать обещания, чтобы Qwik ждал завершения поведения, а затем сериализовал результат?

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

Хорошо, что в Qwik есть возможность асинхронного рендеринга, но у нас все еще нет способа сериализовать потоки RxJS.


Несериализованный RxJS

Что, если мы просто проигнорируем сериализацию для асинхронного поведения? Если наше приложение отрисовывается на сервере только один раз и его никогда не нужно приостанавливать после того, как оно дойдет до клиента, или если можно полностью отказаться от асинхронных задач, нам никогда не потребуется сериализовать что-либо асинхронное. На практике этого должно быть достаточно для большинства приложений.

Давайте реализуем наш typehead в RxJS, не заморачиваясь с сериализацией, и посмотрим, что у нас получится.

Мы по-прежнему хотели бы, чтобы код RxJS загружался отложенно, так как опережающему вводу не нужно ничего делать, пока пользователь не взаимодействует с ним. Поэтому мы должны избегать useClientEffect$, так как он запускается и загружает код сразу же, как только компонент становится видимым в пользовательском интерфейсе. Мы хотим, чтобы код загружался только тогда, когда пользователь взаимодействует с ним, поэтому мы будем использовать useWatch$.

Поскольку компоненты в Qwik постоянно перерисовываются, как и в React, мы хотим использовать RxJS так же, как мы использовали бы его в React: обрабатывать асинхронную логику, устанавливать состояние в одном месте, а затем позволить приложению синхронно получать нижестоящее состояние без любые RxJS или подписки.

В React нам нужен такой порядок событий:

  1. Компонент начинает рендеринг
  2. Поток RxJS подписан на
  3. Пользовательский ввод
  4. RxJS делает свое дело, устанавливает состояние
  5. Компонент перерисовывает, поддерживает подписку

В Qwik такой порядок нам нужен:

  1. Компонент рендерится на сервере
  2. Приложение сериализовано
  3. HTML передается в браузер
  4. Типы пользователей, поиск сохраняется как состояние в магазине Qwik
  5. Реактивность Qwik загружает и запускает useWatch$ что создает BehaviorSubject для передачи значений из хранилища Qwik и серии цепочек RxJS с ленивой загрузкой с вызовом подписки в конце
  6. Значение обрабатывается потоком, а выходное значение помещается в последнее хранилище Qwik по подписке.
  7. Компонент перерисовывается

Это было сложно реализовать, но вот пример, который использует некоторые утилиты, которые я придумал:

export default component$(() => {
  const search = useSubject('');

  const results = useStream([] as Item[]);
  useWatch$((args) => {
    connect(
      [args, [search], results],
      () => rx(search).pipe(
        debounceTime(500),
        filter((search) => !!search.length),
        distinctUntilChanged(),
        switchMap((search) => fetchItems(search))
      )
    );
  });

  useSubscribe(results);

  return (
    <>
      <input
        style={{ border: "1px solid #08f" }}
        onKeyUp$={(e, t: HTMLInputElement) => next(search, t.value)}
      />
      {results.value.map((item) => (
        <div>{item.label}</div>
      ))}
    </>
  );
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Это обеспечивает точное поведение, которое мы хотим: JavaScript не загружается до тех пор, пока пользователь не введет, после чего загружается поток RxJS и передаются вводимые значения.

Поток RxJS должен быть определен в компоненте внутри useWatch$, потому что useWatch$ должен управлять подпиской на поток, но не мог бы получить к нему доступ, если бы он был передан в качестве параметра служебной функции, поскольку для этого потребуется, чтобы канал серийный. Но определение канала прямо в useWatch$ не требует сериализации. Это делает возможным использование RxJS, но недостатком является то, что мы не можем абстрагировать пару store/effect в пользовательский хук, в который мы просто передаем RxJS, как мы могли бы это сделать в React.

Я определил свой собственный next функция для передачи значений в хранилище, чтобы я мог повторно использовать некоторую логику, объединяющую хранилище Qwik с BehaviorSubject.


Как это работает?

В итоге я поддержал 4 особых поведения:

  1. Пользовательский ввод запускает загрузку RxJS
  2. Наблюдаемый (как и таймер) подписывается независимо, ленивая загрузка нисходящего потока RxJS только тогда, когда он выдает свое первое значение.
  3. Подписка создается внутри useClientEffect$в результате чего вся цепочка RxJS загружается, как только компонент становится видимым.
  4. Существующее хранилище Qwik помещает значения в наблюдаемую ленивую загрузку RxJS.

Чтобы охватить все эти сценарии, мне пришлось проделать немало работы.

Для # 1 мне пришлось объединить магазин Qwik с BehaviorSubject таким образом, чтобы позволить нисходящему потоку useWatch$s для загрузки только при нажатии первого значения.

№2 было не так уж и плохо. Я только что определил удобную функцию, которая подписывается на наблюдаемое и устанавливает его в хранилище Qwik.BehaviorSubject комбинация.

Для № 3 мне пришлось определить activated Таким образом, нетерпеливая подписка может вызвать восходящую цепную реакцию всех наблюдаемых зависимостей на каждом уровне с отложенной загрузкой, чтобы этот уровень мог определять свой поток с точки зрения этих зависимостей. На самом деле это также требовалось для # 1, потому что поток 2-го порядка мог иметь несколько BehaviorSubjects как зависимости, поэтому, если 1-й излучает, для определения потока 2-го порядка нам нужно будет вызвать 2-й BehaviorSubject чтобы создать.

№ 4 тоже был довольно простым. Я просто отслеживаю свойство хранилища Qwik, которое необходимо преобразовать в поток RxJS внутри useWatch$ и позвони next на useSubject сохранить с обновленным значением.

Я опубликую исходный код этих утилит, когда буду писать следующую статью. А пока взгляните на видео YouTube для этой статьи если вы заинтересованы в том, чтобы увидеть немного больше деталей. (Я еще не опубликовал видео. Подпишитесь на мой канал на YouTube, и оно должно появиться в течение дня или двух 🙂

В конце концов я попытаюсь также привлечь Redux Devtools, который я реализую в адаптировать состояние.


Но должны ли мы?

Меня не полностью удовлетворил API, который я получил. Возможно, нам стоит просто создать экосистему реактивных пользовательских хуков вместо использования RxJS в Qwik. Мы должны быть в состоянии сделать это более минимальным способом, чем это решение RxJS. Можно дублировать все поведение RxJS, как в React.

Я не знаю ответа. Но приятно знать, что по крайней мере RxJS — это вариант.