跳转到内容

前端项目目录结构规范

页面、组件、hooks、store、路由组织


  • Standard:前端项目统一采用以下顶层目录结构,适用于 React / Vue 项目。
project/
├── public/ # 不经过构建的静态资源(favicon、robots.txt)
├── src/
│ ├── api/ # 接口请求封装
│ ├── assets/ # 经过构建处理的静态资源(图片、字体、样式)
│ ├── components/ # 通用组件(跨页面复用)
│ ├── layouts/ # 布局组件
│ ├── pages/ # 页面级组件
│ ├── hooks/ # 自定义 Hooks(React)/ Composables(Vue)
│ ├── store/ # 全局状态管理
│ ├── router/ # 路由配置
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript 类型定义
│ ├── constants/ # 常量定义
│ ├── styles/ # 全局样式、主题变量
│ └── main.tsx / main.ts # 应用入口
├── tests/ # 测试目录(可选,也可就近放在组件旁)
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts / webpack.config.js
└── .env / .env.local
  • Requirement:页面级组件放在 pages/,复用组件放在 components/,禁止混用。
  • Standard:按功能就近组织,单一功能的组件、hooks、样式放在同一目录下。
  • Standard:utils/ 按职责拆分文件,禁止创建万能 utils/index.ts 堆积所有方法。
  • Caution:components/ 仅放跨页面复用的通用组件,页面私有组件放在对应 pages/xxx/components/ 下。

  • Standard:components/ 下每个组件一个目录,目录名使用 PascalCase,包含组件文件、样式、测试与 index.ts 导出。
src/components/
├── Button/
│ ├── Button.tsx # 组件实现
│ ├── Button.module.css # 样式(CSS Modules)
│ ├── Button.test.tsx # 单元测试
│ └── index.ts # 导出
├── Modal/
│ ├── Modal.tsx
│ ├── Modal.module.css
│ └── index.ts
└── Table/
├── Table.tsx
├── Table.module.css
└── index.ts
  • Standard:页面按业务模块划分目录,页面私有组件放在页面目录的 components/ 子目录下。
src/pages/
├── User/
│ ├── List/
│ │ ├── index.tsx # 页面入口
│ │ ├── List.module.css
│ │ └── components/ # 页面私有组件
│ │ ├── UserTable.tsx
│ │ └── UserFilter.tsx
│ ├── Detail/
│ │ ├── index.tsx
│ │ └── components/
│ └── Edit/
│ └── index.tsx
├── Order/
│ ├── List/
│ └── Detail/
└── Dashboard/
└── index.tsx
  • Requirement:组件文件名使用 PascalCase,与组件名一致,如 UserTable.tsx。
  • Requirement:页面入口文件命名为 index.tsx,目录名使用 PascalCase 或 kebab-case(项目内统一)。
  • Standard:样式文件与组件同名,使用 .module.css / .module.scss 后缀。
  • Standard:每个组件目录提供 index.ts 统一导出,方便外部引用。
// components/Button/index.ts
export { default } from './Button';
export type { ButtonProps } from './Button';
// 使用
import Button from '@/components/Button';
  • Standard:layouts/ 存放页面布局,如 BasicLayout、BlankLayout、AuthLayout。
src/layouts/
├── BasicLayout/
│ ├── index.tsx
│ ├── Sidebar.tsx
│ ├── Header.tsx
│ └── BasicLayout.module.css
└── AuthLayout/
└── index.tsx

  • Standard:全局通用 Hooks 放在 src/hooks/,页面私有 Hooks 放在对应页面目录下。
src/hooks/
├── useDebounce.ts
├── useRequest.ts # 通用请求 Hook
├── usePagination.ts
└── usePermission.ts # 权限 Hook
  • Requirement:Hook 必须以 use 开头,使用驼峰命名,如 useUserInfo、usePagination。
  • Standard:Hook 文件名与 Hook 名一致,使用 camelCase,如 useRequest.ts。
  • Caution:普通函数禁止以 use 开头,避免被误认为 Hook。
// src/hooks/useRequest.ts
import { useState, useEffect, useCallback } from 'react';
 
interface UseRequestOptions {
  manual?: boolean;
  onSuccess?: (data: any) => void;
  onError?: (error: Error) => void;
}
 
export function useRequest<T>(
  service: (...args: any[]) => Promise<T>,
  options: UseRequestOptions = {}
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
 
  const run = useCallback(async (...args: any[]) => {
    setLoading(true);
    setError(null);
    try {
      const result = await service(...args);
      setData(result);
      options.onSuccess?.(result);
    } catch (e) {
      setError(e as Error);
      options.onError?.(e as Error);
    } finally {
      setLoading(false);
    }
  }, [service, options]);
 
  useEffect(() => {
    if (!options.manual) run();
  }, []);
 
  return { data, loading, error, run };
}

  • Requirement:全局共享状态放在 src/store/,页面局部状态放在组件内部(useState / useReducer),禁止滥用全局状态。
  • Standard:以下场景使用全局状态:用户信息、权限、主题、多页面共享的业务数据。
  • Standard:以下场景使用局部状态:表单输入、弹窗开关、列表筛选条件。
src/store/
├── index.ts # Store 入口,统一导出
├── user.ts # 用户状态
├── permission.ts # 权限状态
├── theme.ts # 主题状态
└── app.ts # 应用级状态
  • Standard:按功能模块拆分 store 文件,每个文件包含 state、getters、actions / reducers。
  • Standard:使用 Redux Toolkit / Pinia / Zustand 等现代状态管理库,禁止手写大量模板代码。

Redux Toolkit 示例:

// src/store/user.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 
interface UserState {
  id: string;
  name: string;
  avatar: string;
  token: string;
}
 
const initialState: UserState = {
  id: '',
  name: '',
  avatar: '',
  token: '',
};
 
const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUser(state, action: PayloadAction<Partial<UserState>>) {
      Object.assign(state, action.payload);
    },
    clearUser() {
      return initialState;
    },
  },
});
 
export const { setUser, clearUser } = userSlice.actions;
export default userSlice.reducer;

Pinia 示例:

// src/store/user.ts
import { defineStore } from 'pinia';
 
export const useUserStore = defineStore('user', {
  state: () => ({
    id: '',
    name: '',
    token: '',
  }),
  getters: {
    isLoggedIn: (state) => !!state.token,
  },
  actions: {
    setUser(user: Partial<typeof this.$state>) {
      Object.assign(this, user);
    },
    clearUser() {
      this.$reset();
    },
  },
});

  • Standard:路由配置集中管理在 src/router/ 目录下,按业务模块拆分路由文件。
src/router/
├── index.ts # 路由入口,注册所有路由
├── routes/
│ ├── user.ts # 用户模块路由
│ ├── order.ts # 订单模块路由
│ └── dashboard.ts # 仪表盘路由
└── guards.ts # 路由守卫(权限、登录校验)
// src/router/routes/user.ts
import type { RouteRecordRaw } from 'vue-router';
 
const userRoutes: RouteRecordRaw[] = [
  {
    path: '/user',
    component: () => import('@/layouts/BasicLayout'),
    meta: { title: '用户管理', requiresAuth: true },
    children: [
      {
        path: '',
        name: 'UserList',
        component: () => import('@/pages/User/List'),
        meta: { title: '用户列表', permission: 'user:read' },
      },
      {
        path: ':id',
        name: 'UserDetail',
        component: () => import('@/pages/User/Detail'),
        meta: { title: '用户详情', permission: 'user:read' },
      },
      {
        path: 'edit/:id',
        name: 'UserEdit',
        component: () => import('@/pages/User/Edit'),
        meta: { title: '编辑用户', permission: 'user:write' },
      },
    ],
  },
];
 
export default userRoutes;
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import userRoutes from './routes/user';
import orderRoutes from './routes/order';
import { setupGuards } from './guards';
 
const router = createRouter({
  history: createWebHistory(),
  routes: [
    ...userRoutes,
    ...orderRoutes,
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/pages/Login'),
      meta: { requiresAuth: false },
    },
    {
      path: '/:pathMatch(.*)*',
      name: 'NotFound',
      component: () => import('@/pages/NotFound'),
    },
  ],
});
 
setupGuards(router);
export default router;
  • Requirement:路由 name 使用 PascalCase,与组件名对应,如 UserList、OrderDetail。
  • Standard:路由 path 使用 kebab-case,多单词用连字符,如 /user-list。
  • Standard:动态参数使用 :id 形式,如 /user/:id。
  • Standard:meta 字段统一包含 title(页面标题)、requiresAuth(是否需要登录)、permission(权限标识)。
  • Requirement:除首页外,所有路由组件必须使用动态导入(懒加载),减小首屏包体积。
// 正确
component: () => import('@/pages/User/List')
 
// 错误
import UserList from '@/pages/User/List';
component: UserList
  • Standard:登录校验、权限校验统一在 guards.ts 中实现,禁止在每个组件内重复写校验逻辑。
// src/router/guards.ts
import type { Router } from 'vue-router';
import { useUserStore } from '@/store/user';
 
export function setupGuards(router: Router) {
  router.beforeEach((to, from, next) => {
    const userStore = useUserStore();
 
    if (to.meta.requiresAuth && !userStore.isLoggedIn) {
      next({ name: 'Login', query: { redirect: to.fullPath } });
      return;
    }
 
    if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) {
      next({ name: 'Forbidden' });
      return;
    }
 
    next();
  });
}

反模式问题建议
所有组件堆在 components/ 根目录无法区分通用与私有页面私有组件放 pages/xxx/components/
组件目录无 index.ts导入路径冗长提供 index.ts 统一导出
Hook 不以 use 开头违反 React 规则,可能触发 lint 报错必须以 use 开头
所有状态都放全局 store导致 store 膨胀、不必要的重渲染局部状态优先用组件 state
路由组件未懒加载首屏包体积过大使用动态导入
路由守卫逻辑写在组件内重复代码、难以维护统一在 guards.ts 中处理
utils/index.ts 堆积所有方法难以查找、循环依赖风险按职责拆分文件
页面私有组件放在 components/污染通用组件库移到对应页面目录下