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) => {
  // 仅在用户提交表单时(或以编程方式调用操作时)在服务器上运行
  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">添加用户</button>
      </Form>
      {action.value?.success && (
        // 当操作成功完成时,`action.value` 属性将包含操作的返回值
        <p>用户 {action.value.userID} 添加成功</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">添加用户</button>
      {action.value?.success && <p>用户添加成功</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);
        }}
      >
        添加用户
      </button>
      {action.value?.success && <p>用户添加成功</p>}
    </section>
  );
});

在上面的示例中,当用户点击按钮时,将触发 addUser 操作。action.submit() 方法返回一个 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}`}
          />
          {/* 在提交时将 item.id 与表单数据一起发送 */}
          <input type="hidden" name="id" value={item.id} />
          <button type="submit">
            提交
          </button>
        </Form>
 
        <div>
          <button onClick$={() => (editingIdSignal.value = '')}>
            取消
          </button>
        </div>
      </div>
    );
  }
);
 
export default EditForm;

在此示例中,onSubmitCompleted$ 用于在表单提交成功完成后将 editingIdSignal 值重置为空字符串。这样,应用程序可以更新其状态并返回默认视图。

验证和类型安全

Qwik 提供了对 Zod 的内置支持,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) => {
    // "user" 具有强类型:{ firstName: string, lastName: string }
    const userID = await db.users.add({
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return {
      success: true,
      userID,
    };
  },
  // 使用 Zod 模式验证 FormData 是否包含 "firstName" 和 "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">添加用户</button>
      </Form>
      {action.value?.success && (
        <p>用户 {action.value.userID} 添加成功</p>
      )}
    </>
  );
});

在向 routeAction() 提交数据时,数据将根据 Zod 模式进行验证。如果数据无效,则操作将在 routeAction.value 属性中放置验证错误。

有关如何使用 Zod 模式的更多信息,请参阅 Zod 文档

高级基于事件的验证

zod$ 的构造函数也可以接受一个函数,因为第一个参数是 zod 本身,所以可以直接使用它来构建模式。 第二个参数是 RequestEvent,用于构建基于事件的 zod 模式。 特别是与 zod 中的 refinesuperDefine 结合使用时,唯一的限制就是你的想象力。

高级基于事件的验证
export const useAddUser = routeAction$(
  async (user) => {
    // "user" 仍然具有强类型,但是 firstname 现在是可选的:
    // { firstName?: string | undefined, lastName: string }
    const userID = await db.users.add({
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return {
      success: true,
      userID,
    };
  },
  // 使用 Zod 模式验证 FormData 是否包含 "firstName" 和 "lastName"
  zod$((z, ev) => {
    // 如果 url 包含查询参数 "firstname=optional",则 firstName 是可选的
    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 请求和响应的信息。

这使得操作可以访问请求标头、cookie、URL 和环境变量。

src/routes/product/[user]/index.tsx
import { routeAction$ } from '@builder.io/qwik-city';
 
// 操作的第二个参数是 `RequestEvent` 对象
export const useProductRecommendations = routeAction$(
  async (_data, requestEvent) => {
    console.log('请求标头:', requestEvent.request.headers);
    console.log('请求 cookie:', requestEvent.cookie);
    console.log('请求 URL:', requestEvent.url);
    console.log('请求参数:', 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` 的类型为 { name: string }
    const userID = await db.users.add(user);
    if (!userID) {
      return fail(500, {
        message: '无法添加用户',
      });
    }
    return {
      userID,
    };
  },
  zod$({
    name: z.string(),
  })
);

失败信息存储在 action.value 属性中,就像成功值一样。但是,当操作失败时,action.value.failed 属性设置为 true。此外,失败消息可以在 fieldErrors 对象中找到,根据在 Zod 模式中定义的属性。

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">添加用户</button>
      {action.value?.failed && <p>{action.value.fieldErrors.name}</p>}
      {action.value?.userID && <p>用户添加成功</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) => {
  // 处理操作...
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" value={action.formData?.get('name')} />
      <button type="submit">添加用户</button>
    </Form>
  );
});

action.formData 特别有用,因为它允许在页面刷新时保持用户填写的表单数据,即使在禁用 JS 的情况下,也可以实现完整的 SPA 体验。

路由 vs 全局操作

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

建议从 routeAction$() 开始,仅在需要在多个路由之间共享操作时使用 globalAction$(),或者如果希望在不是路由的组件中使用操作时使用 globalAction$()

routeAction$()

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

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) => {
  // ...
});

Contributors

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

  • manucorporat
  • cunzaizhuyi
  • forresst
  • keuller
  • hamatoyogi
  • AnthonyPAlicea
  • the-r3aper7
  • thejackshelton
  • adnanebrahimi
  • mhevery
  • ulic75
  • CoralWombat
  • tzdesign