Remix布局组件 #
一、布局概述 #
布局组件是 Remix 应用中复用 UI 结构的核心方式。通过合理的布局设计,可以保持代码整洁和一致性。
二、基础布局模式 #
2.1 根布局 #
root.tsx 是应用的根布局:
tsx
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export default function App() {
return (
<html lang="zh-CN">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body className="bg-gray-50">
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
2.2 页面布局 #
创建共享的页面布局:
tsx
// app/routes/_layout.tsx
import { Outlet, Link } from "@remix-run/react";
export default function PageLayout() {
return (
<div className="min-h-screen flex flex-col">
<header className="bg-blue-600 text-white p-4">
<nav className="container mx-auto flex justify-between">
<Link to="/" className="font-bold text-xl">
我的网站
</Link>
<div className="space-x-4">
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/contact">联系</Link>
</div>
</nav>
</header>
<main className="flex-1 container mx-auto p-6">
<Outlet />
</main>
<footer className="bg-gray-800 text-white p-4 text-center">
<p>© 2024 我的网站</p>
</footer>
</div>
);
}
三、条件布局 #
3.1 认证布局 #
区分已登录和未登录用户的布局:
tsx
// app/routes/_auth.tsx
import { Outlet, Link, useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
return json({ user });
}
export default function AuthLayout() {
const { user } = useLoaderData<typeof loader>();
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl mb-4">请先登录</h1>
<Link to="/login" className="text-blue-600">
前往登录
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen">
<header className="bg-white shadow p-4">
<div className="container mx-auto flex justify-between">
<span>欢迎, {user.name}</span>
<form action="/logout" method="post">
<button type="submit">退出</button>
</form>
</div>
</header>
<main className="container mx-auto p-6">
<Outlet context={{ user }} />
</main>
</div>
);
}
3.2 管理后台布局 #
tsx
// app/routes/admin.tsx
import { Outlet, Link, useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
if (!user || user.role !== "admin") {
return redirect("/login");
}
return json({ user });
}
export default function AdminLayout() {
const { user } = useLoaderData<typeof loader>();
return (
<div className="flex min-h-screen">
<aside className="w-64 bg-gray-900 text-white">
<div className="p-4 border-b border-gray-700">
<h2 className="font-bold">管理后台</h2>
<p className="text-sm text-gray-400">{user.name}</p>
</div>
<nav className="p-4 space-y-2">
<Link to="/admin" className="block p-2 hover:bg-gray-800 rounded">
仪表板
</Link>
<Link to="/admin/users" className="block p-2 hover:bg-gray-800 rounded">
用户管理
</Link>
<Link to="/admin/posts" className="block p-2 hover:bg-gray-800 rounded">
文章管理
</Link>
<Link to="/admin/settings" className="block p-2 hover:bg-gray-800 rounded">
系统设置
</Link>
</nav>
</aside>
<div className="flex-1 flex flex-col">
<header className="bg-white shadow p-4">
<div className="flex justify-between items-center">
<h1 className="text-xl font-bold">管理后台</h1>
<Link to="/" className="text-blue-600">
返回前台
</Link>
</div>
</header>
<main className="flex-1 p-6 bg-gray-100">
<Outlet />
</main>
</div>
</div>
);
}
四、多布局切换 #
4.1 不同页面使用不同布局 #
text
app/routes/
├── _index.tsx # 使用默认布局
├── _marketing.tsx # 营销页面布局
├── _marketing.pricing.tsx # /pricing
├── _marketing.features.tsx # /features
├── _app.tsx # 应用布局
├── _app.dashboard.tsx # /dashboard
└── _app.profile.tsx # /profile
4.2 营销布局 #
tsx
// app/routes/_marketing.tsx
import { Outlet, Link } from "@remix-run/react";
export default function MarketingLayout() {
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
<header className="container mx-auto p-6">
<nav className="flex justify-between items-center">
<Link to="/" className="text-2xl font-bold text-blue-600">
产品名称
</Link>
<div className="space-x-6">
<Link to="/features" className="text-gray-600 hover:text-gray-900">
功能
</Link>
<Link to="/pricing" className="text-gray-600 hover:text-gray-900">
定价
</Link>
<Link
to="/login"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
开始使用
</Link>
</div>
</nav>
</header>
<main>
<Outlet />
</main>
<footer className="bg-gray-900 text-white p-8 mt-16">
<div className="container mx-auto">
<div className="grid grid-cols-4 gap-8">
<div>
<h3 className="font-bold mb-4">产品</h3>
<ul className="space-y-2 text-gray-400">
<li><Link to="/features">功能</Link></li>
<li><Link to="/pricing">定价</Link></li>
</ul>
</div>
<div>
<h3 className="font-bold mb-4">公司</h3>
<ul className="space-y-2 text-gray-400">
<li><Link to="/about">关于我们</Link></li>
<li><Link to="/contact">联系我们</Link></li>
</ul>
</div>
</div>
</div>
</footer>
</div>
);
}
五、布局组件封装 #
5.1 可复用布局组件 #
tsx
// app/components/layout/PageLayout.tsx
import { ReactNode } from "react";
interface PageLayoutProps {
children: ReactNode;
title?: string;
actions?: ReactNode;
}
export function PageLayout({ children, title, actions }: PageLayoutProps) {
return (
<div className="bg-white rounded-lg shadow">
{title && (
<div className="flex justify-between items-center p-4 border-b">
<h1 className="text-xl font-bold">{title}</h1>
{actions && <div>{actions}</div>}
</div>
)}
<div className="p-4">
{children}
</div>
</div>
);
}
5.2 使用布局组件 #
tsx
import { PageLayout } from "~/components/layout/PageLayout";
import { Link } from "@remix-run/react";
export default function UsersPage() {
return (
<PageLayout
title="用户管理"
actions={
<Link to="/admin/users/new" className="bg-blue-600 text-white px-4 py-2 rounded">
添加用户
</Link>
}
>
<table className="w-full">
<thead>
<tr>
<th>姓名</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{/* 用户列表 */}
</tbody>
</table>
</PageLayout>
);
}
六、侧边栏布局 #
6.1 可折叠侧边栏 #
tsx
import { useState } from "react";
import { Outlet, Link } from "@remix-run/react";
export default function SidebarLayout() {
const [collapsed, setCollapsed] = useState(false);
return (
<div className="flex min-h-screen">
<aside
className={`bg-gray-900 text-white transition-all duration-300 ${
collapsed ? "w-16" : "w-64"
}`}
>
<div className="p-4 flex justify-between items-center">
{!collapsed && <span className="font-bold">菜单</span>}
<button onClick={() => setCollapsed(!collapsed)}>
{collapsed ? "展开" : "收起"}
</button>
</div>
<nav className="p-2">
<Link
to="/"
className="flex items-center p-2 hover:bg-gray-800 rounded"
>
<span className="icon">🏠</span>
{!collapsed && <span className="ml-2">首页</span>}
</Link>
<Link
to="/dashboard"
className="flex items-center p-2 hover:bg-gray-800 rounded"
>
<span className="icon">📊</span>
{!collapsed && <span className="ml-2">仪表板</span>}
</Link>
</nav>
</aside>
<main className="flex-1 p-6">
<Outlet />
</main>
</div>
);
}
七、响应式布局 #
7.1 移动端适配 #
tsx
import { useState } from "react";
import { Outlet, Link } from "@remix-run/react";
export default function ResponsiveLayout() {
const [menuOpen, setMenuOpen] = useState(false);
return (
<div className="min-h-screen flex flex-col">
<header className="bg-blue-600 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<Link to="/" className="font-bold text-xl">
网站
</Link>
{/* 桌面端导航 */}
<nav className="hidden md:flex space-x-4">
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/contact">联系</Link>
</nav>
{/* 移动端菜单按钮 */}
<button
className="md:hidden"
onClick={() => setMenuOpen(!menuOpen)}
>
菜单
</button>
</div>
{/* 移动端导航 */}
{menuOpen && (
<nav className="md:hidden mt-4 space-y-2">
<Link to="/" className="block p-2">首页</Link>
<Link to="/about" className="block p-2">关于</Link>
<Link to="/contact" className="block p-2">联系</Link>
</nav>
)}
</header>
<main className="flex-1 container mx-auto p-6">
<Outlet />
</main>
</div>
);
}
八、布局与数据 #
8.1 布局级数据加载 #
tsx
// app/routes/admin.tsx
import { Outlet, useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const [user, notifications, settings] = await Promise.all([
getUser(request),
getNotifications(),
getSettings(),
]);
return json({ user, notifications, settings });
}
export default function AdminLayout() {
const { user, notifications, settings } = useLoaderData<typeof loader>();
return (
<div>
<header>
<span>{user.name}</span>
<span>通知 ({notifications.length})</span>
</header>
<main>
<Outlet context={{ settings }} />
</main>
</div>
);
}
九、最佳实践 #
9.1 布局命名规范 #
text
_layout.tsx # 无路径布局
admin.tsx # 带路径布局
_marketing.tsx # 营销布局
_app.tsx # 应用布局
9.2 避免深层嵌套 #
text
不推荐:
_layout._admin._users._detail.tsx
推荐:
_layout.admin.users.$id.tsx
9.3 共享组件提取 #
tsx
// app/components/layout/Header.tsx
export function Header() {
return (
<header className="bg-blue-600 text-white p-4">
{/* ... */}
</header>
);
}
// app/routes/_layout.tsx
import { Header } from "~/components/layout/Header";
export default function Layout() {
return (
<div>
<Header />
<main>
<Outlet />
</main>
</div>
);
}
十、总结 #
本章我们学习了:
- 基础布局模式:根布局和页面布局
- 条件布局:认证布局和管理后台布局
- 多布局切换:不同页面使用不同布局
- 布局组件封装:创建可复用的布局组件
- 响应式布局:移动端适配
核心要点:
- 使用
Outlet标记子路由渲染位置 - 使用
_前缀创建无路径布局 - 在布局 loader 中加载共享数据
- 提取可复用的布局组件
最后更新:2026-03-28