阅读 7 分钟

前端项目目录结构

这篇文章是描述如何在前端项目里组织目录结构。

目录

.
├── docs                      # 项目文档,同时可以给人和给 AI 看
├── static                    # 静态资源文件夹,这个目录下的文件,通常不会被做任何修改,直接 serve 使用
├── src
│   ├── app.config.ts         # 应用全局配置
│   ├── app.{scss|css|less}   # 应用全局样式入口
│   ├── app.tsx               # 应用主组件
│   ├── main.ts               # 应用主入口
│   ├── assets                # 静态资源文件夹,这个目录下的文件通常会被打包工具打包进 js 文件里,或者由打包工具输出到 static
│   ├── packs                 # 可重复使用的功能包(和 modules 类似,但不同的是可以在多个模块中跨模块使用)
│   ├── components            # 全局通用组件
│   ├── index.html            # HTML 模板
│   ├── layouts               # 页面布局组件
│   ├── stores                # 全局跨组件状态库 (例如 Jotai 、 Pinia 等)
│   ├── pages                 # 页面路由组件(目录结构通常对应应用的页面路由路径)
│   ├── styles                # 全局样式文件
│   ├── wrappers              # 全局 Context Provider 包装器
│   ├── modules               # 模块
│   │   ├── <Module1>   # 模块 1
│   │   ├── <Module2>   # 模块 2
│   │   └── <Module3>   # 模块 3
│   │       ├── packs       # 可重复使用的功能包,仅限当前模块使用(和 modules 类似,但不同的是可以在多个模块中跨模块使用)
│   │       ├── components  # 通用组件,仅限当前模块使用
│   │       ├── views       # 视图组件,仅限当前模块使用
│   │       ├── modules       # 当前模块的子模块
│   │       │   ├── <SubModule1>  # 子模块 1
│   │       │   └── <SubModule2>  # 子模块 2
│   │       └── stores      # 跨组件状态库,仅限当前模块使用
│   └────── <Module4>   # 模块 4
└── typings               # 全局 TypeScript 类型定义

对于 pagescomponentsviews 文件夹里的组件类型,可以参考 区分组件类型 这篇文档

命名规范

本项目遵循特定的文件和目录命名规范:

  • kebab-case(短横线命名法):用于固定目录和页面组件
    • 示例:pagesstylespartscomponentsviews
  • PascalCase(帕斯卡命名法,大驼峰命名法):用于业务相关的目录、文件和组件
    • 示例:模块名称、通用组件文件、视图组件文件

模块下的 PascalCase 命名的文件,应该加上前序和当前模块名作为前缀。

示例

  • 页面组件:user-profile.tsxhome-page.tsx
  • 通用/视图组件:DatePicker.tsxUserProfileView.tsx
  • 模块目录:UserManagementTaskScheduler

模块结构

本文档解释了本项目中模块的组织方式,以及组件归属与作用域的相关规则。

应用的功能模块统一放在 parts 目录下。每个模块的结构类似于根目录下的 src,但其作用范围仅限本模块。

除了 parts 也可以使用 modules 作为模块文件夹的名字。

典型模块结构

parts/
└── <ModuleName>/
    ├── components/ # 模块内的通用组件
    ├── views/      # 模块内的视图组件
    ├── stores/     # 模块专属的状态管理
    └── parts/      # 子模块
        ├── <SubModule1>/
        └── <SubModule2>/

作用域规则

组件作用域

各模块下的 components、views 和 stores 必须遵守严格的作用域规则:

模块专属组件

在某个模块中定义的组件禁止被模块以外的地方导入。

示例:

parts/
├── UserManagement/
│   └── components/
│       └── UserAvatar.tsx # 只能在 UserManagement 模块内使用
└── Dashboard/
    └── views/
        └── DashboardView.tsx # ❌ 不应导入 UserAvatar.tsx

全局组件

需要被多个模块共同使用的组件,应放在 src/components/ 下。

示例:

src/
├── components/
│   └── Avatar.tsx # ✓ 可全局使用
└── parts/
    ├── UserManagement/
    │   └── views/
    │       └── UserView.tsx # ✓ 可以导入 Avatar.tsx
    └── Dashboard/
        └── views/
            └── DashboardView.tsx # ✓ 可以导入 Avatar.tsx

Store(状态)作用域

状态管理(Jotai atoms)遵循与组件相同的作用域规则:

  • parts/<Module>/stores/ 下的 store 仅限于本模块内部使用
  • src/stores/ 下的全局 store 可供整个项目任意地方调用

组件归属策略

最近原则

新建组件时,原则上应放置在最近且最局部的位置,即距离其实际被引入(import)处最近的目录。

示例:

如果 ComponentA 只在 parts/ModuleX/views/ViewY.tsx 中使用:

parts/
└── ModuleX/
    ├── components/
    │   └── ComponentA.tsx # ✓ 距离使用点最近
    └── views/
        └── ViewY.tsx # import ComponentA

作用域提升(Scope Promotion

所谓作用域提升,即当组件需要更广泛的复用时,将其从局部目录提升到上一级模块或全局目录。

何时进行提升

当出现以下任一情况时,应考虑提升组件作用域:

  1. 需要在父模块使用该组件
  2. 需要在同级其它子模块间共享
  3. 需要在全局(多模块)范围使用

如何进行提升

场景1:从子模块提升到父模块

  • 提升前:
parts/
└── UserManagement/
    └── parts/
        └── UserProfile/
            └── components/
                └── ProfileCard.tsx # 仅 UserProfile 使用
  • 提升后(需要在 UserManagement 范围使用时):
parts/
└── UserManagement/
    ├── components/
    │   └── ProfileCard.tsx # ✓ 提升至父模块
    └── parts/
        └── UserProfile/

场景2:从模块提升为全局组件

  • 提升前:
parts/
└── UserManagement/
    └── components/
        └── StatusBadge.tsx # 仅本模块使用
  • 提升后(需跨模块复用时):
src/
├── components/
│   └── StatusBadge.tsx # ✓ 提升为全局
└── parts/
    ├── UserManagement/
    ├── TaskManagement/
    └── Dashboard/

最佳实践

1. 局部优先,按需提升

  • 优先将组件放在最具体、最近的位置
  • 只有真的需要时再进行提升
  • 不要提前将所有东西都放在全局目录(避免过早优化)

2. 保持模块独立

  • 各功能模块尽量独立、内聚
  • 避免兄弟模块间的强耦合
  • 如需共用代码可采取
    • 建立父级公共模块
    • 全局提升
    • 提取到新的独立模块

3. 记录依赖与可复用性

组件提升时,请务必注意:

  • 更新组件注释和使用说明文档
  • 检查依赖是否限定于原模块
  • 确认组件在新作用域下具备真正的通用性

4. all file types 共用原则

上述作用域与提升原则,适用于以下文件类型:

  • 通用组件和视图组件
  • 状态管理 store(Jotai atoms)
  • 工具函数和通用方法
  • 类型定义(非全局类型)
  • 样式文件(非全局样式)

示例工作流

新增特性时

  1. Dashboard 另一个视图也用到这个按钮:
    • ✓ 已在组件目录,无需调整
  2. 其它模块视图需要用该按钮:
    • ✗ 当前组件目录作用域过窄
    • ✓ 提升到 src/components/WidgetButton.tsx 全局目录
  3. 其它模块需要同样的 state:
    • ✗ 当前 store 作用域过窄
    • ✓ 提升到 src/stores/widgetStore.ts

为 widget 添加状态管理:

parts/Dashboard/stores/widgetStore.ts

需要一个 Widget 通用按钮组件:

parts/Dashboard/components/WidgetButton.tsx

在最近范围内新建组件:

parts/Dashboard/views/WidgetView.tsx

反模式示例

❌ 跨模块导入

// 错误!!不应跨模块引用私有组件
import { UserCard } from '../../UserManagement/components/UserCard';

❌ 过早全局化

// 仅被某一模块使用的组件 不应放在 src/components/
src/components/VerySpecificDashboardWidget.tsx // ❌ 应放在 parts/Dashboard/

❌ 深层路径引用

// 示例:路径已经很深时,应考虑提升
import { Component } from '../../../parts/Module/SubModule/components/Component'; // ❌ 请考虑作用域提升

总结

  • 组件应放在离使用场景最近的位置
  • 需要更广泛复用时提升作用域
  • 保持模块独立与内聚,防止模块间耦合
  • 所有文件类型均应遵循上述规则
  • 开发时由局部到全局,逐步推广,避免一开始就全局化

如有特殊场景,可结合项目实际适度调整,但整体遵循“最近原则 —> 按需提升 —> 保持独立”的思想。