Vue3项目目录结构最佳实践:从脚手架到中后台的扩展方案
说到vue3项目目录结构很多人第一反应就是脚手架初始化出来的那套骨架——好像照着用就完事了。我做了几年前端也折腾过不少vue2到vue3的迁移项目越来越觉得目录结构这事儿看着不起眼实际影响非常大。一个项目三个月之后好不好维护很大程度上看的就是第一天怎么摆的这些文件夹。这篇文章不是要给你看一张标准的目录树截图就结束。我想结合真正的中大型项目、后台管理系统、还有面试里常问的那些问题把vue3项目目录设计背后的逻辑讲透哪些文件夹是官方默认的、哪些是社区约定俗成的、哪些是你必须根据业务自己加的以及为什么这样分。适合正在学vue3的、准备从vue2升级的、或者想重组现有项目结构的朋友参考。1. 脚手架生成的默认骨架先认清每个文件夹的真实职责1.1 create-vue生成的项目到底长什么样用Vite官方脚手架创建项目之后你会得到一个干净目录。新建项目的命令很简单npm create vuelatest选完TypeScript、Router、Pinia等配置后你可以直接用编辑器打开目录。选完TypeScript、Router、Pinia等配置后你可以直接用编辑器打开目录结构大致长这样my-project/ ├── public/ │ └── favicon.ico ├── src/ │ ├── assets/ │ │ └── main.css │ ├── components/ │ │ └── HelloWorld.vue │ ├── router/ │ │ └── index.js │ ├── stores/ │ │ └── counter.js │ ├── views/ │ │ ├── HomeView.vue │ │ └── AboutView.vue │ ├── App.vue │ └── main.js ├── index.html ├── vite.config.js └── package.json这套骨架是Vue官方团队设计的麻雀虽小五脏俱全。几个目录一眼就能看懂components放组件、views放页面、router放路由、stores放状态。但对刚接触vue3的人来说有几点容易被忽略我逐个说。1.2 五个关键文件和目录的职责拆解先说说入口和顶层文件。index.html在项目根目录不放在public也不放在src这里经常有人困惑。原因是Vite在开发服务器启动时需要把index.html作为应用的HTML入口同时通过script typemodule src/src/main.js加载编译入口。它放在根目录是约定改不了太随意因为Vite默认按这个路径找它。src/main.js是前端打包的真正入口。在vue3中它和vue2最大的不同在于创建应用的方式import { createApp } from vue import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(router) app.use(createPinia()) app.mount(#app)createApp取代了vue2的new Vue()整个应用从一个根组件实例变成了一种应用实例的概念。这也直接影响了目录里其他部分的组织方式。比如router目录默认是一个index.js文件保持扁平还是按模块拆分脚手架没给答案留给项目自己定。stores目录是vue3特有的。vue2时代大家习惯用Vuex目录里放一堆state、mutations、actions文件。vue3搭配官方推荐的Pinia后stores目录直接聚合到一个文件里组织方式完全不一样我后面专门说。views和components都是放组件的但职责完全不同也是我见过新人最容易放混的一个点下一节重点拆。1.3 public和assets的区别很多人栽在这里脚手架里同时出现了public和src/assets两个目录看着都能放静态资源实际语义差别很大。public下的文件不会被编译直接原样拷贝到构建输出目录的根下。你在代码里要引用public下的资源写的是绝对路径比如/favicon.ico而且文件名会保留不会被指纹化处理。src/assets下的文件恰恰相反会经过Vite的编译管道。以图片为例小图片会通过base64内联到代码里减少请求次数大图片会用带哈希的文件名输出方便做缓存控制。所以一般来说需要被打包管理的、和组件逻辑关联紧密的资源放assets不常变动的、希望保持原始URL的放public。一个我自己常用的判断标准这个文件是应用的一部分还是外部引用的静态内容图标、背景图、样式表这类跟随代码仓库的资源放src/assetsfavicon.ico、robots.txt、第三方静态插件文件放public。要是随便放最直接的后果就是线上部署之后图片路径找不到或者缓存策略不对这种小坑排查起来很费时间。2. src目录的核心拆分逻辑views、components和路由之间的关系2.1 页面与组件靠复用和路由挂载来分家views和components的边界问题是我在code review里反复提醒的点。模糊地说views是页面components是组件。但具体到某个模块判断标准其实很简单这个文件会不会被路由直接引用如果会它就是一个view。这个文件会不会被两个以上不同的地方引用如果会它就应该被抽到components。一个文件只在某一个view内部使用但代码量很大要不要拆拆出来的东西优先考虑放在这个view同名的子目录里而不是直接扔到全局components。实际项目里一个后台管理系统的views往往是这样的src/views/ ├── login/ ├── dashboard/ │ └── index.vue ├── user/ │ ├── list.vue │ └── detail.vue └── order/ ├── list.vue └── refund.vue这种按业务模块拆的目录比把所有页面平铺在views下要好得多。因为路由配置可以直接用同样的层级结构搜索代码、定位问题都很快。2026年现在做前端开发很多编辑器插件都能根据页面快速定位到代码但如果目录本身层级混乱再强的插件也救不了你。2.2 共同约定一级目录按类型分二级目录按业务分这是我在多个项目里踩过坑之后总结出的铁律。一级目录只放类型分类components、views、utils、api、stores。但到了二级目录必须按业务模块划分而不是继续按文件类型划分。反面例子就是有人喜欢在views下建module1、module2这种无意义前缀或者在components里按button、table、dialog这种UI层去分。短时间看起来整齐业务一多立刻失控。正面的做法是让所有一级目录的业务命名保持一致——user、order、goods这样你打开任何一个一级目录看到的都是同一套业务名词心智能耗极低。2.3 就近放置还是统一收拢一个务实的选择组件到底放在自己用的页面旁边还是统一堆到components目录vue社区有个经典争论官方文档推荐就近放置——组件只在一个页面里用就放在那个页面的目录下而不是全局components。但国内很多团队习惯所有组件统一收拢到components理由是一眼能看到所有组件方便复用。我的经验是小项目、组件数量少于30个时统一收拢完全没问题效率反而高项目大到几十个页面、上百个组件时统一收拢会让components变成一个垃圾场光找合适组件就很费劲。折中方案是能把组件做通用化的放components并按功能分子目录只服务于某个页面、且不想通用的直接放views/xxx/components/。这样目录结构既不会太散也不会太乱。3. 从后台管理系统的视角看目录扩展真实业务需要哪些默认没有的东西3.1 一个最佳实践目录树官方脚手架之外的六大扩展如果你要做的是一个中后台管理系统光靠脚手架生成的骨架肯定不够。我一般会在项目里追加以下几个目录api、composables或hooks、layout、directives、constants、types。完整目录大致是这样src/ ├── api/ │ ├── request.js │ ├── user.js │ └── order.js ├── assets/ ├── components/ │ ├── common/ │ └── business/ ├── composables/ │ ├── useTable.js │ ├── usePagination.js │ └── usePermission.js ├── constants/ │ └── index.js ├── directives/ │ ├── permission.js │ └── debounce.js ├── layout/ │ └── index.vue ├── router/ │ ├── index.js │ └── routes.js ├── stores/ │ ├── user.js │ └── app.js ├── types/ │ └── api.d.ts ├── utils/ │ └── format.js └── views/多出来的这些目录每一个都有明确的定位。我比较推荐从项目一开始就建好空目录哪怕暂时没东西。因为空目录的存在本身就是一种约定项目成员往里填东西的时候会自觉按规矩放。3.2 api目录和request封装是怎么配合的后台管理系统的所有接口请求都应该走api目录不应该在组件里直接写axios.get的URL。api目录配一个统一的请求实例负责注入token、统一错误处理、返回解包然后每个业务模块导出一组请求函数。比如api/user.jsimport request from ./request export function getUserList(params) { return request.get(/user/list, { params }) } export function updateUser(id, data) { return request.put(/user/${id}, data) }request.js里通常做几件事创建axios实例、设置baseURL和超时时间、请求拦截器里读token并加到请求头、响应拦截器里统一处理后端返回码、500错误弹出统一提示。这样组件里的业务代码只关心调用哪个函数、拿到什么数据请求细节被完全隔离。3.3 composables目录把setup里的一坨拆出来vue3的Composition API让逻辑复用真正有了官方支撑。很多人项目写到一半会发现自己setup里面躺着几百行代码——响应式变量、计算方法、监听器、业务函数全在一起。这时候composables目录就派上大用场。composables目录放的是以use开头的组合式函数它把一类业务逻辑聚合成一个可复用的模块。比如后台管理里最常用的表格分页逻辑可以抽成这样// composables/usePagination.js import { ref, computed } from vue export function usePagination(defaultPageSize 10) { const page ref(1) const pageSize ref(defaultPageSize) const total ref(0) const pageInfo computed(() ({ page: page.value, pageSize: pageSize.value })) function handlePageChange(newPage) { page.value newPage } return { page, pageSize, total, pageInfo, handlePageChange } }你可以在任何一个页面里直接调用usePagination()拿到分页参数和切换函数避免了每个页面都重新写一份一模一样的分页逻辑。这一步是vue3项目从组件封装思维走向逻辑封装思维的关键转变。3.4 layout、store和权限控制落地后台管理系统的权限控制牵扯到router、stores、directives、layout多个目录很多人搞不清它们之间的协作链条。简化版的流程是这样的登录成功后后端返回用户角色和可访问路由列表前端把它存进stores/user.js路由守卫在每次跳转前检查stores里有没有用户信息没有就重定向到登录页菜单栏根据路由表生成页面按钮的显隐通过自定义指令v-permission控制。layout目录在这个链条里扮演壳的角色——顶部导航、侧边菜单、内容区域router-view都放在这里。整个框架的骨架都在layout下页面组件只是往内容区里填充。如果没有layout目录这些结构写在哪都不合适项目一大就乱套。4. 目录结构背后是组织哲学Composition API改变了代码的物理布局4.1 vue2目录和vue3目录的差异不只是名字很多vue2的老项目目录结构是围绕data、methods、computed这些选项组织的代码文件也倾向于按选项类型拆分。顶层路由、状态、组件、页面四块核心本质上跟vue3脚手架很像。但vue3把Composition API引入之后目录结构多了一个维度composables。它代表的是按业务逻辑划分代码区块的新思路。vue2里一段获取列表、处理筛选、分页、删除的逻辑要分散在data、methods、computed甚至watch里阅读一个功能得把四个区块拼起来。vue3里这些逻辑可以塞进一个useUserList()函数变成一个独立文件。目录结构从描述这个文件是什么类型的代码逐渐变成这个文件承载哪些业务能力。composables目录就是为了承载这种变化而存在的。另外vue2的Vuex和vue3的Pinia在组织风格上也有明显不同。Vuex的目录往往是store/modules下按业务模块拆每个模块里再分state、getters、mutations、actions四个文件或者四个区块。Pinia更轻直接支持用一个文件、一个组合式风格函数来定义仓库stores/user.js里写几行就能搞定一件完整的事代码位置更集中。4.2 composable的边界怎么定目录就会怎么长composables目录用得好项目结构会很清爽用不好它就会变成第二个utils垃圾桶。区分这两个目录我的经验是看有没有响应式状态。utils放的是纯函数工具输入输出都是普通值不涉及ref、reactive这类响应式概念。比如日期格式化、手机号脱敏、数组去重。composables放的是和组件状态绑定在一起的逻辑内部一定用到ref、computed、watch等vue API并且返回给组件使用的。按这个标准切分职责很清晰。如果一个composable文件膨胀得太快说明它承担了过多职责——比如useTable既管加载数据、又管筛选、又管导出、又管列设置。应该按更细的维度拆分useTableData、useTableSelection、useTableColumn。目录结构不需要一开始就完美但要允许它随着业务复杂度持续生长。4.3 面试里关于目录和结构的高频问题怎么答vue3面试题里经常出现说说你项目里composables和utils的区别vue3项目目录怎么组织为什么用Pinia不用Vuex这类问题。与其背一堆空洞的答案不如用你真实的目录结构来回答。我一般会从三个角度展开顶层平台能力router、stores、directives 业务模块views、api下的业务分组 复用能力components、composables、utils。讲清楚谁是入口谁依赖谁复用逻辑放在哪比背诵任何最佳实践都有说服力。面试官想听的就是你有没有真正理解目录拆分是服务于职责内聚和依赖关系的而不是为了好看。5. 实际项目里目录最容易烂掉的三个地方踩坑与对策5.1 坑一utils和composables边界模糊最后变成垃圾桶这是我见过最普遍的情况。项目进行到中期总会有人把跟组件状态无关的逻辑塞进utils又把跟响应式相关的代码塞进composables两边的边界越来越模糊。等到后期想重构成本已经很高了。对策是从项目一开始就在README或者CONTRIBUTING.md里写明分类标准并且code review时严格执行那个标准。我自己的经验是一条规则只要用一两句话能说清楚大家就愿意遵守一旦标准模棱两可所有人都会按自己的理解行事那目录就等于没设计。5.2 坑二components越攒越多页面反而变空壳还有一种很常见的情况components目录越来越多里面的组件越来越小页面里全是各种组件的拼装业务逻辑散落在每个子组件里。想找一个字段的校验规则得翻五六个组件文件。这个问题的本质是过度拆分。我的建议是当你在一个页面里连续拼装了七八个只在这个页面用的业务组件时先停下来。如果这些组件之间共享着敏感的业务上下文同一个表单、同一份订单数据把它们再拉回到页面里或者用一个page/xxx的目录整体收纳。目录结构是为了帮助理解和修改代码不是为了好看、多而全。如果它让修改代码的成本变高了那它就是错的。5.3 坑三全局样式、类型、常量乱放拉到最底还是找不到样式和类型定义是目录设计中特别容易被遗忘的部分。项目最开始可能只在assets/main.css放几个全局变量后来各种scoped样式、主题变量、深浅色模式切换都在路上样式文件到底放哪就成了一个大问题。我的习惯是为样式单独建一个styles目录或者沿用assets下的子目录按用途拆分variables.css放设计变量reset.css做基础重置global.css放全局覆盖。scoped样式尽量写在vue组件里避免大量全局样式表互相覆盖。类型定义则单独放types目录接口请求的入参出参、表格行数据类型、Pinia声明统一维护。TypeScript项目里这个目录尤其重要类型一旦乱放编辑器提示就会时有时无开发效率直接减半。5.4 引入新依赖时给目录立规矩一个实用的动作每次引入新的第三方库组件库、图表库、富文本编辑器、权限框架我都建议团队约定好它的文件应该放在哪里。组件库的二次封装组件放components/common图表相关的封装放components/chart在api里写对应接口函数在composables写对应的业务逻辑组合函数。这套规矩不写下来过两周就会有人在一个不知名的目录里创建一套新的封装。做技术负责人的同事尤其应该注意目录结构不是一次规划就永远不变的它跟代码一样需要持续维护。每次加新功能时多想一步这个文件放这里是不是最容易被后来的人找到整个项目就能长期保持清爽。我自己在实际操作中的体会是目录结构没有标准答案适合团队规模和业务特性的才是好结构。小项目可以很扁平十个八个人协作的中大型项目就需要更细的分层。关键不是照着哪篇博客抄一份目录出来而是理解每一层划分解决的问题然后持续地、有意识地维护它。最后分享一个小技巧如果你的项目已经乱到不想看别急着大重构。先把api和composables这两个目录整理出来——它们往往是业务逻辑最密集、最影响后续迭代的部分。改完之后接口请求不再散落在页面里公共逻辑也能被复用你会明显感觉到代码变顺了。目录结构这件事本质上是在为未来的自己省时间。