Remix路由基础 #

一、路由概述 #

Remix 使用基于文件系统的路由,app/routes 目录下的文件自动映射为 URL 路径。

二、路由文件命名规则 #

2.1 基本规则 #

文件名 URL路径 说明
_index.tsx / 首页
about.tsx /about 关于页面
contact.tsx /contact 联系页面
blog.tsx /blog 博客页面

2.2 目录结构 #

使用目录组织路由:

text
app/routes/
├── _index.tsx           # /
├── about.tsx            # /about
├── blog/
│   ├── _index.tsx       # /blog
│   ├── $slug.tsx        # /blog/:slug
│   └── new.tsx          # /blog/new
└── admin/
    ├── _index.tsx       # /admin
    └── users.tsx        # /admin/users

2.3 点号表示法 #

也可以使用点号代替目录:

text
app/routes/
├── _index.tsx           # /
├── blog._index.tsx      # /blog
├── blog.$slug.tsx       # /blog/:slug
├── blog.new.tsx         # /blog/new
└── admin.users.tsx      # /admin/users

三、导航组件 #

3.1 Link组件 #

使用 Link 进行客户端导航:

tsx
import { Link } from "@remix-run/react";

export default function Navbar() {
  return (
    <nav>
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Link to="/blog">博客</Link>
    </nav>
  );
}

3.2 NavLink组件 #

NavLink 提供激活状态:

tsx
import { NavLink } from "@remix-run/react";

export default function Navbar() {
  return (
    <nav>
      <NavLink
        to="/"
        className={({ isActive, isPending }) =>
          isActive ? "active" : isPending ? "pending" : ""
        }
      >
        首页
      </NavLink>
      <NavLink
        to="/about"
        style={({ isActive }) => ({
          color: isActive ? "red" : "black",
        })}
      >
        关于
      </NavLink>
    </nav>
  );
}

3.3 编程式导航 #

使用 useNavigate Hook:

tsx
import { useNavigate } from "@remix-run/react";

export default function LoginForm() {
  const navigate = useNavigate();

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    const success = await login();
    if (success) {
      navigate("/dashboard");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">登录</button>
    </form>
  );
}

3.4 Navigate组件 #

使用 Navigate 组件进行重定向:

tsx
import { Navigate } from "@remix-run/react";

export default function OldPage() {
  const shouldRedirect = true;
  
  if (shouldRedirect) {
    return <Navigate to="/new-page" replace />;
  }
  
  return <div>旧页面</div>;
}

四、路由参数 #

4.1 获取当前路径 #

使用 useLocation Hook:

tsx
import { useLocation } from "@remix-run/react";

export default function Breadcrumb() {
  const location = useLocation();
  
  return (
    <div>
      当前路径: {location.pathname}
      搜索参数: {location.search}
      哈希: {location.hash}
    </div>
  );
}

4.2 获取路由信息 #

使用 useMatches Hook:

tsx
import { useMatches } from "@remix-run/react";

export default function Breadcrumb() {
  const matches = useMatches();
  
  return (
    <nav>
      {matches.map((match) => (
        <span key={match.id}>
          {match.pathname} &gt;
        </span>
      ))}
    </nav>
  );
}

4.3 预加载资源 #

Link 组件支持预加载:

tsx
import { Link } from "@remix-run/react";

export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link
            to={`/posts/${post.id}`}
            prefetch="intent"
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

预加载选项:

选项 说明
none 不预加载(默认)
intent 悬停或聚焦时预加载
render 渲染时立即预加载
viewport 进入视口时预加载

五、路由配置 #

5.1 路由句柄 #

使用 handle 导出添加路由元数据:

tsx
import type { RouteHandle } from "@remix-run/node";

export const handle: RouteHandle = {
  breadcrumb: "文章列表",
  showSidebar: true,
};

export default function Posts() {
  return <div>文章列表</div>;
}

在父组件中使用:

tsx
import { useMatches } from "@remix-run/react";

export default function Breadcrumb() {
  const matches = useMatches();
  
  return (
    <nav>
      {matches
        .filter((match) => match.handle?.breadcrumb)
        .map((match) => (
          <span key={match.id}>
            {match.handle.breadcrumb}
          </span>
        ))}
    </nav>
  );
}

5.2 路由模块导出 #

每个路由文件可以导出以下内容:

tsx
// 元数据
export const meta: MetaFunction = () => [{ title: "页面标题" }];

// 资源链接
export const links: LinksFunction = () => [];

// 数据加载
export async function loader({ params, request }: LoaderFunctionArgs) {}

// 数据变更
export async function action({ request, params }: ActionFunctionArgs) {}

// 错误边界
export function ErrorBoundary() {}

// 路由句柄
export const handle = {};

// 页面组件
export default function Page() {}

六、导航最佳实践 #

6.1 使用Link而非a标签 #

tsx
// 推荐
<Link to="/about">关于</Link>

// 不推荐(会导致整页刷新)
<a href="/about">关于</a>

6.2 合理使用预加载 #

tsx
// 高优先级页面
<Link to="/dashboard" prefetch="render">
  仪表板
</Link>

// 低优先级页面
<Link to="/settings" prefetch="intent">
  设置
</Link>

6.3 处理外部链接 #

tsx
function ExternalLink({ href, children }) {
  return (
    <a
      href={href}
      target="_blank"
      rel="noopener noreferrer"
    >
      {children}
    </a>
  );
}

七、路由守卫 #

7.1 在loader中重定向 #

tsx
import { redirect } from "@remix-run/node";

export async function loader({ request }) {
  const user = await getUser(request);
  
  if (!user) {
    const url = new URL(request.url);
    return redirect(`/login?redirect=${url.pathname}`);
  }
  
  return { user };
}

7.2 使用中间件模式 #

tsx
export async function loader({ request }) {
  const user = await requireUser(request);
  const data = await fetchData(user.id);
  return { data };
}

async function requireUser(request: Request) {
  const user = await getUser(request);
  if (!user) {
    throw redirect("/login");
  }
  return user;
}

八、总结 #

本章我们学习了:

  1. 文件系统路由:文件名映射为URL路径
  2. 导航组件:Link、NavLink、useNavigate
  3. 路由参数:useLocation、useMatches
  4. 预加载:优化页面切换性能
  5. 路由守卫:在loader中实现权限控制

核心要点:

  • 使用 Link 进行客户端导航
  • 合理使用预加载提升用户体验
  • 在 loader 中处理权限和重定向
最后更新:2026-03-28