routeAction$()

routeAction$() 用于定义称为操作的函数,这些函数仅在服务器上执行,并且仅在显式调用时执行。操作可以具有副作用,例如写入数据库或发送电子邮件,这些副作用不能在客户端渲染期间发生。这使得它们非常适合处理表单提交、执行具有副作用的操作,然后将数据返回给客户端/浏览器,以便在其中使用它来更新 UI。

操作可以使用 routeAction$()globalAction$() 声明,它们从 @builder.io/qwik-city 导出。

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、URL 和环境变量。

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 体验。

路由操作 vs 全局操作

操作可以使用从 @builder.io/qwik-city 导出的 routeAction$()globalAction$() 声明,两者之间的唯一区别是 routeAction$() 的作用域限于路由,而 globalAction$() 在整个应用程序中全局可用。

建议从 routeAction$() 开始。仅当在多个路由之间共享操作,或者希望在非路由组件中使用操作时,才使用 globalAction$()

routeAction$()

routeAction$() 只能在 src/routes 文件夹中,在 layout.tsxindex.tsx 文件中声明,并且必须导出,就像 routeLoader$() 一样。由于 routeAction$() 只能在声明它的路由内访问,因此建议在操作需要访问某些用户数据或它是受保护的路由时使用它们。可以将其视为“私有”操作。

如果要管理通用的可重用 routeAction$(),则必须从现有路由的 layout.tsxindex.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