
简介这份 Vue3 示例工程主要面向准备学习 Vue3 组合式新语法、Element-Plus 组件库以及 Mockjs 假数据用法的前端开发者也适合需要快速搭建演示项目或课堂案例的读者。它省去了从零初始化工程、安装依赖和配置接口模拟的繁琐步骤工程内已经引入 Element-Plus、Mockjs 与 axios并完成一个基于表格组件的示例页面运行后可直接查看表格渲染和假数据请求效果也便于在此基础上继续添加新页面与业务模块。压缩包共 27 个文件约 130KB内含 9 个 vue 页面与组件文件、3 个 json 配置、3 个 js 脚本以及 md 说明文档、css 样式、png 图片和 html 入口文件等目录结构清晰体量轻便。已有 2140 人学习可作为 Vue3 项目起步模板、教学示例或二次开发底座帮助读者理解工程目录组织、组件引入、Mock 数据接入和表格页面搭建的完整思路。1. 拿到一个「环境已经配好」的 Vue3 起步包先弄清楚它替你做了哪些事情上周帮一个做外包的朋友看项目他那边后端接口还没有影子产品已经催着要看页面长什么样。他从资源包里翻出一个 vue3-demo解压、装依赖、启动三分钟列表页就出来了。这套东西把 Vue3 框架、Element-Plus 组件库、mockjs 假数据全部串在一起路由、路径别名、请求代理也预先配好了拿到手里不用再从零搭骨架。它的价值在于把「配环境」这段最容易出问题的路先铺平不需要你背 Vue3 语法。适合谁急着验证页面结构、要给甲方演示交互、团队里想统一一套起步模板的人。不适合谁要学 webpack 老配置、或者项目本身要上服务端渲染的这个包帮不上忙。下面按「先跑通、再拆开、最后改造成自己的」这个顺序往下讲。2. 跑通不是终点vite 配置与首屏入口链路逐行拆解先把「能跑」这件事做扎实再去看它内部是怎么组织的。很多人拿到这类包npm run dev一敲看见页面就以为完事了结果第一次改配置就卡住原因是他不知道哪个文件在起作用。这一章把启动前检查、vite 配置、入口链路三块拆开每块都给出可以直接抄的写法。2.1 启动前先做三项检查能省掉一半的启动问题环境已经配好不代表在你机器上就一定能跑。node 版本、包管理器、依赖完整性这三件事是这类起步包最容易卡住新手的地方。node 版本太低vite 的某些语法直接报错包管理器混用lock 文件跟依赖树对不上依赖不全启动时提示模块找不到。常见做法是先跑一遍版本确认再决定要不要重装依赖。# 看当前 node 和包管理器版本建议 node 18 及以上 node -v npm -v # 确认 registry 走的是自己能通的源 npm config get registry # 依赖目录存在不代表依赖完整用它检查缺失或多余的包 npm ls --depth0第一行和第二行决定你能不能跑起来第三行决定装依赖快不快第四行是判断依赖树健不健康的最快方式。npm ls --depth0只列顶层依赖输出里带UNMET DEPENDENCY或者invalid的行就是要处理的目标。如果输出一大片红色别一个个修删掉node_modules和 lock 文件重装更快。提示有些起步包会把 node_modules 一起打包解压后直接能跑。这种包在换机器、换 node 版本之后反而容易出问题我一般会把原来的 node_modules 删掉重新装一次图个干净。依赖装完别急着改业务代码先确认端口没被占。vite 默认端口被占用时会自动往上找一个但如果你在配置里写死了端口它就直接报错退出。终端里看到Port 5173 is in use这类提示要么杀掉占用进程要么改配置里的端口别硬扛。2.2 vite.config.js 里真正需要改的只有四个地方这类起步包的 vite 配置文件通常已经写好了插件、别名、代理第一次打开会觉得选项很多。实际上日常需要动的就四块路径别名、开发服务器、代理规则、构建选项。其余保持原样即可改多了反而容易把好好的环境弄坏。import { defineConfig } from vite import vue from vitejs/plugin-vue import path from node:path export default defineConfig({ plugins: [vue()], resolve: { // 别名注意 jsconfig.json / tsconfig.json 里的 paths 要跟这里保持一致 alias: { : path.resolve(__dirname, src) } }, server: { host: 0.0.0.0, // 局域网内手机、平板也能访问真机联调时很关键 port: 5173, open: true, proxy: { // 只在连真实后端时生效走 mock 时这段不会命中 /api: { target: http://127.0.0.1:8080, changeOrigin: true, // 后端接口本身没有 /api 前缀时把前缀去掉 rewrite: (p) p.replace(/^\/api/, ) } } }, build: { sourcemap: false, chunkSizeWarningLimit: 1500 } })alias这一项最容易出问题vite 里配了但编辑器里写import xxx from /xxx依然标红原因是 jsconfig 或 tsconfig 的paths没同步。两边写法要能对上否则编辑器提示和实际运行会打架。host: 0.0.0.0是给别人演示时最实用的一行。默认只监听本机同事用你的局域网 IP 打不开页面加上这一行才能让别人直接访问。proxy里的rewrite不是必写项。后端接口路径本来就带/api写了这行反而会把前缀去掉导致 404。判断方法很简单把浏览器 Network 里真实请求的路径跟后端路由定义对比一下多一层少一层一眼就能看出来。chunkSizeWarningLimit只是把打包体积警告的阈值调高不会真的减小体积。它解决的是「构建日志刷屏」这个体验问题体积该优化还得优化别把这个当成优化手段。2.3 首屏入口链路main.js 到路由表的三层跳转页面能显示说明这条链路是通的。搞清楚这条链路后面加页面、加权限、加全局组件才知道往哪加。典型结构是index.html挂一个空 divmain.js创建应用实例并挂载插件App.vue里放一个路由出口router/index.js定义路由表。// src/main.js import { createApp } from vue import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(createPinia()) app.use(router) // 全局错误兜底组件里抛出的异常能在这里看到排查白屏时很有用 app.config.errorHandler (err, instance, info) { console.error([global error], info, err) } app.mount(#app)app.config.errorHandler这段是很多人会漏掉的部分。组件渲染阶段抛异常控制台有时只给一行看不懂的警告页面直接白屏。加上这个兜底至少能拿到出错组件和错误来源信息排查速度快很多。// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /user }, { path: /user, name: User, // 路由级懒加载首屏只加载当前页面对应的 chunk component: () import(/views/UserList.vue), meta: { title: 用户列表 } } ] const router createRouter({ history: createWebHistory(), routes }) export default routercreateWebHistory用的是 HTML5 history 模式地址栏干净但部署到静态服务器时需要配置 fallback 到index.html否则刷新子路由会 404。如果这个包是要交给不熟悉服务器配置的人部署把这里改成createWebHashHistory会省掉一堆沟通成本代价是地址栏多个#。component: () import(...)这种懒加载写法作用是让每个路由单独打一个 chunk。页面少的时候感觉不出来页面一多首屏体积差距非常明显。判断有没有生效看构建日志里是不是出现了多个 js 文件只有一个大文件说明懒加载没写对。3. Element-Plus 落地按需引入、主题覆盖与表单校验的组合拳组件库这块起步包里通常已经接好了但接的方式不一样后面维护成本差很多。全量引入省事打包体积大按需引入体积小配置多两步。这一章把两种方式的取舍讲清楚再给出主题色覆盖和表单校验的完整写法这三块是实际项目里改得最多的地方。3.1 全量引入和按需引入怎么选全量引入就是app.use(ElementPlus)一把梭所有组件和样式都进包。优点是零配置缺点是打包产物里躺着大量用不到的组件首屏加载时间会被拖长。按需引入靠两个插件在编译阶段做静态分析只把模板里真正用到的组件和样式引进来。// 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(), // 自动导入 ElMessage、ElMessageBox 这类函数式 API AutoImport({ resolvers: [ElementPlusResolver()], dts: src/auto-imports.d.ts }), // 自动注册模板里出现的 el-xxx 组件 Components({ resolvers: [ElementPlusResolver()], dts: src/components.d.ts }) ] })AutoImport负责函数式调用比如在 script 里直接写ElMessage.success(保存成功)而不用手动 import。Components负责模板标签写el-button就自动注册。两个插件分工不同少配一个就会出现「组件能用但函数报未定义」或者反过来。dts这两个参数是给编辑器补类型提示用的生成的文件建议提交到版本库团队其他人拉下来就有提示。不提交也能跑只是每个人第一次打开都要等插件生成一次。我的判断标准很直接项目页面超过五个、或者要上线给外部用户用就走按需引入只是内部演示、页面就两三个全量引入更省心别为了几百 KB 折腾半天。3.2 主题色覆盖改一个变量不够六个都要改Element-Plus 用的是 CSS 变量改主题色理论上改--el-color-primary一行就行。实际改完会发现主按钮变了但 hover 态、禁用态、浅色背景还是老的蓝色看着很别扭。原因是这些状态用的是--el-color-primary-light-3、light-5、light-7、light-8、light-9和--el-color-primary-dark-2这一组派生变量它们不会跟着主色自动变。/* src/styles/element-override.scss */ :root { --el-color-primary: #2f6fed; /* 下面六个是派生色按主色往白/往黑插值算出来的必须一起给 */ --el-color-primary-light-3: #6d9bf2; --el-color-primary-light-5: #a3c0f7; --el-color-primary-light-7: #c9d9fb; --el-color-primary-light-8: #dce7fd; --el-color-primary-light-9: #eef4fe; --el-color-primary-dark-2: #2659be; }这六个值不用手算拿主色往#ffffff方向按固定比例插值就行light-3约等于 30% 白light-9约等于 90% 白dark-2是往#000000方向 20%。少给一个对应状态的组件就会「跳色」尤其是light-9它常被用作表格斑马纹和选中行背景漏了特别明显。这个 scss 文件要在main.js里排在 Element-Plus 样式之后引入顺序反了会被覆盖。判断顺序对不对看浏览器 DevTools 里:root下这几个变量最终生效的值是不是你写的那个。3.3 表单校验加表格分页一个能直接复用的组合列表页 弹窗表单是这类起步包最常承载的场景。表单校验的关键在rules的写法和validate的调用方式表格分页的关键在页码变化时怎么触发重新请求。script setup import { reactive, ref } from vue import { ElMessage } from element-plus const formRef ref() const form reactive({ name: , age: undefined }) const rules { name: [ { required: true, message: 名称不能为空, trigger: blur }, { min: 2, max: 20, message: 长度需在 2 到 20 之间, trigger: blur } ], age: [ // type 必须写 number否则输入框拿到的是字符串min/max 判断会失效 { type: number, min: 1, max: 120, message: 年龄不合法, trigger: change } ] } async function submit() { try { await formRef.value.validate() // 校验通过走保存逻辑 ElMessage.success(校验通过) } catch (e) { // validate 失败会 reject不 catch 会变成未捕获异常 return } } /script template el-form refformRef :modelform :rulesrules label-width80px el-form-item label名称 propname el-input v-modelform.name / /el-form-item el-form-item label年龄 propage el-input-number v-modelform.age :min1 :max120 / /el-form-item el-button typeprimary clicksubmit提交/el-button /el-form /templateformRef.value.validate()返回的是 Promise校验不通过时会 reject。很多代码里直接await不加 try/catch控制台一堆红色报错看着像出了大问题其实只是校验没过。prop属性必须写且要和rules的键一致。漏写prop是最常见的「规则不生效」原因组件根本不知道这条规则挂在哪个字段上。数字类型字段的type: number别省。省掉之后el-input-number传出来的是数字但校验器按字符串处理min/max就形同虚设。分页部分el-table的data和el-pagination的current-page、page-size三者要联动页码一变就重新请求const query reactive({ page: 1, size: 10 }) const rows ref([]) const total ref(0) async function fetchList() { // 这里调的是 mock 接口后面切真实接口时这段不用改 const res await getUserList({ page: query.page, size: query.size }) rows.value res.records total.value res.total } function onPageChange(p) { query.page p fetchList() }page和size放在一个 reactive 对象里切真实接口时直接把getUserList内部的实现换掉就行页面层代码零改动。这是把「数据来源」和「页面状态」分开的好处后面第 4 章还会用到这个思路。4. mockjs 假数据层拦截位置、分页套路与请求封装后端接口没好的时候前端不能干等。mockjs 的作用就是在浏览器里把请求拦下来返回一份假数据让页面能完整跑起来。看起来简单实际有几个选择点决定后面好不好维护拦在哪一层、数据怎么造、假数据和真接口怎么切换。这一章把这三件事说清楚。4.1 拦截放在哪一层决定了后面切接口痛不痛mockjs 的拦截原理是重写浏览器的 XMLHttpRequest所有走 XHR 的请求都会被它的规则匹配一遍。这里有两个限制要知道一是它拦不住 fetch如果项目里用 axios 就没问题因为 axios 底层是 XHR二是它只在浏览器端生效服务端渲染或者构建阶段用不上。按拦截位置分两种接法。第一种是在main.js里直接引入 mock 文件让 mockjs 全局接管第二种是用vite-plugin-mock把 mock 做成 dev server 的中间件在 node 层返回数据浏览器端完全无感。第一种的好处是零依赖、改起来直观缺点是拦不住 fetch而且 mock 代码会跟着打包进产物。第二种好处是支持 fetch、支持热更新、产物里干净的缺点是多一个开发依赖。我的习惯是项目里用 axios 就选第一种追求简单用了 fetch 或者要严格隔离产物选第二种。不管选哪种接口路径要和请求封装里的baseURL对得上。baseURL是/apimock 规则就得匹配/api/xxx差一个前缀就是 404这是最高频的翻车点。4.2 五个套路覆盖九成场景列表、详情、增删改、延迟、异常假数据不是随便造几条就行形状要和真实接口对齐否则接真接口那天要改一堆字段名。下面这份 mock 文件覆盖了列表分页、关键词过滤、随机网络延迟三件事可以直接改成自己的业务。// src/mock/user.js import Mock from mockjs const Random Mock.Random // 数据只造一次放在模块顶层。 // 如果写在请求回调里每次请求都会换一批数据翻页时会出现同一条记录在两个页面重复出现 const database Array.from({ length: 200 }, (_, i) ({ id: i 1, name: Random.cname(), email: Random.email(example.com), department: Random.pick([研发, 产品, 测试, 运营]), status: Random.pick([0, 1]), createTime: Random.datetime(yyyy-MM-dd HH:mm:ss) })) // 从 url 上解析查询参数mockjs 回调里拿到的是完整 url function parseQuery(url) { const qs url.split(?)[1] || return Object.fromEntries(new URLSearchParams(qs)) } // 让接口有 200 到 800 毫秒的随机延迟页面的 loading 状态才有机会被看见 Mock.setup({ timeout: 200-800 }) Mock.mock(/\/api\/user\/list(\?.*)?$/, get, ({ url }) { const { page 1, size 10, keyword } parseQuery(url) let rows database if (keyword) { rows rows.filter((r) r.name.includes(keyword)) } const start (Number(page) - 1) * Number(size) return { code: 0, message: ok, data: { total: rows.length, records: rows.slice(start, start Number(size)) } } })Mock.setup({ timeout: 200-800 })这行容易被忽略但没有它接口瞬间返回页面上的 loading 动画根本没机会出现等接上真接口才发现 loading 逻辑写错了。数据造在模块顶层而不是回调里这点很关键。写在回调里会导致每次请求都重新生成一批数据翻到第二页时第一页的记录全变了看起来像「数据错乱」实际是造数据的位置不对。Mock.mock第一个参数用正则而不是字符串是为了兼容带查询参数的 url。用字符串精确匹配一加分页参数就匹配不上接口直接 404。返回结构{ code, message, data }要和后面请求封装里的拦截逻辑对齐。真实后端常见的就是这套结构对齐之后切接口时业务代码不用动。详情、新增、删除这几个接口写法类似区别只在Mock.mock的第二个参数换成post/delete回调里从options.body拿请求体。新增接口记得往database里 push否则「新增成功后列表里看不到」演示时很尴尬。4.3 让 mock 和真实接口共用一套请求封装页面代码里不要直接写axios.get中间隔一层封装。这层的职责是统一baseURL、统一超时、统一把{ code, message, data }解成业务数据。有了这层切 mock 和切真实后端都只是改一个环境变量。// src/utils/request.js import axios from axios const service axios.create({ // 从环境变量读开发环境指向 /api由 mock 或代理接管 baseURL: import.meta.env.VITE_API_BASE || , timeout: 10000 }) service.interceptors.response.use( (res) { const { code, message, data } res.data // 业务码非 0 视为失败直接抛出页面层统一 catch if (code ! 0) { return Promise.reject(new Error(message || 业务异常)) } return data }, (err) { // HTTP 层错误常见的是超时和 4xx/5xx return Promise.reject(err) } ) export default service# .env.development VITE_API_BASE/api VITE_USE_MOCKtrue # .env.production VITE_API_BASEhttps://your-domain.example.com/api VITE_USE_MOCKfalse两个环境文件里VITE_API_BASE是接口前缀VITE_USE_MOCK是开关第 6 章会用到。变量名必须以VITE_开头否则 vite 不会把它注入到客户端代码里读出来是undefined这类问题排查起来特别费时间。拦截器里把data直接返回页面层拿到的就是业务数据不用每次写res.data.data。代价是如果哪天要看响应头得改封装。这是常见取舍业务型项目里前者更省事。5. 避坑与排查五类最常踩的坑按现象、原因、解决拆开这一章是前面几章攒下来的问题清单。每一条都按「看到什么现象、为什么会这样、怎么改」三段写遇到对应症状直接对号入座就行。5.1 页面白屏控制台报组件解析失败现象npm run dev起来之后页面一片空白控制台刷出Failed to resolve component: el-button之类的警告。原因有三种可能按需引入插件没在vite.config.js的plugins里生效vite 的依赖预构建缓存是旧的插件新加的解析规则没重新执行模板里组件名写成了ElButton而不是el-button插件按 kebab-case 匹配时对不上。解决先删掉node_modules/.vite目录重启这一步能解决大半缓存问题再确认Components插件和ElementPlusResolver的 import 路径没写错最后统一模板里的标签写法我的习惯是全用 kebab-case跟 Element-Plus 官方示例保持一致少一层心智负担。5.2 按钮有事件没样式或者只有部分组件有样式现象el-button能点、事件也触发但完全没有样式灰扑扑的一个原生按钮或者表格有样式、消息提示没样式。原因按需引入时样式是跟着组件被扫描到才引入的。ElMessage、ElMessageBox、ElNotification、ElLoading这四个是函数式调用模板里没有对应标签插件扫不到样式自然没进来。另外主题覆盖文件的引入顺序排在 Element-Plus 样式之前也会被后者覆盖掉。解决在main.js里手动补这几个的样式引入或者在ElementPlusResolver里把importStyle配成sass走全量样式打包。主题覆盖文件放在所有 Element-Plus 相关引入之后顺序反了就在浏览器里看:root下变量的最终值一眼能确认。5.3 mock 接口一直 404请求跑到了真实后端现象Network 面板里请求地址是http://127.0.0.1:8080/api/user/list状态 404 或者连接被拒mock 规则明明写了。原因vite.config.js里的proxy优先级高于浏览器端的 mock 拦截请求一旦被代理转发出去mockjs 就拦不到了。还有一种情况是项目里用了 fetch 而不是 axiosmockjs 重写的是 XMLHttpRequestfetch 走的另一条路拦不住。解决开发阶段走 mock 时把baseURL设成/api并且确认代理规则没命中这个路径或者干脆在.env.development里把VITE_API_BASE改成空字符串让请求走相对路径由 mock 接管。用了 fetch 的话换成vite-plugin-mock它是 node 层中间件两种请求方式都能拦。5.4 打包产物里能搜到假数据体积也比预期大现象dist目录里的 js 文件用编辑器搜一下能搜到mock相关字符串和造数据用的姓名邮箱包体积比估算的大了一截。原因mock 文件在main.js里被无条件 import或者写在if (process.env.NODE_ENV development)里面但用了普通 import。后者的问题在于构建工具做静态分析时import 语句是一定会被打进依赖图的条件判断发生在运行时打包阶段照样包含。解决用动态 import 配合编译期常量if (import.meta.env.DEV import.meta.env.VITE_USE_MOCK true) { await import(./mock/index.js) }这样生产构建时这段分支会被整体摇掉。改完记得搜一遍产物确认别凭感觉认为已经生效。5.5 改了 mock 文件接口返回的还是老数据现象往database里加了个字段保存文件刷新页面接口返回里没有这个字段。原因mockjs 的注册是执行一次、生效一次。模块热更新触发重新执行时有的场景下会重复注册同一个规则先注册的那条先生效新数据反而轮不上。另外 vite 的热更新对 mock 文件的处理有时候不彻底。解决在 mock 注册前加个标记判断或者干脆改完 mock 文件手动重启 dev server虽然笨但最可靠。用vite-plugin-mock的话它自带热更新改完自动重新加载这是我后来倾向它的主要原因之一。6. 把 mock 做成可开关的再给组件加一道自测假数据层的终点不是「能返回数据」而是「随时能摘掉」。我一般的做法是用环境变量控制引入时机让生产构建里根本不存在 mock 代码这里有个细节条件判断要用编译期常量import.meta.env.DEV用它包住动态 import构建时整段会被摇掉。// src/main.js async function bootstrap() { // 只有开发环境且开关打开时才加载 mock生产构建里这段会被移除 if (import.meta.env.DEV import.meta.env.VITE_USE_MOCK true) { await import(./mock/index.js) } const app createApp(App) app.use(createPinia()) app.use(router) app.mount(#app) } bootstrap()import.meta.env.VITE_USE_MOCK读的是.env.development里的值字符串比较别写成布尔。写完一定要跑一次npm run build然后在产物里搜mock搜不到才算真的摘干净了。第二件事是给关键组件加一个极简的自测。不需要上完整测试体系一个空状态渲染的断言就能挡住大部分低级改动// tests/user-table.spec.js import { mount } from vue/test-utils import { describe, it, expect } from vitest import UserTable from /components/UserTable.vue describe(UserTable, () { it(没有数据时展示空状态文案, () { const wrapper mount(UserTable, { props: { rows: [], total: 0 } }) expect(wrapper.text()).toContain(暂无数据) }) })这个断言的价值在于它强制组件在rows为空时不报错。改表格列、改插槽、改分页逻辑时很容易出现「有数据正常、没数据崩掉」的情况而线上首次进入页面恰好就是空数据。血泪经验是别指望靠肉眼看页面来判断有没有改坏。从那以后我每次拿到这类「环境已经配好」的包都先跑一遍依赖检查、再搜一遍产物里有没有 mock 残留、最后给列表组件补一个空状态断言三件事加起来不到十分钟能省掉后面一堆来回。希望帮到你。本文还有配套的精品资源点击获取