关于 remix
remix 是使用 Remix、React 和 TypeScript 构建全栈应用的开发技能,提供路由、数据加载、变更、会话和性能方面的实践模式。
主要功能
- 01Loader 与 action 模式
- 02嵌套路由与无路径路由
- 03表单处理与 useFetcher
- 04错误边界与资源路由
- 05会话和性能建议
适用场景
- 01组织应用的嵌套路由
- 02处理服务端表单和数据变更
- 03为 Loader 添加身份验证
- 04完善加载与错误状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill remix安装后在 Claude Code 中输入/remix确认已加载。
详细介绍
Remix 开发
你是 Remix、React、TypeScript 和全栈 Web 开发方面的专家。
核心原则
- 编写简洁、专业的 Remix 代码,并提供准确的 TypeScript 示例
- 拥抱渐进增强和 Web 标准
- 使用加载器获取数据,使用操作处理变更
- 利用嵌套路由组织代码
- 优先采用服务器端渲染和 Web 基础功能
项目结构
app/
├── components/ # Reusable React components
├── models/ # Database models and types
├── routes/
│ ├── _index.tsx # / route
│ ├── about.tsx # /about route
│ └── posts/
│ ├── _index.tsx # /posts route
│ └── $slug.tsx # /posts/:slug route
├── styles/ # CSS files
├── utils/ # Utility functions
├── entry.client.tsx # Client entry
├── entry.server.tsx # Server entry
└── root.tsx # Root layout
加载器
基本加载器
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug);
if (!post) {
throw new Response('Not Found', { status: 404 });
}
return json({ post });
}
export default function PostRoute() {
const { post } = useLoaderData<typeof loader>();
return <article>{post.content}</article>;
}
带身份验证的加载器
import { redirect } from '@remix-run/node';
import { getUser } from '~/utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
if (!user) {
throw redirect('/login');
}
return json({ user });
}
操作
表单处理
import type { ActionFunctionArgs } from '@remix-run/node';
import { json, redirect } from '@remix-run/node';
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
const errors: Record<string, string> = {};
if (!title) {
errors.title = 'Title is required';
}
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
const post = await createPost({ title, content });
return redirect(`/posts/${post.slug}`);
}
使用操作数据
import { useActionData, Form } from '@remix-run/react';
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<input name="title" type="text" />
{actionData?.errors?.title && (
<p className="error">{actionData.errors.title}</p>
)}
<textarea name="content" />
<button type="submit">Create Post</button>
</Form>
);
}
嵌套路由
布局路由
// routes/dashboard.tsx (layout)
import { Outlet } from '@remix-run/react';
export default function DashboardLayout() {
return (
<div className="dashboard">
<nav>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
<main>
<Outlet />
</main>
</div>
);
}
无路径布局
// routes/_auth.tsx (pathless layout for /login, /register)
export default function AuthLayout() {
return (
<div className="auth-container">
<Outlet />
</div>
);
}
useFetcher
非导航式获取
import { useFetcher } from '@remix-run/react';
export default function LikeButton({ postId }: { postId: string }) {
const fetcher = useFetcher();
const isLiking = fetcher.state !== 'idle';
return (
<fetcher.Form method="post" action="/api/like">
<input type="hidden" name="postId" value={postId} />
<button type="submit" disabled={isLiking}>
{isLiking ? 'Liking...' : 'Like'}
</button>
</fetcher.Form>
);
}
乐观 UI
export default function TodoItem({ todo }: { todo: Todo }) {
const fetcher = useFetcher();
const isDeleting = fetcher.formData?.get('_action') === 'delete';
if (isDeleting) {
return null; // Optimistically remove
}
return (
<li>
{todo.title}
<fetcher.Form method="post">
<input type="hidden" name="_action" value="delete" />
<input type="hidden" name="id" value={todo.id} />
<button type="submit">Delete</button>
</fetcher.Form>
</li>
);
}
错误边界
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div>
<h1>Error</h1>
<p>{error instanceof Error ? error.message : 'Unknown error'}</p>
</div>
);
}
资源路由
// routes/api/posts.tsx
import { json } from '@remix-run/node';
export async function loader() {
const posts = await getPosts();
return json(posts);
}
// No default export = resource route (no UI)
Meta 和 Links
import type { MetaFunction, LinksFunction } from '@remix-run/node';
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data?.post.title ?? 'Blog' },
{ name: 'description', content: data?.post.excerpt },
];
};
export const links: LinksFunction = () => {
return [
{ rel: 'stylesheet', href: styles },
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
];
};
会话管理
// 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),
},
});
}
性能
- 使用
<Link prefetch="intent">进行预取 - 使用
defer流式传输数据 - 使用标头实现 stale-while-revalidate
- 使用动态导入进行代码拆分
- 适当缓存加载器响应
最佳实践
- 始终在服务器上验证表单数据
- 使用 TypeScript 确保类型安全
- 处理加载和错误状态
- 实施适当的 CSRF 保护
- 使用渐进增强
- 在禁用 JavaScript 的情况下进行测试
评论
支持 Markdown