出处:掘金

原作者:金泽宸


模块化 + 领域分层 + 插件化目录,是大型项目的“骨架系统”

写在前面

项目越大,目录越乱,是所有前端架构师绕不过去的坑。我们常常见到这样的代码结构:

pages/
├── page1.vue
├── page2.vue
├── page3.vue
components/
├── common/
├── header.vue
├── dialog.vue
utils/
api/

很快你就发现:

  • 哪些是用户相关页面?哪些是订单?
  • 哪些组件是通用的?哪些是业务定制的?
  • utils 的函数是哪个模块的?是否重复?

今天这一篇,我们通过实战项目演示:如何设计清晰、高可维护、方便多人协作的大型项目结构

目录结构设计常见 3 种方式

模式特点适合场景
按功能划分(Flat)易于上手、轻量小项目、单人开发
按页面划分(Views)页面结构清晰中型项目、界面主导
按领域划分(Domain Oriented)✅高内聚、低耦合大型项目、多人协作、可平台化发展

推荐目录结构(面向领域 + 插件化)

src/
├── modules/                  # 各个业务模块(重点)
│   ├── user/
│   │   ├── views/            # 页面(profile、login等)
│   │   ├── components/       # 业务组件
│   │   ├── api.ts            # 模块内接口
│   │   ├── store.ts          # 模块状态
│   │   └── index.ts          # 模块入口
│   ├── order/
│   └── dashboard/
├── shared/                   # 通用能力封装
│   ├── components/           # 通用组件(弹窗、表单)
│   ├── hooks/                # 通用 hooks(useDialog、useTable)
│   ├── utils/                # 工具函数
│   └── constants/            # 枚举/状态码
├── router/                   # 动态路由加载逻辑
├── store/                    # 全局状态,如用户/主题
├── assets/                   # 静态资源
├── services/                 # 和后端联动的接口封装
├── App.vue
└── main.ts

实战演示:一个“用户模块”的结构

modules/user/
├── views/
│   ├── Login.vue
│   ├── Profile.vue
├── components/
│   ├── UserAvatar.vue
│   ├── UserInfoCard.vue
├── api.ts
├── store.ts
└── index.ts

模块 API:

// modules/user/api.ts
import request from '@/shared/utils/request'
 
export const getUserInfo = () => request.get('/user/info')
export const login = (data) => request.post('/user/login', data)

好处:

  • 用户相关代码全部集中,清晰明确
  • 删除该模块直接整个目录删掉,低耦合
  • 可以被独立打包发布(用于微前端、插件平台等)

模块化加载 + 路由动态挂载

步骤1:在模块中暴露路由配置:

// modules/user/index.ts
export default {
  routes: [
    {
      path: '/user/login',
      component: () => import('./views/Login.vue'),
    },
    {
      path: '/user/profile',
      component: () => import('./views/Profile.vue'),
    },
  ],
}

步骤2:在全局 router 中自动加载所有模块路由:

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
 
const modules = import.meta.glob('../modules/**/index.ts', { eager: true })
const routes = Object.values(modules)
  .map((m: any) => m.default.routes)
  .flat()
 
export const router = createRouter({
  history: createWebHistory(),
  routes,
})

优点:

  • 每个模块自管理路由,无需集中维护 router 文件
  • 支持按需加载,减少首次包体积
  • 模块即插件,可插拔

组件设计的目录策略

目录位置用途示例组件
shared/components/通用组件Dialog、Pagination、DatePicker
modules/user/components/业务组件UserAvatar、UserStatsCard
pages/组件目录页面私有组件TableColumnRenderer.vue

建议:

  • 通用组件要求文档/测试/可扩展性高
  • 业务组件建议与模块绑定
  • 页面组件不做复用,随页面走

复用策略:避免重复造轮子

  1. 通用模块统一收敛(Dialog、Form、Upload)
  2. 模块复用提炼成插件库
  3. 通过 props + slot 提高复用性
<!-- 通用组件 Dialog -->
<MyDialog :title="title" @confirm="handleConfirm">
  <template #default>
    <p>{{ description }}</p>
  </template>
</MyDialog>
<!-- 业务中使用 -->
<MyDialog title="删除用户" @confirm="deleteUser">
  <template #default>
    <p>是否确认删除该用户?</p>
  </template>
</MyDialog>