server$()

server$() 允许你创建一个总是在服务器上执行的函数,这使得它成为访问数据库或执行仅限服务器操作的绝佳位置。

server$ 是客户端和服务器之间的一种 RPC(远程过程调用)机制,就像传统的 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 = `Hello ${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$ 还可以使用 this 读取 HTTP cookies、headers 或环境变量。在这种情况下,你需要使用一个函数而不是箭头函数。

// 注意,包装的函数声明为 `async function`
const addUser = server$(async function(id: number, fullName: string, address: Address) {
  // `this` 是 `RequestEvent` 对象,
  // 其中包含 HTTP headers、cookies 和环境变量。
  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 节点,甚至是 Promise,仅举几例。

流式响应

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

在客户端上终止生成器(例如通过在生成器上调用 return() 或从异步 for 循环中退出)将终止连接。 与 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$() 包装一个函数并返回一个异步代理函数。在服务器上,代理函数直接调用包装的函数,并且 server$() 函数会自动创建一个 HTTP 端点。

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

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

中间件和 server$

在使用 server$ 时,了解 中间件函数 的执行方式非常重要。在 layout 文件中定义的中间件函数不会对 server$ 请求运行。这可能会导致困惑,特别是当开发人员期望某些中间件对页面请求和 server$ 请求都执行时。

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

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

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • mhevery
  • manucorporat
  • AnthonyPAlicea
  • the-r3aper7