项目写了三个月,目录乱成一锅粥,找个组件要翻半天——问题不在你,在一开始没搭好架子。这篇文章给你一套拿来即用的企业级框架模板。


一、前言

很多开发者拿到 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

# 选项配置
# ✔ TypeScript? … Yes
# ✔ JSX Support? … No(个人偏好,看团队)
# ✔ Vue Router? … Yes
# ✔ Pinia? … Yes
# ✔ Vitest? … Yes
# ✔ ESLint? … Yes
# ✔ Prettier? … Yes

cd admin-framework
npm install

3.2 安装企业级依赖

1
2
3
4
5
6
7
8
9
10
11
# UI 框架 + 自动导入
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
// vite.config.ts
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 # 路由守卫(全局)

三条铁律:

  1. api/ 目录必须和 views/ 一一对应——找接口和找页面路径一致,降低心智负担
  2. components/ 只放纯公共组件——业务组件放各自 views/ 下的 components/ 子目录
  3. **超过 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
# .env.development
VITE_APP_TITLE = '系统管理(开发)'
VITE_APP_BASE_API = '/api'
VITE_APP_FILE_URL = '/file'
VITE_APP_ENV = 'development'

# .env.production
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
// types/env.d.ts - 为环境变量提供类型提示
/// <reference types="vite/client" />

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
// src/utils/request.ts
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

三个关键设计决策:

  1. **response.data 直接返回 data**:业务代码不需要每次都写 .data.data,减少嵌套
  2. 401 不直接跳转:弹窗提示用户,避免正在填写表单时被强制跳转导致数据丢失
  3. 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
// eslint.config.js(ESLint 9 扁平化配置)
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
// .prettierrc
{
"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
// commitlint.config.js
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
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './store'

// Element Plus 样式
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.jsonpaths 见下方配置
1
2
3
4
5
6
7
8
9
// tsconfig.json 补充(追加到 compilerOptions)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

九、总结

这套模板的特点是不追求炫技,只追求稳

  1. Vite + unplugin 自动导入:开发体验流畅,打包体积可控
  2. 目录结构按模块划分api/views/ 一一对应,降低协作成本
  3. Axios 封装一次到位:Token 刷新、统一错误处理、接口类型安全
  4. 环境变量 + 多环境切换:告别”打包前手动改地址”的噩梦
  5. ESLint + Prettier + Commitlint 三板斧:代码风格一致,提交记录可读

如果你是独立开发者,这套模板能让你跳过基础搭建,直接进入业务开发。

如果你是团队合作,这套规范能让新人第一天就能写出风格一致的代码。