
简介一套面向Vue初学者的实战入门教程以从零搭建网站后台管理系统为主线结合Element UI与Vue Router、Vuex等生态帮助读者理解虚拟DOM、数据绑定、指令和组件化开发等核心概念。资源共2000个文件以JavaScript、TypeScript、Vue单文件组件、SCSS/CSS样式表以及JSON/Markdown配置文档为主压缩包约35.17MB除业务源码外还包含大量依赖库与构建产物便于对照理解现代前端工程化结构。教程按环境配置、组件拆分、路由权限、数据交互、样式定制、测试部署等阶段逐步推进覆盖后台管理系统的典型功能模块适合需要从入门到项目落地的前端学习者。已有1525人学习下载资源中既包含完整可运行的示例工程也整理了后台界面开发的关键实现思路可直接用于实战练习或作为二次开发的起点。1. vue实战入门进阶篇从零开始实现网站后台实例先认清这座山很多人在学完Vue基础语法之后会撞上一面很尴尬的墙能写组件、能跑demo一接到“做一个网站后台实例”就不知道手往哪放。后台项目和官网、H5最大的区别在于它不靠奇技淫巧靠的是固定套路——路由守卫、请求封装、状态管理、多级菜单、表格表单、权限控制。这个vue实战入门进阶篇要做的就是把这些套路按真实开发顺序串起来从创建项目到打包上线搭出一个能直接拿去做毕业设计或者公司内部系统改造的后台壳子。文章默认你用过Vue但不要求你写过完整项目涉及的命令和参数都会拆开讲。适合刚学完Vue3基础想用项目实战巩固的人也适合要带前端新人的老员工当培训主线。2. 从零搭建Vue网站后台的前置准备与项目骨架2.1 先把Vue安装及环境配置这步走稳Node版本是第一个坑Vue3默认使用Vite做构建工具而Vite对Node版本是有硬性要求的。Vite 5要求Node 18以上Node 20是当前最稳的LTS版本。很多新手卡在npm run dev起不来、安装依赖报错根因都不是Vue本身而是Node版本太老。第一步先确认环境node -v npm -v如果主版本低于18不要直接去官网下安装包覆盖建议用nvm管理Node版本同一个机器上可以随时切换避免老项目因升级Node挂掉。nvm install 20.15.0装新版本nvm use 20切过去重启终端再node -v验证。这个习惯在一台机器同时维护多个项目时能少掉很多头发。环境就绪后建议顺手把npm源切到国内镜像不然安装依赖的速度会让人怀疑人生npm config set registry https://registry.npmmirror.comnpm config get registry可以确认是否生效。这一步不是必须的但安装vue-router、element-plus这些依赖时体感差别非常大。环境推荐版本说明Node.js18 LTS 或 20 LTSVite 5 起要求 1820 以上依赖构建更稳npm随 Node 自带用 npmmirror 源加速Vue^3.4本文基于 Vue3 组合式 APIVue2 不适用Vite^5.0create-vite 会按模板自动安装2.2 用Vite创建Vue3后台实例项目并安装依赖创建项目的命令很短但有一个容易踩坑的参数要说明。使用npm create vitelatest时会先问“是否安装create-vite”然后交互式选择框架和模板。想要跳过交互直接用Vue模板需要写成npm create vitelatest vue-admin-demo -- --template vue cd vue-admin-demo npm install-- --template vue的作用是让create-vite跳过选择环节直接生成Vue模板。npm install装的是项目基础依赖。装完之后先别急着写业务把后台必用的几个库一次性装好npm install vue-router4 pinia axios element-plus npm install -D sass这里每个库在后台实例里的职责都不同vue-router管页面跳转和路由守卫pinia管登录状态、用户信息这类全局数据axios负责和后端接口通信element-plus提供表格、表单、弹窗、分页这些现成UI组件。sass是样式预处理器后台项目里嵌套写法能省不少事。这是最常见的后台技术选型社区资料多遇问题搜得到答案。提示如果公司后端接口返回结构有统一包裹axios是必选如果项目只做纯前端演示axios也可以不装用mock数据顶着但真实后台迟早要对接接口。2.3 目录结构怎么安排才方便后续长线开发创建出来的项目只有src目录和几个文件直接往里面堆页面写到第三个页面就会乱。后台实例的目录一般按“模块职责”拆分而不是按“文件类型”拆分src/ ├── api/ # 所有接口请求按业务模块拆文件 ├── components/ # 全局通用组件如分页、搜索栏 ├── layout/ # 后台布局顶栏、侧边栏、面包屑 ├── router/ # 路由表和路由守卫 ├── store/ # pinia 状态管理 ├── views/ # 页面组件一个业务文件夹一个目录 ├── utils/ # 工具函数如格式化时间 ├── styles/ # 全局样式 ├── App.vue └── main.js页面文件夹内部再按“组件拆分”来放比如views/user/index.vue、views/user/dialog.vue。长线开发时改用户功能就钻进user目录改订单就钻进order目录互不干扰。目录建好后必须改一个地方Vite的别名。不然每一层组件里都要写一串../../去找文件路径对错了特别耗时间。在项目根目录的vite.config.js里加上别名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: 5173, host: true } })alias是把指向src目录之后import request from /utils/request就不用关心当前文件在第几层。server.port指定开发服务器端口host: true允许局域网设备通过本机IP访问调试移动端样式时用到。改完配置后需要重启npm run dev才生效这是Vite的一个常见机制改配置不重启不报错但也不起作用。3. Vue后台的路由守卫、状态管理与请求封装3.1 路由参数、嵌套路由与守卫后台导航的骨架后台页面有一个恒定的特征顶部导航和左侧菜单固定不动只有内容区域随路由变化。这种结构必须用嵌套路由实现外层组件叫layout内层才是业务页面。路由表先建起来// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, name: Login, component: () import(/views/login/index.vue) }, { path: /, component: () import(/layout/index.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 工作台, icon: HomeFilled } } ] } ] export default createRouter({ history: createWebHistory(), routes })components里使用箭头函数加import()这叫路由懒加载页面只有被访问时才加载对应JS文件首屏速度会明显改善。meta字段用来挂页面标题和图标后面做面包屑和动态菜单都靠它。子路由的path不要写前面的/因为它是相对父路由的写了反而匹配不上。路由参数在后台列表页很常见。从列表页跳详情页时通常用router.push({ path: /user/detail, query: { id: 1 } })传参在详情页用route.query.id读取。query参数刷新后不会丢适合传业务IDparams传参刷新后会丢掉只适合页面内部临时传值。面试时这道题出现频率很高实际开发中也确实容易被坑到。路由守卫是后台权限的第一道门。没有登录就不能进系统页面登录了再去登录页就自动跳回首页。前置守卫的写法比较固定// src/router/index.js 追加 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { if (token) next(/) else next() return } if (!token) { next(/login?redirect encodeURIComponent(to.fullPath)) return } next() })redirect参数是整个守卫逻辑的关键用户未登录访问任意页面时会被踢到登录页但目标地址被带在query里登录成功后跳回原页面体验比登录后永远回首页好得多。encodeURIComponent是为了防止路径里的特殊字符破坏URL结构。3.2 在后台实例里用Pinia管理登录状态与用户信息Vue3的项目状态管理基本默认用Pinia它比Vuex少了一堆样板代码没有mutations没有命名空间的重复声明store之间还能互相调用。后台实例里最需要全局管理的数据就是用户状态。新建store文件// src/store/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, logout() { this.token this.userInfo null localStorage.removeItem(token) } } })token的初始值直接从localStorage读而不是默认空字符串这样刷新页面后状态还能恢复。这是后台系统必须考虑的场景用户点一下浏览器刷新整个pinia重新初始化如果token只存在内存里刷新就等于登出。setToken方法里同步写localStorage是为了让路由守卫也能直接拿到token做判断。在main.js里注册Pinia后才能使用// src/main.js import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import router from ./router import ./styles/index.scss createApp(App).use(createPinia()).use(router).mount(#app)use(createPinia())会往每个组件的实例里注入$store子组件里useUserStore()才能正常工作。写完后启动npm run dev在任意组件里试试const userStore useUserStore()浏览器控制台能访问到user对象说明状态管理已经跑通。3.3 用Axios封装请求与响应拦截器后台项目的接口返回通常会包一层结构体比如{ code: 200, data: {...}, message: 成功 }。如果每个页面都去判断code代码会重复到没法维护。正确做法是封装一个统一请求实例// src/utils/request.js import axios from axios import { ElMessage } from element-plus import { useUserStore } from /store/user import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) request.interceptors.request.use( config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }, error Promise.reject(error) ) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response?.status 401) { const userStore useUserStore() userStore.logout() router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default request请求拦截器统一把token塞进请求头业务代码里不用每个接口都加一遍。响应拦截器做了两件事业务错误码用消息提示统一通知HTTP状态码401直接登出并跳登录页。这里最容易被忽略的是401处理的顺序必须先调logout()清掉本地token再跳转否则路由守卫读到旧token又把人放回系统页面。HTTP 状态码后台处理动作200检查业务 code非 200 提示 message401清 token跳转登录页403提示“无权限访问”500提示“服务异常请稍后重试”封装完成后业务侧调用就干净了import request from /utils/request export function fetchUserList(params) { return request.get(/admin/user/list, { params }) }params里的分页参数、搜索关键词会由axios拼成query string。后端拿到的字段名要与接口文档对齐比如page和pageSize拼错了列表页第一轮联调就会扑街。这一步的意义在于以后后端接口改了地址只改baseURL一个地方不用满项目搜索接口路径。4. 从登录到权限网站后台实例的第一个完整业务闭环4.1 登录表单校验与登录后的重定向登录是后台实例里最典型的“表单接口跳转”组合。先看表单部分使用element-plus的Form组件template div classlogin-page el-form refloginFormRef :modelloginForm :rulesrules label-width0 el-form-item propusername el-input v-modelloginForm.username placeholder用户名 / /el-form-item el-form-item proppassword el-input v-modelloginForm.password typepassword placeholder密码 show-password / /el-form-item el-form-item el-button typeprimary :loadingloading clickhandleLogin 登录 /el-button /el-form-item /el-form /div /templateprop属性把表单项和校验规则绑定起来rules里定义对应规则。:loading绑定登录中的状态防止用户重复提交。接下来是登录逻辑script setup import { ref, reactive } from vue import { useRouter, useRoute } from vue-router import { ElMessage } from element-plus import { useUserStore } from /store/user const router useRouter() const route useRoute() const userStore useUserStore() const loginFormRef ref() const loading ref(false) const loginForm reactive({ username: , password: }) const rules { username: [{ required: true, message: 请输入用户名, trigger: blur }], password: [ { required: true, message: 请输入密码, trigger: blur }, { min: 6, message: 密码不能少于6位, trigger: blur } ] } function handleLogin() { loginFormRef.value.validate(async valid { if (!valid) return loading.value true try { const { token } await loginApi(loginForm) userStore.setToken(token) const redirect route.query.redirect || / router.replace(redirect) } finally { loading.value false } }) } /scriptvalidate是element-plus表单的校验方法回调里的valid为false时直接return说明用户名或密码没填对。登录成功后route.query.redirect就是第3章路由守卫里存的原始目标页面用router.replace而不是push这样用户按浏览器返回键不会掉回登录页。4.2 路由权限从后端拿菜单再用addRoute动态挂载后台系统的权限分两层菜单级和按钮级。菜单元控制接口返回前端根据返回结果动态生成路由这种方案叫动态路由。静态路由表只保留/login和/404登录后通过router.addRoute挂载真实路由// src/router/dynamic.js import router from ./index const componentMap { Dashboard: () import(/views/dashboard/index.vue), UserList: () import(/views/user/list.vue), RoleList: () import(/views/system/role.vue) } export function addDynamicRoutes(menus) { menus.forEach(menu { router.addRoute({ path: menu.path, name: menu.name, component: componentMap[menu.component], meta: { title: menu.title, icon: menu.icon } }) }) }menus是登录后从后端拉取的菜单数组里面包含path、name、component字段。这里最容易踩的坑是component不能直接写() import(/views/ menu.component .vue)Vite在构建时无法静态分析这种动态拼接路径项目打包时会报错。正确做法是像上面这样维护一个组件映射表把后端传来的字符串映射到真实组件对象。菜单接口新增页面时前端映射表也要同步更新这是动态路由方案的常见边界。按钮级权限用一个自定义指令控制// src/directives/permission.js export default { mounted(el, binding) { const roles JSON.parse(localStorage.getItem(roles) || []) if (!roles.includes(binding.value)) { el.parentNode?.removeChild(el) } } }模板里的用法是v-permissionadmin意思是这个按钮只在当前用户角色是admin时显示。binding.value是传入的角色值roles是登录时存的角色数组。这个指令实现简单但在真实项目里够用删除按钮、审核按钮都能挡住。提示动态路由挂载后要调用router.replace(redirect)重进一次菜单地址否则刷新页面时动态路由还在构建中直接命中404。4.3 用Mock数据让前端不用等后端接口后台项目最头疼的是后端进度跟不上。Mock能解决这个依赖关系前端自己造数据把整个流程先跑通。Vite生态下用vite-plugin-mock比较顺手npm install -D vite-plugin-mock mockjs在vite.config.js里启用import { viteMockServe } from vite-plugin-mock plugins: [ vue(), viteMockServe({ mockPath: mock, enable: true }) ]mockPath指定mock文件目录enable控制是否开启。在mock目录下建user.js// mock/user.js export default [ { url: /api/login, method: post, response: ({ body }) { if (body.username admin body.password 123456) { return { code: 200, data: { token: mock-token-admin } } } return { code: 401, message: 用户名或密码错误 } } } ]访问/api/login这个Mock接口时Vite会直接返回response里的数据配合第3章封装的request.js前端在不依赖后端的情况下能完整走完登录流程。开发阶段这个方案非常好用但发布打包前一定要把Mock关掉。enable参数最好改成读环境变量比如enable: import.meta.env.DEV开发环境自动开启打包时自动关闭省得自己手动切。5. 表格、表单与弹窗后台实例里最常用的UI工程化套路5.1 用Element Plus实现表格的搜索、翻页与删除后台实例里出现频率最高的页面形态就是“顶部搜索区右侧表格底部翻页”。搜索区负责收集查询条件表格负责展示数据翻页负责分段加载。先看列表页核心逻辑// src/views/user/list.vue (script 部分) import { ref, reactive, onMounted } from vue import { fetchUserList, deleteUser } from /api/user const tableData ref([]) const total ref(0) const loading ref(false) const query reactive({ page: 1, pageSize: 10, keyword: , status: }) async function loadList() { loading.value true try { const res await fetchUserList(query) tableData.value res.data.rows total.value res.data.total } finally { loading.value false } } function handleSearch() { query.page 1 // 搜索时重置到第一页 loadList() } function handlePageChange(page) { query.page page loadList() } onMounted(loadList)query对象就是搜索条件的唯一数据源page和pageSize控制分页。搜索按钮把page重置为1再查询是因为当前在第5页时输入了新关键词如果不重置页码很可能搜出一页空数据造成“搜索结果不存在”的假象。翻页时只改page字段pageSize保持不变这是后台列表的标准交互。模板部分对应这个逻辑el-table :datatableData v-loadingloading el-table-column propusername label用户名 / el-table-column propstatus label状态 / el-table-column label操作 template #default{ row } el-button typedanger link clickhandleDelete(row)删除/el-button /template /el-table-column /el-table el-pagination :current-pagequery.page :page-sizequery.pageSize :totaltotal layouttotal, prev, pager, next current-changehandlePageChange /v-loading是element-plus的加载态指令请求发出时表格区域转圈防止用户重复点击筛选。row是#default插槽里解构出来的当前行数据删除操作直接拿row.id调接口。layout属性控制分页器显示哪些模块current-change事件返回最新的页码。5.2 弹窗里的表单与校验规则新增和编辑共用同一个弹窗组件是后台实例里最经济的做法。组件接收一个open方法由父组件传入编辑行的初始值template el-dialog v-modelvisible :titleformData.id ? 编辑用户 : 新增用户 width480px el-form refformRef :modelformData :rulesrules label-width80px el-form-item label用户名 propusername el-input v-modelformData.username / /el-form-item el-form-item label状态 propstatus el-select v-modelformData.status stylewidth: 100% el-option label启用 :value1 / el-option label停用 :value0 / /el-select /el-form-item /el-form template #footer el-button clickvisible false取消/el-button el-button typeprimary :loadingsubmitting clickhandleSubmit确定/el-button /template /el-dialog /templatetitle根据formData.id是否存在动态切换这是新增和编辑共用一个弹窗的常见标识。父组件传过来的行数据在打开弹窗时用Object.assign回填确保新增时字段干净不残留上次数据function open(type, row) { visible.value true formRef.value?.resetFields() if (type edit) { Object.assign(formData, row) } else { formData.id undefined } }组件通过defineExpose把open方法暴露给父组件父组件用模板引用调用dialogRef.value.open(edit, row)。提交时调用表单的validate方法校验通过再发请求function handleSubmit() { formRef.value.validate(async valid { if (!valid) return submitting.value true try { await saveUser(formData) ElMessage.success(保存成功) visible.value false emit(refresh) } finally { submitting.value false } }) }emit(refresh)让父组件重新加载列表页面数据立即更新。这个“弹窗组件自身管理显示隐藏、父组件监听刷新事件”的模式在后台表单场景里能处理绝大多数新增编辑需求。5.3 封装一个SearchBar组件减少重复工作后台项目页面一多搜索区代码会大量重复。可以抽出一个SearchBar组件把搜索条件配置化通过一个数组驱动渲染// src/components/search-bar/config.js export const userSearchConfig [ { prop: keyword, label: 用户名, type: input }, { prop: status, label: 状态, type: select, options: [ { label: 启用, value: 1 }, { label: 停用, value: 0 } ] } ]SearchBar组件内部根据配置数组循环渲染对应控件点击搜索按钮时把配置里所有prop值组成一个query对象emit给父页面。新增查询条件时只需要改userSearchConfig配置数组加一项页面模板一行都不用动。这一步属于后期优化项目少于三个列表页时可以不做但做后台外包项目时配置化收益非常大。表单项类型element-plus 组件常见校验 trigger文本输入el-inputblur下拉选择el-selectchange日期范围el-date-pickerchange数字输入el-input-numberchange6. 打包、Mock切换与vue打包后布局异常的修复6.1 用环境变量控制接口地址与Mock开关开发环境和生产环境的接口地址基本不会相同。Vite用.env文件区分环境项目根目录新建.env.development和.env.production# .env.production VITE_API_BASE_URL/api VITE_USE_MOCKfalse在代码里通过import.meta.env.VITE_API_BASE_URL读取值。.env.production里把地址配置为站内路径/api实际请求由nginx将/api反向代理到后端服务这样浏览器没有跨域问题后端地址也不会暴露到前端代码中。nginx配置注意try_files要配合vue-router的history模式写/index.html否则打包部署后在子路由刷新会出现404页面。6.2 vue打包后布局异常的第一轮排查打包后布局异常最常见的诱因是路由模式。createWebHistory要求服务器把所有路径重定向到index.html如果只配置了静态托管刷新二级路由页面会404表现为“白屏或内容不加载”。排查顺序先看Network面板请求的HTML是不是正常返回再去确认服务器是否有try_files回退配置。第二步是静态资源路径Vite默认构建资源的引用路径是绝对路径/assets部署到子目录时打开页面就是空的需要在vite.config.js里设置base: ./。移动端调试时还遇到过100vh在地址栏收起时塌陷的问题用100dvh代替即可。6.3 用路由懒加载和骨架屏收口首屏体验最后检查打包产物npm run build构建完成后查看dist/目录如果单个JS文件超过500KB可以到vite.config.js里配置手动分包build: { rollupOptions: { output: { manualChunks: { vue: [vue, vue-router, pinia], element: [element-plus] } } } }manualChunks把框架代码单独拆包浏览器能利用HTTP缓存机制用户换页面时不需要重新下载框架代码。后台实例的骨架屏可以直接用element-plus的el-skeleton做列表占位放在路由懒加载的组件加载期间显示避免切换路由时内容区短暂空白。这些优化做完后重点关注首屏资源总数把无用的全局组件改为按需引入后台实例的性能问题基本就收口了。本文还有配套的精品资源点击获取