简介面向希望快速上手 Vue3 新语法与组件库集成的开发者这份工程模板将 Vue3、Element-Plus、Mockjs 与 axios 的基础引入和配置搭好省去从零初始化与依赖联调的时间。它适合需要快速验证页面原型、学习 Element-Plus 表格组件用法或在此基础上继续扩展新页面的初中级前端学习者。压缩包为 7z 格式共 27 个文件、约 130KB包含 9 个 Vue 单文件组件以及 JSON、JS 配置脚本、Markdown 说明、CSS 样式和图标图片等体量轻便而结构完整。已有 2140 人学习可作为入门参照。工程内已接入 Mockjs 假数据与 axios 请求链路并完成一个基于 Element-Plus table 的示例页面读者能查看组件组织、数据模拟与页面搭建方式再沿用现有结构添加新页面适合作为 Vue3 项目起手骨架与语法练习样本。1. 拿到一套 Vue3 后台模板先别急着改页面上周接了个后台管理的小活工期三天需求方要的是能登录、能看列表、能改数据。真从零起一个 Vue3 工程光配路由、封请求、造测试数据就得搭进去一天。这类场景最适合拿一套环境已经配好的模板开工Vue3 框架负责响应式和组合式逻辑Element-Plus 库把表格、表单、弹窗、分页这些后台刚需组件直接摆出来mockjs 假数据让前端在后端接口还没影的时候也能把整条流程跑通。这套 vue3-demo 就是按这个思路攒的依赖装完、命令一敲登录页和列表页就能出来。它适合刚接手后台需求的前端、想看清工程化目录怎么分层的开发者也适合需要一套能改的底座做二次开发的人。2. 技术选型与目录结构这套脚手架把哪些活儿提前干完了判断一套模板值不值得用我不看它能跑出多少个页面只看它把哪些重复劳动提前收进了目录里。后台项目真正耗时的从来不是写页面而是路由怎么组织、请求怎么封、菜单和权限怎么跟路由对上、接口没通之前怎么造数据。这套模板的价值就在于这几件事都已经有固定位置你接手之后是往格子里填不是从白纸开始画。2.1 Vite 冷启动链路与组合式 API 的挂载姿势构建工具用的是 Vite这点对后台项目体感差别很大。后台页面多、组件多Webpack 那套每次改一行样式要等几秒的体验在需要频繁调表格列的阶段特别折磨人Vite 开发期走的是原生 ESM改哪个文件只重新编译哪个模块热更新基本是即时的。生产构建它走 Rollup产物体积和分包策略跟常规工程没区别所以不用担心「开发爽了上线翻车」。入口文件是整个应用的装配台Vue 实例创建、路由注入、状态管理注入、UI 组件库注入、mock 数据挂载都在这一个文件里完成顺序错了会出各种莫名其妙的问题。// src/main.js import { createApp } from vue import App from ./App.vue import router from ./router import pinia from ./stores import ElementPlus from element-plus import element-plus/dist/index.css // mock 必须在应用挂载前引入否则首屏请求会先发出去 import ./mock const app createApp(App) app.use(pinia) // 状态管理先装路由守卫里要用 store app.use(router) app.use(ElementPlus) // 全量注册按需引入的写法见 5.3 app.mount(#app)这段代码里有两个容易忽略的细节。一是pinia要装在router前面因为路由守卫里常要读用户信息和权限列表如果 store 还没注册就去useUserStore()会直接抛错。二是import ./mock放在最前面mockjs 的原理是劫持浏览器的 XHR 对象应用一旦挂载、组件里的onMounted开始发请求劫持就必须已经生效晚一步就是接口 404 而不是假数据。页面组件统一用script setup写这是 Vue3 组合式 API 最省心的形态不用return一堆变量不用纠结this指向ref、computed、生命周期钩子直接声明即可。相比选项式 API 的data/methods/mounted三件套组合式更接近「按功能聚合」而不是「按类型聚合」一个列表页的搜索条件、分页参数、加载状态可以写在一块儿后面拆成 composable 也顺手。2.2 目录分层每个文件夹对应一类改动后台模板最怕目录乱改一个字段要在五六个文件夹之间跳。这套的结构是按「改动类型」分的想加接口就去api想加页面就去views想加通用组件就去components心智负担比较低。vue3-demo/ ├── mock/ # mock 数据源一模块一文件 ├── public/ # 不被构建处理的静态资源 ├── src/ │ ├── api/ # 接口函数一模块一文件只负责发请求 │ ├── assets/ # 参与构建的图片与字体 │ ├── components/ # 全局通用组件如分页封装、搜索表单 │ ├── layouts/ # 布局壳侧边栏 顶栏 内容区 │ ├── router/ # 路由表与全局守卫 │ ├── stores/ # Pinia 模块用户、权限、标签页 │ ├── styles/ # 全局样式与变量 │ ├── utils/ # request.js、日期格式化等纯工具 │ ├── views/ # 业务页面按模块建子目录 │ ├── App.vue │ └── main.js ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 ├── index.html ├── package.json └── vite.config.js几个目录的职责边界值得说清楚因为二次开发翻车大多出在边界模糊上。api目录里只写「函数」不写业务判断比如getUserList(params)就是request.get(/user/list, { params })它不知道调用方是列表页还是导出功能。views里的页面负责组装拿数据、处理 loading、弹提示都在这儿。components只放被两个以上页面用到的组件只用一个页面用的组件就放在该页面的子目录里别急着往全局塞。layouts是很多新手会忽略的一层。后台的侧边栏、顶栏、面包屑、标签页在几乎所有页面都一样把它抽成布局组件、页面作为router-view的出口切换页面时布局不重新渲染滚动位置和菜单展开状态都能保住。后面要做「某些页面不要侧边栏」比如登录页、大屏页只要在路由里指定不同的 layout 就行。2.3 package.json 脚本每条命令解决什么问题拿到项目先看scripts比看 README 快。{ scripts: { dev: vite, build: vite build, preview: vite preview, lint: eslint . --ext .vue,.js --fix }, dependencies: { vue: ^3.x, vue-router: ^4.x, pinia: ^2.x, element-plus: ^2.x, axios: ^1.x, mockjs: ^1.x }, devDependencies: { vite: ^5.x, vitejs/plugin-vue: ^5.x, unplugin-auto-import: ^0.x, unplugin-vue-components: ^0.x, sass: ^1.x, eslint: ^8.x } }dev起开发服务器带热更新build出生产包默认输出到distpreview是本地预览产物这一步别跳过很多路径问题只有跑产物才暴露lint带--fix能在提交前自动收拾掉缩进和分号。依赖里dependencies和devDependencies的分界也要留意mockjs放在dependencies里意味着它会被打进产物后面第 5 章会讲怎么在生产构建里把它摘掉。3. 环境搭建与首次启动从安装依赖到登录页出现环境这一块模板口号喊的是「拿来就用」但 Node 版本、包管理器、代理配置这三样只要有一样不对第一屏就是白屏或者一串红字。按下面的顺序走基本能一次过。3.1 Node 版本与包管理器的选择Vite 对 Node 版本有硬性要求版本太低会在启动时直接报crypto.hash is not a function这类看起来跟业务毫无关系的错。判断标准很简单Node 18 及以上用node -v确认一遍再动手。# 确认 Node 版本低于 18 先升级 node -v npm -v # 项目里有 lock 文件就用对应包管理器别混用 npm install # 或者 pnpm install包管理器别混用是血泪经验项目里已经有package-lock.json你偏要用 pnpm 装一遍node_modules 里会同时出现两套依赖树随后出现「组件找不到」「样式重复注入」这种玄学问题。看不出来是哪套在生效时最稳的办法是删掉node_modules和所有 lock 文件只保留一种重装。国内网络装依赖慢的话配一下镜像源就行这一步跟项目本身无关纯粹是下载速度问题。装完之后node_modules有几个 G 是正常的别以为是装错了。3.2 启动开发服务器与首屏验证npm run dev终端会打印本地地址通常是http://localhost:5173。浏览器打开后按这个顺序验证比漫无目的地乱点高效首屏是否渲染出登录页没有白屏说明入口链路通了打开控制台 Network 面板看登录请求是否返回了假数据返回code: 200说明 mock 生效随便输个账号密码点登录能跳进主布局说明路由守卫放行正常点侧边栏几个菜单页面能切、地址栏变化、刷新不 404说明路由模式配对了。第 4 步最容易被忽略。模板如果用 history 模式本地 dev 下 Vite 会自动做 fallback所以刷新不报错一旦部署到静态服务器刷新就 404这是后面排查章要专门讲的点。3.3 vite.config.js别名、代理与环境变量配置文件决定了本地开发和线上部署的差异改动频率仅次于业务代码。// vite.config.js import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd()) return { plugins: [vue()], resolve: { alias: { // 别名让深层目录里的 import 不用写一长串 ../../ : path.resolve(__dirname, src) } }, server: { port: 5173, open: true, proxy: { // 只在接真实后端时生效走 mock 时这段用不到 /api: { target: env.VITE_PROXY_TARGET, changeOrigin: true, rewrite: (p) p.replace(/^\/api/, ) } } }, build: { outDir: dist, sourcemap: false, chunkSizeWarningLimit: 1500 } } })alias里的指向src写import request from /utils/request就比../../utils/request稳得多目录一挪也不用改一堆引用。proxy只在真实后端联调时用得上target从环境变量读避免把测试环境地址硬编码进仓库。rewrite那行是看后端接口有没有/api前缀有就删掉没有就留着跟后端对一次就能确定。环境变量放在.env.development和.env.production两个文件里变量名必须以VITE_开头才会被注入到客户端代码非VITE_前缀的只有 Node 侧能读到。# .env.development VITE_API_BASE_URL/api VITE_USE_MOCKtrue VITE_PROXY_TARGEThttp://127.0.0.1:8080 # .env.production VITE_API_BASE_URLhttps://example-api.invalid/api VITE_USE_MOCKfalseVITE_USE_MOCK这个开关很关键它是第 5 章「生产包别带假数据」那一条的开关。代码里用import.meta.env.VITE_USE_MOCK true判断构建时 Vite 会做静态替换不满足条件的import(./mock)分支会被摇树摇掉mockjs 就不会进产物。4. 核心模块拆解路由、请求封装与 Mock 数据怎么咬合模板真正的骨架就三块路由决定「有哪些页面、谁能看」请求封装决定「数据怎么进出」mock 决定「后端没到位时怎么活」。这三块一旦咬合好页面层就是纯体力活。4.1 路由表设计与菜单的自动生成后台的路由表不只是路径映射它还兼任菜单数据源和权限声明的角色。把meta写丰富了侧边栏可以直接从路由表推导出来不用再维护一份菜单配置。// src/router/index.js import { createRouter, createWebHashHistory } from vue-router export const constantRoutes [ { path: /login, component: () import(/views/login/index.vue), meta: { hidden: true } // hidden 的项不进菜单 }, { path: /, component: () import(/layouts/index.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 工作台, icon: HomeFilled, affix: true } }, { path: system/user, name: SystemUser, component: () import(/views/system/user.vue), meta: { title: 用户管理, icon: User, roles: [admin] } } ] }, { path: /:pathMatch(.*)*, component: () import(/views/error/404.vue), meta: { hidden: true } } ] const router createRouter({ history: createWebHashHistory(), // 换 createWebHistory 需服务端 fallback routes: constantRoutes }) router.beforeEach((to, _from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) return next(/login) if (token to.path /login) return next(/) next() }) export default routermeta.hidden控制是否出现在菜单里meta.title是菜单文案和面包屑meta.icon存 Element-Plus 的图标组件名meta.roles是权限声明。侧边栏组件递归读constantRoutes用hidden过滤、用roles和当前用户权限取交集一份路由表同时喂给了菜单和守卫。createWebHashHistory和createWebHistory的选择直接影响部署。哈希模式地址栏带#刷新永远不会 404缺点是链接不好看、对 SEO 不友好history 模式干净但需要服务端把未匹配路径 fallback 到index.html用 Nginx 的话就是try_files $uri $uri/ /index.html;。后台项目我一般先用 hash 跑通部署环境确认能配 fallback 再换。路由懒加载的() import(...)不是可选项。后台页面几十个全部同步引入会让首屏包体大到离谱懒加载让每个页面单独成 chunk进哪个页面加载哪个。4.2 axios 封装拦截器里该放什么、不该放什么请求层的目标是让页面里只写const data await getUserList({ page: 1 })拿到的直接是业务数据不用每次判断res.data.code 200。// src/utils/request.js import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, // 显式指定 XHR adaptermockjs 才能劫持到 adapter: xhr }) service.interceptors.request.use( (config) { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }, (error) Promise.reject(error) ) service.interceptors.response.use( (response) { const { code, data, message } response.data if (code 200) return data // 只把业务数据抛给调用方 if (code 401) { localStorage.removeItem(token) location.href /#/login return Promise.reject(new Error(登录已失效)) } ElMessage.error(message || 请求失败) return Promise.reject(new Error(message)) }, (error) { const msg error.response?.status 500 ? 服务异常请稍后重试 : 网络异常 ElMessage.error(msg) return Promise.reject(error) } ) export default service请求拦截器只干一件事带 token。别在这里做加签、加密、埋点那是业务层该管的塞进来会让所有人调接口都背上同样的开销。响应拦截器统一剥壳把{ code, data, message }里的data直接返回页面里就不用层层解构。adapter: xhr这一行是踩坑踩出来的。axios 新版本在某些构建配置下会走 fetch adapter而 mockjs 的实现是替换XMLHttpRequest走 fetch 就绕过了劫持表现是假数据一条都不返回、网络面板里显示请求真实发出去了。显式钉死 xhr adapter问题消失。接口函数按模块拆文件一个模块一个文件函数名就是动作名// src/api/user.js import request from /utils/request export function getUserList(params) { return request.get(/user/list, { params }) } export function saveUser(data) { return request.post(/user/save, data) } export function removeUser(id) { return request.delete(/user/${id}) }4.3 mockjs 拦截规则与真实接口的切换假数据的写法决定了后面切真接口的成本。核心思路是mock 的 URL、方法、返回结构跟真实接口严格一致切换时只改一个环境变量页面代码一行不动。// src/mock/index.js import Mock from mockjs // 模拟网络延迟让 loading 状态能看见别设成 0 Mock.setup({ timeout: 200-600 }) const userList Mock.mock({ rows|56: [ { id|1: 1, name: cname, age|18-55: 1, status|1: [active, disabled], email: email, createTime: datetime } ] }) Mock.mock(/\/api\/user\/list/, get, (options) { // 从 query 里取分页参数模拟服务端分页 const url new URL(options.url, location.origin) const page Number(url.searchParams.get(page) || 1) const size Number(url.searchParams.get(size) || 10) const start (page - 1) * size return { code: 200, message: ok, data: { total: userList.rows.length, list: userList.rows.slice(start, start size) } } }) Mock.mock(/\/api\/user\/save/, post, { code: 200, message: ok, data: null }) Mock.mock(/\/api\/user\/\d/, delete, { code: 200, message: ok, data: null })Mock.mock第一个参数用正则而不是字符串是为了兼容带 query 的请求字符串匹配会把?page1也当成路径的一部分导致匹配不上。rows|56生成 56 条数据正好够翻几页看分页效果。cname、email、datetime是 mockjs 内置的占位符比手写假名字省事得多。切换真实接口靠main.js里的一句条件判断// src/main.js if (import.meta.env.VITE_USE_MOCK true) { await import(./mock) }生产环境.env.production里VITE_USE_MOCKfalse构建时这个动态 import 分支会被静态分析判定为不可达mockjs 连同假数据一起被摇掉产物里不会留痕。4.4 Element-Plus 表格页增删改查的最小闭环后台 80% 的页面都是同一个形状顶部搜索条件、中间表格、底部分页、弹窗表单。把这套架子写熟后面就是复制粘贴改字段名。!-- src/views/system/user.vue -- template div classpage el-form :inlinetrue :modelquery el-form-item label姓名 el-input v-modelquery.name clearable keyup.enterhandleSearch / /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleAdd新增/el-button /el-form-item /el-form el-table v-loadingloading :datalist border stripe el-table-column propid labelID width80 / el-table-column propname label姓名 / el-table-column propemail label邮箱 / el-table-column label状态 width100 template #default{ row } el-tag :typerow.status active ? success : info {{ row.status active ? 正常 : 停用 }} /el-tag /template /el-table-column el-table-column label操作 width160 fixedright template #default{ row } el-button link typeprimary clickhandleEdit(row)编辑/el-button el-button link typedanger clickhandleDelete(row)删除/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.page v-model:page-sizequery.size :totaltotal layouttotal, sizes, prev, pager, next changefetchList / el-dialog v-modeldialogVisible :titledialogTitle width480px el-form refformRef :modelform :rulesrules label-width80px el-form-item label姓名 propname el-input v-modelform.name / /el-form-item el-form-item label邮箱 propemail el-input v-modelform.email / /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clickhandleSubmit确定/el-button /template /el-dialog /div /template script setup import { ref, reactive, onMounted } from vue import { ElMessage, ElMessageBox } from element-plus import { getUserList, saveUser, removeUser } from /api/user const loading ref(false) const list ref([]) const total ref(0) const query reactive({ name: , page: 1, size: 10 }) // 拉列表查询时把页码重置避免停留在越界页 async function fetchList() { loading.value true try { const data await getUserList(query) list.value data.list total.value data.total } finally { loading.value false // 无论成功失败都要关 loading } } function handleSearch() { query.page 1 fetchList() } const dialogVisible ref(false) const dialogTitle ref(新增用户) const formRef ref() const form reactive({ id: null, name: , email: }) const rules { name: [{ required: true, message: 请输入姓名, trigger: blur }], email: [{ type: email, message: 邮箱格式不正确, trigger: blur }] } function handleAdd() { Object.assign(form, { id: null, name: , email: }) dialogTitle.value 新增用户 dialogVisible.value true } function handleEdit(row) { Object.assign(form, row) dialogTitle.value 编辑用户 dialogVisible.value true } async function handleSubmit() { await formRef.value.validate() // 校验不过会 reject直接终止 await saveUser({ ...form }) ElMessage.success(保存成功) dialogVisible.value false fetchList() } async function handleDelete(row) { await ElMessageBox.confirm(确认删除「${row.name}」, 提示, { type: warning }) await removeUser(row.id) ElMessage.success(删除成功) fetchList() } onMounted(fetchList) /scriptloading用try/finally关是防止接口报错后表格永远转圈。handleSearch里把page重置为 1否则在第 5 页搜索一个只有 2 条结果的姓名会看到一张空表。编辑时用Object.assign(form, row)而不是form row因为form是reactive对象直接赋值会丢掉响应性弹窗里输入框敲字不更新——这是 Vue3 里最常见的翻车点之一。删除前用ElMessageBox.confirm兜一道await一个 reject 的 Promise 会让后续代码不执行也就自然实现了「点取消什么都不做」。5. 常见问题排查启动白屏、mock 不生效、产物路径 404下面五条是这类模板最高频的翻车现场每条都按「看到什么 → 为什么 → 怎么救」写照着对号入座就行。5.1 启动后白屏控制台没有任何业务报错现象npm run dev起来了地址能打开但页面全白控制台只有一行模糊的模块解析错误或者干脆干净得像没事发生。原因多半是 Node 版本低于 Vite 要求或者node_modules装了一半被中断。还有一种情况是main.js里某个import指向的路径不存在Vite 在预构建阶段没报运行时才炸。解决先node -v确认 18 及以上再删掉node_modules和 lock 文件重装仍然白屏就打开 Network 面板看main.js是否 200再看index.html里script的src有没有写错。这三步能覆盖九成白屏。5.2 接口一直 404假数据一条都没返回现象页面正常表格空着Network 面板里请求是红色的 404或者请求真实发到了后端地址。原因三种可能。一是import ./mock的位置在app.mount()之后劫持晚于首次请求二是 axios 走了 fetch adaptermockjs 只劫持 XHR三是Mock.mock的第一个参数用了字符串带 query 的请求匹配不上。解决把 mock 引入提到main.js最顶部在axios.create里加adapter: xhr匹配规则统一改成正则Mock.mock(/\/api\/user\/list/, get, ...)。改完硬刷新一次浏览器有缓存。5.3 Element-Plus 组件渲染了但完全没有样式现象按钮、表格都出来了但长得像十年前的 HTML间距全无图标是方块。原因配了unplugin-vue-components做按需引入但没给ElementPlusResolver指定importStyle组件 JS 被引入而样式文件没被引入。全量引入的场景则是import element-plus/dist/index.css被误删。解决按需引入的配置里补上样式解析全量引入就检查那行 CSS 引入是否存在。改了vite.config.js必须重启 dev server热更新不会重新加载插件配置。// vite.config.js 里按需引入的插件配置 Components({ resolvers: [ElementPlusResolver({ importStyle: sass })] }), AutoImport({ resolvers: [ElementPlusResolver({ importStyle: sass })] })5.4 本地刷新正常部署后一刷新就 404现象本地 dev 下点菜单、刷新都正常扔到静态服务器上进首页没事一按 F5 就 404。原因路由用了createWebHistory地址是/system/user服务器上根本不存在这个文件只有index.html。开发服务器自带 fallback所以本地看不出来。解决要么换回createWebHashHistory要么在服务器上配 fallbackNginx 加try_files $uri $uri/ /index.html;。同时确认vite.config.js的base和实际部署子路径一致部署在/admin/下而base是/静态资源会全部 404。5.5 生产包里翻出了假数据和 mockjs现象打开线上包的dist/assets目录能看到几MB的 chunk里面搜得到cname之类的占位符。原因import ./mock写成了静态引入构建工具无法判断它是否需要只能老实打进去。解决改成条件动态引入if (import.meta.env.VITE_USE_MOCK true) await import(./mock)并把.env.production里的开关设为false。改完跑一次npm run build npm run preview在产物里搜关键词确认干净了再发布。6. 二次开发把模板改成自己业务的三步走和一点验证习惯模板改成自己的项目我一般按固定顺序走不走回头路。第一步改「壳」把layouts里的标题、logo、侧边栏宽度、主题色换掉主题色优先改 Element-Plus 的 CSS 变量而不是到处写style改一处全局生效。第二步改「表」把不需要的示例页面和路由删干净包括views下的目录、router里的条目、api里的文件留着不删后面接手的人会以为还有用。第三步加「肉」新模块按「路由 → api → 页面」三件套加顺序不能反先有路由才能跳先有 api 函数页面里才不用写裸axios。改造目标动手位置注意点换主题色src/styles/下的变量文件覆盖--el-color-primary系列别改组件样式新增业务模块router→api→views路由meta.roles不写就是所有人可见接真实后端.env.development的VITE_USE_MOCKfalse同时打开vite.config.js的proxy加权限控制router/index.js的全局守卫守卫里取 store 前确认 pinia 已注册打包体积超标vite.config.js的build.rollupOptions先看分析图别盲目手动分包新增一个模块的最小改动是这样三处文件、一条路由// 1) src/api/order.js import request from /utils/request export const getOrderList (params) request.get(/order/list, { params }) // 2) src/router/index.js 的 children 里追加 { path: business/order, name: BusinessOrder, component: () import(/views/business/order.vue), meta: { title: 订单管理, icon: Tickets, roles: [admin, operator] } } // 3) src/views/business/order.vue —— 直接复制 user.vue 改字段名和接口函数改完必须做一次「关 mock 验证」把VITE_USE_MOCK切成false重启看页面是不是只在请求真实后端时报错而不是连请求都没发出去。这一步能提前暴露「页面里绕过了api层直接写地址」的问题。再跑一次npm run build npm run preview确认产物能打开、路由能跳、静态资源路径对。早些年我图快改完页面直接npm run dev看一眼就算完结果上线当天发现所有按需引入的组件样式全丢原因是新加的组件用了ElDatePicker而解析器配置里漏了样式路径本地因为之前全量引入过一遍 CSS 所以看不出来。从那以后我每次改完vite.config.js或动过依赖都会强制走一遍「删缓存、重装、build、preview」再打开产物目录搜一遍 mock 关键词和console.log。这套动作花不了五分钟能省掉半夜被叫起来回滚的麻烦。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?