Appearance
代码组织规范 - React项目结构最佳实践
1. 项目结构基础
1.1 标准项目结构
my-react-app/
├── public/ # 静态资源
│ ├── index.html
│ ├── favicon.ico
│ └── manifest.json
├── src/ # 源代码
│ ├── assets/ # 资源文件
│ │ ├── images/
│ │ ├── fonts/
│ │ └── styles/
│ ├── components/ # 通用组件
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Modal/
│ ├── features/ # 功能模块
│ │ ├── auth/
│ │ ├── dashboard/
│ │ └── profile/
│ ├── hooks/ # 自定义Hooks
│ ├── services/ # API服务
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript类型
│ ├── constants/ # 常量
│ ├── config/ # 配置
│ ├── App.tsx
│ ├── index.tsx
│ └── routes.tsx
├── tests/ # 测试文件
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── .env # 环境变量
├── .eslintrc.js # ESLint配置
├── .prettierrc # Prettier配置
├── tsconfig.json # TypeScript配置
├── package.json
└── README.md1.2 组件组织
components/
├── Button/
│ ├── Button.tsx # 组件实现
│ ├── Button.test.tsx # 测试文件
│ ├── Button.stories.tsx # Storybook故事
│ ├── Button.module.css # 样式(如果使用CSS Modules)
│ ├── Button.types.ts # 类型定义
│ ├── index.ts # 导出
│ └── README.md # 文档
├── Input/
│ ├── Input.tsx
│ ├── Input.test.tsx
│ ├── Input.types.ts
│ └── index.ts
└── Modal/
├── Modal.tsx
├── ModalHeader.tsx # 子组件
├── ModalBody.tsx
├── ModalFooter.tsx
├── Modal.test.tsx
├── Modal.types.ts
└── index.ts1.3 功能模块组织
features/
├── auth/
│ ├── components/ # 功能专属组件
│ │ ├── LoginForm/
│ │ └── RegisterForm/
│ ├── hooks/ # 功能专属Hooks
│ │ ├── useAuth.ts
│ │ └── useLogin.ts
│ ├── api/ # 功能专属API
│ │ └── authApi.ts
│ ├── store/ # 功能专属状态
│ │ └── authSlice.ts
│ ├── types/ # 功能专属类型
│ │ └── auth.types.ts
│ ├── utils/ # 功能专属工具
│ │ └── tokenUtils.ts
│ └── index.ts # 导出
├── dashboard/
│ ├── components/
│ ├── hooks/
│ ├── api/
│ └── index.ts
└── profile/
├── components/
├── hooks/
├── api/
└── index.ts2. 代码组织原则
2.1 单一职责原则
tsx
// ❌ 不好: 组件职责过多
function UserProfile() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [followers, setFollowers] = useState([]);
useEffect(() => {
fetchUser().then(setUser);
fetchPosts().then(setPosts);
fetchFollowers().then(setFollowers);
}, []);
const handleFollow = () => { /* ... */ };
const handleUnfollow = () => { /* ... */ };
const handleEditProfile = () => { /* ... */ };
return (
<div>
<div>{/* 用户信息 */}</div>
<div>{/* 帖子列表 */}</div>
<div>{/* 关注者列表 */}</div>
<div>{/* 操作按钮 */}</div>
</div>
);
}
// ✅ 好: 拆分为多个职责单一的组件
function UserProfile() {
return (
<div>
<UserInfo />
<UserPosts />
<UserFollowers />
<UserActions />
</div>
);
}
function UserInfo() {
const { user, loading, error } = useUser();
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
return (
<div>
<Avatar src={user.avatar} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
function UserPosts() {
const { posts, loading } = useUserPosts();
return (
<div>
<h3>Posts</h3>
{loading ? <Skeleton count={3} /> : (
<PostList posts={posts} />
)}
</div>
);
}
function UserFollowers() {
const { followers } = useUserFollowers();
return (
<div>
<h3>Followers</h3>
<FollowerList followers={followers} />
</div>
);
}
function UserActions() {
const { isFollowing, handleFollow, handleUnfollow } = useFollowActions();
const { handleEditProfile } = useProfileActions();
return (
<div>
{isFollowing ? (
<Button onClick={handleUnfollow}>Unfollow</Button>
) : (
<Button onClick={handleFollow}>Follow</Button>
)}
<Button onClick={handleEditProfile}>Edit Profile</Button>
</div>
);
}2.2 关注点分离
tsx
// ❌ 不好: 数据获取、业务逻辑、UI混在一起
function ProductList() {
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);
useEffect(() => {
let filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
filtered = filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
return 0;
});
setFilteredProducts(filtered);
}, [products, filter, sortBy]);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
);
}
// ✅ 好: 分离数据层、逻辑层、展示层
// 1. 数据层 (hooks/useProducts.ts)
function useProducts() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
return { products, loading, error };
}
// 2. 逻辑层 (hooks/useProductFilter.ts)
function useProductFilter(products) {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
const filteredProducts = useMemo(() => {
let filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
filtered = filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
return 0;
});
return filtered;
}, [products, filter, sortBy]);
return { filteredProducts, filter, setFilter, sortBy, setSortBy };
}
// 3. 展示层 (components/ProductList.tsx)
function ProductList() {
const { products, loading, error } = useProducts();
const { filteredProducts, filter, setFilter, sortBy, setSortBy } = useProductFilter(products);
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
return (
<div>
<ProductFilters
filter={filter}
onFilterChange={setFilter}
sortBy={sortBy}
onSortChange={setSortBy}
/>
<ProductGrid products={filteredProducts} />
</div>
);
}
function ProductFilters({ filter, onFilterChange, sortBy, onSortChange }) {
return (
<div>
<Input value={filter} onChange={e => onFilterChange(e.target.value)} placeholder="Search..." />
<Select value={sortBy} onChange={e => onSortChange(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</Select>
</div>
);
}
function ProductGrid({ products }) {
return (
<ul>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</ul>
);
}
function ProductCard({ product }) {
return (
<li>
{product.name} - ${product.price}
</li>
);
}2.3 容器组件与展示组件
tsx
// 展示组件: 只关注UI,通过props接收数据
// components/UserCard.tsx
interface UserCardProps {
user: User;
onFollow: (userId: string) => void;
onUnfollow: (userId: string) => void;
isFollowing: boolean;
}
function UserCard({ user, onFollow, onUnfollow, isFollowing }: UserCardProps) {
return (
<div className="user-card">
<Avatar src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
<Button onClick={() => isFollowing ? onUnfollow(user.id) : onFollow(user.id)}>
{isFollowing ? 'Unfollow' : 'Follow'}
</Button>
</div>
);
}
// 容器组件: 处理数据和逻辑,将数据传递给展示组件
// containers/UserCardContainer.tsx
function UserCardContainer({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId);
const { isFollowing, follow, unfollow } = useFollowStatus(userId);
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
if (!user) return null;
return (
<UserCard
user={user}
onFollow={follow}
onUnfollow={unfollow}
isFollowing={isFollowing}
/>
);
}3. 文件命名规范
3.1 命名约定
typescript
const namingConventions = {
组件文件: {
React组件: 'PascalCase.tsx',
示例: ['Button.tsx', 'UserProfile.tsx', 'ProductCard.tsx']
},
非组件文件: {
工具函数: 'camelCase.ts',
示例: ['formatDate.ts', 'validateEmail.ts', 'apiClient.ts']
},
Hooks: {
格式: 'use开头 + PascalCase.ts',
示例: ['useAuth.ts', 'useLocalStorage.ts', 'useFetch.ts']
},
类型文件: {
格式: 'name.types.ts',
示例: ['user.types.ts', 'api.types.ts', 'common.types.ts']
},
常量文件: {
格式: 'SCREAMING_SNAKE_CASE.ts',
示例: ['API_ENDPOINTS.ts', 'ERROR_MESSAGES.ts', 'ROUTES.ts']
},
测试文件: {
格式: 'name.test.ts' 或 'name.spec.ts',
示例: ['Button.test.tsx', 'formatDate.spec.ts']
},
样式文件: {
CSSModules: 'name.module.css',
StyledComponents: 'name.styles.ts',
示例: ['Button.module.css', 'Button.styles.ts']
}
};3.2 目录命名
typescript
const directoryNaming = {
规则: 'kebab-case',
示例: [
'user-profile/',
'product-catalog/',
'shopping-cart/',
'error-boundary/'
],
例外: {
组件目录: 'PascalCase',
示例: ['Button/', 'Modal/', 'Dropdown/']
}
};4. 导入导出规范
4.1 导入顺序
tsx
// ✅ 推荐的导入顺序
// 1. React核心
import React, { useState, useEffect, useMemo } from 'react';
// 2. 第三方库
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import classNames from 'classnames';
// 3. 内部模块 (绝对路径)
import { Button } from '@/components/Button';
import { useAuth } from '@/hooks/useAuth';
import { formatDate } from '@/utils/date';
// 4. 相对路径导入
import { UserCard } from './UserCard';
import { useUserData } from './hooks/useUserData';
// 5. 类型导入
import type { User } from '@/types/user';
import type { Props } from './types';
// 6. 样式
import styles from './Component.module.css';
import './Component.css';3.2 导出规范
tsx
// ❌ 不好: 默认导出 (难以重构和追踪)
export default function Button() {
return <button>Click</button>;
}
// ✅ 好: 命名导出
export function Button() {
return <button>Click</button>;
}
// ✅ 使用index.ts统一导出
// components/Button/Button.tsx
export function Button(props: ButtonProps) {
return <button {...props} />;
}
export type { ButtonProps } from './Button.types';
// components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button.types';
// 使用时
import { Button } from '@/components/Button';4.3 路径别名配置
json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"],
"@hooks/*": ["hooks/*"],
"@utils/*": ["utils/*"],
"@types/*": ["types/*"],
"@features/*": ["features/*"],
"@assets/*": ["assets/*"]
}
}
}
// vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@utils': path.resolve(__dirname, './src/utils'),
'@types': path.resolve(__dirname, './src/types'),
'@features': path.resolve(__dirname, './src/features'),
'@assets': path.resolve(__dirname, './src/assets')
}
}
});5. 代码分组与注释
5.1 组件内部结构
tsx
function Component() {
// 1. Hooks (状态、副作用、上下文)
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
const { theme } = useTheme();
const navigate = useNavigate();
// 2. 计算值 (useMemo、派生状态)
const doubleCount = useMemo(() => count * 2, [count]);
const isAuthenticated = Boolean(user);
// 3. 回调函数 (useCallback、事件处理)
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleLogin = useCallback(async (credentials: Credentials) => {
const user = await loginUser(credentials);
setUser(user);
}, []);
// 4. 副作用 (useEffect、useLayoutEffect)
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
useEffect(() => {
if (user) {
loadUserPreferences(user.id);
}
}, [user]);
// 5. 条件渲染
if (!isAuthenticated) {
return <LoginForm onLogin={handleLogin} />;
}
// 6. JSX
return (
<div>
<h1>Count: {count}</h1>
<p>Double: {doubleCount}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}5.2 注释规范
tsx
/**
* 用户个人资料组件
*
* 显示用户的基本信息、帖子和关注者列表
*
* @example
* ```tsx
* <UserProfile userId="123" />
* ```
*/
function UserProfile({ userId }: UserProfileProps) {
// TODO: 添加错误边界
// FIXME: 修复用户头像加载失败的问题
// NOTE: 这个组件需要用户已登录
const { user, loading } = useUser(userId);
// 加载状态
if (loading) return <Skeleton />;
return (
<div>
{/* 用户信息区域 */}
<UserInfo user={user} />
{/* 帖子列表 */}
<UserPosts userId={userId} />
{/* 关注者列表 */}
<UserFollowers userId={userId} />
</div>
);
}
/**
* JSDoc注释示例
*
* @param user - 用户对象
* @param onUpdate - 更新回调函数
* @returns React组件
*
* @throws {Error} 当user为null时抛出错误
*
* @deprecated 使用UserProfileV2代替
*
* @see {@link UserProfileV2} 新版本组件
*/
function deprecatedComponent(user: User, onUpdate: (user: User) => void) {
// ...
}6. 常量管理
6.1 集中管理常量
typescript
// constants/API_ENDPOINTS.ts
export const API_ENDPOINTS = {
AUTH: {
LOGIN: '/api/auth/login',
LOGOUT: '/api/auth/logout',
REGISTER: '/api/auth/register',
REFRESH: '/api/auth/refresh'
},
USER: {
PROFILE: '/api/users/:id',
UPDATE: '/api/users/:id',
FOLLOWERS: '/api/users/:id/followers'
},
POSTS: {
LIST: '/api/posts',
DETAIL: '/api/posts/:id',
CREATE: '/api/posts',
UPDATE: '/api/posts/:id',
DELETE: '/api/posts/:id'
}
} as const;
// constants/ROUTES.ts
export const ROUTES = {
HOME: '/',
LOGIN: '/login',
REGISTER: '/register',
DASHBOARD: '/dashboard',
PROFILE: '/profile/:id',
SETTINGS: '/settings',
NOT_FOUND: '/404'
} as const;
// constants/ERROR_MESSAGES.ts
export const ERROR_MESSAGES = {
AUTH: {
INVALID_CREDENTIALS: '用户名或密码错误',
SESSION_EXPIRED: '会话已过期,请重新登录',
UNAUTHORIZED: '您没有权限执行此操作'
},
VALIDATION: {
REQUIRED_FIELD: '此字段为必填项',
INVALID_EMAIL: '请输入有效的邮箱地址',
PASSWORD_TOO_SHORT: '密码长度至少为8个字符'
},
NETWORK: {
CONNECTION_FAILED: '网络连接失败',
TIMEOUT: '请求超时',
SERVER_ERROR: '服务器错误,请稍后重试'
}
} as const;
// constants/THEMES.ts
export const THEMES = {
LIGHT: {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
text: '#212529'
},
DARK: {
primary: '#0d6efd',
secondary: '#6c757d',
background: '#212529',
text: '#ffffff'
}
} as const;
// constants/CONFIG.ts
export const CONFIG = {
APP_NAME: 'MyApp',
APP_VERSION: '1.0.0',
API_BASE_URL: process.env.REACT_APP_API_URL || 'http://localhost:3000',
DEFAULT_LANGUAGE: 'zh-CN',
ITEMS_PER_PAGE: 20,
MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
SUPPORTED_FILE_TYPES: ['image/jpeg', 'image/png', 'image/gif']
} as const;6.2 枚举vs常量对象
typescript
// ✅ 使用枚举 (TypeScript特性)
enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
enum HttpStatus {
OK = 200,
CREATED = 201,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
NOT_FOUND = 404,
SERVER_ERROR = 500
}
// ✅ 使用常量对象 (更灵活)
export const USER_ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
} as const;
export type UserRole = typeof USER_ROLES[keyof typeof USER_ROLES];
// 使用
function checkPermission(role: UserRole) {
if (role === USER_ROLES.ADMIN) {
// ...
}
}7. 配置管理
7.1 环境配置
typescript
// config/env.ts
interface EnvConfig {
apiUrl: string;
environment: 'development' | 'staging' | 'production';
enableAnalytics: boolean;
enableDebugger: boolean;
logLevel: 'error' | 'warn' | 'info' | 'debug';
}
function getEnvConfig(): EnvConfig {
const env = process.env.NODE_ENV || 'development';
const config: Record<string, EnvConfig> = {
development: {
apiUrl: 'http://localhost:3000',
environment: 'development',
enableAnalytics: false,
enableDebugger: true,
logLevel: 'debug'
},
staging: {
apiUrl: 'https://staging-api.myapp.com',
environment: 'staging',
enableAnalytics: true,
enableDebugger: true,
logLevel: 'info'
},
production: {
apiUrl: 'https://api.myapp.com',
environment: 'production',
enableAnalytics: true,
enableDebugger: false,
logLevel: 'error'
}
};
return config[env] || config.development;
}
export const ENV_CONFIG = getEnvConfig();
// 使用
import { ENV_CONFIG } from '@/config/env';
if (ENV_CONFIG.enableDebugger) {
console.log('Debug mode enabled');
}7.2 功能开关 (Feature Flags)
typescript
// config/featureFlags.ts
interface FeatureFlags {
newDashboard: boolean;
darkMode: boolean;
betaFeatures: boolean;
experimentalUI: boolean;
}
export const FEATURE_FLAGS: FeatureFlags = {
newDashboard: process.env.REACT_APP_NEW_DASHBOARD === 'true',
darkMode: true,
betaFeatures: process.env.REACT_APP_BETA === 'true',
experimentalUI: false
};
// hooks/useFeatureFlag.ts
export function useFeatureFlag(flag: keyof FeatureFlags): boolean {
return FEATURE_FLAGS[flag];
}
// 使用
function Dashboard() {
const isNewDashboard = useFeatureFlag('newDashboard');
if (isNewDashboard) {
return <NewDashboard />;
}
return <OldDashboard />;
}8. 代码质量工具
8.1 ESLint配置
javascript
// .eslintrc.js
module.exports = {
extends: [
'react-app',
'react-app/jest',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
plugins: ['@typescript-eslint', 'react-hooks', 'import'],
rules: {
// React规则
'react/jsx-no-target-blank': 'warn',
'react/jsx-key': 'error',
'react/no-array-index-key': 'warn',
// Hooks规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// TypeScript规则
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_'
}],
// 导入规则
'import/order': ['error', {
'groups': [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index'
],
'pathGroups': [
{
'pattern': 'react',
'group': 'external',
'position': 'before'
},
{
'pattern': '@/**',
'group': 'internal',
'position': 'before'
}
],
'pathGroupsExcludedImportTypes': ['react'],
'newlines-between': 'always',
'alphabetize': {
'order': 'asc',
'caseInsensitive': true
}
}],
'import/no-duplicates': 'error',
'import/no-cycle': 'warn'
}
};8.2 Prettier配置
json
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}8.3 EditorConfig
ini
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,jsx,ts,tsx,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false9. 总结
良好的代码组织规范包括:
- 项目结构: 清晰的目录层次
- 组织原则: 单一职责、关注点分离
- 命名规范: 一致的文件和目录命名
- 导入导出: 规范的模块管理
- 代码分组: 有序的代码结构
- 常量管理: 集中式常量定义
- 配置管理: 环境和功能配置
- 质量工具: ESLint、Prettier等
遵循这些规范可以提高代码可维护性和团队协作效率。