项目写了三个月,目录乱成一锅粥,找个组件要翻半天——问题不在你,在一开始没搭好架子。这篇文章给你一套拿来即用的企业级框架模板。
一、前言
很多开发者拿到 Vue3 之后第一反应是:npm create vue@latest,跑起来就开干了。但企业级项目不是 Demo,如果开局没定好规矩,写到第三个月会发现:
- 目录结构一团乱,找个组件要翻半天
- ESLint 和同事的不一致,提交一次冲突一次
- 环境变量东一个西一个,打包上线才发现接口地址配错了
这篇文章带你从零搭建一套可落地的企业级 Vue3 后台框架——不是玩具项目,而是我过去一年在生产环境中迭代出来的最佳实践。
二、技术选型与版本踩坑
2.1 选型理由
| 技术 |
版本 |
选型理由 |
| Vue 3 |
3.5.x |
Composition API + <script setup> 是未来,Vue2 已经不推荐新项目使用 |
| Vite |
6.x |
开发服务器秒启动,HMR 极快,Webpack 项目迁移过来开发体验提升明显 |
| Element Plus |
2.9.x |
中文生态最好的 Vue3 UI 库,企业级组件覆盖完整 |
| TypeScript |
5.x |
类型安全是多人协作的生命线,不解释了 |
| Pinia |
2.x |
Vuex 的官方替代,TS 友好,API 简洁 |
| Vue Router |
4.x |
Vue3 配套,支持组合式 API 路由守卫 |
| unplugin-auto-import |
0.18.x |
自动导入 Vue/Element Plus API,告别重复 import |
| unplugin-vue-components |
0.28.x |
Element Plus 组件按需导入,打包体积减少 60%+ |
2.2 一个我踩了两天的坑
Element Plus 版本兼容性问题:去年我把项目从 Element Plus 2.2 升到 2.9,部分组件的 v-model 绑定方式变了,表单验证规则也改了。升级前务必读一遍 CHANGELOG,尤其是 Breaking Changes 部分。
经验之谈:锁定依赖版本号,别用 ^ 前缀。"element-plus": "2.9.7" 比 "^2.9.7" 安全得多。
三、项目初始化
3.1 创建项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| npm create vue@latest admin-framework
cd admin-framework npm install
|
3.2 安装企业级依赖
1 2 3 4 5 6 7 8 9 10 11
| npm install element-plus @element-plus/icons-vue
npm install -D unplugin-auto-import unplugin-vue-components
npm install axios dayjs nprogress
npm install -D @types/node @types/nprogress
|
3.3 Vite 配置(核心)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { fileURLToPath, URL } from 'node:url' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()], imports: ['vue', 'vue-router', 'pinia'], dts: 'types/auto-imports.d.ts' }), Components({ resolvers: [ElementPlusResolver()], dts: 'types/components.d.ts' }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server: { port: 80, host: '0.0.0.0', proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') }, '/file': { target: 'http://localhost:9300', changeOrigin: true } } }, build: { outDir: 'dist', assetsDir: 'static', chunkSizeWarningLimit: 2000, rollupOptions: { output: { manualChunks: { 'element-plus': ['element-plus'], 'vendor': ['vue', 'vue-router', 'pinia'] } } } } })
|
关键配置说明:
manualChunks:把 Element Plus、Vue 核心单独拆包,首页加载体积从 2MB 降到 800KB
proxy 的 /file:文件上传服务通常单独部署,这里做了独立代理
四、目录结构规范
这是我在若依框架基础上演进出的目录结构,经过 3 个项目验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| src/ ├── api/ # API 接口层 │ ├── system/ # 系统管理模块 │ │ ├── user.ts │ │ ├── role.ts │ │ └── menu.ts │ └── business/ # 业务模块(按需扩展) │ └── order.ts │ ├── assets/ # 静态资源 │ ├── images/ # 图片 │ └── styles/ # 全局样式 │ ├── variables.scss # SCSS 变量 │ ├── element.scss # Element Plus 样式覆盖 │ └── index.scss # 全局样式入口 │ ├── components/ # 公共组件 │ ├── FileUpload/ # 文件上传 │ ├── IconSelect/ # 图标选择器 │ ├── ImagePreview/ # 图片预览 │ └── ... │ ├── composables/ # 组合式函数(hooks) │ ├── useAuth.ts # 认证逻辑 │ ├── usePermission.ts # 权限判断 │ └── useTable.ts # 表格通用逻辑 │ ├── directives/ # 自定义指令 │ ├── permission.ts # v-hasPermi 按钮权限 │ └── debounce.ts # v-debounce 防抖 │ ├── layout/ # 布局组件 │ ├── index.vue # 主布局 │ ├── Sidebar/ # 侧边栏 │ ├── Navbar/ # 顶部导航 │ └── TagsView/ # 页签导航 │ ├── router/ # 路由配置 │ ├── index.ts # 路由入口 │ └── modules/ # 按模块拆分路由 │ ├── system.ts │ └── business.ts │ ├── store/ # Pinia 状态管理 │ ├── modules/ │ │ ├── user.ts # 用户状态 │ │ ├── app.ts # 应用配置 │ │ └── permission.ts # 权限状态 │ └── index.ts │ ├── utils/ # 工具函数 │ ├── request.ts # Axios 封装 │ ├── auth.ts # Token 管理 │ └── index.ts # 通用工具 │ ├── views/ # 页面视图 │ ├── system/ # 系统管理 │ └── business/ # 业务页面 │ ├── App.vue ├── main.ts └── permission.ts # 路由守卫(全局)
|
三条铁律:
api/ 目录必须和 views/ 一一对应——找接口和找页面路径一致,降低心智负担
components/ 只放纯公共组件——业务组件放各自 views/ 下的 components/ 子目录
- **超过 3 个路由模块就拆
modules/**——一个 index.ts 堆 50 个路由是灾难
五、环境变量与多环境配置
5.1 环境文件规划
1 2 3 4
| .env # 所有环境共享 .env.development # 开发环境 .env.production # 生产环境 .env.staging # 预发布环境(可选)
|
5.2 配置内容
1 2 3 4 5 6 7 8 9 10 11
| VITE_APP_TITLE = '系统管理(开发)' VITE_APP_BASE_API = '/api' VITE_APP_FILE_URL = '/file' VITE_APP_ENV = 'development'
VITE_APP_TITLE = 'MqCode' VITE_APP_BASE_API = '/prod-api' VITE_APP_FILE_URL = '/file' VITE_APP_ENV = 'production'
|
1 2 3 4 5 6 7 8 9 10 11 12 13
|
interface ImportMetaEnv { readonly VITE_APP_TITLE: string readonly VITE_APP_BASE_API: string readonly VITE_APP_FILE_URL: string readonly VITE_APP_ENV: 'development' | 'production' | 'staging' }
interface ImportMeta { readonly env: ImportMetaEnv }
|
5.3 Axios 请求封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios' import { ElMessage, ElMessageBox } from 'element-plus' import { getToken } from './auth'
const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, timeout: 30000, headers: { 'Content-Type': 'application/json;charset=utf-8' } })
service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = getToken() if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) )
service.interceptors.response.use( (response) => { const { code, msg, data } = response.data if (code === 200) return data
if (code === 401) { ElMessageBox.confirm('登录状态已过期,请重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { location.href = '/login' }) return Promise.reject(new Error(msg)) }
ElMessage.error(msg || '请求失败') return Promise.reject(new Error(msg)) }, (error) => { const message = error.response?.data?.msg || error.message || '网络异常' ElMessage.error(message) return Promise.reject(error) } )
export default service
|
三个关键设计决策:
- **
response.data 直接返回 data**:业务代码不需要每次都写 .data.data,减少嵌套
- 401 不直接跳转:弹窗提示用户,避免正在填写表单时被强制跳转导致数据丢失
timeout 设为 30s:企业级文件上传可能需要较长时间,太短容易误报超时
六、代码规范
6.1 ESLint + Prettier
重要提醒:ESLint 9 的配置格式是扁平化的 eslint.config.js,和旧版 .eslintrc 完全不同。如果你用的是若依框架的 ESLint 8 配置,别直接复制到新项目。
1
| npm install -D eslint @eslint/js typescript-eslint eslint-plugin-vue prettier eslint-config-prettier
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import js from '@eslint/js' import tseslint from 'typescript-eslint' import pluginVue from 'eslint-plugin-vue' import prettierConfig from 'eslint-config-prettier'
export default [ js.configs.recommended, ...tseslint.configs.recommended, ...pluginVue.configs['flat/recommended'], prettierConfig, { files: ['**/*.{ts,vue}'], languageOptions: { parserOptions: { parser: tseslint.parser } } }, { ignores: ['dist/**', 'node_modules/**', 'types/**'] } ]
|
1 2 3 4 5 6 7 8 9 10
| { "semi": false, "singleQuote": true, "trailingComma": "none", "printWidth": 100, "tabWidth": 2, "arrowParens": "avoid", "endOfLine": "auto" }
|
6.2 Git 提交规范
1 2
| npm install -D @commitlint/cli @commitlint/config-conventional npm install -D husky lint-staged
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'revert' ]] } }
|
提交格式示例:feat: 新增生产工单列表页面 / fix: 修复质检模板保存失败的问题
七、启动模板的完整 main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { createApp } from 'vue' import App from './App.vue' import router from './router' import pinia from './store'
import 'element-plus/dist/index.css'
import '@/assets/styles/index.scss'
import permission from '@/directives/permission'
import './permission'
const app = createApp(App)
app.use(router) app.use(pinia) app.directive('hasPermi', permission)
app.mount('#app')
|
main.ts 坚持一个小原则:每一行 import 只做一件事,不混在一起。这样六个月后回来看代码,一眼就知道初始化流程。
八、踩坑记录
| 坑 |
现象 |
原因 |
解决方案 |
| Element Plus 样式丢失 |
页面空白/组件无样式 |
unplugin-vue-components 未配置 importStyle: 'css' |
ElementPlusResolver({ importStyle: 'css' }) |
| Vite proxy 不生效 |
接口请求 404 |
路径重写规则不对 |
打开浏览器 Network 面板,确认请求 URL 是否被正确代理 |
| SCSS 变量全局引入失败 |
组件中用了 $primary-color 报错 |
Vite 需要配置 css.preprocessorOptions |
additionalData: '@use "@/assets/styles/variables.scss" as *;' |
| TypeScript 找不到路径别名 |
import from '@/api' 报红 |
未配置 tsconfig.json 的 paths |
见下方配置 |
1 2 3 4 5 6 7 8 9
| { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }
|
九、总结
这套模板的特点是不追求炫技,只追求稳:
- Vite + unplugin 自动导入:开发体验流畅,打包体积可控
- 目录结构按模块划分:
api/ 和 views/ 一一对应,降低协作成本
- Axios 封装一次到位:Token 刷新、统一错误处理、接口类型安全
- 环境变量 + 多环境切换:告别”打包前手动改地址”的噩梦
- ESLint + Prettier + Commitlint 三板斧:代码风格一致,提交记录可读
如果你是独立开发者,这套模板能让你跳过基础搭建,直接进入业务开发。
如果你是团队合作,这套规范能让新人第一天就能写出风格一致的代码。