Remix项目结构 #

一、项目目录概览 #

一个标准的 Remix 项目结构如下:

text
my-remix-app/
├── app/                    # 应用源代码
│   ├── components/         # 共享组件
│   ├── routes/             # 路由文件
│   ├── models/             # 数据模型
│   ├── services/           # 服务层
│   ├── utils/              # 工具函数
│   ├── styles/             # 样式文件
│   ├── root.tsx            # 根组件
│   ├── entry.client.tsx    # 客户端入口
│   └── entry.server.tsx    # 服务端入口
├── public/                 # 静态资源
├── build/                  # 构建输出(gitignore)
├── node_modules/           # 依赖包
├── package.json            # 项目配置
├── vite.config.ts          # Vite配置
├── tsconfig.json           # TypeScript配置
└── .gitignore              # Git忽略配置

二、app目录详解 #

2.1 root.tsx - 根组件 #

根组件是应用的入口,定义了 HTML 文档结构:

tsx
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: "/styles/global.css" },
];

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>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

关键组件说明:

组件 作用
<Meta /> 渲染页面元数据
<Links /> 渲染样式表等资源链接
<Outlet /> 渲染子路由内容
<Scripts /> 加载JavaScript脚本
<ScrollRestoration /> 恢复滚动位置
<LiveReload /> 开发模式热更新

2.2 entry.client.tsx - 客户端入口 #

客户端入口文件负责hydration(注水):

tsx
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});

2.3 entry.server.tsx - 服务端入口 #

服务端入口负责渲染HTML:

tsx
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  loadContext: AppLoadContext
) {
  return isbot(request.headers.get("user-agent") || "")
    ? handleBotRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      )
    : handleBrowserRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      );
}

三、routes目录详解 #

3.1 路由文件命名规则 #

文件名 URL路径 说明
_index.tsx / 首页
about.tsx /about 关于页面
posts._index.tsx /posts 文章列表
posts.$id.tsx /posts/:id 文章详情
posts.new.tsx /posts/new 新建文章
admin._index.tsx /admin 管理首页
admin.users.tsx /admin/users 用户管理
_layout.tsx - 布局组件(不生成URL)

3.2 路由文件结构 #

一个完整的路由文件可以包含以下导出:

tsx
import type {
  LoaderFunctionArgs,
  ActionFunctionArgs,
  MetaFunction,
  LinksFunction,
} from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useLoaderData, Form, Link } from "@remix-run/react";

// 1. Meta - 页面元数据
export const meta: MetaFunction = () => {
  return [
    { title: "页面标题" },
    { name: "description", content: "页面描述" },
  ];
};

// 2. Links - 资源链接
export const links: LinksFunction = () => {
  return [
    { rel: "stylesheet", href: stylesUrl },
  ];
};

// 3. Loader - 数据加载
export async function loader({ params, request }: LoaderFunctionArgs) {
  const data = await fetchData();
  return json({ data });
}

// 4. Action - 数据变更
export async function action({ request, params }: ActionFunctionArgs) {
  const formData = await request.formData();
  await saveData(formData);
  return redirect("/success");
}

// 5. ErrorBoundary - 错误边界
export function ErrorBoundary() {
  const error = useRouteError();
  return <div>错误: {error.message}</div>;
}

// 6. default - 页面组件
export default function Page() {
  const { data } = useLoaderData<typeof loader>();
  return <div>{data}</div>;
}

3.3 嵌套路由 #

使用目录结构创建嵌套路由:

text
app/routes/
├── posts._index.tsx        # /posts
├── posts.$id.tsx           # /posts/:id
├── posts.new.tsx           # /posts/new
├── posts.edit.$id.tsx      # /posts/:id/edit
└── posts/
    └── comments.$id.tsx    # /posts/comments/:id

3.4 布局路由 #

使用 _layout.tsx 创建布局:

text
app/routes/
├── _index.tsx              # /
├── about.tsx               # /about
├── admin._index.tsx        # /admin
├── admin.users.tsx         # /admin/users
└── admin.tsx               # admin布局

admin.tsx 布局文件:

tsx
import { Outlet } from "@remix-run/react";
import AdminNav from "~/components/admin-nav";

export default function AdminLayout() {
  return (
    <div className="flex">
      <AdminNav />
      <main>
        <Outlet />
      </main>
    </div>
  );
}

四、其他目录 #

4.1 components目录 #

存放可复用的UI组件:

text
app/components/
├── Button.tsx
├── Card.tsx
├── Header.tsx
├── Footer.tsx
└── ui/
    ├── Input.tsx
    ├── Select.tsx
    └── Modal.tsx

4.2 models目录 #

存放数据模型和数据访问逻辑:

text
app/models/
├── user.server.ts
├── post.server.ts
└── comment.server.ts

4.3 services目录 #

存放业务逻辑和服务:

text
app/services/
├── auth.server.ts
├── email.server.ts
└── storage.server.ts

4.4 utils目录 #

存放工具函数:

text
app/utils/
├── format.ts
├── validation.ts
└── constants.ts

五、public目录 #

存放静态资源:

text
public/
├── favicon.ico
├── images/
│   ├── logo.png
│   └── hero.jpg
└── fonts/
    └── custom-font.woff2

访问方式:/images/logo.png

六、路径别名 #

Remix 默认支持 ~ 路径别名:

tsx
import Button from "~/components/Button";
import { getUser } from "~/models/user.server";
import { formatDate } from "~/utils/format";

七、环境变量 #

7.1 定义环境变量 #

创建 .env 文件:

text
DATABASE_URL="postgresql://..."
SESSION_SECRET="your-secret-key"
API_KEY="your-api-key"

7.2 使用环境变量 #

服务端代码可以直接访问:

tsx
export async function loader() {
  const apiKey = process.env.API_KEY;
  // ...
}

7.3 客户端环境变量 #

PUBLIC_ 开头的变量可以暴露给客户端:

text
PUBLIC_API_URL="https://api.example.com"

客户端使用:

tsx
const apiUrl = window.ENV.PUBLIC_API_URL;

八、配置文件 #

8.1 vite.config.ts #

Vite 构建配置:

typescript
import { defineConfig } from "vite";
import remix from "@remix-run/dev/vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [remix(), tsconfigPaths()],
});

8.2 tsconfig.json #

TypeScript 配置:

json
{
  "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "types": ["@remix-run/node", "vite/client"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "target": "ES2022",
    "strict": true,
    "noEmit": true,
    "paths": {
      "~/*": ["./app/*"]
    }
  }
}

九、最佳实践 #

9.1 文件命名规范 #

  • 使用小写字母和连字符
  • 路由文件使用点号分隔
  • 服务端文件添加 .server.ts 后缀

9.2 目录组织建议 #

text
app/
├── components/          # 共享组件
│   ├── ui/             # 基础UI组件
│   └── features/       # 功能组件
├── routes/             # 路由文件
├── models/             # 数据模型
├── services/           # 服务层
├── hooks/              # 自定义Hooks
├── utils/              # 工具函数
└── styles/             # 全局样式

9.3 关注点分离 #

  • .server.ts 文件只在服务端运行
  • 客户端代码避免引入服务端依赖
  • 使用 loader/action 处理敏感数据

十、总结 #

本章我们学习了:

  1. 项目结构:app、public、build等目录的作用
  2. 路由文件:命名规则和导出项
  3. 根组件:root.tsx 的结构和组件
  4. 配置文件:vite.config.ts 和 tsconfig.json

理解项目结构是开发的基础,让我们继续深入学习路由系统!

最后更新:2026-03-28