
简介面向Vue.js初学者与进阶者的后台管理系统实战教程资源包以从零构建网站后台实例为主线覆盖Vue核心概念、Vue Router路由配置、Vuex状态管理、axios接口交互以及Element UI组件库的集成与主题定制。压缩包内共2000个文件约35.17MB核心内容以JavaScript/TypeScript源码、Vue单文件组件、JSON配置、Markdown说明及CSS/SCSS样式文件为主便于查看工程结构并按模块学习。目前已有1525人学习下载。读者可从中获得完整的后台实例源码与配套素材涵盖新闻管理、内容发布、系统管理等模块的设计思路与实现代码还包含后台常用表单、日期选择、表格、标签页等场景的样式与组件封装。通过对照Vue Router、Vuex与axios的集成方式可快速掌握登录验证、权限控制、响应式布局、测试优化及部署上线的完整实践路径适合希望结合项目入门VueElement UI的开发者。1. 为什么后台管理系统值得从零手写做了几年 Vue 开发最有价值的练习不是各种页面特效而是老老实实从空目录搭一个网站后台。后台管理系统看起来只是增删改查但背后的路由权限、登录状态、请求拦截、按钮级控制几乎覆盖了前端工程化里最常见的问题。把这一套亲手落地一遍之后再面对各类中后台项目思路会清晰很多。这篇文章不做文档堆砌而是围绕「从零开始实现网站后台实例」这个目标讲清楚我会怎么设计工程结构、怎么封装请求层、怎么写路由守卫和动态权限以及最后怎么打包上线。整个方案基于 Vue 3 Vite组件库选 Element Plus 便于后台场景快速产出状态管理用 Pinia。如果你正在做 Vue 入门到进阶的过渡或者准备接一个后台项目可以照着这套思路往下走中间每一步都附带了命令、参数和常见坑点。2. 搭建网站后台基础工程Vite 初始化与核心依赖配置2.1 用 Vite 创建 Vue 项目创建项目有几种方式vue-cli 已经不再适合新项目现在官方推荐的是 create-vite。执行下面的命令会创建一个基于 Vue 3 的最小模板npm create vitelatest admin-demo -- --template vue cd admin-demo npm install命令里admin-demo是项目名--template vue指定使用 Vue 模板。Vite 默认生成的项目结构很干净只有src/main.js、src/App.vue和public目录适合作为网站后台的起点。接下里安装后续需要用的核心库npm install vue-router4 pinia axios element-plus以上依赖分别负责路由、状态管理、网络请求和 UI 组件。在真实项目中我把它们当作后台工程的基础配置缺一不可。同时建议安装sass因为很多后台的布局样式会用到 SCSS 语法。npm install -D sass2.2 按后台场景划分目录结构很多初学者习惯把所有页面堆在views里组件随便放结果项目变大后很难维护。通常一个标准的后台前端工程会让目录结构按照职责划分我一般这样建src/ api/ # 接口请求定义 assets/ # 静态资源 components/ # 通用组件 router/ # 路由配置 store/ # Pinia 状态 styles/ # 全局样式 utils/ # 工具函数 views/ # 页面组件这样的结构不是强制的但好处是项目变大后无论新人还是自己回来维护都能根据目录名快速定位到对应代码。比如新增一个用户管理功能就去views/user写页面api/user.js里写接口router里注册路由边界非常清晰。2.3 注册路由和状态管理模块在src/router/index.js里先放一个基础路由实例此时不用考虑权限只要保证页面能跳转import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, name: Login, component: () import(/views/login/LoginView.vue) }, { path: /, component: () import(/layout/AdminLayout.vue), children: [ { path: , name: Dashboard, component: () import(/views/dashboard/DashboardView.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes }) export default router注意我把/login和后台主框架拆开登录页是独立存在的而进入后台后所有页面都挂在AdminLayout.vue的 children 下。这种嵌套路由方式让侧边栏和顶栏只需在布局组件中写一次子页面通过router-view渲染符合后台管理系统的常见布局需求。组件里使用了/作为src的别名这个需要在vite.config.js里手动配置否则会报模块找不到。加一段 resolve 配置即可import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 3000, open: true } })配置项说明建议值resolve.alias路径别名避免../../层级混乱 - srcserver.port开发服务器端口避免和后台接口冲突3000 或 8080server.proxy开发期代理/api请求到目标后端视后端地址而定开发服务器端口用 3000 是因为很多 Java 后端接口默认跑在 8080端口错开可以少踩不少跨域交互的坑。如果用的是 Spring Boot 默认端口server.proxy可以转发请求避免前端直接跨域请求接口。之后再创建src/store/index.js并注册 Piniaimport { createPinia } from pinia const pinia createPinia() export default pinia在main.js里同时挂载 router 和 piniaimport { createApp } from vue import App from ./App.vue import router from ./router import pinia from ./store import ElementPlus from element-plus import element-plus/dist/index.css createApp(App).use(router).use(pinia).use(ElementPlus).mount(#app)到这里一个带路由、状态管理和 UI 库的基础工程就能跑起来了。用npm run dev启动后访问首页看到空白布局就说明环境正常接下来开始填充业务逻辑。3. 登录认证与请求封装网站后台的鉴权基石3.1 登录流程的设计网站后台第一个页面通常是登录页。用户输入用户名密码前端把数据提交到后端接口成功后拿到 token。这个 token 需要持久化保存我会选择 localStorage 而不是 sessionStorage因为刷新浏览器后 sessionStorage 会清空而用户通常希望登录状态能保持一段时间。下面是登录页的核心逻辑关键是提交后调用loginApi拿到 token把它存储到 Pinia 和 localStorageimport { ref } from vue import { useRouter } from vue-router import { loginApi } from /api/auth import { useUserStore } from /store/user const username ref() const password ref() const router useRouter() const userStore useUserStore() const handleLogin async () { if (!username.value || !password.value) { ElMessage.warning(请输入用户名和密码) return } const res await loginApi({ username: username.value, password: password.value }) userStore.setToken(res.data.token) router.push(/) }loginApi是接口定义文件里的函数实际项目里后端接口路径、请求方法都应该集中写在src/api目录下方便接口变更时统一维护。比如src/api/auth.js里import request from /utils/request export const loginApi (data) { return request.post(/auth/login, data) }3.2 Axios 拦截器里的 token 注入与错误处理后台项目的每个请求几乎都要携带 token如果每个请求都手动加 header代码会很冗余。常规做法是封装一个request.js工具用 axios 实例统一处理拦截器import axios from axios import { ElMessage } from element-plus import { useUserStore } from /store/user import router from /router const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.request.use( (config) { const userStore useUserStore() const token userStore.token if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) Promise.reject(error) ) request.interceptors.response.use( (response) response.data, (error) { const status error.response?.status if (status 401) { const userStore useUserStore() userStore.clearToken() router.push(/login) ElMessage.error(登录状态已过期请重新登录) } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } ) export default request这段代码解决了两件事请求发出之前自动加上Authorization请求头后端可以通过这个字段解析出用户身份。响应状态码是 401 时说明 token 失效前端清掉本地存储的用户状态跳回登录页。这个逻辑在后台系统里非常关键它是整个鉴权链路闭环的一部分。baseURL设置为/api是因为开发环境可以通过 Vite 的server.proxy转发到真实接口生产环境由 Nginx 统一转发前端代码不需要根据环境反复修改接口地址。3.3 路由守卫实现登录拦截有了 token 还不够前端还要保证用户没登录时不能访问后台页面。这需要依靠 vue-router 的全局前置守卫来实现router.beforeEach((to, from, next) { const userStore useUserStore() const token userStore.token if (token to.path /login) { next(/) return } if (!token to.path ! /login) { next(/login) return } next() })beforeEach会在每次路由跳转前执行我这里做了两个判断。已经登录的用户访问登录页会被送回首页未登录的用户访问后台则会跳到登录页整个拦截逻辑就完整了。配合 401 响应拦截后端失效场景也能兜住。路由守卫里判断 token 是否存在的逻辑并不复杂但要注意从 Pinia 里取 token 时不能直接使用userStore.token因为这个操作会触发响应式依赖收集。在路由守卫这样的非组件环境中Pinia 实例创建后就可用用useUserStore()直接调用不会出问题实际项目里可以这样使用。4. 动态路由与权限控制从零实现后台菜单和按钮权限4.1 权限模型的选择网站后台的权限控制实际产品里主要有两种做法。第一种是后端返回当前用户的角色码前端根据角色码自己匹配路由表来决定显示哪些菜单第二种是后端直接返回一个已过滤好的路由数组前端拿到后通过router.addRoute动态注册。比较常见的是第一种因为实现简单权限变更只需要调整角色和路由的映射关系不需要后端额外开发接口把路由表吐出来。不过从扩展性角度看后端的动态路由方案更灵活。用户登录后调用接口拿到菜单列表再把这些菜单结构解析成 vue-router 需要的route对象动态添加路由。这种做法新增页面时后端只需要配置菜单记录前端代码改动很小。在这篇文章的实战例子里我采用折中方案后端返回菜单标识数组前端在静态路由表的基础上添加权限字段。4.2 静态路由表加权限标记定义一个完整路由表给每个需要权限的页面加上meta.roles字段表示该页面允许哪些角色访问const routes [ { path: /admin, component: () import(/layout/AdminLayout.vue), children: [ { path: user, name: UserManage, component: () import(/views/user/UserManage.vue), meta: { title: 用户管理, roles: [admin, editor] } }, { path: role, name: RoleManage, component: () import(/views/role/RoleManage.vue), meta: { title: 角色管理, roles: [admin] } }, { path: dashboard, name: Dashboard, component: () import(/views/dashboard/DashboardView.vue) } ] } ]如果当前用户的角色是editor那么他能访问用户管理但不能访问角色管理。运行时通过router.beforeEach里检查to.meta.roles是否存在以及当前用户的角色是否包含在内router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.roles !to.meta.roles.includes(userStore.role)) { next(/403) return } next() })这样实现的是页面级权限简单直接适合多数后台系统。配合侧边栏菜单的渲染v-for 遍历路由表时过滤掉没有权限的路由侧边栏自然只显示当前角色可见的菜单项。4.3 按钮权限的自定义指令页面级权限控制得住菜单和路由但很多需求还要控制按钮级别。比如删除按钮只让管理员看到普通编辑角色看不到。这种场景用自定义指令最合适我通常封装一个v-permission指令import { useUserStore } from /store/user const permission { mounted(el, binding) { const userStore useUserStore() const requiredPerms binding.value const userPerms userStore.permissions const hasPermission requiredPerms.some((perm) userPerms.includes(perm)) if (!hasPermission) { el.parentNode el.parentNode.removeChild(el) } } } export default permission在模板里这样使用el-button v-permission[user:delete] typedanger删除/el-buttonbinding.value是从模板传入的权限标识数组用户的权限列表来自登录时接口返回的数据存放在 Pinia 中。通过比对如果用户没有这个权限码指令就把这个按钮从 DOM 上移除。这个方案理解成本低维护也直观权限标识由前后端约定好比如user:add、user:edit、user:delete。需要注意指令只处理了初次挂载时的权限判断如果用户权限是异步加载的需要确保指令执行时权限列表已经填充完成否则按钮会被误删。实际开发中我会在权限列表加载完成后再挂载主应用组件避免这个时序问题。权限粒度实现方式优点缺点菜单级路由表 meta 过滤简单直观开发效率高权限变化需要前端配合发布页面级路由守卫拦截防止直接输入 URL 绕过菜单只能控制页面入口不能控制页面内功能按钮级自定义指令精细控制操作权限需要前后端约定权限码工作量稍大三种权限粒度在真实后台中经常组合使用。路由表负责控制可访问页面路由守卫兜底防止越权按钮级指令控制页面内的操作共同构成一套完整的权限体系。4.4 权限与路由跳转的联动鉴权和路由还有一个容易忽略的点动态路由的清理。如果退出登录时把路由表替换掉了但之前动态添加的路由没有移除用户换账号登录后上一个账号的页面仍然可以访问。解决方式是维护一个dynamicRoutes数组记录所有动态添加的路由名称退出时用router.removeRoute(name)清除const dynamicRouteNames [] export function addDynamicRoutes(routes) { routes.forEach((route) { router.addRoute(route) dynamicRouteNames.push(route.name) }) } export function clearDynamicRoutes() { dynamicRouteNames.forEach((name) { router.removeRoute(name) }) }在用户退出登录时先调用clearDynamicRoutes()清掉动态路由再clearToken()清掉登录态然后跳转登录页。这样可以避免账号切换后的越权访问和数据残留。5. 打包优化和部署细节给网站后台减重5.1 组件按需加载去掉全量引入很多后台项目的首屏加载慢很大原因是在main.js里全量引入了 Element Plusimport ElementPlus from element-plus import element-plus/dist/index.css全量引入会把所有组件都打进包里但后台实际用到的组件通常只是一部分。改成按需引入后包体可以明显减小。社区最常用的方案是unplugin-auto-import和unplugin-vue-components配合自动导入用到的组件和 API不需要手动在main.js注册。安装依赖并配置 Vite 后代码里可以直接使用组件而无需 importnpm install -D unplugin-auto-import unplugin-vue-components在vite.config.js里加入插件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()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })这样构建时只会打包真正被用到的组件体积变化非常明显。5.2 路由懒加载与构建分包路由懒加载在 Vite 里很简单组件导入写成函数形式即可{ path: user, component: () import(/views/user/UserManage.vue) }打包时 Vite 会为每个懒加载路由单独生成一个 chunk首屏只加载当前需要的内容。更进一步手动分包可以避免第三方库和业务代码混在一起build: { rollupOptions: { output: { manualChunks: { vendor: [vue, vue-router, pinia], element: [element-plus] } } } }manualChunks把 Vue 全家桶和 Element Plus 分别独立成包利用浏览器缓存特性后续业务代码更新时第三方库不用重新下载。5.3 部署时的路由模式配置网站后台路由默认用的是createWebHistory也就是 HTML5 History 模式URL 更干净。但部署到服务器后如果用户直接访问/user页面Nginx 会把请求打到后端的/user路径找不到资源就返回 404。解决方案是在 Nginx 配置里加try_files指令让所有路由请求都回退到index.htmlserver { listen 80; server_name admin.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html的含义是先按请求的 URI 找真实文件找不到就把请求交给前端入口index.html由 vue-router 接管前端路由。location /api/负责把接口请求转发到后端服务这在后台项目部署中是经常需要用到的步骤因为前后端分离后静态资源和接口通常分别承担在不同的服务上。这些优化做完后网站后台的打包体积和首屏速度会明显改善交互体验也更接近生产环境的要求。平时我会在本地模拟线上环境跑一遍确认登录、权限和路由都正常后再部署能省去很多不必要的返工时间。本文还有配套的精品资源点击获取