loader
每个路由都可以定义一个 loader
函数,用于在渲染时向路由提供数据。
¥Each route can define a loader
function that provides data to the route when rendering.
import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async () => {
return json({ ok: true });
};
此函数仅在服务器端运行。在初始服务器渲染时,它会将数据提供给 HTML 文档。在浏览器中导航时,Remix 将通过浏览器中的 fetch
调用该函数。
¥This function is only ever run on the server. On the initial server render, it will provide data to the HTML document. On navigations in the browser, Remix will call the function via fetch
from the browser.
这意味着你可以直接与数据库通信,使用仅限服务器的 API 密钥等。任何未用于渲染 UI 的代码都将从浏览器打包包中删除。
¥This means you can talk directly to your database, use server-only API secrets, etc. Any code not used to render the UI will be removed from the browser bundle.
以数据库 ORM Prisma 为例:
¥Using the database ORM Prisma as an example:
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
中使用,因此它将被编译器从浏览器包中删除,如高亮的行所示。
¥Because prisma
is only used in the loader
it will be removed from the browser bundle by the compiler, as illustrated by the highlighted lines.
loader
返回的任何内容都将暴露给客户端。请像对待公共 API 端点一样谨慎对待你的 loader
。
¥Type Safety
你可以使用 useLoaderData<typeof loader>
通过网络为你的 loader
和组件获取类型安全。
¥You can get type safety over the network for your loader
and component with useLoaderData<typeof 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.name
will know that it's a string
即使我们向 json
传递了一个日期对象,data.date
也能识别出它是一个字符串。当为客户端转换获取数据时,值会使用 JSON.stringify
通过网络进行序列化,并且类型会感知到这一点。
¥data.date
will also know that it's a string even though we passed a date object to json
. When data is fetched for client transitions, the values are serialized over the network with JSON.stringify
, and the types are aware of that
params
路由参数由路由文件名定义。如果某个片段以 $
开头,例如 $invoiceId
,则该片段 URL 的值将传递给你的 loader
。
¥Route params are defined by route file names. If a segment begins with $
like $invoiceId
, the value from the URL for that segment will be passed to your loader
.
// if the user visits /invoices/123
export async function loader({
params,
}: LoaderFunctionArgs) {
params.invoiceId; // "123"
}
参数主要用于通过 ID 查找记录:
¥Params are mostly useful for looking up records by ID:
// 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);
}
request
这是一个 获取请求 实例。你可以阅读 MDN 文档以了解其所有属性。
¥This is a Fetch Request instance. You can read the MDN docs to see all of its properties.
loader
中最常见的用例是从请求中读取 headers(如 Cookie)和 URL URLSearchParams
:
¥The most common use cases in loader
s are reading headers (like cookies) and URL URLSearchParams
from the request:
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 应用之间差距的方法。
¥This is the context passed in to your server adapter's getLoadContext()
function. It's a way to bridge the gap between the adapter's request/response API with your Remix app.
以 Express 适配器为例:
¥Using the express adapter as an example:
const {
createRequestHandler,
} = require("@remix-run/express");
app.all(
"*",
createRequestHandler({
getLoadContext(req, res) {
// this becomes the loader context
return { expressUser: req.user };
},
})
);
然后你的 loader
就可以访问它了。
¥And then your loader
can access it.
export async function loader({
context,
}: LoaderFunctionArgs) {
const { expressUser } = context;
// ...
}
¥Returning Response Instances
你需要从 loader
返回 获取响应。
¥You need to return a Fetch Response from your loader
.
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
助手 可以简化这一过程,因此你无需自行构建它们,但这两个示例实际上是相同的!
¥Using the json
helper simplifies this, so you don't have to construct them yourself, but these two examples are effectively the same!
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
助手在响应中添加标头或状态码:
¥You can see how json
just does a little of the work to make your loader
a lot cleaner. You can also use the json
helper to add headers or a status code to your response:
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);
};
另请参阅:
¥See also:
¥Throwing Responses in Loaders
除了返回响应之外,你还可以从 loader
中抛出 Response
对象。这允许你突破调用堆栈并执行以下两项操作之一:
¥Along with returning responses, you can also throw Response
objects from your loader
s. This allows you to break through the call stack and do one of two things:
重定向到另一个 URL
¥Redirect to another URL
通过 ErrorBoundary
显示包含上下文数据的替代 UI
¥Show an alternate UI with contextual data through the ErrorBoundary
以下是一个完整的示例,展示了如何创建实用函数,该函数会抛出响应以停止加载器中的代码执行并显示另一个 UI。
¥Here is a full example showing how you can create utility functions that throw responses to stop code execution in the loader and show an alternative UI.
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;
}
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");
}
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>
);
}