文档
Remix 入门教程:全栈 Web 应用开发
一、Remix 是什么?
Remix 是一个基于 Web 标准的全栈框架,核心理念是"浏览器就是你的运行时"。它由 React Router 团队创建,后被 Shopify 收购。与 Next.js 不同,Remix 拥抱 window.fetch、FormData、Response 等原生 API,让你写更少的"框架魔法代码"。
Remix 的核心优势
- 嵌套路由:页面自然分层,并行加载数据,无需手动管理 loading
- Mutation 即表单:用 HTML
<form>做数据变更,无需 useState + fetch - 错误边界:每层路由都有独立 error boundary,一个模块崩溃不影响其他
- 渐进增强:禁用 JS 时,表单仍能提交(传统 HTML 方式)
二、路由系统
Remix 使用文件系统路由,app/routes/ 下的文件对应 URL:
app/routes/
├── _index.tsx → /
├── about.tsx → /about
├── posts.$postId.tsx → /posts/:postId(动态参数)
├── admin/
│ ├── _index.tsx → /admin
│ └── users.tsx → /admin/users
└── _layout.tsx → 包裹子路由的布局(不参与 URL)
关键约定
| 文件名 | URL | 说明 |
|---|---|---|
_index.tsx |
父路径的 index | 下划线表示"不参与 URL" |
$param.tsx |
动态段 | $ 前缀表示动态参数 |
_.tsx |
通配 | 捕获所有剩余路径 |
三、数据加载(Loader)
Loader 只在服务端运行,组件渲染前数据已就绪:
// app/routes/products.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ request, params }) {
const url = new URL(request.url);
const page = url.searchParams.get("page") || "1";
const products = await db.product.findMany({
skip: (Number(page) - 1) * 20,
take: 20,
});
return json({ products, page });
}
export default function Products() {
const { products, page } = useLoaderData<typeof loader>();
// 数据已就绪,不会出现 loading 闪烁
return <ProductGrid items={products} />;
}
四、数据变更(Action)
用原生 <form> 标签,无需 JavaScript:
import { redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
export async function action({ request }) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
// 服务端校验
if (!title || title.length < 3) {
return json({ error: "标题至少3个字符" }, { status: 400 });
}
await db.post.create({ data: { title, content } });
return redirect("/posts");
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<input name="title" placeholder="标题" />
{actionData?.error && <p style={{color:'red'}}>{actionData.error}</p>}
<textarea name="content" placeholder="内容" />
<button type="submit">发布</button>
</Form>
);
}
五、嵌套布局与 Outlet
// app/routes/_layout.tsx
export default function Layout() {
return (
<div>
<Navbar /> {/* 所有子路由共享 */}
<Sidebar />
<main>
<Outlet /> {/* 子路由内容在这里渲染 */}
</main>
</div>
);
}
六、错误处理
// 每个路由都可以导出 ErrorBoundary
export function ErrorBoundary() {
const error = useRouteError();
return (
<div style={{ textAlign: "center", padding: 40 }}>
<h1>出错了</h1>
<p>{error.message}</p>
<Link to="/">回到首页</Link>
</div>
);
}
七、最佳实践
- 充分利用表单——不要手动 fetch + useState 做 CRUD
- 善用嵌套路由——避免全局 loading,每个模块独立加载
- SEO 优化:Remix 默认 SSR,用
meta导出函数设置<title>等 - 环境变量通过
process.env在 loader/action 中访问,不会泄漏到客户端
八、思考题
- Remix 的 loader 和 React 的 useEffect 数据加载有什么本质区别?
- 为什么 Remix 推荐用 HTML
<form>而不是onClick+fetch做数据变更? - 嵌套路由中,子路由报错如何不影响父布局?