routeAction$()

routeAction$() 用於定義稱為「動作」的函數,這些函數僅在明確呼叫時才在伺服器端執行。動作可以產生副作用,例如寫入資料庫或發送電子郵件,這些副作用在用戶端渲染期間無法發生。這使得它們非常適合處理表單提交、執行具有副作用的操作,然後將數據返回給用戶端/瀏覽器,以便用於更新 UI。

可以使用從 @builder.io/qwik-city 匯出的 routeAction$()globalAction$() 來宣告動作。

src/routes/layout.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (data, requestEvent) => {
  // This will only run on the server when the user submits the form (or when the action is called programmatically)
  const userID = await db.users.add({
    firstName: data.firstName,
    lastName: data.lastName,
  });
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
 
  return (
    <>
      <Form action={action}>
        <input name="firstName" />
        <input name="lastName" />
        <button type="submit">Add user</button>
      </Form>
      {action.value?.success && (
        // When the action is done successfully, the `action.value` property will contain the return value of the action
        <p>User {action.value.userID} added successfully</p>
      )}
    </>
  );
});

由於動作不會在渲染期間執行,因此它們可以產生副作用,例如寫入資料庫或傳送電子郵件。動作只有在明確呼叫時才會執行。

將動作與 <Form/> 搭配使用

呼叫動作的最佳方式是使用 @builder.io/qwik-city 中匯出的 <Form/> 元件。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  const userID = await db.users.add(user);
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" />
      <button type="submit">Add user</button>
      {action.value?.success && <p>User added successfully</p>}
    </Form>
  );
});

在底層,<Form/> 元件使用原生的 HTML <form> 元素,因此它可以在沒有 JavaScript 的情況下運作。

啟用 JS 後,<Form/> 元件將攔截表單提交並以 SPA 模式觸發動作。允許完整的 SPA 體驗。

這是為了闡明伺服器會重新渲染整個頁面並重新執行所有內容,因此如果您有任何 routeLoader$,它們也將被執行。

可以使用點表示法建立複雜表單

以程式設計方式使用動作

動作也可以使用 action.submit() 方法以程式設計方式觸發(即您不需要 <Form/> 元件)。但是,您可以像使用函數一樣,從按鈕點擊或任何其他事件觸發動作。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$ } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  const userID = await db.users.add(user);
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <section>
      <button
        onClick$={async () => {
          const { value } = await action.submit({ name: 'John' });
          console.log(value);
        }}
      >
        Add user
      </button>
      {action.value?.success && <p>User added successfully</p>}
    </section>
  );
});

在上面的範例中,當用戶點擊按鈕時會觸發 addUser 動作。action.submit() 方法會返回一個 Promise,該 Promise 會在動作完成時解析。

具有事件處理程式的動作

onSubmitCompleted$ 事件處理程式可以在動作成功執行並返回一些數據後使用。這對於在動作完成後執行任務很有用,例如重置 UI 元素或更新應用程式狀態。

以下是在待辦事項應用程式的 EditForm 元件中使用 onSubmitCompleted$ 處理程式來編輯項目的範例。

src/components/EditForm.tsx
import { component$, type Signal, useSignal } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
import { type ListItem, useEditFromListAction } from '../../routes/index';
 
export interface EditFormProps {
  item: listItem;
  editingIdSignal: Signal<string>;
}
 
const EditForm = component$(
  ({ item, editingIdSignal }: EditFormProps) => {
    const editAction = useEditFromListAction();
 
    return (
      <div>
        <Form
          action={editAction}
          onSubmitCompleted$={() => {
            editingIdSignal.value = '';
          }}
          spaReset
        >
          <input
            type="text"
            value={item.text}
            name="text"
            id={`edit-${item.id}`}
          />
          {/* Sends item.id with form data on submission. */}
          <input type="hidden" name="id" value={item.id} />
          <button type="submit">
            Submit
          </button>
        </Form>
 
        <div>
          <button onClick$={() => (editingIdSignal.value = '')}>
            Cancel
          </button>
        </div>
      </div>
    );
  }
);
 
export default EditForm;

在此範例中,onSubmitCompleted$ 用於在表單提交成功完成後將 editingIdSignal 值重置為空字串。這允許應用程式更新其狀態並返回預設視圖。

驗證和類型安全

Qwik 內建支援 Zod,這是一種 TypeScript 優先的結構描述驗證,可以使用 zod$() 函數直接與動作一起使用。

動作 + Zod 允許建立類型安全的表單,其中數據在執行動作之前會在伺服器端進行驗證。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(
  async (user) => {
    // The "user" is strongly typed: { firstName: string, lastName: string }
    const userID = await db.users.add({
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return {
      success: true,
      userID,
    };
  },
  // Zod schema is used to validate that the FormData includes "firstName" and "lastName"
  zod$({
    firstName: z.string(),
    lastName: z.string(),
  })
);
 
export default component$(() => {
  const action = useAddUser();
  return (
    <>
      <Form action={action}>
        <input name="firstName" />
        <input name="lastName" />
 
        {action.value?.failed && <p>{action.value.fieldErrors?.firstName}</p>}
        <button type="submit">Add user</button>
      </Form>
      {action.value?.success && (
        <p>User {action.value.userID} added successfully</p>
      )}
    </>
  );
});

當將數據提交到 routeAction() 時,數據將根據 Zod 結構描述進行驗證。如果數據無效,動作會將驗證錯誤放入 routeAction.value 屬性中。

如需如何使用 Zod 結構描述的更多資訊,請參閱 Zod 文件

進階基於事件的驗證

zod$ 的建構函數也可以接受一個函數,因為第一個參數是 zod 本身,因此您可以直接使用它來建構結構描述。第二個參數是 RequestEvent,用於建構基於事件的 zod 結構描述。特別是與 zod 中的 refinesuperDefine 結合使用,唯一的限制是您的想像力。

進階基於事件的驗證
export const useAddUser = routeAction$(
  async (user) => {
    // The "user" is still strongly typed, but firstname 
    // is now optional: { firstName?: string | undefined, lastName: string }
    const userID = await db.users.add({
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return {
      success: true,
      userID,
    };
  },
  // Zod schema is used to validate that the FormData includes "firstName" and "lastName"
  zod$((z, ev) => {
    // The first name is optional if the url contains the query parameter "firstname=optional"
    const firstName =
      ev.url.searchParams.get("firstname") === "optional"
        ? z.string().optional()
        : z.string().nonempty();
 
    return z.object({
      firstName,
      lastName: z.string(),
    });
  })
);

HTTP 請求和回應

routeAction$globalAction$ 可以訪問 RequestEvent 物件,其中包含有關當前 HTTP 請求和響應的資訊。

這讓動作可以在 routeAction$ 函式中訪問請求標頭、Cookie、網址和環境變數。

src/routes/product/[user]/index.tsx
import { routeAction$ } from '@builder.io/qwik-city';
 
// The second argument of the action is the `RequestEvent` object
export const useProductRecommendations = routeAction$(
  async (_data, requestEvent) => {
    console.log('Request headers:', requestEvent.request.headers);
    console.log('Request cookies:', requestEvent.cookie);
    console.log('Request url:', requestEvent.url);
    console.log('Request params:', requestEvent.params);
    console.log('MY_ENV_VAR:', requestEvent.env.get('MY_ENV_VAR'));
  }
);
 

動作失敗

為了返回非成功值,動作必須使用 fail() 方法。

import { routeAction$, zod$, z } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(
  async (user, { fail }) => {
    // `user` is typed { name: string }
    const userID = await db.users.add(user);
    if (!userID) {
      return fail(500, {
        message: 'User could not be added',
      });
    }
    return {
      userID,
    };
  },
  zod$({
    name: z.string(),
  })
);

失敗儲存在 action.value 屬性中,就像成功值一樣。但是,當動作失敗時,action.value.failed 屬性會設定為 true。此外,可以在 fieldErrors 物件中根據 Zod 綱要中定義的屬性找到失敗訊息。

fieldErrors 會變成點記號物件。如需更多資訊,請參閱複雜表單

import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" />
      <button type="submit">Add user</button>
      {action.value?.failed && <p>{action.value.fieldErrors.name}</p>}
      {action.value?.userID && <p>User added successfully</p>}
    </Form>
  );
});

感謝 TypeScript 類型辨別,您可以使用 action.value.failed 屬性來區分成功和失敗。

先前的表單狀態

觸發動作時,先前的狀態會儲存在 action.formData 屬性中。這在動作執行時顯示載入狀態很有用。

import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  // handle action...
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" value={action.formData?.get('name')} />
      <button type="submit">Add user</button>
    </Form>
  );
});

action.formData 對於在頁面重新整理後保留使用者填寫的表單資料特別有用。即使停用 JS,這也能實現無縫的 SPA 體驗。

路由動作與全域動作

可以使用從 @builder.io/qwik-city 匯出的 routeAction$()globalAction$() 來宣告動作,兩者之間的唯一區別是 routeAction$() 的範圍限定在路由中,而 globalAction$() 在整個應用程式中全域可用。

建議先從 routeAction$() 開始。僅當在多個路由之間共用動作,或希望在非路由的元件中使用動作時,才使用 globalAction$()

routeAction$()

routeAction$() 只能在 src/routes 資料夾中的 layout.tsxindex.tsx 檔案中宣告,而且它們必須像 routeLoader$() 一樣匯出。由於 routeAction$() 只能在其宣告的路由中訪問,因此建議在動作需要訪問某些使用者資料或它是受保護路由時使用它們。可以將其視為「私有」動作。

如果您想管理通用的可重複使用 routeAction$(),則必須從現有路由的 'layout.tsx' 或 'index.tsx 檔案中重新匯出此函式,否則它將無法執行或引發異常。如需更多資訊,請查看食譜

src/routes/form/index.tsx
import { routeAction$ } from '@builder.io/qwik-city';
 
export const useChangePassword = routeAction$((data) => {
  // ...
});

globalAction$()

globalAction$() 可以在 src 資料夾中的任何位置宣告。由於 globalAction$() 是全域可用的,因此建議在需要在多個路由之間共用動作,或動作不需要訪問任何使用者資料時使用它們。例如,用於登入使用者的 useLogin 動作。可以將其視為「公開」動作。

src/components/login/login.tsx
import { globalAction$ } from '@builder.io/qwik-city';
 
export const useLogin = globalAction$((data) => {
  // ...
});

貢獻者

感謝所有幫助改進此文件的貢獻者!

  • manucorporat
  • cunzaizhuyi
  • forresst
  • keuller
  • hamatoyogi
  • AnthonyPAlicea
  • the-r3aper7
  • thejackshelton
  • adnanebrahimi
  • mhevery
  • ulic75
  • CoralWombat
  • tzdesign
  • igorbabko
  • gioboa
  • mrhoodz
  • VinuB-Dev
  • aivarsliepa
  • wtlin1228
  • adamdbradley
  • gioboa
  • jemsco
  • tzdesign