routeAction$()
操作允许处理表单提交,允许执行诸如写入数据库或发送电子邮件等副作用。
操作还可以将数据返回给客户端/浏览器,从而可以相应地更新 UI,例如在表单提交后显示成功消息。
可以使用 @builder.io/qwik-city
中导出的 routeAction$()
或 globalAction$()
来声明操作。
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/>
组件。
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/>
组件,而是可以从按钮点击或任何其他事件中触发操作,就像使用函数一样。
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$
处理程序的示例。
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 允许创建类型安全的表单,在执行操作之前在服务器端验证数据。
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 中的 refine
和 superDefine
结合使用时,唯一的限制就是你的想象力。
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 和环境变量。
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.tsx
或 index.tsx
文件中声明,并且它们必须被导出,就像 routeLoader$()
一样。由于 routeAction$()
只能在声明它的路由内部访问,因此当操作需要访问某些用户数据或是受保护的路由时,建议使用它。可以将其视为“私有”操作。
import { routeAction$ } from '@builder.io/qwik-city';
export const useChangePassword = routeAction$((data) => {
// ...
});
globalAction$()
globalAction$()
可以在 src
文件夹的任何位置声明。由于 globalAction$()
是全局可用的,因此建议在操作需要在多个路由之间共享时使用它,或者当操作不需要访问任何用户数据时使用它。例如,一个用于登录用户的 useLogin
操作。可以将其视为“公共”操作。
import { globalAction$ } from '@builder.io/qwik-city';
export const useLogin = globalAction$((data) => {
// ...
});