Remix项目架构 #

一、推荐目录结构 #

text
app/
├── components/           # 共享组件
│   ├── ui/              # 基础UI组件
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   └── Modal.tsx
│   └── features/        # 功能组件
│       ├── SearchBar.tsx
│       └── UserMenu.tsx
├── routes/              # 路由文件
│   ├── _index.tsx
│   ├── posts._index.tsx
│   ├── posts.$id.tsx
│   └── admin.tsx
├── models/              # 数据模型
│   ├── user.server.ts
│   └── post.server.ts
├── services/            # 业务逻辑
│   ├── auth.server.ts
│   └── email.server.ts
├── utils/               # 工具函数
│   ├── format.ts
│   └── validation.ts
├── hooks/               # 自定义Hooks
│   ├── useUser.ts
│   └── useDebounce.ts
├── styles/              # 全局样式
│   ├── global.css
│   └── variables.css
├── root.tsx             # 根组件
├── entry.client.tsx     # 客户端入口
└── entry.server.tsx     # 服务端入口

二、路由组织 #

2.1 按功能分组 #

text
app/routes/
├── _index.tsx
├── auth/
│   ├── login.tsx
│   ├── register.tsx
│   └── forgot-password.tsx
├── posts/
│   ├── _index.tsx
│   ├── $id.tsx
│   └── new.tsx
└── admin/
    ├── _index.tsx
    ├── users.tsx
    └── settings.tsx

2.2 布局路由 #

text
app/routes/
├── _index.tsx
├── _layout.tsx          # 公共布局
├── _layout.about.tsx
├── _layout.contact.tsx
├── admin.tsx            # 管理布局
├── admin._index.tsx
└── admin.users.tsx

三、数据层组织 #

3.1 Model层 #

tsx
export interface User {
  id: string;
  email: string;
  name: string;
}

export async function getUser(id: string): Promise<User | null> {
  return db.user.findUnique({ where: { id } });
}

export async function getUserByEmail(email: string): Promise<User | null> {
  return db.user.findUnique({ where: { email } });
}

export async function createUser(data: CreateUserInput): Promise<User> {
  return db.user.create({ data });
}

3.2 Service层 #

tsx
import { getUserByEmail, createUser } from "~/models/user.server";
import { hash, compare } from "~/utils/crypto";

export async function registerUser(email: string, password: string) {
  const existingUser = await getUserByEmail(email);
  
  if (existingUser) {
    throw new Error("用户已存在");
  }
  
  const hashedPassword = await hash(password);
  
  return createUser({
    email,
    password: hashedPassword,
  });
}

export async function authenticateUser(email: string, password: string) {
  const user = await getUserByEmail(email);
  
  if (!user) {
    throw new Error("用户不存在");
  }
  
  const isValid = await compare(password, user.password);
  
  if (!isValid) {
    throw new Error("密码错误");
  }
  
  return user;
}

四、组件组织 #

4.1 组件结构 #

tsx
import type { LinksFunction } from "@remix-run/node";
import styles from "./Button.css?url";

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

interface ButtonProps {
  variant?: "primary" | "secondary";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  children: React.ReactNode;
}

export function Button({
  variant = "primary",
  size = "md",
  disabled,
  children,
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

4.2 组件导出 #

tsx
export { Button } from "./Button";
export { Input } from "./Input";
export { Modal } from "./Modal";

五、工具函数 #

5.1 格式化工具 #

tsx
export function formatDate(date: Date | string): string {
  return new Date(date).toLocaleDateString("zh-CN", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
}

export function formatCurrency(amount: number): string {
  return new Intl.NumberFormat("zh-CN", {
    style: "currency",
    currency: "CNY",
  }).format(amount);
}

5.2 验证工具 #

tsx
import { z } from "zod";

export const emailSchema = z.string().email("无效的邮箱地址");
export const passwordSchema = z.string()
  .min(8, "密码至少8个字符")
  .regex(/[A-Z]/, "密码需要包含大写字母");

export function validateEmail(email: string) {
  return emailSchema.safeParse(email);
}

六、配置管理 #

6.1 环境变量 #

tsx
export const config = {
  database: {
    url: process.env.DATABASE_URL!,
  },
  session: {
    secret: process.env.SESSION_SECRET!,
    maxAge: 60 * 60 * 24 * 7,
  },
  email: {
    host: process.env.EMAIL_HOST,
    port: parseInt(process.env.EMAIL_PORT || "587"),
  },
};

6.2 类型定义 #

tsx
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      SESSION_SECRET: string;
      EMAIL_HOST: string;
      EMAIL_PORT: string;
    }
  }
}

export {};

七、最佳实践 #

7.1 文件命名 #

  • 组件:PascalCase (Button.tsx)
  • 工具:camelCase (formatDate.ts)
  • 路由:kebab-case (user-profile.tsx)
  • 服务端:.server.ts 后缀

7.2 导入顺序 #

tsx
import { json, redirect } from "@remix-run/node";
import { useLoaderData, Form } from "@remix-run/react";
import { useState, useEffect } from "react";
import { getUser } from "~/models/user.server";
import { formatDate } from "~/utils/format";
import { Button } from "~/components/ui/Button";

7.3 代码注释 #

tsx
/**
 * 获取用户信息
 * @param userId 用户ID
 * @returns 用户信息或null
 */
export async function getUser(userId: string) {
  // ...
}

八、总结 #

本章我们学习了:

  1. 目录结构:推荐的项目组织方式
  2. 路由组织:按功能和布局分组
  3. 数据层:Model和Service分离
  4. 组件组织:组件结构和导出
  5. 最佳实践:命名、导入、注释

核心要点:

  • 保持清晰的目录结构
  • 分离数据层和业务逻辑
  • 统一命名规范
最后更新:2026-03-28