入门教程 - 全栈 Web 应用开发

知识库
知识库文档
/tech-stacks/remix/tutorial/入门教程 - 全栈 Web 应用开发.md

文档

Remix 入门教程:全栈 Web 应用开发

一、Remix 是什么?

Remix 是一个基于 Web 标准的全栈框架,核心理念是"浏览器就是你的运行时"。它由 React Router 团队创建,后被 Shopify 收购。与 Next.js 不同,Remix 拥抱 window.fetchFormDataResponse 等原生 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>
  );
}

七、最佳实践

  1. 充分利用表单——不要手动 fetch + useState 做 CRUD
  2. 善用嵌套路由——避免全局 loading,每个模块独立加载
  3. SEO 优化:Remix 默认 SSR,用 meta 导出函数设置 <title>
  4. 环境变量通过 process.env 在 loader/action 中访问,不会泄漏到客户端

八、思考题

  1. Remix 的 loader 和 React 的 useEffect 数据加载有什么本质区别?
  2. 为什么 Remix 推荐用 HTML <form> 而不是 onClick + fetch 做数据变更?
  3. 嵌套路由中,子路由报错如何不影响父布局?

信息

路径
/tech-stacks/remix/tutorial/入门教程 - 全栈 Web 应用开发.md
更新时间
2026/5/31