简介:面向企业级后台系统开发者的 Web 后台管理模板,基于 StartBootstrap 构建,整合登录页、数据图表、按钮样式与 Tab 切换等常见管理端模块。模板采用 HTML、CSS、JavaScript 技术,兼顾响应式与多浏览器适配,适合快速搭建信息管理、数据监控或运营后台。压缩包共 165 个文件,以 58 个 JS 脚本、29 个 CSS 样式表为主,辅以 Less/SCSS 预处理源文件、HTML 页面及字体图标等资源,整体仅 1.48MB,轻量且易定制;JS 负责交互与图表渲染,CSS 与预处理文件便于统一主题,HTML 提供可直接套用的页面结构。已有 225 人学习下载。解包后可见 Bootstrap 系列样式、Font Awesome 图标、DataTables 表格插件等常用组件,开发者可直接复用登录表单、图表容器、按钮状态与 Tab 标签页,减少重复造轮子;同时保留 PSD 源文件,方便视觉调整。无论是初创项目快速上线,还是企业内部系统标准化,这套模板都能显著缩短后台界面的搭建周期。
1. web 后台管理模板:拿到手先别急着改页面,先搞懂三件事
如果你正在找一个能直接用的后台管理模板,多半是受够了从空项目开始搭路由、写登录、调权限的重复劳动。市面上的 web 后台管理模板很多,但真正能让你两天内交付一版可演示后台的,核心就三件事:路由怎么串联、权限怎么控制、菜单怎么生成。拿到这份资源后,先不要改 logo、换皮肤,先把这三条链路捋清楚。这个模板属于前后端分离的脚手架型资源,前端代码完整,自带登录页、动态菜单、权限指令、表格表单组件、Mock 数据接口和构建配置,适合需要快速出后台原型的前端开发者,也适合一个人包全栈的从业者。它能帮你省掉 70% 的基础设施搭建时间,但前提是你知道改哪里、不该改哪里。
2. 模板骨架拆解:路由、权限、菜单三件套是怎么咬合的
这份模板不像老式后台那样一个 HTML 文件里塞几百个页面,而是按模块拆分的现代前端工程。拿到压缩包先别急着npm install,打开源码目录,把 src 底下的分层看懂,后面的改造才不会翻车。很多开发者第一次用模板失败,不是模板本身有问题,而是把“约定优于配置”这件事理解成了“黑匣子”,到处乱找入口。
2.1 目录结构与职责边界
常见的后台管理模板一般会区分 views、router、store、api、components、utils 这几个顶级目录。你手上这份模板也是这个套路,我建议按下面的视角去读目录:
├── public │ └── favicon.ico ├── src │ ├── api # 接口请求封装 │ │ ├── user.js # 登录/用户信息接口 │ │ └── ... │ ├── assets # 静态资源 │ ├── components # 全局组件(上传、富文本、分页等) │ ├── layout # 主框架布局(侧边栏、顶栏、面包屑) │ ├── router # 路由配置 + 路由守卫 │ ├── store # 全局状态(用户、权限、标签页) │ ├── styles # 全局样式 │ ├── utils # 工具函数(token、request 封装) │ └── views # 页面视图 │ ├── login │ ├── dashboard │ └── system │ ├── user │ └── role └── package.json分层逻辑要理解成“单向依赖”:views 只负责渲染页面,不直接操作 token;api 只负责发请求,不关心页面状态;router 和 store 结合起来控制谁可以进入哪个页面。这样的分工意味着你改页面时不需要动权限逻辑,改权限时不需要翻页面代码。
参数说明:src/api下每个文件对应一种资源,比如user.js里同时包含登录、获取用户信息、退出登录的请求;src/layout是整体壳子,如果你要改侧边栏的 logo 和标题,去 layout 里找,而不是去 views 里找。很多新手一上来就全局搜索“后台管理系统”这几个字,然后改了一堆页面里的标题,结果侧边栏和浏览器标签页的标题还是旧的,这就是没搞懂目录职责。
模板里的views/system/user是完整的用户管理示例页面,包含列表查询、新增弹窗、编辑回显、删除确认、分页跳转,这套页面是整份模板的“说明书”,理解它的实现,你基本就理解了模板 80% 的用法。
2.2 路由守卫与权限指令:两条防线缺一不可
后台管理系统的权限不能只靠隐藏菜单实现,因为用户可以直接在地址栏输入 URL 绕过菜单。所以模板里有两层拦截:第一层是路由守卫,控制“能不能进页面”;第二层是按钮级权限指令,控制“进来后能干什么”。
路由守卫的核心代码一般在src/router/guard.js或者直接写在router/index.js里,核心逻辑如下:
// src/router/guard.js import router from './index' import { getToken } from '@/utils/auth' router.beforeEach(async (to, from, next) => { const token = getToken() if (token) { if (to.path === '/login') { next({ path: '/' }) // 已登录还去登录页,直接送回首页 } else if (!store.getters.userInfo) { // 有 token 但没有用户信息,说明页面被刷新过,需要重新拉取 try { await store.dispatch('user/fetchUserInfo') next() } catch (e) { store.dispatch('user/resetToken') next({ path: '/login' }) } } else { next() } } else { // 未登录,去白名单页面(login、404等)直接放行 if (whiteList.includes(to.path)) { next() } else { next({ path: '/login', query: { redirect: to.fullPath } }) } } })这段逻辑要拆开看:to.path是目标路由,from是来源路由,next是放行函数。整个函数的核心是“先看 token,再看用户信息”。其中store.dispatch('user/fetchUserInfo')这一步是很多模板的隐性坑——如果后端没有提供/me这类获取当前用户信息的接口,直接刷新页面就会一直跳登录页。我在实际用模板时,第一件事就是确认后端有没有这个接口,没有的话先把这个 action 改成返回固定角色。
按钮级权限的实现是一个自定义指令:
// src/directive/permission.js import { useUserStore } from '@/store/user' export default { mounted(el, binding) { const { value } = binding const userStore = useUserStore() const userRoles = userStore.roles const requiredRoles = value || [] const allowed = requiredRoles.some(role => userRoles.includes(role)) if (!allowed) { el.parentNode && el.parentNode.removeChild(el) } } }这里的逻辑是:在按钮元素上写v-permission="['admin']",如果当前用户角色不在列表里,这个按钮的 DOM 会被直接移除。注意它是“事后移除”,不是“渲染前判断”,所以如果你检测到页面闪了一下按钮然后再消失,这是正常现象,不是 bug。这个指令只控制前端显示,接口层面的越权操作必须靠后端拦住,前端权限只是交互层面的体验优化。
2.3 菜单数据是怎么长出来的
读模板时最让人疑惑的往往是菜单:“我没有配置侧边栏,为什么登录后自动出现了菜单?” 答案在路由配置和 store 的配合里。模板的菜单是从路由表映射出来的,而不是单独维护一份 JSON。每个路由记录里带上了meta.title和meta.icon,侧边栏组件拿到当前用户可访问的路由列表后,循环渲染成菜单。
// src/router/modules/system.js const systemRoutes = { path: '/system', component: Layout, redirect: '/system/user', meta: { title: '系统管理', icon: 'Setting' }, children: [ { path: 'user', name: 'SystemUser', component: () => import('@/views/system/user/index.vue'), meta: { title: '用户管理', icon: 'User' } }, { path: 'role', name: 'SystemRole', component: () => import('@/views/system/role/index.vue'), meta: { title: '角色管理', icon: 'UserFilled' } } ] }关键点在于component: Layout和 children 的配合。第一层路由component: Layout表示“这个模块的所有子页面都渲染在同一个布局壳里”,children 里的path: 'user'是相对路径,完整访问路径是/system/user。菜单的 title 和 icon 全部来自meta,如果你新增页面后菜单没出现,先检查这块。还有一种情况是模板支持后端返回动态路由,那么菜单就不是在router/modules里写死的了,而是在用户登录后请求一个/menu接口,用返回的数据去router.addRoute()动态加路由。这里不争论哪种方式好——静态路由适合模板演示,动态路由适合真实业务,因为你的权限模型往往在服务端早就定义好了。
3. 本地跑通与改造:从启动命令到第一套真实菜单
把目录结构看清楚之后,就可以动手跑了。这个阶段的目标只有一个:让项目在你机器上以“真实数据”的状态运行起来。注意是“真实数据”,不是 Mock 数据——很多模板默认开启 Mock 是为了让你不用后端也能看效果,但你早晚要接真实接口,所以这个阶段顺便把环境切换也做了。
3.1 安装依赖与启动命令
先看package.json里的 scripts 字段,然后按顺序执行:
# 安装依赖,推荐使用 npm 或 pnpm npm install # 或者 pnpm install # 启动开发服务 npm run dev参数说明:npm install会按package.json中的依赖声明安装所有包。如果项目用的是 pnpm,但你用了 npm,有可能会导致node_modules结构不同而出现幽灵依赖问题。我的习惯是看根目录有没有pnpm-lock.yaml文件——有就用 pnpm,没有就 npm。dev命令内部一般对应 Vite 或 Webpack,打开终端会看到本地访问地址,例如http://localhost:5173。
这一步最常见的坑是 Node 版本不匹配。模板的构建工具如果是 Vite 5 及以上,通常要求 Node 18+;如果是旧版 Webpack 4,可能只能在 Node 14 下跑。启动时报错先看 Node 版本,而不是先怀疑代码出了问题。
node -v # 输出 v18.x 或 v20.x 通常没问题3.2 环境变量与接口地址切换
模板默认给你配好了两套环境:开发环境和生产环境。找到根目录下的.env.development和.env.production,这是决定“项目跑起来后请求哪个后端”的开关。
# .env.development VITE_API_BASE_URL=/api VITE_USE_MOCK=true开发环境默认走/api这个代理前缀,同时打开了 Mock。你要接真实后端时,需要做两件事:第一,把VITE_USE_MOCK改成false;第二,在 Vite 的配置文件里把/api代理到你的后端地址。
// vite.config.js server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }这里的target是你的后端服务地址,rewrite去掉/api前缀是因为很多后端接口本身不带这个前缀。注意这只是开发环境的代理配置,生产环境通常通过 Nginx 反向代理解决,不在构建配置里写死。改完这两处后重启 dev server,让环境变量生效。
如果你发现改了VITE_USE_MOCK=false之后接口请求报 404,大概率是后端接口路径和前端api模块里的路径拼不上。我一般会在浏览器 Network 面板看实际请求的 URL,然后把src/api/xxx.js里的路径改成后端真实路径,而不是去调代理配置。
3.3 新增一套业务页面并接入菜单
跑通之后,练手方式不是去改现有页面,而是从零新增一个业务模块。假设你要加一个“订单管理”模块,操作分四步:
第一步,在src/views下新建目录order,里面放一个index.vue:
<template> <div> <el-card> <template #header>订单列表</template> <el-empty description="暂无数据" /> </el-card> </div> </template> <script setup> // 页面级入口,先占位,后续填表格和表单 </script>第二步,在src/router/modules下新建order.js:
const orderRoutes = { path: '/order', component: Layout, redirect: '/order/list', meta: { title: '订单管理', icon: 'Document' }, children: [ { path: 'list', name: 'OrderList', component: () => import('@/views/order/index.vue'), meta: { title: '订单列表', icon: 'List' } } ] }注意新增模块文件后,检查router/index.js里有没有自动加载modules下所有文件的机制。模板如果用的是import.meta.glob自动加载,那你新建的order.js无需手动注册;如果用的是静态 import 数组,你就得去index.js里手动加一行 import 和数组项。
第三步,在src/api下新建order.js:
import request from '@/utils/request' export function getOrderList(params) { return request({ url: '/order/list', method: 'get', params }) }第四步,启动项目,你会看到侧边栏多出“订单管理”菜单。如果没出现,先刷新浏览器,再检查路由文件是否被加载。我习惯在路由守卫里加一行console.log(to.path),确认路由是否真的注册进去了——这种“加日志确认”的做法,比反复刷新页面猜问题要快得多。
4. 页面与组件实战:按后台管理开发的套路去改表格和表单
后台管理的日常开发 80% 都是在“列表 + 表单 + 弹窗”三种形态里反复横跳。模板里已经封装好了成套的表格组件、弹窗组件、分页组件和表单校验方案,你要做的不是从零写,而是把模板已有的用户管理页拷贝一份,然后把字段替换成业务的字段。这个思路是很多有经验的开发者的工作方式——把模板当代码生成器来用。
4.1 表格页的列配置与操作按钮
打开src/views/system/user/index.vue,你会看到表格部分的代码大致长这样:
<el-table :data="tableData" v-loading="loading"> <el-table-column prop="username" label="用户名" min-width="120" /> <el-table-column prop="nickname" label="昵称" min-width="120" /> <el-table-column prop="email" label="邮箱" min-width="180" /> <el-table-column prop="status" label="状态" width="90" /> <el-table-column label="操作" width="150" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="handleEdit(row)">编辑</el-button> <el-popconfirm title="确认删除?" @confirm="handleDelete(row)"> <template #reference> <el-button link type="danger">删除</el-button> </template> </el-popconfirm> </template> </el-table-column> </el-table>这里要理解几个面板上的细节:prop是后端返回字段名,label是表头显示文本,min-width是表格列的弹性宽度,fixed="right"表示操作列固定在表格右侧,数据横着滚动时操作按钮始终保持可见。如果你做的是订单列表,把username/nickname/email换成orderNo/productName/amount就行,但要注意后端字段名带下划线(比如order_no)时,前端可以直接用order_no,也可以让后端转成驼峰,这个看团队约定,不要前端转一次后端转一次,两边对不上就是各种 undefined。
操作按钮里的#default="{ row }"是作用域插槽,代表当前这一行的数据,handleEdit(row)就是把这一行数据传给编辑方法。所以表格页的代码本质上是“列配置 + 行数据操作”的绑定关系。
列表页还有一个重头戏是搜索区。模板一般会在表格上方放一组el-form inline表单项,点击“查询”按钮时把表单值作为请求参数:
const queryParams = reactive({ pageNum: 1, pageSize: 10, username: '', status: null }) function fetchList() { loading.value = true getOrderList({ ...queryParams }) .then(res => { tableData.value = res.rows total.value = res.total }) .finally(() => { loading.value = false }) }注意传给接口的参数里一定要带pageNum和pageSize,这是后台管理系统分页的默认约定。reactive创建的响应式对象展开到请求参数里,表单输入的值会自动同步,查询按钮只需要把queryParams.pageNum重置为 1 再去请求接口。
4.2 表单弹窗的校验与提交
表单弹窗一般是新建和编辑共用的。新建时form是空对象,编辑时把row的数据填充进来。模板里这个模式很固定,建议直接沿用:
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close> <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form-item label="用户名" prop="username"> <el-input v-model="form.username" placeholder="请输入用户名" /> </el-form-item> <el-form-item label="手机号" prop="phone"> <el-input v-model="form.phone" placeholder="请输入手机号" /> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button> </template> </el-dialog>校验规则写在rules对象里:
const rules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' } ], phone: [ { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ] }提交阶段的核心是“先校验,再请求”:
function handleSubmit() { formRef.value.validate(valid => { if (!valid) return submitLoading.value = true const api = form.id ? updateOrder(form) : createOrder(form) api .then(() => { ElMessage.success('保存成功') dialogVisible.value = false fetchList() }) .finally(() => { submitLoading.value = false }) }) }这里的form.id ? updateOrder(form) : createOrder(form)是三元判断:有id就走更新接口,没有就走新增接口,这是后台管理系统最常用的分支写法。submitLoading绑定到确定按钮上,防止用户重复点击提交。注意弹窗上一定要加destroy-on-close,否则关闭弹窗后表单数据不清空,第二次打开时还残留上一次的内容——这是最容易踩的交互坑。
4.3 复用模板里的弹窗、抽屉和图表
除了表格和表单,模板里还封装了一些高频业务组件,比如上传组件、富文本编辑器、图表的 ECharts 封装。这些组件的使用姿势通常是:在页面里引入,传几个 props,回调事件处理结果。例如上传组件的典型用法:
<el-upload v-model:file-list="fileList" action="/api/upload" :headers="{ Authorization: 'Bearer ' + token }" list-type="picture-card" :on-success="handleUploadSuccess" > <el-icon><Plus /></el-icon> </el-upload>需要注意action是上传接口地址,headers里要带上认证 token,否则后端会返回 401。如果你的模板已经封装了Upload组件,大概率内部已经处理了 token 注入,此时你就不要去传 headers 了。
图表方面,模板一般会在src/components里放一个ECharts封装组件,用法是:
<template> <v-chart :option="chartOption" autoresize /> </template> <script setup> import VChart from 'vue-echarts' import { use } from 'echarts/core' import { CanvasRenderer } from 'echarts/renderers' import { PieChart } from 'echarts/charts' import { TooltipComponent, LegendComponent } from 'echarts/components' use([CanvasRenderer, PieChart, TooltipComponent, LegendComponent]) const chartOption = { tooltip: { trigger: 'item' }, legend: { bottom: '0%' }, series: [ { name: '订单来源', type: 'pie', radius: ['40%', '65%'], data: [ { value: 1048, name: '小程序' }, { value: 735, name: 'PC' }, { value: 580, name: 'H5' } ] } ] } </script>这段代码展示的是“按需加载 ECharts 模块”的用法,核心是use()函数只注册你用到的图表类型和组件,这样打包时不会把整个 ECharts 塞进去。如果你改了图表没有生效,优先检查有没有正确注册对应的类型,比如折线图要用LineChart,柱状图要用BarChart,漏注册的结果就是渲染空白。
5. 常见问题排查:模板跑到生产还踩到的五个坑
模板在自己电脑上跑通只是第一步,真正的问题往往出现在“部署给别人用”或者“接上真实接口”的时候。下面这几个坑,我每一轮接手后台模板项目都会遇到至少其中两三个,按现象到原因到解决写清楚。
5.1 左侧菜单改了路由配置却不更新
现象:新增了一个路由模块,或者修改了某个菜单的 title,重启项目后侧边栏没有任何变化,甚至旧菜单还在。
原因大概率不是代码没生效,而是浏览器的热更新缓存没有同步到侧边栏的状态。侧边栏数据可能被持久化到了sessionStorage或localStorage里,刷新页面时优先读缓存,而不是重新计算。另一个原因是菜单由 store 里的routes生成,store 的数据在 dev server 热更新时不会自动重置。
解决方法是打开浏览器开发者工具,找到 Application 面板,清掉localStorage和sessionStorage中跟路由、权限相关的 key,然后硬刷新页面。如果问题依旧,检查router/modules下的文件命名——某些模板的自动加载机制只识别特定文件名格式,比如默认只加载*.js而忽略了*.ts。
5.2 登录后刷新页面直接 404
现象:在系统内点了某个子页面,比如/system/user,按 F5 刷新,页面变成 404 或者空白。
原因是前端用了 HTML5 History 模式路由(createWebHistory),这种模式下,浏览器会向服务器真实请求/system/user这个路径,而你的服务器静态文件里没有这个路径对应的文件。开发环境因为 Vite dev server 做了 fallback,所以没问题,但部署到 Nginx 后就必须配 try_files。
解决方法是修改 Nginx 配置:
location / { try_files $uri $uri/ /index.html; }这样所有不存在的路径都会回退到index.html,由前端路由接管。还有一个容易被忽略的点:如果你把前端部署在子路径下,比如https://xxx.com/admin/,那么createWebHistory创建时要传 base 参数,并且 Nginx 的try_files要相应调整。
5.3 按钮权限指令失效,路由守卫也拦不住
现象:给按钮加了v-permission="['admin']",普通用户登录后按钮仍然可见;或者用户直接在地址栏输入某个子页面 URL,竟然能进入。
原因是把权限控制做成了一锤子买卖:permission指令在mounted阶段执行一次,但用户信息是异步获取的,指令执行时userStore.roles还是空数组,判断结果自然是“无权”,但某些模板的处理逻辑反过来是“没匹配到就放行”。更致命的是路由守卫只做了 token 判断,没有做页面级角色判断。
解决思路分两层:第一,调整指令判断时机,在mounted里调nextTick再检查;更稳妥的做法是改为在 store 设置完用户信息后再统一渲染菜单和按钮,或者在v-permission内部监听roles变化,变化时重新判断然后移除按钮。第二,在路由守卫里增加页面级权限校验,用to.meta.roles和当前用户角色做交集判断,没有权限就重定向到 403 页。这里要强调:前端无论怎么控制都只是体验优化,后端接口必须校验权限。
5.4 打包部署后白屏,控制台一堆资源 404
现象:本地npm run build成功,把 dist 目录丢到服务器上,访问首页白屏,F12 看到 js/css 文件报 404。
原因是构建时base路径配置不对。默认 Vite 的base是/,意味着资源路径是https://你的域名/assets/xxx.js,但如果你的站点是通过https://域名/子目录/访问的,这些资源实际在/子目录/assets/xxx.js,自然 404。
解决方法是修改构建配置:
// vite.config.js export default { base: '/子目录/' }如果你不确定部署后的访问路径,最简单的判断方法是把 dist 里的index.html打开看里面 script 标签的 src,如果是以/开头,说明是绝对路径;如果相对路径,则不受部署目录影响。还有一格坑,就是后端接口的地址写死成了http://localhost:8080,上线以后前端页面能加载,但一调接口就报跨域或连接失败。比较省心的做法是在.env.production里配置:
VITE_API_BASE_URL=/api然后让 Nginx 把/api反向代理到真实后端,这样前端没有任何明文的后端地址,切换环境只改配置,不碰代码。
5.5 关闭 Mock 后登录接口还是返回假数据
现象:已经设置了VITE_USE_MOCK=false,重启项目,登录接口依然能登录成功,而且返回的用户是模板里写死的账号。
原因是 Mock 逻辑有两种实现方式:一种是通过环境变量控制插件挂载,挂在 main.js 里;另一种是直接写死了请求拦截器,在 axios 请求发出去之前拦截并返回假数据。前一种关闭比较容易,后一种你需要在代码里找到那个 mock 目录的注册入口,把它注释掉或者不 import 进来。
排查技巧:打开控制台 Network 面板,看登录请求有没有真的发到后端。如果 Network 面板里没有请求记录,但页面却提示登录成功,那一定是走了 Mock 或本地拦截。这时全局搜一下代码里的/login字符串,看看有没有什么条件判断,一般都能追到。如果模板用的是 MSW 或 vite-plugin-mock 这类工具,还需要确认插件的初始化函数有没有被环境变量控制。
6. 从模板快速拼一页 CRUD:照着抄也算一种工程习惯
模板的最终价值,是你形成自己的“后台开发套路”。这里分享一个我自己的操作流程:每次拿到新需求,不慌着写代码,而是先新建文件、先抄模板里的用户管理页结构,然后按“查询区 — 表格区 — 弹窗表单区”三段式把它拆了重组。对一个典型的后台管理模块,我会按这个顺序依次排查:路由是否注册、菜单是否生成、接口是否通、表格字段显示是否正确、新增/编辑弹窗是否能打开、校验规则覆盖了几个字段、删除有没有二次确认、搜索和重置是否正常。每过一项就在心里打一个勾,全部打完后这个页面的完成度基本就有 80% 了。
这里有一个值得养成的习惯:先做完“新增 + 列表”这条最小闭环,再补“编辑 + 删除 + 分页 + 搜索”。因为新增成功后列表能看到新数据,你才确认接口是真的联通了;如果你把前后端字段名对不上的问题留到最后一次性排查,那结果往往是十几个 undefined 叠在一起,根本分不清是前端 bug 还是后端返回问题。再就是分页的坑——表格改了当前页数后,数据没有回到第一页,这个是业务侧最容易看遗漏的,所以在handleSearch里一定要先pageNum = 1再请求。
模板本质上是固化了这些约定,但约定不等于银弹。我从第一次拿模板做完整个后台,到后来自己搭内部脚手架,最大的收获是:模板不是给你省掉思考的,而是帮你把那些“每个后台都一样的部分”给抽象好,让你能把精力集中在业务本身的规则上。从那以后,我每接一个后台管理系统,都强制自己先跑一遍“建路由、建菜单、写一页 CRUD”的流程打底,再把模板里的权限机制和请求封装替换成项目的实际需求。这个习惯帮我少走了很多弯路,也让我真正理解了模板里每个目录存在的意义。希望帮到你。
本文还有配套的精品资源,点击获取