1. 为什么选择Vue3和Element Plus来构建企业级后台?
如果你正在为团队或公司搭建一个后台管理系统,或者想从零开始学习如何构建一个完整的、能上线的项目,那么你找对地方了。我做过不少这类项目,从早期的jQuery时代到后来的Vue 2,再到现在的Vue 3,一路踩坑过来,最大的感受就是:选对技术栈,真的能省下一半的头发。今天,我就来跟你聊聊,为什么Vue 3加上Element Plus,是目前搭建企业级后台最舒服、最高效的组合之一。
首先说说Vue 3。你可能听说过它性能好、体积小,但这到底意味着什么?我举个实际的例子。以前用Vue 2做一个稍微复杂点的数据看板,页面里图表、表格、筛选器一大堆,数据一更新,有时候能感觉到轻微的卡顿。后来重构到Vue 3,最直观的感受就是“跟手”了。这背后是Vue 3全新的响应式系统(用的是Proxy)和编译时优化在起作用。对于后台管理系统这种交互复杂、组件繁多、状态管理要求高的应用,这种性能提升带来的体验改善是实实在在的。更重要的是它的组合式API,这玩意儿简直是管理复杂业务逻辑的“神器”。以前用选项式API,一个功能相关的data、methods、computed、watch散落在文件各处,维护起来像在玩“找不同”。现在用<script setup>配合组合式函数,可以把一个功能的所有代码聚合在一起,复用起来也极其方便。比如一个复杂的用户权限判断逻辑,我封装成一个usePermission的组合式函数,在任何页面里都是一行import的事,清晰又高效。
然后是Element Plus。市面上基于Vue 3的UI库不少,为什么我偏爱它?就三个字:全、稳、熟。“全”是指组件库覆盖了后台管理系统99%的场景。从基础的布局、按钮、表单,到复杂的表格、树形控件、步骤条,再到图表、富文本编辑器这类增强组件,它几乎都有现成的、经过大量项目验证的实现。你不需要为了一个日期范围选择器或者一个可拖拽排序的表格去满世界找第三方库,省去了大量集成和兼容性调试的时间。“稳”是指它的设计和代码质量。Element Plus脱胎于经典的Element UI,继承了其严谨的设计规范和稳定的API。在企业级项目里,稳定性和一致性往往比炫酷的动画更重要。它的组件设计符合大多数后台操作习惯,文档清晰,社区遇到问题也基本都能找到解决方案。“熟”则是对国内开发者非常友好。中文文档齐全,社区活跃,很多你想到的、没想到的“坑”,前辈们都踩过并分享了经验。这对于需要快速上手的团队项目来说,沟通和学习成本都低得多。
把这两者结合起来,你的开发体验会是这样:用Vite瞬间创建一个Vue 3项目,感受飞快的启动和热更新;然后引入Element Plus,像搭积木一样,用现成的高质量组件快速拼出页面的骨架;最后,用Vue 3的组合式API去填充那些独特的、复杂的业务逻辑。整个过程流畅自然,你可以把绝大部分精力聚焦在业务本身,而不是在框架选型、UI适配或者性能调优上反复折腾。接下来,我们就从零开始,一步步把这个“积木”搭起来。
2. 项目初始化与环境搭建:从零到一的正确姿势
万事开头难,但一个好的开始能让后续开发顺风顺水。这里我不只告诉你命令怎么敲,更想分享我实践中总结的、能让项目结构更清晰、更易于维护的初始化套路。
2.1 用Vite创建项目,为什么是现在的最佳选择?
几年前,Vue CLI是标准答案。但现在,我强烈推荐你从Vite开始。它的速度优势太明显了。用npm create vite@latest,感觉还没按回车,项目就建好了。冷启动和热更新快到让你怀疑人生,这在开发大型后台系统时,能显著提升你的“心流”体验,减少等待编译的烦躁感。我们来实际操作一下:
# 使用npm 7+,这个命令会启动一个交互式创建向导,非常方便
npm create vite@latest my-admin-system -- --template vue
# 进入项目目录
cd my-admin-system
# 安装依赖
npm install
执行完后,你会得到一个非常干净的项目结构。我建议你立刻做一件事:打开package.json,把scripts里的dev命令稍微改一下,加上--host。这样你可以在同一局域网下的其他设备(比如手机或测试机)上访问开发服务器,方便调试。
{
"scripts": {
"dev": "vite --host", // 加上 --host 参数
"build": "vite build",
"preview": "vite preview"
}
}
然后运行npm run dev,你会看到一个本地地址和一个网络地址,这就搞定了。
2.2 引入Element Plus:全局引入还是按需引入?
这是很多人的第一个纠结点。我的建议是:在项目初期,尤其是学习或中小型项目,直接用全局引入,简单粗暴。等到项目体积变大,你明显感觉到首屏加载需要优化时,再考虑按需引入。过早优化是万恶之源。全局引入只需要两步:
首先,安装Element Plus和它的图标库(图标库是独立包,但很多组件会用到):
npm install element-plus @element-plus/icons-vue
然后,在main.js(或main.ts)中引入:
import { createApp } from 'vue'
import App from './App.vue'
// 1. 引入Element Plus核心库和样式
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 2. 引入图标库(可选,但推荐)
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 3. 全局注册所有图标(方便在模板中直接使用 `<el-icon><Edit /></el-icon>`)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 4. 使用Element Plus
app.use(ElementPlus)
app.mount('#app')
搞定!现在你就可以在项目的任何地方使用<el-button>这样的组件了。这种方式的好处是心智负担小,不用担心哪个组件没引入。当然,它的代价是打包体积会包含整个Element Plus。但对于企业内网后台或者初期项目,这点体积通常不是问题。
2.3 项目结构规划:为增长做好准备
默认的src目录结构太简单了。一个可维护的企业级项目,需要有清晰的结构。我通常会这样组织,你可以参考:
src/
├── api/ # 所有接口请求函数,按模块划分(user.js, order.js)
├── assets/ # 静态资源(图片、字体等)
├── components/ # 全局公共组件(如SearchBar, Pagination)
│ ├── common/ # 真正全局通用的(按钮、弹窗确认框)
│ └── business/ # 业务通用组件(如UserSelector)
├── composables/ # Vue 3组合式函数(usePagination, useFormValidate)
├── directives/ # 自定义指令(如权限判断指令 v-permission)
├── layout/ # 布局组件(Header, Sidebar, AppMain)
├── router/ # 路由配置和路由守卫
├── store/ # Pinia状态管理(modules模式)
├── styles/ # 全局样式、变量覆盖、混入
├── utils/ # 工具函数(日期格式化、金额处理、正则校验)
├── views/ # 页面级组件(对应路由)
│ ├── dashboard/ # 每个页面可以有自己的子组件
│ ├── user/
│ └── ...
└── App.vue
main.js
不要一开始就把所有文件夹都建好,但要有这个意识。随着功能增加,把代码放到该放的位置,而不是全部堆在views里。比如,当你发现好几个页面都需要分页逻辑时,就把分页的currentPage、pageSize、total和翻页方法抽到composables/usePagination.js里,然后在各个页面导入使用。这才是Vue 3组合式API的精髓。
3. 核心功能开发:打造后台的骨架与血肉
一个后台管理系统,抛开具体业务,其核心功能模块是相通的。我们把这几块硬骨头啃下来,项目就完成了一大半。
3.1 布局架构:使用Container容器组件
Element Plus提供了<el-container>系列组件来快速搭建后台经典布局。我习惯在src/layout目录下创建一个index.vue文件作为主布局。
<!-- src/layout/index.vue -->
<template>
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="asideWidth" class="aside">
<div class="logo">Admin Pro</div>
<SidebarMenu /> <!-- 这是一个自定义的导航菜单组件 -->
</el-aside>
<el-container>
<!-- 顶部导航栏 -->
<el-header class="header">
<div class="left">
<el-icon @click="toggleCollapse"><Fold /></el-icon>
<Breadcrumb /> <!-- 面包屑组件 -->
</div>
<div class="right">
<UserAvatar /> <!-- 用户头像下拉菜单 -->
</div>
</el-header>
<!-- 标签页(如果有多页签需求) -->
<!-- <TagsView /> -->
<!-- 主内容区 -->
<el-main class="main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews"> <!-- 缓存页面 -->
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</el-main>
<!-- 页脚 -->
<!-- <el-footer>Footer</el-footer> -->
</el-container>
</el-container>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Fold } from '@element-plus/icons-vue'
import SidebarMenu from './components/SidebarMenu.vue'
import Breadcrumb from './components/Breadcrumb.vue'
import UserAvatar from './components/UserAvatar.vue'
const isCollapse = ref(false)
// 根据折叠状态动态计算侧边栏宽度
const asideWidth = computed(() => isCollapse.value ? '64px' : '200px')
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
</script>
<style scoped lang="scss">
.layout-container {
height: 100vh;
.aside {
background-color: #304156;
transition: width 0.3s;
.logo {
height: 60px;
line-height: 60px;
text-align: center;
color: #fff;
font-size: 18px;
font-weight: bold;
overflow: hidden;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e6e6e6;
background: #fff;
.left {
display: flex;
align-items: center;
.el-icon {
margin-right: 15px;
cursor: pointer;
font-size: 20px;
}
}
}
.main {
background-color: #f0f2f5;
padding: 20px;
}
}
</style>
这个布局组件包含了侧边栏折叠、面包屑导航、用户信息等常见元素。在App.vue中,我们只需要引入这个布局,并用<router-view>来承载页面内容。路由配置则对应到views目录下的各个页面组件。
3.2 路由与权限管理:动态路由的实践
后台系统几乎都涉及权限。一种简单有效的方案是动态路由。思路是:用户登录后,根据其角色或权限标识,从后端获取有权限访问的菜单列表,然后前端根据这个列表动态添加路由。
首先,在router/index.js中,我们定义一些所有用户都能访问的静态路由,比如登录页、404页。
import { createRouter, createWebHistory } from 'vue-router'
// 静态路由(无需权限)
export const constantRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
hidden: true // 不在侧边栏显示
},
{
path: '/404',
name: '404',
component: () => import('@/views/error-page/404.vue'),
hidden: true
},
// 重定向
{
path: '/',
redirect: '/dashboard'
}
]
// 创建路由实例,先只挂载静态路由
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes
})
// 动态路由(需要权限)
export const asyncRoutes = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '仪表盘', icon: 'Odometer' }
},
{
path: '/user',
name: 'User',
component: () => import('@/views/user/index.vue'),
meta: { title: '用户管理', icon: 'User' }
},
// ... 更多需要权限的路由
]
export default router
然后,在用户登录成功后,我们调用一个generateRoutes函数。这个函数会根据用户权限,从asyncRoutes中过滤出有权限的路由,然后通过router.addRoute()动态添加到路由实例中。同时,这个过滤后的路由列表也用于生成侧边栏菜单。这里的关键是router.addRoute()的用法,它允许我们在运行时添加路由。记得,添加动态路由前,最好先移除之前可能添加的旧路由,避免重复。
3.3 表格页面的深度实践:封装一个万能表格组件
后台管理,十有八九是在和表格打交道。Element Plus的<el-table>功能强大,但每个页面都写一遍分页、查询、加载状态会很冗余。我的做法是封装一个高阶的表格组件。
假设我们有一个components/business/ProTable.vue:
<template>
<div class="pro-table">
<!-- 查询表单区域(可插槽自定义) -->
<div v-if="$slots.search" class="search-area">
<slot name="search"></slot>
</div>
<!-- 操作按钮区域 -->
<div class="operation-area" v-if="$slots.operation || showAdd">
<slot name="operation">
<el-button v-if="showAdd" type="primary" @click="$emit('add')">新增</el-button>
</slot>
</div>
<!-- 表格主体 -->
<el-table
v-loading="loading"
:data="tableData"
:border="border"
:stripe="stripe"
@selection-change="handleSelectionChange"
v-bind="$attrs" <!-- 透传其他el-table属性 -->
>
<!-- 选择列 -->
<el-table-column v-if="showSelection" type="selection" width="55" />
<!-- 序号列 -->
<el-table-column v-if="showIndex" type="index" label="序号" width="80" align="center" />
<!-- 动态渲染列 -->
<template v-for="col in columns" :key="col.prop">
<el-table-column v-bind="col">
<!-- 支持自定义单元格内容 -->
<template v-if="col.slot" #default="scope">
<slot :name="col.slot" :row="scope.row"></slot>
</template>
</el-table-column>
</template>
<!-- 操作列(可插槽自定义) -->
<el-table-column v-if="showOperation" label="操作" width="180" fixed="right">
<template #default="scope">
<slot name="operation-column" :row="scope.row">
<el-button link type="primary" size="small" @click="$emit('edit', scope.row)">编辑</el-button>
<el-button link type="danger" size="small" @click="$emit('delete', scope.row)">删除</el-button>
</slot>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-area" v-if="showPagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
// 数据相关
data: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
total: { type: Number, default: 0 },
// 列配置
columns: { type: Array, required: true },
// 功能开关
showSelection: { type: Boolean, default: false },
showIndex: { type: Boolean, default: false },
showOperation: { type: Boolean, default: true },
showPagination: { type: Boolean, default: true },
showAdd: { type: Boolean, default: true },
// 样式
border: { type: Boolean, default: true },
stripe: { type: Boolean, default: true },
// 分页初始值
page: { type: Number, default: 1 },
size: { type: Number, default: 10 }
})
const emit = defineEmits(['update:page', 'update:size', 'selection-change', 'add', 'edit', 'delete', 'query'])
const tableData = ref(props.data)
const currentPage = ref(props.page)
const pageSize = ref(props.size)
const selectedRows = ref([])
watch(() => props.data, (val) => { tableData.value = val })
watch(() => props.page, (val) => { currentPage.value = val })
watch(() => props.size, (val) => { pageSize.value = val })
const handleSelectionChange = (val) => {
selectedRows.value = val
emit('selection-change', val)
}
const handleSizeChange = (val) => {
pageSize.value = val
emit('update:size', val)
emit('query')
}
const handleCurrentChange = (val) => {
currentPage.value = val
emit('update:page', val)
emit('query')
}
</script>
在用户管理页面views/user/index.vue中,使用这个组件就变得非常简洁:
<template>
<ProTable
:columns="columns"
:data="userList"
:loading="loading"
:total="total"
v-model:page="queryParams.page"
v-model:size="queryParams.size"
@query="fetchUserList"
@add="handleAdd"
@edit="handleEdit"
>
<!-- 自定义查询表单插槽 -->
<template #search>
<el-form :inline="true" :model="queryParams">
<el-form-item label="用户名">
<el-input v-model="queryParams.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchUserList">查询</el-button>
</el-form-item>
</el-form>
</template>
<!-- 自定义操作列插槽 -->
<template #operation-column="{ row }">
<el-button link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
<el-button link type="warning" size="small" @click="handleResetPwd(row)">重置密码</el-button>
</template>
</ProTable>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ProTable from '@/components/business/ProTable.vue'
import { getUserList } from '@/api/user'
const columns = ref([
{ prop: 'username', label: '用户名', width: 120 },
{ prop: 'nickname', label: '昵称' },
{ prop: 'phone', label: '手机号' },
{ prop: 'status', label: '状态', slot: 'status' }, // 使用插槽自定义状态显示
{ prop: 'createTime', label: '创建时间', width: 180 }
])
const userList = ref([])
const loading = ref(false)
const total = ref(0)
const queryParams = ref({
name: '',
page: 1,
size: 10
})
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(queryParams.value)
userList.value = res.data.list
total.value = res.data.total
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUserList()
})
</script>
通过这样的封装,表格的通用逻辑被收拢,页面只需关注业务数据和交互。这才是高效开发的样子。
4. 样式定制与主题切换:让系统拥有品牌感
一个专业的企业后台,UI应该符合公司的品牌形象,而不是千篇一律的默认蓝色。Element Plus提供了非常灵活的定制方案。
4.1 全局主题定制:修改SCSS变量
这是最彻底的定制方式。我们在项目根目录创建styles/element/index.scss文件:
// styles/element/index.scss
// 只需要重写你需要的变量
// 主色调
$--color-primary: #1890ff; // 品牌蓝
$--color-success: #52c41a;
$--color-warning: #faad14;
$--color-danger: #f5222d;
$--color-info: #909399;
// 背景色
$--background-color-base: #f5f7fa;
// 边框
$--border-color-base: #dcdfe6;
$--border-radius-base: 4px;
// 字体
$--font-path: 'element-plus/theme-chalk/fonts'; // 确保字体路径正确
// 引入Element Plus的SCSS入口文件
@use "element-plus/theme-chalk/src/index.scss" as *;
然后,在main.js中引入这个文件,替换掉之前引入的CSS文件:
// main.js
// 删除这行:import 'element-plus/dist/index.css'
// 添加这行:
import '@/styles/element/index.scss'
这样,所有基于这些变量的组件样式都会自动更新。你可以去Element Plus的官方文档找到完整的SCSS变量列表,按需修改。
4.2 暗黑模式一键切换
现在很多系统都支持暗黑模式。Element Plus内置了暗黑主题,我们可以结合一个状态管理(比如Pinia)和CSS变量,实现一键切换。
首先,安装并配置Pinia(Vue官方推荐的状态管理库):
npm install pinia
// store/index.js
import { createPinia } from 'pinia'
export const pinia = createPinia()
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { pinia } from './store'
app.use(pinia)
创建一个管理主题的store:
// store/modules/theme.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useThemeStore = defineStore('theme', () => {
const theme = ref('light') // 'light' or 'dark'
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
// 切换时,给html标签加上/移除 'dark' 类名
if (theme.value === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
// 你也可以把主题偏好保存到localStorage
localStorage.setItem('theme', theme.value)
}
const initTheme = () => {
const savedTheme = localStorage.getItem('theme') || 'light'
theme.value = savedTheme
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark')
}
}
return { theme, toggleTheme, initTheme }
})
在App.vue的onMounted中初始化主题。然后,在全局样式文件(如styles/index.scss)中,利用Element Plus的暗黑模式类名来覆盖样式:
// styles/index.scss
// 暗黑模式下的自定义覆盖
html.dark {
// 背景色
--el-bg-color-page: #0a0a0a;
--el-bg-color: #141414;
--el-bg-color-overlay: #1d1e1f;
// 文字色
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
// 边框色
--el-border-color: #434343;
--el-border-color-light: #434343;
}
最后,在布局的右上角放一个切换按钮,调用themeStore.toggleTheme()即可。Element Plus的组件会自动响应html.dark这个类名,应用其内置的暗黑样式,而我们自定义的CSS变量会进一步微调全局背景和文字颜色,达到和谐统一的效果。
4.3 组件级样式覆盖:深度选择器的正确用法
有时候我们只想修改某个特定场景下组件的样式。比如想让某个页面里的所有按钮圆角大一些。可以使用深度选择器:deep()。
<template>
<div class="special-page">
<el-button type="primary">特制按钮</el-button>
</div>
</template>
<style scoped lang="scss">
.special-page {
// 使用 :deep() 穿透scoped限制,修改子组件样式
:deep(.el-button) {
border-radius: 20px;
}
}
</style>
注意:在Vue 3的单文件组件中,使用
<style scoped>时,如果要修改子组件(如Element Plus组件)的样式,必须使用:deep()选择器。直接写.el-button是无效的。这是Vue 3为了更好的样式隔离引入的规则。
5. 状态管理与数据请求:让数据流动清晰可控
后台系统数据流复杂,好的状态管理能让代码像乐高一样清晰可组合。我现在的首选是 Pinia + 基于Composition API封装的数据请求。
5.1 使用Pinia管理全局状态
Pinia比Vuex更简单,完全支持Composition API,并且TypeScript友好。我们用它来管理用户信息、权限、主题等全局状态。
创建用户store:store/modules/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { login, logout, getUserInfo } from '@/api/user'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref({})
const roles = ref([])
const setToken = (newToken) => {
token.value = newToken
localStorage.setItem('token', newToken)
}
const loginAction = async (loginForm) => {
try {
const res = await login(loginForm)
setToken(res.data.token)
// 登录成功后获取用户信息
await getUserInfoAction()
return Promise.resolve(res)
} catch (error) {
return Promise.reject(error)
}
}
const getUserInfoAction = async () => {
if (!token.value) return
const res = await getUserInfo()
userInfo.value = res.data.user
roles.value = res.data.roles
}
const logoutAction = async () => {
try {
await logout()
} finally {
// 无论后端是否成功,前端都清空状态
token.value = ''
userInfo.value = {}
roles.value = []
localStorage.removeItem('token')
// 跳转到登录页
router.push('/login')
}
}
return {
token,
userInfo,
roles,
setToken,
loginAction,
getUserInfoAction,
logoutAction
}
})
在组件中使用时,直接导入并调用即可,非常直观:
<script setup>
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const handleLogin = async () => {
await userStore.loginAction({ username: 'admin', password: '123456' })
}
</script>
5.2 封装一个“聪明”的请求层
直接使用axios发请求,每个请求都要写try-catch,重复代码太多。我习惯封装一个更易用的请求层。首先是用axios创建实例,配置拦截器。
// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore() // 注意:需要在能访问到pinia的地方调用
if (userStore.token) {
config.headers['Authorization'] = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data
// 假设后端统一返回格式: { code: 200, data: {}, message: 'success' }
if (res.code === 200) {
return res
} else {
// 业务错误
ElMessage.error(res.message || 'Error')
// 特殊错误码处理,如未登录(401)
if (res.code === 401) {
// 触发登出
userStore.logoutAction()
router.push('/login')
}
return Promise.reject(new Error(res.message || 'Error'))
}
},
(error) => {
// HTTP错误(网络错误、超时等)
ElMessage.error(error.message || 'Request Error')
return Promise.reject(error)
}
)
export default service
更进一步,我们可以基于这个service,封装一个更符合Hooks风格的请求函数,在composables/目录下创建useRequest.js:
// composables/useRequest.js
import { ref } from 'vue'
import service from '@/utils/request'
export function useRequest(options) {
const loading = ref(false)
const data = ref(null)
const error = ref(null)
const run = async (params) => {
loading.value = true
error.value = null
try {
const result = await service({
...options,
...(params && { data: params }) // 或 params,根据请求方法调整
})
data.value = result.data
return Promise.resolve(result)
} catch (err) {
error.value = err
return Promise.reject(err)
} finally {
loading.value = false
}
}
// 立即执行(可选)
if (options.immediate) {
run()
}
return {
loading,
data,
error,
run
}
}
在组件中使用时,代码会非常简洁:
<script setup>
import { useRequest } from '@/composables/useRequest'
// 获取用户列表
const { loading: userLoading, data: userList, run: fetchUsers } = useRequest({
url: '/api/users',
method: 'get'
})
// 提交表单
const { loading: submitLoading, run: submitForm } = useRequest({
url: '/api/user',
method: 'post'
})
onMounted(() => {
fetchUsers() // 手动触发
})
const handleSubmit = async (formData) => {
await submitForm(formData)
ElMessage.success('创建成功')
fetchUsers() // 重新拉取列表
}
</script>
这种模式把加载状态、数据和错误都集中管理,逻辑清晰,复用性极高。
6. 打包优化与部署上线:让应用飞起来
开发完成,最后一步就是打包部署。这里有几个关键点能显著提升用户体验。
6.1 生产环境构建优化
直接运行npm run build没问题,但我们可以通过vite.config.js进行一些优化配置。
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src') // 路径别名
}
},
build: {
// 生产环境移除 console 和 debugger
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// 资源分块策略
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
// 手动拆包,把 node_modules 里的大依赖单独打包
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('element-plus')) {
return 'element-plus'
}
if (id.includes('vue')) {
return 'vue'
}
return 'vendor'
}
}
}
}
},
// 开发服务器配置
server: {
port: 3000,
open: true,
proxy: { // 配置代理,解决跨域
'/api': {
target: 'http://your-backend-api.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
这个配置做了几件事:1) 设置了路径别名,方便导入;2) 打包时移除调试代码;3) 对输出文件进行哈希命名,利于缓存;4) 将element-plus、vue等大依赖单独打包,利用浏览器并行加载和缓存;5) 配置了开发服务器的代理。
6.2 使用插件实现按需引入
如果项目后期体积确实成了问题,可以引入unplugin-vue-components和unplugin-auto-import这两个Vite插件,实现真正的按需自动引入。这会让你的main.js非常干净,组件也无需手动import。
npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
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(),
// 自动导入 Vue 相关函数,如:ref, reactive, computed 等
AutoImport({
imports: ['vue', 'vue-router'],
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts' // 生成类型声明文件
}),
// 自动导入组件
Components({
resolvers: [
ElementPlusResolver(),
],
dts: 'src/components.d.ts' // 生成类型声明文件
})
]
})
配置好后,你在模板中直接使用<el-button>,插件会自动帮你引入ElButton组件和对应的CSS。<script setup>里直接写ref、onMounted,插件也会自动导入。非常智能,能极大减少样板代码。
6.3 部署到常见服务器
打包后生成的dist目录是纯静态文件,可以部署到任何Web服务器。
Nginx配置示例:
server {
listen 80;
server_name your-domain.com;
# 静态资源
location / {
root /path/to/your/dist;
index index.html;
try_files $uri $uri/ /index.html; # 支持Vue Router的history模式
}
# 代理API请求到后端
location /api/ {
proxy_pass http://your-backend-server:port/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 开启gzip压缩
gzip on;
gzip_min_length 1k;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
}
Docker部署(更现代的方式):
创建一个简单的Dockerfile:
# 使用Node镜像构建
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# 使用Nginx镜像提供服务
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
然后构建并运行镜像:
docker build -t my-admin .
docker run -d -p 8080:80 --name admin-container my-admin
访问http://localhost:8080就能看到你的应用了。Docker化部署保证了环境一致性,迁移和扩展都更方便。
走到这一步,一个功能完整、结构清晰、性能优良且易于部署的企业级后台管理系统就真正从你的手中诞生了。从项目初始化到核心功能开发,再到样式定制和最终部署,每一个环节的选择和实践都凝聚了实际项目中的经验与思考。记住,技术栈是工具,最终目的是高效、稳定地实现业务价值。Vue 3和Element Plus这个组合,能让你在追求这个目标的路上,走得更稳、更快。剩下的,就是在具体的业务场景中,不断打磨和迭代你的系统了。如果在实践中遇到具体问题,不妨多看看官方文档和社区讨论,很多坑都已经有现成的解决方案了。
1万+



被折叠的 条评论
为什么被折叠?



