学完这篇,你能拿到一套可以直接套用到中大型 Vue 3 项目的目录骨架,知道每个文件夹放什么、为什么这么放,以及项目长到几十个页面时怎么拆分才不烂。
第一步:先用官方脚手架跑出骨架
不要空手创建目录,先用 Vue 官方脚手架生成标准结构,再在它上面做分层调整:
npm create vue@latest my-app
交互式选项里把 TypeScript、Router、Pinia、ESLint、Prettier 都勾上。生成后执行:
cd my-app
npm install
npm run dev
得到的是 src/{assets,components,router,stores,views} 这种最小结构。小项目够用,但页面一多就会全部堆在 views 和 components 里,这时候就需要下一步。
注意:不要用 npm create vite@latest 选 vue 模板当成最终方案,它连路由和状态管理都不带,后面全靠手动补,容易配得七零八落。
第二步:顶层目录按“职责”划分,不按文件类型堆
在 src 下把目录定成这几类,之后所有代码只往这几类里放:
| 目录 | 职责 |
|---|
api/ | 接口请求函数,一个业务一个文件 |
assets/ | 图片、字体等静态资源(会被构建处理) |
components/ | 全局通用组件,如按钮、弹窗、表格封装 |
composables/ | 组合式函数,如 useTable、usePagination |
layouts/ | 布局容器,如后台布局、前台布局 |
router/ | 路由配置 |
stores/ | Pinia 状态 |
styles/ | 全局样式、变量、mixin |
types/ | 全局 TypeScript 类型声明 |
utils/ | 纯函数工具,不含业务 |
views/ | 页面级组件,与路由一一对应 |
判断标准很简单:能脱离本项目复用的放 utils,跟某个页面强绑定的放业务模块。
第三步:大型项目的关键——按业务域拆模块
页面超过 30 个以后,只做类型分层就不够了,要引入业务域层。在 src 下新建 modules/(有些团队叫 features/ 或 pages/),每个业务一个目录,内部自包含:
src/modules/
└── order/
├── api.ts # 该业务的接口
├── components/ # 只属于订单的组件
├── composables/ # 只属于订单的逻辑
├── stores/ # 订单状态
├── types.ts # 订单类型
└── views/ # 订单列表、详情等页面
规则是:跨模块复用的代码往上提,只服务本模块的代码留在模块内。这样删掉一个业务时,删一个目录就够了,不会牵一发动全身。
注意:模块之间不要互相 import 内部文件。需要通信就走 stores 或事件,否则模块边界会很快被打破,最后又变回一锅粥。
第四步:路由、状态、接口三件套跟着模块走
路由不要写成一个巨型 router/index.ts。按模块拆:
src/router/
├── index.ts # 创建 router、挂守卫
└── modules/
├── order.ts
└── user.ts
index.ts 里用 import.meta.glob('./modules/*.ts', { eager: true }) 自动聚合,加模块时不用改主文件。
Pinia 同理,src/stores/index.ts 只负责 createPinia(),具体 store 放在各模块或 stores/modules/ 下。接口层建议统一封装一个请求实例(src/api/request.ts),在里面做 token 注入、错误码统一处理、loading 控制,业务 api.ts 只写路径和参数。
注意:import.meta.glob 的路径是相对当前文件的,写成 ./modules/*.ts 而不是 @/router/modules/*.ts,写错不会报错,只会静默聚合出空数组。
第五步:配好别名和自动导入,避开相对路径地狱
在 vite.config.ts 里配别名:
import { fileURLToPath, URL } from 'node:url'
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
同时 tsconfig.json 的 compilerOptions.paths 里加 "@/*": ["./src/*"]。
大项目还可以装 unplugin-auto-import 和 unplugin-vue-components,让 ref、computed、store 和 components/ 下的组件免手动 import。但建议只对 Vue / Router / Pinia 这几个高频 API 开启自动导入,业务函数仍然显式 import,否则半年后没人说得清某个函数从哪来。
第六步:定好沉淀规则,目录才不会随时间烂掉
两条硬规则,建议写进团队规范:
- 同一个组件被第二个页面用到,就提到
components/;只被一个页面用,就留在模块内。
- 目录层级不超过四层,超过说明该拆模块而不是继续往下嵌套。
命名统一:组件用 PascalCase(OrderList.vue),其余文件用 kebab-case 或 camelCase 二选一,别混着来。
小结
- 先用
npm create vue@latest 生成标准骨架,别手搓。
src 下按职责分 api / components / composables / layouts / router / stores / styles / types / utils / views。
- 页面多了就上
modules/ 业务域层,模块内部自包含,跨模块复用才往上提。
- 路由、状态、接口都按模块拆分,用
import.meta.glob 聚合,加模块不改主文件。
vite.config.ts 别名和 tsconfig.json 的 paths 必须同步配。
- 模块之间禁止互相 import 内部文件,靠 store 或事件通信。
- 层级不超过四层,组件第二次被复用就提到全局。