跳转到内容

loader

loader

观看 📼 Remix Single将数据加载到组件中

每个路由可以定义一个 loader 函数,在渲染时向路由提供数据。

import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async () => {
return json({ ok: true });
};

此函数仅在服务器上运行。在初始服务器渲染时,它将向 HTML 文档提供数据。在浏览器中导航时,Remix 将通过浏览器中的 fetch 调用该函数。

这意味着您可以直接与数据库对话,使用仅限服务器的 API 机密等。任何未用于呈现 UI 的代码都将从浏览器包中删除。

以数据库 ORM Prisma为例:

import { useLoaderData } from "@remix-run/react";
import { prisma } from "../db";
export async function loader() {
return json(await prisma.user.findMany());
}
export default function Users() {
const data = useLoaderData<typeof loader>();
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

因为 prisma 仅在 loader 中使用,所以它将被编译器从浏览器包中删除,如突出显示的行所示。

请注意,即使组件不渲染它,从`加载器`返回的任何内容都将暴露给客户端。请像对待公共 API 端点一样小心对待您的`加载器`。

类型安全

您可以使用 useLoaderData<typeof loader> 通过网络为您的 loader 和组件获得类型安全。

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader() {
return json({ name: "Ryan", date: new Date() });
}
export default function SomeRoute() {
const data = useLoaderData<typeof loader>();
}
  • data.name 会知道它是一个字符串
  • data.date 也会知道它是一个字符串,即使我们向 json 传递了一个日期对象。当为客户端转换获取数据时,这些值会通过 JSON.stringify 在网络上序列化,并且类型知道这一点

params

路由参数由路由文件名定义。如果某个段以 $ 开头,如 $invoiceId,则该段的 URL 值将传递给您的加载器

// app/routes/invoices.$invoiceId.tsx
// if the user visits /invoices/123
export async function loader({
params,
}: LoaderFunctionArgs) {
params.invoiceId; // "123"
}

参数主要用于通过 ID 查找记录:

// app/routes/invoices.$invoiceId.tsx
// if the user visits /invoices/123
export async function loader({
params,
}: LoaderFunctionArgs) {
const invoice = await fakeDb.getInvoice(params.invoiceId);
if (!invoice) throw new Response("", { status: 404 });
return json(invoice);
}

请求

这是一个 Fetch Request 实例。您可以阅读 MDN 文档以查看其所有属性。

loader 中最常见的用例是从请求中读取 headers(如 cookies)和 URL URLSearchParams

export async function loader({
request,
}: LoaderFunctionArgs) {
// read a cookie
const cookie = request.headers.get("Cookie");
// parse the search params for `?q=`
const url = new URL(request.url);
const query = url.searchParams.get("q");
}

context

这是传递到服务器适配器的 getLoadContext() 函数的上下文。这是弥补适配器请求 / 响应 API 与 Remix 应用之间差距的一种方法。

此 API 是一个紧急出口,很少需要它

以 express 适配器为例:

server.ts
const {
createRequestHandler,
} = require("@remix-run/express");
app.all(
"*",
createRequestHandler({
getLoadContext(req, res) {
// this becomes the loader context
return { expressUser: req.user };
},
})
);

然后你的 loader 就可以访问它了。

app/routes/some-route.tsx
export async function loader({
context,
}: LoaderFunctionArgs) {
const { expressUser } = context;
// ...
}

返回响应实例

您需要从加载器返回一个 获取响应

export async function loader() {
const users = await db.users.findMany();
const body = JSON.stringify(users);
return new Response(body, {
headers: {
"Content-Type": "application/json",
},
});
}

使用 json 助手 简化了这个过程,所以您不必自己构建它们,但这两个示例实际上是相同的!

import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async () => {
const users = await fakeDb.users.findMany();
return json(users);
};

您可以看到 json 如何完成少量工作,让您的 loader 更加简洁。您还可以使用 json 助手向您的响应添加标头或状态代码:

import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
const project = await fakeDb.project.findOne({
where: { id: params.id },
});
if (!project) {
return json("Project not found", { status: 404 });
}
return json(project);
};

参见:

在 Loaders 中抛出响应

除了返回响应之外,您还可以从加载器中抛出 Response 对象。这允许您突破调用堆栈并执行以下两项操作之一:

  • 重定向到另一个 URL
  • 通过 ErrorBoundary 显示带有上下文数据的替代 UI

这是一个完整的示例,展示了如何创建实用函数来抛出响应以停止加载器中的代码执行并显示备用 UI。

app/db.ts
import { json } from "@remix-run/node"; // or cloudflare/deno
export function getInvoice(id) {
const invoice = db.invoice.find({ where: { id } });
if (invoice === null) {
throw json("Not Found", { status: 404 });
}
return invoice;
}
app/http.ts
import { redirect } from "@remix-run/node"; // or cloudflare/deno
import { getSession } from "./session";
export async function requireUserSession(request) {
const session = await getSession(
request.headers.get("cookie")
);
if (!session) {
// You can throw our helpers like `redirect` and `json` because they
// return `Response` objects. A `redirect` response will redirect to
// another URL, while other responses will trigger the UI rendered
// in the `ErrorBoundary`.
throw redirect("/login", 302);
}
return session.get("user");
}
// app/routes/invoice.$invoiceId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import {
isRouteErrorResponse,
useLoaderData,
useRouteError,
} from "@remix-run/react";
import { getInvoice } from "~/db";
import { requireUserSession } from "~/http";
export const loader = async ({
params,
request,
}: LoaderFunctionArgs) => {
const user = await requireUserSession(request);
const invoice = getInvoice(params.invoiceId);
if (!invoice.userIds.includes(user.id)) {
throw json(
{ invoiceOwnerEmail: invoice.owner.email },
{ status: 401 }
);
}
return json(invoice);
};
export default function InvoiceRoute() {
const invoice = useLoaderData<typeof loader>();
return <InvoiceView invoice={invoice} />;
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
switch (error.status) {
case 401:
return (
<div>
<p>You don't have access to this invoice.</p>
<p>
Contact {error.data.invoiceOwnerEmail} to get
access
</p>
</div>
);
case 404:
return <div>Invoice not found!</div>;
}
return (
<div>
Something went wrong: {error.status}{" "}
{error.statusText}
</div>
);
}
return (
<div>
Something went wrong:{" "}
{error?.message || "Unknown Error"}
</div>
);
}