server$()

server$() 允许您定义仅在服务器上执行的函数,使其成为服务器端操作和数据库访问的理想选择。它充当客户端和服务器之间的 RPC(远程过程调用)机制。这类似于传统的 HTTP 端点,但使用 TypeScript 强类型化,并且更容易维护。

server$ 可以接受任意数量的参数,并返回任何可以被 Qwik 序列化的值。这包括基本类型、对象、数组、bigint、JSX 节点,甚至 Promise,仅举几例。

AbortSignal 是可选的,允许您通过终止连接来取消长时间运行的请求。
您的新函数将具有以下签名
([AbortSignal, ...yourOtherArgs]): Promise<T>

请注意,根据您的服务器运行时,服务器上的函数可能不会立即终止。这取决于运行时如何处理客户端断开连接。

import { component$, useSignal } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
// By wrapping a function with `server$()` we mark it to always
// execute on the server. This is a form of RPC mechanism.
export const serverGreeter = server$(
  function (firstName: string, lastName: string) {
    const greeting = `Hello ${firstName} ${lastName}`;
    console.log('Prints in the server', greeting);
    return greeting;
  }
);
 
export default component$(() => {
  const firstName = useSignal('');
  const lastName = useSignal('');
 
  return (
    <section>
      <label>First name: <input bind:value={firstName} /></label>
      <label>Last name: <input bind:value={lastName} /></label>
 
      <button
        onClick$={
          async () => {
            const greeting = await serverGreeter(firstName.value, lastName.value);
            alert(greeting);
          }
        }
      >
        greet
      </button>
    </section>
  );
});

使用 RequestEvent 访问请求信息

使用 server$ 时,您可以通过 this 访问 RequestEvent 对象。此对象提供有关 HTTP 请求的有用信息,包括环境变量、cookie、URL 和标头。以下是您可以使用它的方法

环境变量

您可以使用 this.env.get() 访问环境变量。

export const getEnvVariable = server$(
  function () {
    const dbKey = this.env.get('DB_KEY');
    console.log('Database Key:', dbKey);
    return dbKey;
  }
);

Cookie

您可以使用 this.cookie.get()this.cookie.set() 读取 cookie。

使用 handleCookies(在下面的示例中)时,如果它在初始请求期间运行的 useTask$ 函数中使用,则设置 cookie 将无法按预期工作。这是因为在服务器端渲染 (SSR) 期间,响应是流式传输的,而 HTTP 要求在发送第一个响应之前设置所有标头。但是,如果在 useVisibleTask$ 中使用 handleCookies,则不会出现此问题。如果您需要为初始文档请求设置 cookie,可以使用 plugin@<name>.ts 或中间件。

export const handleCookies = server$(
  function () {
    const userSession = this.cookie.get('user-session')?.value;
    if (!userSession) {
      this.cookie.set('user-session', 'new-session-id', { path: '/', httpOnly: true });
    }
    return userSession;
  }
);

URL

您可以使用 this.url 访问请求 URL 及其组件。

export const getRequestUrl = server$(
  function () {
    const requestUrl = this.url;
    console.log('Request URL:', requestUrl);
    return requestUrl;
  }
);

标头

您可以使用 this.headers.get() 读取标头。

export const getHeaders = server$(
  function () {
    const userAgent = this.headers.get('User-Agent');
    console.log('User-Agent:', userAgent);
    return userAgent;
  }
);

使用多个 RequestEvent 信息

以下是一个将环境变量、cookie、URL 和标头组合在单个函数中的示例。

export const handleRequest = server$(
  function () {
    // Access environment variable
    const dbKey = this.env.get('DB_KEY');
 
    // Access cookies
    const userSession = this.cookie.get('user-session')?.value;
    if (!userSession) {
      this.cookie.set('user-session', 'new-session-id', { path: '/', httpOnly: true });
    }
 
    // Access request URL
    const requestUrl = this.url;
 
    // Access headers
    const userAgent = this.headers.get('User-Agent');
 
    console.log('Environment Variable:', dbKey);
    console.log('User Session:', userSession);
    console.log('Request URL:', requestUrl);
    console.log('User-Agent:', userAgent);
 
    return {
      dbKey,
      userSession,
      requestUrl,
      userAgent
    };
  }
);

流式响应

server$ 可以通过使用异步生成器函数返回数据流,这对于将数据从服务器流式传输到客户端很有用。

在客户端终止生成器(例如,通过在生成器上调用 .return() 或从异步 for-of 循环中退出)将终止连接。与 AbortSignal 类似,生成器在服务器端如何终止取决于服务器运行时以及如何处理客户端断开连接。

import { component$, useSignal } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
export const streamFromServer = server$(
  // Async Generator Function
  async function* () {
    // Creation of an array with 10 undefined values
    const iterationRange = Array(10).fill().entries(); 
  
    for (const [value] of iterationRange) {
      // Yield returns the array value during each iteration
      yield value;
  
      // Waiting for 1 second before the next iteration
      // This simulates a delay in the execution
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
);
 
 
export default component$(() => {
  const message = useSignal('');
  return (
    <div>
      <button
        onClick$={
          async () => {
            // call the async stream function and wait for the response
            const response = await streamFromServer(); 
            // use a for-await-of loop to asynchronously iterate over the response
            for await (const value of response) {
              // add each value from the response to the message value
              message.value += ` ${value}`;
            }
            // do anything else
          }
        }
      >
        start
      </button>
      <div>{message.value}</div>
    </div>
  );
});

此 API 实际上用于在我们的文档网站中实现 QwikGPT 流式响应。

server$() 如何工作?

server$() 包装一个函数并返回一个指向该函数的异步代理。在服务器上,代理函数直接调用包装的函数,并且 server$() 函数会自动创建一个 HTTP 端点。

在客户端,代理函数使用 fetch() 通过 HTTP 请求调用包装的函数。

注意:server$() 函数必须确保服务器和客户端执行的代码版本相同。如果存在版本偏差,则行为将是不确定的,并且可能导致错误。如果版本偏差是一个常见问题,则应使用更正式的 RPC 机制,例如 tRPC 或其他库。

重要注意事项onClick$ 中定义和调用 server$() 时,请注意这可能会导致潜在的错误。为了避免它们,请确保处理程序周围有 $ 包裹。
不要这样做
onClick$={() => server$(() => // 某些服务器代码!)}
这样做
onClick$={$(() => server$(() => // 某些服务器代码!))}

中间件和 server$

使用 server$ 时,了解 中间件函数 如何执行非常重要。在 layout 文件中定义的中间件函数不会为 server$ 请求运行。这可能会导致混淆,尤其是在开发人员期望某些中间件为页面请求和 server$ 请求执行时。

为了确保中间件函数为两种类型的请求都运行,它应该在 plugin.ts 文件中定义。这确保了中间件为所有传入请求一致地执行,无论它们是正常的页面请求还是 server$ 请求。

通过 plugin.ts 文件中定义中间件,开发人员可以维护一个用于共享中间件逻辑的集中位置,确保一致性并减少潜在的错误或疏忽。

贡献者

感谢所有帮助改进此文档的贡献者!

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