server$()

server$() позволяет вам создать функцию, которая всегда будет выполняться на сервере, что делает её отличным местом для доступа к БД или выполнения действий только на сервере.

server$ - это форма механизма RPC (Remote Procedure Call) между клиентом и сервером, как и традиционная конечная точка HTTP, но сильно типизированная благодаря TypeScript, и более простая в обслуживании.

Ваша новая функция будет иметь следующую сигнатуру:

([AbortSignal, ] ...): Promise<T>

Сигнал AbortSignal является необязательным и позволяет вам отменить давно выполняющийся запрос, прервав соединение.

Обратите внимание, что в зависимости от среды исполнения вашего сервера, функция на сервере может завершиться или не завершиться немедленно, так как это зависит от того, как клиентские отключения обрабатываются в текущем времени выполнения.

import { component$, useSignal } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
// Обернув функцию в `server$()`, мы помечаем её, как всегда
// выполняющуюся на сервере. Это одна из форм механизма RPC.
const serverGreeter = server$((firstName: string, lastName: string) => {
  const greeting = `Привет ${firstName} ${lastName}`;
  console.log('Вывод на сервере', greeting);
  return greeting;
});
 
export default component$(() => {
  const firstName = useSignal('');
  const lastName = useSignal('');
 
  return (
    <section>
      <label>Имя: <input bind:value={firstName} /></label>
      <label>Фамилия: <input bind:value={lastName} /></label>
 
      <button
        onClick$={async () => {
          const greeting = await serverGreeter(firstName.value, lastName.value);
          alert(greeting);
        }}
      >
        приветствие
      </button>
    </section>
  );
});

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

// Обратите внимание, что обёрнутая функция объявлена как `async function`.
const addUser = server$(async function(id: number, fullName: string, address: Address) {
  // `this` - это объект `RequestEvent`, который содержит
  // HTTP-заголовки, куки и переменные окружения.
  const db = createClient(this.env.get('DB_KEY'));
  if (this.cookie.get('user-session')) {
    await db.from('users').insert({
      id,
      fullName,
      address
    });
    return {
      success: true,
    }
  }
  return {
    success: false,
  }
})

Server$ может принимать любое количество аргументов и возвращать любое значение, которое может быть сериализовано Qwik, включая примитивы, объекты, массивы, bigint, узлы JSX и даже промисы, и это только некоторые из них.

Потоковые ответы

server$ может возвращать поток данных с помощью асинхронного генератора. Это полезно для потоковой передачи данных от сервера к клиенту.

Завершение работы генератора на стороне клиента (например, вызовом return() на генераторе, или выходом из вашего async for-loop) прервёт соединение. Как и в случае с AbortSignal - как генератор завершится на стороне сервера, зависит от среды исполнения сервера и от того, как обрабатывается отключение клиента.

import { component$, useSignal } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
const stream = server$(async function* () {
  for (let i = 0; i < 10; i++) {
    yield i;
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
});
 
export default component$(() => {
  const message = useSignal('');
  return (
    <div>
      <button
        onClick$={async () => {
          const response = await stream();
          for await (const i of response) {
            message.value += ` ${i}`;
          }
        }}
      >
        старт
      </button>
      <div>{message.value}</div>
    </div>
  );
});

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

Как работает server$()?

server$() оборачивает функцию и возвращает асинхронный прокси-объект к функции. На сервере прокси-функция напрямую вызывает обернутую функцию, а конечная точка HTTP автоматически создаётся функцией server$().

На клиенте прокси-функция вызывает обёрнутую функцию через HTTP-запрос, используя fetch().

Примечание: функция server$() должна гарантировать, что на сервере и клиенте выполняется одна и та же версия кода. При наличии перекоса в версии поведение не определено и может привести к ошибке. Если перекос версий является общей проблемой, то следует использовать более формальный механизм RPC, например, tRPC или другую библиотеку.

Промежуточное ПО и server$

При использовании server$ важно понимать, как выполняются функции промежуточного ПО. Функции промежуточного ПО, определённые в файлах layout, не выполняются для запросов server$. Это может привести к путанице, особенно когда разработчики ожидают, что определённое промежуточное ПО будет выполняться как для запросов страниц, так и для запросов server$.

Чтобы гарантировать, что функция промежуточного ПО будет работать для обоих типов запросов, ее следует определить в файле plugin.ts. Это гарантирует, что промежуточное ПО выполняется последовательно для всех входящих запросов, независимо от того, являются ли они обычными запросами страниц или запросами server$.

Определив промежуточное ПО в файле plugin.ts, разработчики могут поддерживать централизованное расположение общей логики промежуточного ПО, обеспечивая согласованность и уменьшая потенциальные ошибки или упущения.

Участники

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

  • mhevery
  • manucorporat
  • AnthonyPAlicea
  • the-r3aper7
  • igorbabko
  • RaeesBhatti
  • mrhoodz
  • DanielAdolfsson
  • mjschwanitz
  • wtlin1228
  • adamdbradley