大型项目验证的企业级 Vue3 项目架构模板 从零到生产可用的架构骨架
一个真正能落地、经过大型项目验证的企业级 Vue3 项目模板。不是那种"hello world 级别"的脚手架,而是从零到生产可用的架构骨架。
一、技术栈选型(先定武器)
领域 | 选型 | 理由 |
|---|---|---|
框架 | Vue 3.4+ ( | 性能 + 组合式 API |
语言 | TypeScript 5.x(严格模式) | 类型即文档 |
构建 | Vite 5.x | 快,生态成熟 |
路由 | Vue Router 4.x | 标配 |
状态 | Pinia + pinia-plugin-persistedstate | 比 Vuex 更适合组合式 |
HTTP | Axios(封装层)或 fetch 包装 | 拦截器、取消请求 |
UI | 不绑定具体库(Element Plus / Ant Design Vue 均可) | 解耦 UI 框架 |
样式 | UnoCSS + CSS Variables + SCSS(按需) | 原子化 + 主题 |
表单 | 自建 | 看团队偏好 |
表格 | 自建 | 配置驱动 |
校验 | Zod 或 | Schema 校验 |
测试 | Vitest + @vue/test-utils | 单元测试 |
代码质量 | ESLint + Prettier + simple-git-hooks + lint-staged | 强制约束 |
Monorepo(可选) | pnpm workspace | 超大型项目拆分 |
二、完整目录结构
my-enterprise-app/ ├── .husky/ ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── public/ ├── src/ │ ├── main.ts # 入口:只做 app 实例创建 & 插件注册 │ ├── App.vue # 根组件:只放 <RouterView /> │ ├── env.d.ts │ │ │ ├── assets/ # 纯静态资源 │ │ ├── images/ │ │ └── styles/ │ │ ├── variables.scss # 全局变量 │ │ ├── reset.scss # 重置样式 │ │ └── index.scss # 统一导出 │ │ │ ├── components/ # ✅ 通用基础组件(纯 UI,零业务) │ │ ├── base/ # 最底层:Button、Input、Modal... │ │ │ ├── BaseButton.vue │ │ │ └── BaseInput.vue │ │ ├── feedback/ # 反馈类:Toast、Loading、Empty... │ │ ├── layout/ # 布局类:Container、Header、Sider... │ │ └── index.ts # 统一 export + 自动全局注册 │ │ │ ├── layouts/ # ✅ 页面级布局壳 │ │ ├── DefaultLayout.vue # 侧边栏 + 头部 + 内容区 │ │ ├── BlankLayout.vue # 空白(登录页用) │ │ └── FullPageLayout.vue # 全屏页面 │ │ │ ├── pages/ # ✅ 页面(按业务域划分) │ │ ├── login/ │ │ │ ├── index.vue # 只做模板编排 │ │ │ ├── LoginForm.vue # 页面内子组件 │ │ │ └── composables/ │ │ │ └── useLogin.ts # 登录逻辑 │ │ ├── dashboard/ │ │ │ └── index.vue │ │ ├── system/ │ │ │ ├── users/ │ │ │ │ ├── index.vue │ │ │ │ ├── UserTable.vue │ │ │ │ ├── UserDrawer.vue │ │ │ │ ├── composables/ │ │ │ │ │ ├── useUserList.ts │ │ │ │ │ ├── useUserForm.ts │ │ │ │ │ └── useUserDelete.ts │ │ │ │ └── types.ts │ │ │ ├── roles/ │ │ │ └── permissions/ │ │ └── orders/ │ │ ├── index.vue │ │ ├── OrderTable.vue │ │ └── composables/ │ │ └── useOrderList.ts │ │ │ ├── composables/ # ✅ 跨页面复用的通用逻辑 │ │ ├── state/ │ │ │ ├── useToggle.ts │ │ │ ├── useCounter.ts │ │ │ └── useLoading.ts │ │ ├── ui/ │ │ │ ├── useDialog.ts # 命令式弹窗 │ │ │ ├── useMessage.ts # 全局提示 │ │ │ ├── usePagination.ts # 分页逻辑 │ │ │ └── useTable.ts # 表格通用逻辑 │ │ ├── dom/ │ │ │ ├── useEventListener.ts │ │ │ ├── useClickOutside.ts │ │ │ └── useDebounceFn.ts │ │ └── auth/ │ │ ├── usePermission.ts │ │ └── useAuth.ts │ │ │ ├── stores/ # ✅ Pinia 状态管理 │ │ ├── modules/ │ │ │ ├── app.store.ts # 应用级:sidebar collapsed、theme │ │ │ ├── user.store.ts # 用户信息、token │ │ │ ├── permission.store.ts # 权限路由、按钮权限 │ │ │ └── dict.store.ts # 字典数据(枚举映射) │ │ └── index.ts # 统一导出 + persist 配置 │ │ │ ├── services/ # ✅ API 请求层 │ │ ├── request/ │ │ │ ├── http.ts # axios 实例 + 拦截器 │ │ │ ├── types.ts # 请求相关类型 │ │ │ ├── errorHandler.ts # 统一错误处理 │ │ │ └── cancelRequest.ts # 请求取消管理 │ │ ├── modules/ │ │ │ ├── user.service.ts │ │ │ ├── order.service.ts │ │ │ └── auth.service.ts │ │ └── index.ts │ │ │ ├── domain/ # ✅ 领域模型 & 业务规则(核心!) │ │ ├── user/ │ │ │ ├── types.ts # User 实体定义 │ │ │ ├── rules.ts # 业务规则:canEdit、canDelete │ │ │ ├── transformers.ts # DTO ↔ Entity 转换 │ │ │ └── constants.ts # 枚举、状态码 │ │ ├── order/ │ │ │ ├── types.ts │ │ │ ├── rules.ts │ │ │ └── flow.ts # 订单流转规则 │ │ └── shared/ │ │ ├── pagination.ts # 分页通用类型 │ │ └── result.ts # ApiResult<T> 包装 │ │ │ ├── router/ # ✅ 路由 │ │ ├── index.ts # createRouter │ │ ├── routes/ │ │ │ ├── base.ts # 基础路由(登录、404) │ │ │ ├── system.ts # 系统管理模块路由 │ │ │ └── business.ts # 业务模块路由 │ │ ├── guards/ # 导航守卫 │ │ │ ├── auth.guard.ts # 登录校验 │ │ │ ├── permission.guard.ts# 权限校验 │ │ │ └── progress.guard.ts # 进度条 │ │ └── helpers/ │ │ └── routeMatch.ts # 路由匹配工具 │ │ │ ├── directives/ # ✅ 自定义指令 │ │ ├── permission.ts # v-permission │ │ ├── debounce.ts # v-debounce │ │ ├── copy.ts # v-copy │ │ └── index.ts │ │ │ ├── plugins/ # ✅ 第三方插件初始化 │ │ ├── iconify.ts # 图标注册 │ │ ├── echarts.ts # 图表注册 │ │ └── index.ts │ │ │ ├── utils/ # ✅ 纯工具函数 │ │ ├── storage.ts # localStorage/sessionStorage 包装 │ │ ├── format.ts # 日期、金额格式化 │ │ ├── validate.ts # 通用校验 │ │ ├── tree.ts # 树形数据处理 │ │ ├── download.ts # 文件下载 │ │ └── crypto.ts # 加密解密 │ │ │ ├── config/ # ✅ 应用配置(编译时确定) │ │ ├── app.ts # 应用名称、版本 │ │ ├── menu.ts # 菜单配置 │ │ └── api.ts # API 路径前缀 │ │ │ ├── constants/ # ✅ 运行时常量 │ │ ├── storageKeys.ts │ │ ├── regexps.ts │ │ └── httpStatus.ts │ │ │ └── types/ # ✅ 全局类型声明 │ ├── api.d.ts │ ├── env.d.ts │ ├── shims-vue.d.ts │ └── business.d.ts │ ├── tests/ # ✅ 测试 │ ├── unit/ │ └── e2e/ │ ├── scripts/ # ✅ 构建/开发脚本 │ ├── generateApi.js # swagger → ts types │ └── generatePage.js # 自动生成页面模板 │ ├── .env.development ├── .env.production ├── .env.staging ├── .eslintrc.cjs ├── .prettierrc.cjs ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── package.json └── README.md三、核心模块代码示例
1️⃣ 入口文件(main.ts)—— 只做装配
// src/main.ts import { createApp } from 'vue' import App from './App.vue' import { setupRouter } from './router' import { setupStores } from './stores' import { setupDirectives } from './directives' import { setupPlugins } from './plugins' import { setupGlobalComponents } from './components' import './assets/styles/index.scss' function bootstrap() { const app = createApp(App) setupStores(app) // Pinia setupRouter(app) // Vue Router setupDirectives(app) // 自定义指令 setupPlugins(app) // 第三方插件 setupGlobalComponents(app) // 全局基础组件 app.mount('#app') } bootstrap()📌原则:入口不写业务逻辑,只做"接线"
2️⃣ HTTP 请求层(services/request/http.ts)
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios' import { useUserStore } from '@/stores/modules/user.store' import { showMessage } from '@/composables/ui/useMessage' import { handleBusinessError, handleHttpError } from './errorHandler' const http: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' } }) // 请求拦截 http.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers!.Authorization = `Bearer ${userStore.token}` } return config }) // 响应拦截 http.interceptors.response.use( (response) => { const { code, data, message } = response.data // 业务码处理 if (code === 200) return data if (code === 401) { // token 过期 → 跳转登录 window.location.href = '/login' return Promise.reject(new Error('未登录')) } handleBusinessError(code, message) return Promise.reject(new Error(message)) }, (error) => { handleHttpError(error) return Promise.reject(error) } ) export default http3️⃣ Service 层(services/modules/user.service.ts)
import http from '../request/http' import type { User, CreateUserDto, UpdateUserDto } from '@/domain/user/types' import type { PaginatedResult } from '@/domain/shared/result' export const userApi = { getList(params: { page: number; pageSize: number; keyword?: string }) { return http.get<PaginatedResult<User>>('/users', { params }) }, getById(id: string) { return http.get<User>(`/users/${id}`) }, create(data: CreateUserDto) { return http.post<User>('/users', data) }, update(id: string, data: UpdateUserDto) { return http.put<User>(`/users/${id}`, data) }, delete(id: string) { return http.delete(`/users/${id}`) }, batchDelete(ids: string[]) { return http.post('/users/batch-delete', { ids }) } }📌Service 只管"怎么调接口",不管"什么时候调、调完干什么"
4️⃣ Composable —— 业务逻辑核心(useUserList.ts)
// src/pages/system/users/composables/useUserList.ts import { ref, onMounted } from 'vue' import { userApi } from '@/services/modules/user.service' import { usePagination } from '@/composables/ui/usePagination' import { useLoading } from '@/composables/state/useLoading' import { useMessage } from '@/composables/ui/useMessage' import type { User } from '@/domain/user/types' export function useUserList() { const { page, pageSize, total, resetPage } = usePagination() const { loading, withLoading } = useLoading() const { success, error } = useMessage() const list = ref<User[]>([]) const keyword = ref('') const fetchList = async () => { try { const res = await withLoading( userApi.getList({ page: page.value, pageSize: pageSize.value, keyword: keyword.value }) ) list.value = res.items total.value = res.total } catch (e) { error('获取用户列表失败') } } const handleSearch = () => { resetPage() fetchList() } const handleReset = () => { keyword.value = '' resetPage() fetchList() } onMounted(fetchList) return { list, loading, page, pageSize, total, keyword, fetchList, handleSearch, handleReset } }5️⃣ 页面组件(pages/system/users/index.vue)—— 只做编排
<script setup lang="ts"> import UserTable from './UserTable.vue' import UserDrawer from './UserDrawer.vue' import { useUserList } from './composables/useUserList' import { useUserDelete } from './composables/useUserDelete' const { list, loading, page, pageSize, total, keyword, handleSearch, handleReset } = useUserList() const { deleting, handleDelete, handleBatchDelete } = useUserDelete(() => fetchList()) // 抽屉状态 const drawerVisible = ref(false) const currentUserId = ref<string>() </script> <template> <div class="user-page"> <!-- 搜索栏 --> <a-card :bordered="false" class="mb-4"> <a-form layout="inline"> <a-form-item label="关键词"> <a-input v-model:value="keyword" placeholder="姓名/手机号" allow-clear /> </a-form-item> <a-form-item> <a-button type="primary" @click="handleSearch">搜索</a-button> <a-button class="ml-2" @click="handleReset">重置</a-button> </a-form-item> </a-form> </a-card> <!-- 操作栏 + 表格 --> <a-card :bordered="false"> <template #extra> <a-button type="primary" @click="drawerVisible = true">新增用户</a-button> <a-button danger class="ml-2" :disabled="!selectedIds.length">批量删除</a-button> </template> <UserTable :data="list" :loading="loading" :pagination="{ page, pageSize, total }" @delete="handleDelete" @edit="(id) => { currentUserId = id; drawerVisible = true }" /> </a-card> <!-- 新增/编辑抽屉 --> <UserDrawer v-model:visible="drawerVisible" :user-id="currentUserId" @success="fetchList" /> </div> </template>📌页面组件里没有一行数据请求代码、没有业务逻辑判断
6️⃣ 领域规则(domain/user/rules.ts)
import type { User } from './types' export function canEditUser(user: User, currentUserId: string): boolean { if (user.id === currentUserId) return true if (user.role === 'super_admin') return false return true } export function canDeleteUser(user: User): boolean { return user.status !== 'active' && user.role !== 'super_admin' } export function getUserDisplayName(user: User): string { return user.nickname || user.username }7️⃣ usePagination —— 通用复用逻辑
// src/composables/ui/usePagination.ts import { ref } from 'vue' export function usePagination(defaultPageSize = 20) { const page = ref(1) const pageSize = ref(defaultPageSize) const total = ref(0) const resetPage = () => { page.value = 1 } const onChange = (p: number, ps: number) => { page.value = p pageSize.value = ps } return { page, pageSize, total, resetPage, onChange } }8️⃣ 权限指令(directives/permission.ts)
import type { Directive } from 'vue' import { usePermission } from '@/composables/auth/usePermission' export const permissionDirective: Directive = { mounted(el, binding) { const { hasPermission } = usePermission() const required = binding.value // ['user:create', 'user:update'] if (!hasPermission(required)) { el.parentNode?.removeChild(el) } } }模板中使用:
<a-button v-permission="['user:create']">新增用户</a-button>四、数据流全景图
┌─────────────────────────────────────────────┐ │ Template │ │ (只消费数据 + 触发事件) │ └──────────────────┬──────────────────────────┘ │ ┌──────────────────▼──────────────────────────┐ │ Page Component │ │ (编排 composables) │ └──────┬───────────┬────────────┬────────────┘ │ │ │ ┌──────▼───┐ ┌────▼────┐ ┌───▼──────────┐ │Composable│ │Composable│ │ Domain Rules │ │(业务逻辑) │ │(UI 逻辑) │ │ (业务规则) │ └──────┬───┘ └────┬────┘ └───┬──────────┘ │ │ │ ┌──────▼───────────▼────────────▼──────────┐ │ Pinia Store │ │ (跨页面共享状态) │ └──────────────────┬───────────────────────┘ │ ┌──────────────────▼───────────────────────┐ │ Services Layer │ │ (HTTP 请求封装) │ └──────────────────┬───────────────────────┘ │ ┌──────────────────▼───────────────────────┐ │ HTTP Client (Axios) │ └──────────────────┬───────────────────────┘ │ Backend API五、关键设计决策说明
决策 | 为什么这么做 |
|---|---|
Service 不直接返回 response | 统一在拦截器拆包,调用方拿到纯数据 |
Composable 返回函数引用而非执行结果 | 页面控制时机 |
页面组件不 | 用 |
Domain 层不依赖 Vue | 规则函数可单独测试、可复用到 Node.js |
指令做权限而非 v-if | 声明式、无侵入、可统一处理 |
每个页面域有自己的 composables/ | 避免跨页面耦合,同时也允许提取到全局 composables/ |
六、团队协作规范(必加)
组件分类约定
层级 | 位置 | 能否调 API | 能否有业务 |
|---|---|---|---|
Base 组件 |
| ❌ | ❌ |
业务组件 |
| 通过 props/emit | ✅ |
页面 |
| 通过 composable | 编排 |
Git Commit 规范
feat(user): 新增用户批量导出功能 fix(order): 修复订单状态流转异常 refactor(domain): 重构用户权限判断逻辑 style(table): 调整表格列宽七、后续演进路线
阶段 | 动作 |
|---|---|
初期 | 按上面结构搭建,先跑通 |
中期 | 把 |
后期 | 如果多项目共用,拆成 pnpm monorepo |
成熟期 | Domain 层独立成包,Vue 只是渲染层 |
这就是一个能支撑 50+ 页面、10+ 开发者协作、持续迭代 3 年以上不崩盘的 Vue 3 企业级项目模板。
如果这篇文章对你有帮助,欢迎点赞收藏,关注我,我会持续分享前端项目框架和gis领域的实战经验~
