项目结构 #

一、基本结构 #

1.1 小型项目 #

text
my-app/
├── public/
│   └── favicon.ico
├── src/
│   ├── components/
│   │   ├── Button.jsx
│   │   ├── Input.jsx
│   │   └── Modal.jsx
│   ├── hooks/
│   │   └── useLocalStorage.js
│   ├── pages/
│   │   ├── Home.jsx
│   │   └── About.jsx
│   ├── App.jsx
│   ├── main.jsx
│   └── index.css
├── index.html
├── package.json
└── vite.config.js

1.2 中型项目 #

text
my-app/
├── public/
│   ├── favicon.ico
│   └── images/
├── src/
│   ├── components/
│   │   ├── common/
│   │   │   ├── Button/
│   │   │   │   ├── Button.jsx
│   │   │   │   ├── Button.module.css
│   │   │   │   └── index.js
│   │   │   ├── Input/
│   │   │   └── Modal/
│   │   └── layout/
│   │       ├── Header/
│   │       ├── Footer/
│   │       └── Sidebar/
│   ├── features/
│   │   ├── auth/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── store.js
│   │   │   └── index.js
│   │   └── cart/
│   ├── hooks/
│   │   ├── useFetch.js
│   │   ├── useLocalStorage.js
│   │   └── index.js
│   ├── pages/
│   │   ├── Home/
│   │   ├── About/
│   │   └── NotFound/
│   ├── router/
│   │   ├── index.jsx
│   │   └── routes.js
│   ├── store/
│   │   ├── index.js
│   │   └── useStore.js
│   ├── utils/
│   │   ├── api.js
│   │   ├── helpers.js
│   │   └── constants.js
│   ├── App.jsx
│   ├── main.jsx
│   └── index.css
├── index.html
├── package.json
└── vite.config.js

二、组件组织 #

2.1 组件文件夹结构 #

text
components/
├── Button/
│   ├── Button.jsx          组件实现
│   ├── Button.module.css   样式文件
│   ├── Button.test.jsx     测试文件
│   └── index.js            导出入口

2.2 组件文件示例 #

jsx
// Button/Button.jsx
import styles from './Button.module.css';

export function Button({ 
  children, 
  variant = 'primary', 
  size = 'medium',
  disabled = false,
  onClick 
}) {
  return (
    <button
      class={`${styles.button} ${styles[variant]} ${styles[size]}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// Button/index.js
export { Button } from './Button';
export { Button as default } from './Button';

2.3 组件分类 #

text
components/
├── common/           通用组件
│   ├── Button/
│   ├── Input/
│   ├── Modal/
│   └── Loading/
├── layout/           布局组件
│   ├── Header/
│   ├── Footer/
│   ├── Sidebar/
│   └── Layout/
└── features/         功能组件
    ├── UserCard/
    ├── ProductList/
    └── ShoppingCart/

三、特性驱动结构 #

3.1 按功能组织 #

text
src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.jsx
│   │   │   └── RegisterForm.jsx
│   │   ├── hooks/
│   │   │   └── useAuth.js
│   │   ├── api/
│   │   │   └── authApi.js
│   │   ├── store.js
│   │   └── index.js
│   ├── products/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api/
│   │   ├── store.js
│   │   └── index.js
│   └── cart/
│       ├── components/
│       ├── hooks/
│       ├── api/
│       ├── store.js
│       └── index.js
└── components/
    └── common/

3.2 特性模块示例 #

jsx
// features/auth/index.js
export { LoginForm } from './components/LoginForm';
export { RegisterForm } from './components/RegisterForm';
export { useAuth } from './hooks/useAuth';
export { authStore } from './store';

// features/auth/store.js
import { signal, computed } from '@preact/signals';

export const user = signal(null);
export const isAuthenticated = computed(() => !!user.value);

export const login = async (credentials) => { /* ... */ };
export const logout = () => { user.value = null; };

// features/auth/hooks/useAuth.js
import { user, isAuthenticated, login, logout } from '../store';

export function useAuth() {
  return {
    user: user.value,
    isAuthenticated: isAuthenticated.value,
    login,
    logout
  };
}

四、路由组织 #

4.1 路由配置 #

jsx
// router/routes.js
export const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  LOGIN: '/login',
  REGISTER: '/register',
  DASHBOARD: '/dashboard',
  USER_PROFILE: (id) => `/users/${id}`,
  PRODUCT_DETAIL: (id) => `/products/${id}`
};

// router/index.jsx
import { lazy, Suspense } from 'preact/compat';
import { Router } from 'preact-router';
import { ROUTES } from './routes';

const Home = lazy(() => import('../pages/Home'));
const About = lazy(() => import('../pages/About'));
const Login = lazy(() => import('../pages/Login'));
const Dashboard = lazy(() => import('../pages/Dashboard'));

export function AppRouter() {
  return (
    <Suspense fallback={<Loading />}>
      <Router>
        <Home path={ROUTES.HOME} />
        <About path={ROUTES.ABOUT} />
        <Login path={ROUTES.LOGIN} />
        <Dashboard path={ROUTES.DASHBOARD} />
        <NotFound default />
      </Router>
    </Suspense>
  );
}

五、状态管理组织 #

5.1 全局状态 #

text
store/
├── index.js          统一导出
├── user.js           用户状态
├── cart.js           购物车状态
├── ui.js             UI 状态
└── api.js            API 状态

5.2 Store 示例 #

jsx
// store/cart.js
import { signal, computed, batch } from '@preact/signals';

export const items = signal([]);
export const isOpen = signal(false);

export const count = computed(() => 
  items.value.reduce((sum, item) => sum + item.quantity, 0)
);

export const total = computed(() =>
  items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const addItem = (product) => {
  const existing = items.value.find(item => item.id === product.id);
  
  if (existing) {
    items.value = items.value.map(item =>
      item.id === product.id
        ? { ...item, quantity: item.quantity + 1 }
        : item
    );
  } else {
    items.value = [...items.value, { ...product, quantity: 1 }];
  }
};

export const removeItem = (id) => {
  items.value = items.value.filter(item => item.id !== id);
};

export const clearCart = () => {
  batch(() => {
    items.value = [];
    isOpen.value = false;
  });
};

// store/index.js
export * from './user';
export * from './cart';
export * from './ui';

六、API 组织 #

6.1 API 模块 #

text
api/
├── index.js          统一导出
├── client.js         HTTP 客户端
├── auth.js           认证 API
├── products.js       产品 API
└── users.js          用户 API

6.2 API 示例 #

jsx
// api/client.js
const BASE_URL = '/api';

export async function request(endpoint, options = {}) {
  const url = `${BASE_URL}${endpoint}`;
  
  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    },
    ...options
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

// api/auth.js
import { request } from './client';

export const authApi = {
  login: (credentials) => request('/auth/login', {
    method: 'POST',
    body: JSON.stringify(credentials)
  }),
  
  logout: () => request('/auth/logout', {
    method: 'POST'
  }),
  
  register: (data) => request('/auth/register', {
    method: 'POST',
    body: JSON.stringify(data)
  }),
  
  getCurrentUser: () => request('/auth/me')
};

// api/index.js
export * from './client';
export { authApi } from './auth';
export { productsApi } from './products';
export { usersApi } from './users';

七、工具函数组织 #

7.1 工具模块 #

text
utils/
├── index.js          统一导出
├── helpers.js        通用辅助函数
├── constants.js      常量定义
├── validators.js     验证函数
└── formatters.js     格式化函数

7.2 工具示例 #

jsx
// utils/helpers.js
export const debounce = (fn, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

export const throttle = (fn, limit) => {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
};

export const classNames = (...classes) => {
  return classes.filter(Boolean).join(' ');
};

// utils/constants.js
export const API_URL = '/api';
export const ITEMS_PER_PAGE = 10;
export const MAX_FILE_SIZE = 5 * 1024 * 1024;

export const STATUS = {
  IDLE: 'idle',
  LOADING: 'loading',
  SUCCESS: 'success',
  ERROR: 'error'
};

// utils/formatters.js
export const formatPrice = (price) => {
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY'
  }).format(price);
};

export const formatDate = (date) => {
  return new Intl.DateTimeFormat('zh-CN').format(new Date(date));
};

八、最佳实践 #

8.1 命名约定 #

text
组件:PascalCase
├── Button.jsx
├── UserProfile.jsx

函数:camelCase
├── formatDate.js
├── useAuth.js

常量:UPPER_SNAKE_CASE
├── API_URL
├── MAX_ITEMS

文件夹:kebab-case 或 PascalCase
├── user-profile/
├── UserProfile/

8.2 导出规范 #

jsx
// 命名导出
export function Button() {}
export const useAuth = () => {};

// 默认导出(页面组件)
export default function HomePage() {}

// 统一导出
export { Button } from './Button';
export { Input } from './Input';

九、总结 #

要点 说明
组件 按功能分类
特性 按领域组织
状态 集中管理
API 模块化
工具 统一导出

核心原则:

  • 保持结构清晰
  • 按功能/领域组织
  • 统一命名规范
  • 便于扩展维护
最后更新:2026-03-28