项目写了三个月,目录乱成一锅粥,找个组件要翻半天------问题不在你,在一开始没搭好架子。这篇文章给你一套拿来即用的企业级框架模板。
一、前言
很多开发者拿到 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 创建项目
bash
# 使用官方脚手架创建
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 安装企业级依赖
bash
# 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 配置(核心)
ts
// 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 降到 800KBproxy的/file:文件上传服务通常单独部署,这里做了独立代理
四、目录结构规范
这是我在若依框架基础上演进出的目录结构,经过 3 个项目验证:
perl
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 环境文件规划
bash
.env # 所有环境共享
.env.development # 开发环境
.env.production # 生产环境
.env.staging # 预发布环境(可选)
5.2 配置内容
bash
# .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'
ts
// 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 请求封装
ts
// 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
三个关键设计决策:
response.data直接返回data:业务代码不需要每次都写.data.data,减少嵌套- 401 不直接跳转:弹窗提示用户,避免正在填写表单时被强制跳转导致数据丢失
timeout设为 30s:企业级文件上传可能需要较长时间,太短容易误报超时
六、代码规范
6.1 ESLint + Prettier
重要提醒 :ESLint 9 的配置格式是扁平化的
eslint.config.js,和旧版.eslintrc完全不同。如果你用的是若依框架的 ESLint 8 配置,别直接复制到新项目。
bash
npm install -D eslint @eslint/js typescript-eslint eslint-plugin-vue prettier eslint-config-prettier
js
// 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/**']
}
]
json
// .prettierrc
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "avoid",
"endOfLine": "auto"
}
6.2 Git 提交规范
bash
npm install -D @commitlint/cli @commitlint/config-conventional
npm install -D husky lint-staged
js
// 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
ts
// 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.json 的 paths |
见下方配置 |
json
// tsconfig.json 补充(追加到 compilerOptions)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
九、总结
这套模板的特点是不追求炫技,只追求稳:
- Vite + unplugin 自动导入:开发体验流畅,打包体积可控
- 目录结构按模块划分 :
api/和views/一一对应,降低协作成本 - Axios 封装一次到位:Token 刷新、统一错误处理、接口类型安全
- 环境变量 + 多环境切换:告别"打包前手动改地址"的噩梦
- ESLint + Prettier + Commitlint 三板斧:代码风格一致,提交记录可读
如果你是独立开发者,这套模板能让你跳过基础搭建,直接进入业务开发。
如果你是团队合作,这套规范能让新人第一天就能写出风格一致的代码。
📌 关于作者
我是一名全栈开发者,目前在深圳创业,专注于印刷包装行业的数字化系统建设。技术栈:Java / Spring Boot / Vue3 / uni-app / MySQL / Redis。
我会持续分享全栈开发实战、MES & CRM 产品设计,以及独立开发者的技术与商业思考。
如果这篇文章帮你省下了搭建后台框架的时间,点个赞 👍 让我知道。想获取更多实战文章和完整代码,欢迎关注微信公众号「MqCode」,每周更新,绝不断更。