一、为什么是 Remix:SSR 与渐进增强
Remix 是当下备受关注的全栈 React 框架。它把数据加载交给服务端的 Loader、把表单提交交给 Action,并默认服务端渲染(SSR)与渐进增强。本文将用最小可运行示例,带你从 0 跑通一个带数据库的 Remix 全栈应用,并给出 Docker 与 CI 上线方案。
Remix 的核心理念是「在 Web 标准之上构建」。路由即文件,每个路由都能导出 loader 与 action 两个函数:loader 在服务端运行,负责读取数据;action 同样在服务端运行,负责处理 POST 等变更请求。前端通过 <Form> 提交时,Remix 会自动调用对应路由的 action,无需手写 fetch。
对比传统纯客户端 SPA,Remix 首屏由服务端渲染,SEO 与首屏速度更好;对比手写 SSR,它用约定式 Loader/Action 省去了大量胶水代码。更重要的是,Remix 把「表单」这一最朴素的 Web 能力重新放回一等公民的位置:你写的 <Form> 在没有 JavaScript 时依然能工作,这正是渐进增强(Progressive Enhancement)的精髓——网络慢、JS 没加载完,核心功能也不受影响。
三者差异如下:
| 维度 | 传统 SPA | Next.js | Remix |
|---|---|---|---|
| 数据获取 | 客户端 fetch | getServerSideProps / RSC | 路由级 loader |
| 表单提交 | 手写 fetch | 路由 handler | 路由级 action |
| 渐进增强 | 弱 | 中 | 强 |
| 嵌套路由 | 手动拼装 | 支持 | 原生嵌套 + 并行 |
二、快速起步:用 Vite 创建 Remix 项目
Remix 官方已全面迁移到 Vite 构建。一条命令即可初始化,推荐在 VS Code 高效前端开发配置 的基础上安装官方 Remix 扩展,获得路由跳转与类型提示。
# 使用官方模板创建项目
npx create-remix@latest my-remix-app
cd my-remix-app
npm install
npm run dev
启动后访问 http://localhost:3000 即可看到欢迎页。目录约定很简单:app/routes/ 下的每个文件对应一个路由,_index.tsx 是首页。动态段用 $ 前缀,例如 routes/posts.$id.tsx 匹配 /posts/123。路由文件里除了默认导出的组件,还可以导出 loader、action、meta、ErrorBoundary 等,约定即能力。
三、Loader:在服务端加载数据
loader 是 Remix 数据加载的核心。它永远在服务端执行,返回的 json() 会自动序列化为前端可用的数据,useLoaderData 拿到的类型由 loader 推断,无需重复声明接口类型。
// app/routes/posts.$id.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
if (!res.ok) throw new Response("Not Found", { status: 404 });
return json(await res.json());
}
export default function Post() {
const post = useLoaderData();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
并行加载与错误边界
当某个 loader 抛错时,Remix 会渲染该路由的 ErrorBoundary。配合嵌套路由,父级与子级 loader 会并行执行、互不影响,避免「一个慢接口拖垮整页」。
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <div>{error.status}:{error.statusText}</div>;
}
return <div>出错了:{(error as Error).message}</div>;
}
四、嵌套路由:并行数据加载与布局复用
这是 Remix 区别于多数框架的杀手锏。一个 URL 往往对应多段 UI,例如 /dashboard/orders:左侧是用户信息与导航,右侧是订单列表。在 Remix 中,你可以把它拆成 dashboard.tsx(布局 + 自己的 loader 取用户信息)与 dashboard.orders.tsx(子路由取订单)。两者各自独立加载、互不阻塞,父布局在子路由切换时不会重新挂载,导航丝滑且不会闪屏。
// app/routes/dashboard.tsx —— 布局 + 并行取用户信息
export async function loader() {
const user = await getUser();
return json({ user });
}
export default function Dashboard() {
const { user } = useLoaderData();
return (
<div className="layout">
<aside>{user.name} 的后台</aside>
<main><Outlet /></main> {/* 子路由渲染在这里 */}
</div>
);
}
当用户在子路由间跳转,父级 loader 不会重跑,只有目标子路由重新加载,配合 <Outlet /> 实现局部刷新。这种「布局复用 + 数据隔离」的模型,特别适合中后台与仪表盘类应用。
五、Action:处理表单提交
Remix 的 <Form> 组件在不写任何 fetch 的情况下就能提交到当前路由的 action。更妙的是它天然支持渐进增强——即使浏览器关闭 JavaScript,表单仍能正常提交。
// app/routes/contact.tsx
import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const email = String(form.get("email") || "");
if (!email.includes("@")) {
return json({ error: "邮箱格式不正确" }, { status: 400 });
}
// 这里写库 / 发邮件 / 调接口
return redirect("/thanks");
}
export default function Contact() {
const data = useActionData();
return (
<Form method="post">
<input name="email" type="email" placeholder="you@example.com" />
<button type="submit">订阅</button>
{data?.error && <p>{data.error}</p>}
</Form>
);
}
六、真实案例:用 Remix 搭一个待办后台
假设我们用 Prisma + SQLite 持久化。在 loader 中读取列表,在 action 中新增一条,整个 CRUD 闭环都在路由文件内完成。
// app/routes/todos.tsx
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { db } from "~/utils/db.server";
export async function loader() {
const todos = await db.todo.findMany({ orderBy: { id: "desc" } });
return json({ todos });
}
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const title = String(form.get("title") || "").trim();
if (title) await db.todo.create({ data: { title } });
return json({ ok: true });
}
部署时建议用 Docker 多阶段构建实战 产出精简镜像,并接入 GitHub Actions 持续集成实战 自动构建与推送,实现「提交即上线」。
七、客户端导航与水合:和 SPA 到底差在哪
Remix 默认是「服务端渲染 + 客户端 hydration」。首屏 HTML 由服务端直接吐出,浏览器拿到即可显示;随后 React 在客户端「水合」,接管后续交互。点击链接时,Remix 通过 <Link> 做客户端导航,只向服务端请求目标路由的 loader 数据(而非整页 HTML),再局部更新——既保留了 SPA 的流畅,又避免了 SPA「白屏等 JS」的问题。
对慢数据,还能用 defer 让首屏先返回关键内容,把重数据以 Promise 形式延迟到客户端再 Await 解析,实现「骨架先出、内容后补」。这套机制让 Remix 在 SEO 与体验之间取得很好的平衡,是它相对纯 SPA 的核心优势。
八、部署与上线要点
Remix 是标准的 Node 服务,可直接用多阶段 Docker 构建,再交给任意容器平台运行。
# Dockerfile —— 多阶段构建
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS run
WORKDIR /app
COPY --from=build /app /app
EXPOSE 3000
CMD ["npm", "run", "start"]
配套的 CI 也很简单:安装依赖、构建、登录镜像仓库、推送,最后在服务器拉取新镜像重启。借助 GitHub Actions 持续集成实战 的流水线,开发者只需 push 代码,Remix 应用就会自动上线。
- 生产构建:执行
remix vite:build产出 server/client,再用 Node 启动,或部署到任意支持 Node 的平台。 - 环境变量:数据库连接串、密钥一律走环境变量,切勿硬编码进仓库。
- 流式渲染:对慢接口使用
defer+Await,首屏先吐骨架、数据到达再填充,体验接近 SPA 但 SEO 更佳。
九、避坑与最佳实践
- loader 里别用浏览器 API:loader/action 跑在服务端,
document、window均不可用。 - 大列表必须分页:loader 内做好
limit/offset,避免一次性查全表把数据库打爆。 - 重计算走缓存:热点数据用内存或 Redis 缓存,别让每个请求都回源数据库。
- 表单校验两端都做:action 内必须校验(安全底线),前端校验只是体验优化。
- 善用 loader 抛错:用
throw new Response(...)表达 4xx/5xx,让 ErrorBoundary 优雅兜底。
十、总结
Remix 用「路由级 loader/action + 服务端渲染 + 渐进增强」把全栈 React 开发变得异常顺手。它不追求把所有逻辑搬进客户端,而是尊重 HTTP 与 Web 标准:表单就是表单,链接就是链接,数据加载有明确边界。如果你受够了在 SPA 里手写一堆 fetch 与 loading 态,Remix 值得一试。下一步可以把本文的待办后台接进 Docker 与 CI,把开发到上线的链路完全自动化,把精力真正放在业务上。




