前端项目目录结构规范
页面、组件、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/ | 污染通用组件库 | 移到对应页面目录下 |