Remix Loader基础 #
一、Loader概述 #
Loader 是 Remix 中用于在服务端加载数据的函数。它在渲染页面之前执行,将数据传递给页面组件。
二、基本用法 #
2.1 定义Loader #
tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params, request }: LoaderFunctionArgs) {
const posts = await getPosts();
return json({ posts });
}
export default function Posts() {
const { posts } = useLoaderData<typeof loader>();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
2.2 Loader参数 #
Loader 函数接收一个参数对象:
tsx
export async function loader({
params, // 路由参数
request, // Request对象
context, // 应用上下文
}: LoaderFunctionArgs) {
// ...
}
2.3 返回响应 #
使用 json 返回 JSON 响应:
tsx
import { json } from "@remix-run/node";
export async function loader() {
const data = await fetchData();
return json({ data });
}
// 带状态码
export async function loader() {
const data = await fetchData();
return json({ data }, { status: 200 });
}
三、获取参数 #
3.1 路由参数 #
tsx
// 文件: app/routes/posts.$id.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params;
const post = await getPost(id);
if (!post) {
throw new Response("文章不存在", { status: 404 });
}
return json({ post });
}
3.2 查询参数 #
tsx
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const query = url.searchParams.get("q");
const page = url.searchParams.get("page") || "1";
const results = await search(query, parseInt(page));
return json({ results });
}
3.3 请求头 #
tsx
export async function loader({ request }: LoaderFunctionArgs) {
const userAgent = request.headers.get("User-Agent");
const cookie = request.headers.get("Cookie");
const authorization = request.headers.get("Authorization");
// ...
}
四、响应类型 #
4.1 JSON响应 #
tsx
import { json } from "@remix-run/node";
export async function loader() {
return json({
message: "Hello",
data: { id: 1, name: "Test" }
});
}
4.2 重定向 #
tsx
import { redirect } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
if (!user) {
return redirect("/login");
}
return json({ user });
}
4.3 自定义响应 #
tsx
export async function loader() {
return new Response("Not Found", {
status: 404,
headers: {
"Content-Type": "text/plain",
},
});
}
4.4 文件下载 #
tsx
export async function loader() {
const file = await getFile();
return new Response(file, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": "attachment; filename=document.pdf",
},
});
}
五、错误处理 #
5.1 抛出错误 #
tsx
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
if (!post) {
throw new Response("文章不存在", { status: 404 });
}
return json({ post });
}
5.2 错误边界 #
tsx
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return <div>出错了: {error.message}</div>;
}
5.3 自定义错误类 #
tsx
class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
}
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
if (!post) {
throw new NotFoundError("文章不存在");
}
return json({ post });
}
export function ErrorBoundary() {
const error = useRouteError();
if (error instanceof NotFoundError) {
return <div>找不到资源: {error.message}</div>;
}
return <div>服务器错误</div>;
}
六、类型安全 #
6.1 类型推断 #
tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
return json({ post });
}
export default function Post() {
// 自动推断类型
const { post } = useLoaderData<typeof loader>();
return <div>{post.title}</div>;
}
6.2 定义返回类型 #
tsx
interface LoaderData {
post: Post;
comments: Comment[];
}
export async function loader({ params }: LoaderFunctionArgs): Promise<Response> {
const [post, comments] = await Promise.all([
getPost(params.id),
getComments(params.id),
]);
return json<LoaderData>({ post, comments });
}
七、加载状态 #
7.1 使用useNavigation #
tsx
import { useNavigation, useLoaderData } from "@remix-run/react";
export default function Page() {
const navigation = useNavigation();
const data = useLoaderData<typeof loader>();
const isLoading = navigation.state === "loading";
return (
<div>
{isLoading && <div className="loading">加载中...</div>}
<div>{data.content}</div>
</div>
);
}
7.2 使用useFetcher #
tsx
import { useFetcher } from "@remix-run/react";
export default function Page() {
const fetcher = useFetcher<typeof loader>();
return (
<div>
<button onClick={() => fetcher.load("/api/data")}>
刷新数据
</button>
{fetcher.state === "loading" && <div>加载中...</div>}
{fetcher.data && (
<div>{fetcher.data.message}</div>
)}
</div>
);
}
八、条件加载 #
8.1 用户认证 #
tsx
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
if (!user) {
throw redirect("/login");
}
const data = await getPrivateData(user.id);
return json({ user, data });
}
8.2 权限检查 #
tsx
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUser(request);
if (user.role !== "admin") {
throw new Response("无权限", { status: 403 });
}
const data = await getAdminData();
return json({ data });
}
九、数据缓存 #
9.1 缓存控制 #
tsx
export async function loader() {
const data = await fetchData();
return json(data, {
headers: {
"Cache-Control": "public, max-age=60",
},
});
}
9.2 使用Headers对象 #
tsx
export async function loader() {
const data = await fetchData();
return json(data, {
headers: new Headers({
"Cache-Control": "public, max-age=3600",
"X-Custom-Header": "value",
}),
});
}
十、最佳实践 #
10.1 数据验证 #
tsx
import { z } from "zod";
const paramsSchema = z.object({
id: z.string().uuid(),
});
export async function loader({ params }: LoaderFunctionArgs) {
const result = paramsSchema.safeParse(params);
if (!result.success) {
throw new Response("无效参数", { status: 400 });
}
const post = await getPost(result.data.id);
return json({ post });
}
10.2 并行请求 #
tsx
export async function loader({ params }: LoaderFunctionArgs) {
const [post, comments, author] = await Promise.all([
getPost(params.id),
getComments(params.id),
getAuthor(params.id),
]);
return json({ post, comments, author });
}
10.3 错误处理封装 #
tsx
export async function loader({ params }: LoaderFunctionArgs) {
try {
const post = await getPost(params.id);
return json({ post });
} catch (error) {
if (error instanceof NotFoundError) {
throw new Response("文章不存在", { status: 404 });
}
throw error;
}
}
十一、总结 #
本章我们学习了:
- Loader定义:使用
loader函数加载数据 - 参数获取:params、request、context
- 响应类型:json、redirect、自定义响应
- 错误处理:抛出错误和错误边界
- 类型安全:使用 TypeScript 类型推断
核心要点:
- Loader 在服务端执行,数据在渲染前准备好
- 使用
json返回 JSON 响应 - 使用
throw处理错误情况 - 始终验证输入参数
最后更新:2026-03-28