Zodix
Zodix是一个为Remix加载器和动作设计的Zod实用工具集合。它抽象了解析和验证FormData
和URLSearchParams
的复杂性,使您的加载器/动作保持简洁并具有强类型。
Remix加载器通常看起来像这样:
export async function loader({ params, request }: LoaderArgs) {
const { id } = params;
const url = new URL(request.url);
const count = url.searchParams.get('count') || '10';
if (typeof id !== 'string') {
throw new Error('id必须是字符串');
}
const countNumber = parseInt(count, 10);
if (isNaN(countNumber)) {
throw new Error('count必须是数字');
}
// 使用id和countNumber获取数据
};
使用Zodix后,同样的加载器如下所示:
export async function loader({ params, request }: LoaderArgs) {
const { id } = zx.parseParams(params, { id: z.string() });
const { count } = zx.parseQuery(request, { count: zx.NumAsString });
// 使用id和countNumber获取数据
};
查看示例应用以获取常见模式的完整示例。
亮点
- 显著减少Remix动作/加载器的冗余代码
- 避免FormData和URLSearchParams的奇怪之处
- 体积小,无外部依赖(gzip压缩后小于1kb)
- 使用现有的Zod模式,或即时编写
- 为字符串化的数字、布尔值和复选框提供自定义Zod模式
- 默认抛出适用于Remix CatchBoundary的错误
- 支持非抛出式解析,用于自定义验证/错误
- 适用于所有Remix运行时(Node、Deno、Vercel、Cloudflare等)
- 完整的单元测试覆盖
设置
使用npm、yarn、pnpm等安装。
npm install zodix zod
导入zx
对象或特定函数:
import { zx } from 'zodix';
// import { parseParams, NumAsString } from 'zodix';
使用方法
zx.parseParams(params: Params, schema: Schema)
使用Zod形状解析和验证来自LoaderArgs['params']
或ActionArgs['params']
的Params
对象:
export async function loader({ params }: LoaderArgs) {
const { userId, noteId } = zx.parseParams(params, {
userId: z.string(),
noteId: z.string(),
});
};
与上面相同,但使用现有的Zod对象模式:
// 如果您有多个页面共享相同的参数,可以这样做。
export const ParamsSchema = z.object({ userId: z.string(), noteId: z.string() });
export async function loader({ params }: LoaderArgs) {
const { userId, noteId } = zx.parseParams(params, ParamsSchema);
};
zx.parseForm(request: Request, schema: Schema)
在Remix动作中解析和验证来自Request
的FormData
,避免繁琐的FormData
操作:
export async function action({ request }: ActionArgs) {
const { email, password, saveSession } = await zx.parseForm(request, {
email: z.string().email(),
password: z.string().min(6),
saveSession: zx.CheckboxAsString,
});
};
与现有的Zod模式和模型/控制器集成:
// db.ts
export const CreateNoteSchema = z.object({
userId: z.string(),
title: z.string(),
category: NoteCategorySchema.optional(),
});
export function createNote(note: z.infer<typeof CreateNoteSchema>) {}
import { CreateNoteSchema, createNote } from './db';
export async function action({ request }: ActionArgs) {
const formData = await zx.parseForm(request, CreateNoteSchema);
createNote(formData); // 这里没有TypeScript错误
};
zx.parseQuery(request: Request, schema: Schema)
解析和验证Request
的查询字符串(搜索参数):
export async function loader({ request }: LoaderArgs) {
const { count, page } = zx.parseQuery(request, {
// NumAsString解析字符串数字("5")并返回数字(5)
count: zx.NumAsString,
page: zx.NumAsString,
});
};
zx.parseParamsSafe() / zx.parseFormSafe() / zx.parseQuerySafe()
这些函数的工作方式与非安全版本相同,但在验证失败时不会抛出错误。它们使用z.parseSafe()
,始终返回包含解析数据或错误的对象。
export async function action(args: ActionArgs) {
const results = await zx.parseFormSafe(args.request, {
email: z.string().email({ message: "无效的电子邮件" }),
password: z.string().min(8, { message: "密码至少需要8个字符" }),
});
return json({
success: results.success,
error: results.error,
});
}
查看登录页面示例以获取完整示例。
错误处理
parseParams()
、parseForm()
和 parseQuery()
这些函数在解析失败时会抛出 400 响应。这与 Remix 捕获边界 配合使用效果很好,应该用于解析那些很少失败且不需要自定义错误处理的内容。您可以传递自定义错误消息或状态码。
export async function loader({ params }: LoaderArgs) {
const { postId } = zx.parseParams(
params,
{ postId: zx.NumAsString },
{ message: "无效的 postId 参数", status: 400 }
);
const post = await getPost(postId);
return { post };
}
export function CatchBoundary() {
const caught = useCatch();
return <h1>捕获到错误:{caught.statusText}</h1>;
}
查看 帖子页面示例 以获取完整示例。
parseParamsSafe()
、parseFormSafe()
和 parseQuerySafe()
这些函数非常适合表单验证,因为它们在解析失败时不会抛出错误。它们总是返回一个具有以下结构的对象:
{ success: boolean; error?: ZodError; data?: <解析后的数据>; }
然后您可以在 action 中处理错误,并在组件中使用 useActionData()
访问它们。查看 登录页面示例 以获取完整示例。
辅助 Zod 模式
由于 FormData
和 URLSearchParams
将所有值序列化为字符串,您经常会遇到像 "5"
、"on"
和 "true"
这样的情况。辅助模式处理解析和验证表示其他数据类型的字符串,旨在与解析函数一起使用。
可用的辅助函数
zx.BoolAsString
"true"
→true
"false"
→false
"notboolean"
→ 抛出ZodError
zx.CheckboxAsString
"on"
→true
undefined
→false
"anythingbuton"
→ 抛出ZodError
zx.IntAsString
"3"
→3
"3.14"
→ 抛出ZodError
"notanumber"
→ 抛出ZodError
zx.NumAsString
"3"
→3
"3.14"
→3.14
"notanumber"
→ 抛出ZodError
查看 测试文件 以获取更多详细信息。
用法
const Schema = z.object({
isAdmin: zx.BoolAsString,
agreedToTerms: zx.CheckboxAsString,
age: zx.IntAsString,
cost: zx.NumAsString,
});
const parsed = Schema.parse({
isAdmin: 'true',
agreedToTerms: 'on',
age: '38',
cost: '10.99'
});
/*
parsed = {
isAdmin: true,
agreedToTerms: true,
age: 38,
cost: 10.99
}
*/
附加功能
自定义 URLSearchParams
解析
您可能会遇到像 ?ids[]=1&ids[]=2
或 ?ids=1,2
这样的查询字符串 URL,内置的 URLSearchParams
解析无法按照预期处理。
您可以传递一个自定义函数,或使用像 query-string 这样的库来与 Zodix 一起解析它们。
// 创建自定义解析器函数
type ParserFunction = (params: URLSearchParams) => Record<string, string | string[]>;
const customParser: ParserFunction = () => { /* ... */ };
// 解析非标准搜索参数
const search = new URLSearchParams(`?ids[]=id1&ids[]=id2`);
const { ids } = zx.parseQuery(
request,
{ ids: z.array(z.string()) }
{ parser: customParser }
);
// ids = ['id1', 'id2']
具有多个意图的 Actions
Zod 区分联合类型非常适合处理具有多个意图的 actions,如下所示:
// 这通过 intent 属性添加类型缩小
const Schema = z.discriminatedUnion('intent', [
z.object({ intent: z.literal('delete'), id: z.string() }),
z.object({ intent: z.literal('create'), name: z.string() }),
]);
export async function action({ request }: ActionArgs) {
const data = await zx.parseForm(request, Schema);
switch (data.intent) {
case 'delete':
// data 现在被缩小为 { intent: 'delete', id: string }
return;
case 'create':
// data 现在被缩小为 { intent: 'create', name: string }
return;
default:
// data 现在被缩小为 never。如果缺少某个 case,这里会报错。
const _exhaustiveCheck: never = data;
}
};