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>&copy; 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>
  );
}

十、总结 #

本章我们学习了:

  1. 基础布局模式:根布局和页面布局
  2. 条件布局:认证布局和管理后台布局
  3. 多布局切换:不同页面使用不同布局
  4. 布局组件封装:创建可复用的布局组件
  5. 响应式布局:移动端适配

核心要点:

  • 使用 Outlet 标记子路由渲染位置
  • 使用 _ 前缀创建无路径布局
  • 在布局 loader 中加载共享数据
  • 提取可复用的布局组件
最后更新:2026-03-28