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 或 Middleware。

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