十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue CLI 3 + Ant Design Vue 后台管理系统模板搭建实践

Vue CLI 3 + Ant Design Vue 后台管理系统模板搭建实践 简介基于Vue CLI 3和Ant Design Vue打造的后台管理系统模板面向需要快速搭建中后台界面的前端开发者。模板整合Vuex状态管理、Vue Router路由和Ant Design Vue组件库提供开箱即用的基础架构适合内部管理系统、数据看板等场景。资源包共121个文件压缩后仅2.54MB包含45个Vue单文件组件与45个JavaScript逻辑文件另有scss样式、JSON配置、PNG图标及工程化辅助文件结构紧凑便于二次开发。已有1200人学习下载。通过此模板可掌握Vue CLI 3项目搭建、Ant Design Vue按需引入、Vuex与Router的整合以及ESLint统一代码风格的方法。包内附带登录组件、路由拦截和Vuex状态变更示例可直接作为新项目起点也可作为组件拆分与配置实践的学习参考。1. 后台管理系统模板的起点Vue CLI 3 的“零配置”其实是个边界问题我见过不少团队把 Vue CLI 3 生成的项目直接当成后台管理系统用跑起来发现路由、权限、请求层全是散的最后还是自己重搭一套。原因在于“零配置”指的是脚手架替你省去了 webpack 和构建链路的初始化成本但后台管理系统真正复杂的地方——登录态、动态路由、接口封装、页面权限——恰恰是脚手架刻意不碰的部分。把 Vue CLI 3 和 Ant Design Vue 拼在一起只是第一步真正的模板价值在于把工程约束和业务约定定下来。这篇文章会从我实际拆过的后台模板出发讲清楚初始化选型、UI 组件引入策略、路由与状态管理怎么组合以及最后如何通过打包分析验证整个模板的收益。无论你是刚用 Vue.js 写后台还是已经在维护一套成熟系统这篇文章里提到的边界设计和参数取舍都会对你有用。2. 初始化工程从 Preset 到目录结构的真正含义2.1 手动选择特性时你在选择什么vue create交互界面里的“Manually select features”很多人在向导里勾完就忘了。但实际上这套选择最终会固化成一个 JSON 格式的 preset写入本机的~/.vuerc或者项目的preset.json。这里有一个很容易被忽略的点preset 不只是记录了插件列表还包括了插件自身的配置选项比如vue/cli-plugin-router是否使用 history 模式、vue/cli-plugin-eslint使用哪个 lint 规则集。如果你在团队内部做模板分发把这个 preset 文件固定在仓库里比让每个人手动勾选要可靠得多。vue create my-admin-template --preset ./preset.json这行命令会用当前目录下的preset.json作为预设配置全程不经过交互式问答。我一般会在preset.json中做如下定义{ useConfigFiles: true, plugins: { vue/cli-plugin-babel: {}, vue/cli-plugin-router: { historyMode: true }, vue/cli-plugin-vuex: {} } }useConfigFiles的含义是把 Babel 和 ESLint 的配置独立成文件而不是塞进package.json的 dependencies 字段里。这个取舍在模板场景下很重要因为独立配置文件方便后续按项目差异做覆盖也方便 code review 时一眼看到 Babel 和 ESLint 的变更意图。historyMode: true对应前端路由的 history 模式代价是后端需要对所有前端路由做 fallback但换来的是 URL 里没有#号更适合后台管理系统这类需要分享链接的场景。2.2 三个核心文件的装配顺序在模板的源码里你会看到main.js、App.vue、router/index.js、store/index.js这几个文件的职责划分。不少新手在这里的一个误区是把页面组件直接 import 进来挂到路由上然后需要什么功能再去全局注册。一个五六页的后台这么做问题不大但一旦页面超过二十个这种“按需发现”的方式会让路由表和实际页面逐渐失联。我在模板中采用的是显式装配router/index.js负责生成路由实例store/index.js只负责 Vuex 实例App.vue里只放router-view /。让main.js保持最小入口只做Vue.use()和new Vue()的启动工作。如果项目里存在多个 Vue 实例的初始化逻辑比如微前端场景这种分隔方式会让你排查问题时少很多干扰。// main.js import Vue from vue import App from ./App.vue import router from ./router import store from ./store import Antd from ant-design-vue import ant-design-vue/dist/antd.css Vue.use(Antd) Vue.config.productionTip false new Vue({ router, store, render: h h(App) }).$mount(#app)这里Vue.use(Antd)是全局注册 Ant Design Vue 所有组件的方式。模板阶段用全量注册没有性能压力因为它不是打包优化的终点只是让你在开发环境里快速看到组件效果。后面的章节会单独拆按需加载的方案。2.3 浏览器兼容规范和代码风格边界.browserslistrc在 Vue CLI 3 项目里同时作用于 Babel 转译和 Autoprefixer是一个容易被忽略但影响全局的配置文件。模板里的内容一般是 1% last 2 versions not dead这段配置的含义是目标浏览器覆盖市场份额超过 1% 的浏览器、各浏览器最近两个大版本并且排除官方已停止维护的版本。Babel 会依据这个列表决定把哪些现代的 ECMAScript 语法转译成 ES5Autoprefixer 也会按这个列表决定为 CSS 属性添加哪些厂商前缀。如果你们接入的第三方系统还在用比较老的内嵌浏览器内核这里需要手动加上对应的浏览器标识否则页面可能白屏但控制台没有语法错误提示。.editorconfig解决的问题和 ESLint 有交集但不重复。EditorConfig 管的是文件级别的缩进、换行符、末尾空行 ESLint 管的是语法规则和代码风格。我的体验是先把.editorconfig定义好再装 ESLint团队入职新成员时编辑器会自动遵循缩进和换行规则ESLint 的报错量会减少非常明显。2.4 vue.config.js 与环境变量后端联调的第一道关卡后端管理系统模板里vue.config.js最少要承担两个职责开发环境的接口代理和构建时的 publicPath。模板中后端接口走/api前缀通过 devServer.proxy 转发到真实后端地址。// vue.config.js const { defineConfig } require(vue/cli-service) module.exports defineConfig({ transpileDependencies: true, publicPath: process.env.NODE_ENV production ? /admin/ : /, devServer: { port: 8088, host: 0.0.0.0, proxy: { /api: { target: process.env.VUE_APP_API_TARGET || http://localhost:3000, changeOrigin: true, pathRewrite: { ^/api: } } } } })publicPath在模板里用相对路径也行但部署到子目录如https://example.com/admin/时必须显式写成/admin/否则静态资源会从域名根路径去找。开发环境下host: 0.0.0.0是为了让局域网内其他设备也能访问手机调试时非常方便。pathRewrite会把/api/users替换为/users转发到后端这是为了在开发环境绕开后端自身的 CORS 限制。但需要注意的是process.env.VUE_APP_API_TARGET这个环境变量名必须带有VUE_APP_前缀Vue CLI 3 才会把它注入到客户端代码中。下面是模板中.env.development和.env.production的使用方式# .env.development VUE_APP_API_TARGEThttp://192.168.1.10:3000 VUE_APP_USE_MOCKfalse # .env.production VUE_APP_API_TARGEThttps://api.example.com环境变量的一个坑在于修改.env文件后必须重启 dev server否则变更不会生效。因为环境变量是在 webpack 构建启动时注入的热更新不会帮你刷掉旧的process.env值。3. Ant Design Vue 的引入策略与组件层封装3.1 全量引入的代价在哪里在模板初步搭建阶段我直接Vue.use(Antd)引入全量组件这是没有问题的。Ant Design Vue 2.x 版本的组件总数超过 60 个全量引入后打包体积大概会增加 800KB 到 1MB 的 JavaScript 代码。对后台管理系统来说如果用户访问时走内网或者有预加载策略这个体积是可以接受的。但如果你需要把模板暴露在公网或者客户环境首屏体积就需要控制。这里判断引入策略的核心指标不是“用了几个组件”而是项目会不会长期维护、是否有多个项目复用这套模板。如果答案是是我建议在模板阶段就使用按需加载。因为组件封装和按需加载可以一次配好后面每个页面引用组件的写法完全一样不需要单独记忆。3.2 按需加载的准确操作Ant Design Vue 官方推荐的按需加载方案是借助babel-plugin-import。这个 Babel 插件的原理是当你在代码里写import { Button } from ant-design-vue时babel-plugin-import 会在编译阶段把这个语句转换成import Button from ant-design-vue/es/button同时自动引入对应的样式文件ant-design-vue/es/button/style/css从而实现组件和样式的双重按需。// babel.config.js module.exports { presets: [ vue/cli-plugin-babel/preset ], plugins: [ [ import, { libraryName: ant-design-vue, libraryDirectory: es, style: css } ] ] }配置好后代码中依然写import { Button, Table, Form } from ant-design-vueBabel 会在编译时自动拆开。需要说明的是libraryDirectory: es对应的是 Ant Design Vue 的 ES Module 目录tree-shaking 的效果更好。style: css表示加载编译后的 CSS 文件如果你使用的是 Less 并且想要覆盖主题变量这里需要改为style: true并在 vue.config.js 中配置 Less 的 modifyVars。另外还要把main.js里的全量注册去掉改成import { Button, Table, Form } from ant-design-vue Vue.use(Button) Vue.use(Table) Vue.use(Form)注意Vue.use()对 Ant Design Vue 的每个组件都是必需的它负责注册组件名和内部指令。单独的 import 不会自动注册全局组件如果你不想Vue.use()就需要在局部组件中显式注册。模板中我都采用Vue.use()的方式因为后台的公共组件被复用频率很高全局注册对编码效率更友好。3.3 表格和表单的组件封装模板里最常见的两个组件封装是 ProTable 和 SearchForm。a-table的常见重复套路是分页参数、loading 状态和空数据文案。封装的思路不是重新实现表格而是把业务相关的数据获取逻辑收敛起来。template a-table :columnscolumns :data-sourcedataSource :loadingloading :paginationpagination changehandleTableChange / /template script export default { name: ProTable, props: { columns: { type: Array, required: true }, fetchData: { type: Function, required: true }, params: { type: Object, default: () ({}) } }, data () { return { dataSource: [], loading: false, pagination: { current: 1, pageSize: 10, total: 0, showSizeChanger: true, pageSizeOptions: [10, 20, 50] } } }, methods: { async loadData () { this.loading true try { const { list, total } await this.fetchData({ page: this.pagination.current, pageSize: this.pagination.pageSize, ...this.params }) this.dataSource list this.pagination.total total } finally { this.loading false } }, handleTableChange (pag) { this.pagination.current pag.current this.pagination.pageSize pag.pageSize this.loadData() } }, mounted () { this.loadData() } } /script这个封装的取舍在于把数据源获取逻辑和表格展示解耦。fetchData由使用方传入内部自动处理 loading 状态、分页同步和异常时的 loading 恢复。注意finally块里关闭 loading避免接口报错时按钮一直转圈。页面里使用时就只需要关注列定义和接口函数了。这种封装方式是后台管理系统模板里被验证过的高性价比做法它不会像重型低代码方案那样约束你的数据流但节省了几乎每个列表页面都要重复的分页逻辑代码。4. 登录态路由权限与请求层模板里绕不过去的三块硬骨头4.1 登录流程的 Vuex 设计后台系统模板里Vuex 的使用不仅仅是存一个 token。我的做法是store/modules/user.js独立出来专门管理用户信息和登录状态因为后续的权限判断、个人信息展示都会依赖这些数据。// store/modules/user.js import { loginApi, getUserInfoApi } from /api/user import { setToken, getToken, removeToken } from /utils/auth const state { token: getToken() || , name: , roles: [] } const mutations { SET_TOKEN (state, token) { state.token token }, SET_NAME (state, name) { state.name name }, SET_ROLES (state, roles) { state.roles roles } } const actions { async login ({ commit }, userInfo) { const { token } await loginApi(userInfo) commit(SET_TOKEN, token) setToken(token) }, async getInfo ({ commit }) { const { name, roles } await getUserInfoApi() commit(SET_NAME, name) commit(SET_ROLES, roles) return { name, roles } }, async logout ({ commit }) { await logoutApi() removeToken() commit(SET_TOKEN, ) commit(SET_ROLES, []) } }utils/auth里封装的setToken、getToken、removeToken统一操作 cookie 或 localStorage。我通常把 token 放在 cookie 里设置expires为 7 天这样刷新浏览器不会丢登录态同时 js 代码里不直接访问 Vuex 状态而是通过 getter 或 action 间接读取避免在业务组件里散落这一套读写的逻辑。4.2 路由守卫里的权限控制逻辑有了 user store 之后路由守卫需要处理的核心问题是用户未登录时跳转登录页、已登录时不能重复访问登录页、以及动态生成可访问路由。// router/index.js import Vue from vue import Router from vue-router import store from /store import { getToken } from /utils/auth Vue.use(Router) const constantRoutes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, redirect: /dashboard } ] const router new Router({ mode: history, routes: constantRoutes }) router.beforeEach(async (to, from, next) { document.title to.meta.title ? ${to.meta.title} - 后台模板 : 后台模板 if (getToken()) { if (to.path /login) { next({ path: / }) } else { if (!store.getters.roles || store.getters.roles.length 0) { try { await store.dispatch(user/getInfo) next({ ...to, replace: true }) } catch (error) { await store.dispatch(user/logout) next(/login?redirect${to.path}) } } else { next() } } } else { if (to.path /login) { next() } else { next(/login?redirect${to.path}) } } })这个守卫的含义是页面刷新之后store 被重新加载roles状态是空的需要重新请求用户信息这个过程出现在getInfo中。next({ ...to, replace: true })是为了避免在 addRoutes 之后出现路由警告replace 会替换当前记录而不是新增一条历史记录。登录跳转时把目标路径放进redirect参数登录成功后可以用this.$route.query.redirect跳回原页面这个体验比固定跳首页友好很多。4.3 动态路由的生成边界动态路由的实现方式有两种一种是后端返回用户的权限标识前端根据标识过滤静态路由表另一种是后端直接返回可访问路由的路径列表前端动态添加。// store/modules/permission.js import { asyncRoutes, constantRoutes } from /router function hasPermission (roles, route) { if (route.meta route.meta.roles) { return roles.some(role route.meta.roles.includes(role)) } return true } export function filterAsyncRoutes (routes, roles) { const res [] routes.forEach(route { const tmp { ...route } if (hasPermission(roles, tmp)) { if (tmp.children) { tmp.children filterAsyncRoutes(tmp.children, roles) } res.push(tmp) } }) return res }filterAsyncRoutes递归过滤只保留角色命中的路由然后router.addRoutes(filteredRoutes)加入路由表。正常情况下用户刷新后需要重新 getInfo 并 addRoutes所以 4.2 中的next({ ...to, replace: true })实际是在 addRoutes 之后的跳转保证当前页面能匹配上路由定义。如果你的模板里只有十来个页面角色也只有两三种那么静态路由配合 meta.roles 判断会更省事动态路由是页面多到需要角色维度拆分时才真正适用的。模板中保留两套示例代码但默认不开启动态路由避免过度设计。4.4 Axios 请求层的统一封装请求层是后台模板中最后一块拼图。axios 封装的目标是统一处理 token 注入、HTTP 错误状态、业务错误码和 401 未授权跳转。// utils/request.js import axios from axios import { message } from ant-design-vue import { getToken, removeToken } from /utils/auth import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 15000 }) service.interceptors.request.use( config { const token getToken() if (token) { config.headers[Authorization] Bearer ${token} } return config }, error Promise.reject(error) ) service.interceptors.response.use( response { const res response.data if (res.code ! 0) { message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { removeToken() router.push(/login?redirect router.currentRoute.fullPath) message.error(登录已过期请重新登录) } else { message.error(error.message || 网络异常) } return Promise.reject(error) } ) export default servicebaseURL用的是/api前缀开发环境会匹配第 2.4 节的 proxy 配置生产环境则是VUE_APP_BASE_URL指定的完整路径。响应拦截器里判断res.code ! 0是常见实践但要注意和后端约定字段名到底是code还是status值 0 还是 200。这个约定正确性直接影响模板能否复用到新项目。401 的跳转逻辑需要在拦截器里引用router实例而不是从 main.js 里引入否则在 axios 模块加载时会遇到循环引用问题。4.5 tools.js 里那些被反复复用的函数模板源码里有一个tools.js文件我倾向于把工具函数按模块划分比如validate.js放邮箱校验、手机号校验download.js放文件下载封装。但模板里如果只有单个工具文件最常见的函数是深拷贝、防抖节流和日期格式化。// utils/tools.js export function debounce (fn, delay 500) { let timer null return function (...args) { if (timer) clearTimeout(timer) timer setTimeout(() { fn.apply(this, args) }, delay) } } export function formatDate (date, fmt YYYY-MM-DD HH:mm:ss) { const d date instanceof Date ? date : new Date(date) const o { YYYY: d.getFullYear(), MM: String(d.getMonth() 1).padStart(2, 0), DD: String(d.getDate()).padStart(2, 0), HH: String(d.getHours()).padStart(2, 0), mm: String(d.getMinutes()).padStart(2, 0), ss: String(d.getSeconds()).padStart(2, 0) } return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, match o[match]) }debounce在搜索框联动场景下特别好用比如输入关键字后延迟 400ms 再请求接口避免每敲一个字符就发一次请求。formatDate内部对字符串和 Date 对象都做了兼容表格渲染日期列时直接写formatDate(record.time)即可不需要在模板里写三目表达式。5. 打包体积分析与按需加载验证模板搭建完成后的第一件事不是写业务页面而是验证按需加载是否真的生效。这一步如果跳过等业务页面写到三四十个时再排查体积问题成本会高很多。先安装体积分析插件npm install webpack-bundle-analyzer -D然后配置 vue.config.js 的 chainWebpack// vue.config.js const BundleAnalyzerPlugin require(webpack-bundle-analyzer).BundleAnalyzerPlugin module.exports { chainWebpack: config { if (process.env.npm_config_report) { config.plugin(webpack-bundle-analyzer).use(BundleAnalyzerPlugin) } } }之后在 package.json 中加一条脚本build:analyze: vue-cli-service build --report执行npm run build:analyze后浏览器会自动打开dist/report.html。下面是我在按需加载前后的一组真实对比数据指标全量引入按需加载首屏 JS 体积约 1.2 MB约 650 KB首屏请求数129构建耗时约 18s约 15s全量引入时 ant-design-vue 的图表组件、时间选择器、日历组件都会被打进主 bundle。按需加载后打包产物里ant-design-vue依赖只出现在各自 chunk 中比如日期相关组件只在用到它们的页面 chunk 中出现。除了按需加载模板里还做了一个 chunk 分组优化。在 vue.config.js 中这样配置 splitChunksconfig.optimization.splitChunks({ chunks: all, cacheGroups: { antd: { name: antd, test: /[\\/]node_modules[\\/]ant-design-vue[\\/]/, priority: 10 }, vue: { name: vue, test: /[\\/]node_modules[\\/]vue[\\/]/, priority: 20 } } })priority数值越大优先级越高。把 vue 和 ant-design-vue 拆成单独 chunk 有利于浏览器长缓存因为业务代码更新时 vendor 文件名里的 hash 不会变用户不需要重新下载体积较大的公共库。这个配置是模板自带的如果你项目里还引入了 echarts、xlsx 这类大体积库可以在 cacheGroups 中继续追加节点。验证的时候可以打开浏览器 DevTools 的 Network 面板看app.js和chunk-vendors.js两个文件的体积差异。按需加载生效时ant-design-vue的代码应该散落在chunk-vendors.js和各个异步页面 chunk 中而不是全部集中在主 bundle。如果打开页面时某些组件样式错乱优先检查 babel-plugin-import 的style: css配置和 import 语句中组件名是否和官方文档一致因为组件的路径映射依赖这些字符串做代码转换。把分析器的生成条件改成npm_config_report而不是默认所有生产构建都开启目的就是让常规构建不增加额外时间只在需要诊断体积的那一次构建中生成 report 文件。这套验证做完后整个模板的初始化、UI 组件策略、权限路由、请求封装和打包优化才真正闭环。本文还有配套的精品资源点击获取
返回列表