name: remix description: Remix模式,包括加载器、操作、嵌套路由、渐进增强和部署策略。 allowed-tools: 读取、写入、编辑、Bash、Glob、Grep
Remix 技能
用于使用Remix构建全栈应用程序的专家级协助。
能力
- 实现用于数据获取的加载器
- 创建用于数据变更的操作
- 配置带出口的嵌套路由
- 构建渐进增强的表单
- 处理错误和边界
- 为各种平台设置部署
使用场景
在以下情况下调用此技能:
- 构建全栈React应用程序
- 实现渐进增强
- 创建带数据的嵌套布局
- 正确处理表单提交
- 部署到边缘平台
输入参数
| 参数 | 类型 | 是否必需 | 描述 |
|---|---|---|---|
| routePath | 字符串 | 是 | 路由路径 |
| hasLoader | 布尔值 | 否 | 包含加载器 |
| hasAction | 布尔值 | 否 | 包含操作 |
| nested | 布尔值 | 否 | 具有嵌套路由 |
路由模式
加载器和操作
// app/routes/users.tsx
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
import { json, redirect } from '@remix-run/node';
import { useLoaderData, Form, useNavigation } from '@remix-run/react';
import { db } from '~/utils/db.server';
import { requireUser } from '~/utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) {
await requireUser(request);
const url = new URL(request.url);
const search = url.searchParams.get('search') || '';
const users = await db.user.findMany({
where: search ? { name: { contains: search } } : undefined,
orderBy: { name: 'asc' },
});
return json({ users, search });
}
export async function action({ request }: ActionFunctionArgs) {
await requireUser(request);
const formData = await request.formData();
const intent = formData.get('intent');
if (intent === 'create') {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
if (!name || !email) {
return json({ error: '姓名和邮箱为必填项' }, { status: 400 });
}
await db.user.create({ data: { name, email } });
return redirect('/users');
}
if (intent === 'delete') {
const id = formData.get('id') as string;
await db.user.delete({ where: { id } });
return json({ success: true });
}
return json({ error: '无效的操作意图' }, { status: 400 });
}
export default function Users() {
const { users, search } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const isSearching = navigation.state === 'loading' &&
navigation.location.pathname === '/users';
return (
<div>
<h1>用户管理</h1>
{/* 搜索表单 - GET请求,渐进增强 */}
<Form method="get">
<input
type="search"
name="search"
defaultValue={search}
placeholder="搜索用户..."
/>
<button type="submit">搜索</button>
</Form>
{/* 创建表单 - POST请求 */}
<Form method="post">
<input type="hidden" name="intent" value="create" />
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="邮箱" required />
<button type="submit">添加用户</button>
</Form>
{isSearching ? (
<p>搜索中...</p>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
<Form method="post" style={{ display: 'inline' }}>
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={user.id} />
<button type="submit">删除</button>
</Form>
</li>
))}
</ul>
)}
</div>
);
}
嵌套路由
// app/routes/dashboard.tsx
import { Outlet, NavLink } from '@remix-run/react';
export default function Dashboard() {
return (
<div className="dashboard">
<nav>
<NavLink to="/dashboard" end>概览</NavLink>
<NavLink to="/dashboard/analytics">分析</NavLink>
<NavLink to="/dashboard/settings">设置</NavLink>
</nav>
<main>
<Outlet />
</main>
</div>
);
}
// app/routes/dashboard._index.tsx
export default function DashboardIndex() {
return <h2>仪表板概览</h2>;
}
// app/routes/dashboard.analytics.tsx
export async function loader() {
const analytics = await getAnalytics();
return json({ analytics });
}
export default function Analytics() {
const { analytics } = useLoaderData<typeof loader>();
return <AnalyticsChart data={analytics} />;
}
错误边界
// app/routes/users.$userId.tsx
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
export async function loader({ params }: LoaderFunctionArgs) {
const user = await db.user.findUnique({
where: { id: params.userId },
});
if (!user) {
throw new Response('用户未找到', { status: 404 });
}
return json({ user });
}
export default function User() {
const { user } = useLoaderData<typeof loader>();
return <UserProfile user={user} />;
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error">
<h2>{error.status} {error.statusText}</h2>
<p>{error.data}</p>
</div>
);
}
return (
<div className="error">
<h2>出错了</h2>
<p>{error instanceof Error ? error.message : '未知错误'}</p>
</div>
);
}
乐观UI
// app/routes/todos.tsx
import { useFetcher } from '@remix-run/react';
function TodoItem({ todo }: { todo: Todo }) {
const fetcher = useFetcher();
const isDeleting = fetcher.state !== 'idle' &&
fetcher.formData?.get('intent') === 'delete';
const isToggling = fetcher.state !== 'idle' &&
fetcher.formData?.get('intent') === 'toggle';
// 乐观状态
const completed = isToggling
? !todo.completed
: todo.completed;
if (isDeleting) return null;
return (
<li style={{ opacity: fetcher.state !== 'idle' ? 0.5 : 1 }}>
<fetcher.Form method="post">
<input type="hidden" name="id" value={todo.id} />
<input type="hidden" name="intent" value="toggle" />
<button type="submit">
{completed ? '✓' : '○'}
</button>
</fetcher.Form>
<span>{todo.title}</span>
<fetcher.Form method="post">
<input type="hidden" name="id" value={todo.id} />
<input type="hidden" name="intent" value="delete" />
<button type="submit">×</button>
</fetcher.Form>
</li>
);
}
会话和身份验证
// app/utils/session.server.ts
import { createCookieSessionStorage, redirect } from '@remix-run/node';
const sessionStorage = createCookieSessionStorage({
cookie: {
name: '__session',
httpOnly: true,
path: '/',
sameSite: 'lax',
secrets: [process.env.SESSION_SECRET!],
secure: process.env.NODE_ENV === 'production',
},
});
export async function createUserSession(userId: string, redirectTo: string) {
const session = await sessionStorage.getSession();
session.set('userId', userId);
return redirect(redirectTo, {
headers: {
'Set-Cookie': await sessionStorage.commitSession(session),
},
});
}
export async function getUserId(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get('Cookie')
);
return session.get('userId');
}
export async function requireUser(request: Request) {
const userId = await getUserId(request);
if (!userId) {
throw redirect('/login');
}
return userId;
}
export async function logout(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get('Cookie')
);
return redirect('/login', {
headers: {
'Set-Cookie': await sessionStorage.destroySession(session),
},
});
}
最佳实践
- 使用加载器处理GET请求(数据获取)
- 使用操作处理POST/PUT/DELETE(数据变更)
- 利用Form实现渐进增强
- 使用useFetcher进行非导航变更
- 实现适当的错误边界
目标流程
- remix全栈开发
- 渐进增强
- 边缘部署
- 表单处理