zodix 中文文档教程
Zodix
Zodix 是 Zod< 的集合< /a> 用于 Remix 加载器和操作的实用程序。它抽象了解析和验证 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 must be a string');
}
const countNumber = parseInt(count, 10);
if (isNaN(countNumber)) {
throw new Error('count must be a number');
}
// Fetch data with id and 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 });
// Fetch data with id and countNumber
};
检查示例应用以获取常见模式的完整示例。
亮点
- 显着减少 Remix 操作/加载器膨胀
- 避免 FormData 和 URLSearchParams 的奇怪现象
- 没有外部依赖项的 Tiny(压缩后小于 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)
解析并验证来自 LoaderArgs['params']
或 ActionArgs['params']
Params 对象code> 使用 Zod 形状:
export async function loader({ params }: LoaderArgs) {
const { userId, noteId } = zx.parseParams(params, {
userId: z.string(),
noteId: z.string(),
});
};
与上面相同,但使用现有的 Zod 对象模式:
// This is if you have many pages that share the same params.
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)
从 Request< 解析并验证
FormData
/code> 在 Remix 操作中,避免繁琐的 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); // No TypeScript errors here
};
zx.parseQuery(request: Request, schema: Schema)
解析并验证查询字符串 ( Request
的搜索参数:
export async function loader({ request }: LoaderArgs) {
const { count, page } = zx.parseQuery(request, {
// NumAsString parses a string number ("5") and returns a number (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: "Invalid email" }),
password: z.string().min(8, { message: "Password must be at least 8 characters" }),
});
return json({
success: results.success,
error: results.error,
});
}
有关完整示例,请查看登录页面示例。
错误处理
parseParams()
、parseForm()
和 parseQuery()
当解析失败时,这些函数会抛出 400 响应。这与 Remix catch 边界 配合得很好,并且应该用于解析事物这应该很少失败并且不需要自定义错误处理。您可以传递自定义错误消息或状态代码。
export async function loader({ params }: LoaderArgs) {
const { postId } = zx.parseParams(
params,
{ postId: zx.NumAsString },
{ message: "Invalid postId parameter", status: 400 }
);
const post = await getPost(postId);
return { post };
}
export function CatchBoundary() {
const caught = useCatch();
return <h1>Caught error: {caught.statusText}</h1>;
}
查看帖子页面示例以获取完整示例。
parseParamsSafe()
、parseFormSafe()
和 parseQuerySafe()
这些函数非常适合表单验证,因为它们在解析失败时不会抛出异常。它们总是返回具有以下形状的对象:
{ success: boolean; error?: ZodError; data?: <parsed data>; }
然后您可以处理操作中的错误并使用 useActionData()
在组件中访问它们。有关完整示例,请查看登录页面示例。
Helper Zod 架构
由于 FormData
和 URLSearchParams
将所有值序列化为字符串,因此您通常会得到诸如 "5"
、"on 之类的结果”
和 “true”
。帮助器模式处理解析和验证表示其他数据类型的字符串,并且旨在与解析函数一起使用。
可用帮助器
zx.BoolAsString
"true"
→true
"false"
→false
"notboolean"< /code> → 抛出
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
解析
您的 URL 可能带有类似于 ?ids[]=1&ids[]=2
或 ?ids=1 的查询字符串, 2
未按内置 URLSearchParams
解析的要求进行处理。
您可以传递自定义函数,或使用 query-string 等库来使用 Zodix 解析它们。
// Create a custom parser function
type ParserFunction = (params: URLSearchParams) => Record<string, string | string[]>;
const customParser: ParserFunction = () => { /* ... */ };
// Parse non-standard search params
const search = new URLSearchParams(`?ids[]=id1&ids[]=id2`);
const { ids } = zx.parseQuery(
request,
{ ids: z.array(z.string()) }
{ parser: customParser }
);
// ids = ['id1', 'id2']
具有多个意图的操作
Zod 可区分联合非常适合帮助处理多个意图的操作,如下所示:
// This adds type narrowing by the intent property
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 is now narrowed to { intent: 'delete', id: string }
return;
case 'create':
// data is now narrowed to { intent: 'create', name: string }
return;
default:
// data is now narrowed to never. This will error if a case is missing.
const _exhaustiveCheck: never = data;
}
};
Zodix
Zodix is a collection of Zod utilities for Remix loaders and actions. It abstracts the complexity of parsing and validating FormData
and URLSearchParams
so your loaders/actions stay clean and are strongly typed.
Remix loaders often look like:
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 must be a string');
}
const countNumber = parseInt(count, 10);
if (isNaN(countNumber)) {
throw new Error('count must be a number');
}
// Fetch data with id and countNumber
};
Here is the same loader with Zodix:
export async function loader({ params, request }: LoaderArgs) {
const { id } = zx.parseParams(params, { id: z.string() });
const { count } = zx.parseQuery(request, { count: zx.NumAsString });
// Fetch data with id and countNumber
};
Check the example app for complete examples of common patterns.
Highlights
- Significantly reduce Remix action/loader bloat
- Avoid the oddities of FormData and URLSearchParams
- Tiny with no external dependencies (Less than 1kb gzipped)
- Use existing Zod schemas, or write them on the fly
- Custom Zod schemas for stringified numbers, booleans, and checkboxes
- Throw errors meant for Remix CatchBoundary by default
- Supports non-throwing parsing for custom validation/errors
- Works with all Remix runtimes (Node, Deno, Vercel, Cloudflare, etc)
- Full unit test coverage
Setup
Install with npm, yarn, pnpm, etc.
npm install zodix zod
Import the zx
object, or specific functions:
import { zx } from 'zodix';
// import { parseParams, NumAsString } from 'zodix';
Usage
zx.parseParams(params: Params, schema: Schema)
Parse and validate the Params
object from LoaderArgs['params']
or ActionArgs['params']
using a Zod shape:
export async function loader({ params }: LoaderArgs) {
const { userId, noteId } = zx.parseParams(params, {
userId: z.string(),
noteId: z.string(),
});
};
The same as above, but using an existing Zod object schema:
// This is if you have many pages that share the same params.
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)
Parse and validate FormData
from a Request
in a Remix action and avoid the tedious FormData
dance:
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,
});
};
Integrate with existing Zod schemas and models/controllers:
// 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); // No TypeScript errors here
};
zx.parseQuery(request: Request, schema: Schema)
Parse and validate the query string (search params) of a Request
:
export async function loader({ request }: LoaderArgs) {
const { count, page } = zx.parseQuery(request, {
// NumAsString parses a string number ("5") and returns a number (5)
count: zx.NumAsString,
page: zx.NumAsString,
});
};
zx.parseParamsSafe() / zx.parseFormSafe() / zx.parseQuerySafe()
These work the same as the non-safe versions, but don't throw when validation fails. They use z.parseSafe()
and always return an object with the parsed data or an error.
export async function action(args: ActionArgs) {
const results = await zx.parseFormSafe(args.request, {
email: z.string().email({ message: "Invalid email" }),
password: z.string().min(8, { message: "Password must be at least 8 characters" }),
});
return json({
success: results.success,
error: results.error,
});
}
Check the login page example for a full example.
Error Handling
parseParams()
, parseForm()
, and parseQuery()
These functions throw a 400 Response when the parsing fails. This works nicely with Remix catch boundaries and should be used for parsing things that should rarely fail and don't require custom error handling. You can pass a custom error message or status code.
export async function loader({ params }: LoaderArgs) {
const { postId } = zx.parseParams(
params,
{ postId: zx.NumAsString },
{ message: "Invalid postId parameter", status: 400 }
);
const post = await getPost(postId);
return { post };
}
export function CatchBoundary() {
const caught = useCatch();
return <h1>Caught error: {caught.statusText}</h1>;
}
Check the post page example for a full example.
parseParamsSafe()
, parseFormSafe()
, and parseQuerySafe()
These functions are great for form validation because they don't throw when parsing fails. They always return an object with this shape:
{ success: boolean; error?: ZodError; data?: <parsed data>; }
You can then handle errors in the action and access them in the component using useActionData()
. Check the login page example for a full example.
Helper Zod Schemas
Because FormData
and URLSearchParams
serialize all values to strings, you often end up with things like "5"
, "on"
and "true"
. The helper schemas handle parsing and validating strings representing other data types and are meant to be used with the parse functions.
Available Helpers
zx.BoolAsString
"true"
→true
"false"
→false
"notboolean"
→ throwsZodError
zx.CheckboxAsString
"on"
→true
undefined
→false
"anythingbuton"
→ throwsZodError
zx.IntAsString
"3"
→3
"3.14"
→ throwsZodError
"notanumber"
→ throwsZodError
zx.NumAsString
"3"
→3
"3.14"
→3.14
"notanumber"
→ throwsZodError
See the tests for more details.
Usage
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
}
*/
Extras
Custom URLSearchParams
parsing
You may have URLs with query string that look like ?ids[]=1&ids[]=2
or ?ids=1,2
that aren't handled as desired by the built in URLSearchParams
parsing.
You can pass a custom function, or use a library like query-string to parse them with Zodix.
// Create a custom parser function
type ParserFunction = (params: URLSearchParams) => Record<string, string | string[]>;
const customParser: ParserFunction = () => { /* ... */ };
// Parse non-standard search params
const search = new URLSearchParams(`?ids[]=id1&ids[]=id2`);
const { ids } = zx.parseQuery(
request,
{ ids: z.array(z.string()) }
{ parser: customParser }
);
// ids = ['id1', 'id2']
Actions with Multiple Intents
Zod discriminated unions are great for helping with actions that handle multiple intents like this:
// This adds type narrowing by the intent property
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 is now narrowed to { intent: 'delete', id: string }
return;
case 'create':
// data is now narrowed to { intent: 'create', name: string }
return;
default:
// data is now narrowed to never. This will error if a case is missing.
const _exhaustiveCheck: never = data;
}
};