资讯动态

从入门到独立搭建页面:Vue3完整学习链路与实战总结

发布时间:2026/10/1 13:41:38 来源:尧图企业网站定制
最早开始学 Vue 的时候市面上讲 Vue 入门的教程已经非常多了。但有一个现象很普遍视频看了一堆跟着敲了 demo觉得自己会了真到了要独立搭建一个页面时还是不知道从哪下手。后来我慢慢意识到“会写 Vue 代码”和“能独立搭建页面”之间差的不是某个冷门 API而是一条完整的链路——环境能不能搭起来、组件怎么拆、数据怎么流动、路由怎么组织、接口怎么接、项目怎么跑起来。这篇文章就是我整理出来的学习心得与技能总结按“从入门到独立搭建页面”的顺序来讲覆盖环境配置、组件化、路由、数据交互、状态管理、实战场景和部署兼容。不管是准备入门的初学者还是已经会用模板但没独立做过完整项目的开发者都可以对照自查看看自己在哪一环还缺经验然后有针对性地补。1. 环境搭不起来后面全是空中楼阁安装与配置实操很多新手学到一半就放弃了不是因为概念难而是卡在环境上。命令装不上、插件报错、DevTools 不显示每一步都在消磨耐心。所以我想先花一整章把环境这件事讲透。1.1 三种常见搭建方式怎么选第一个要解决的问题是Vue 项目从哪里来最简单的是 CDN 引入。一个 HTML 文件里直接放script srchttps://unpkg.com/vue3/dist/vue.global.js/script然后在页面里写模板和脚本。这种方式适合快速验证一个想法、做一个小工具页或者你只是想体验一下 Vue 的响应式是什么感觉。代码大概长这样script srchttps://unpkg.com/vue3/dist/vue.global.js/script div idapp{{ message }}/div script const { createApp } Vue; createApp({ data() { return { message: Hello Vue }; } }).mount(#app); /scriptCDN 方式的问题是项目一旦复杂JS、CSS 全部堆在一个 HTML 里组件化优势完全发挥不出来多人协作更是噩梦。最常见的还是脚手架方式。我推荐直接走 Vitenpm create vitelatest my-app然后按提示选择 Vue进入目录装依赖启动cd my-app npm install npm run dev浏览器打开 http://localhost:5173 一个 Vue 项目就跑起来了。这里有两个坑我得提一下。第一Node 版本不能太老Vite 5 以上一般要求 Node 18否则启动时会直接报错。我自己一开始没装 nvm系统里只有一个很老的 Node折腾了一晚上才发现是版本问题。现在我的习惯是装一个 nvm 来管理多个 Node 版本哪个项目要求什么版本就切什么版本。第二npm install如果卡住或报错先删掉node_modules和package-lock.json再重装一次比到处找报错原因快得多。还有一个老牌方式叫 vue-clinpm install -g vue/cli后vue create project。它的生态非常成熟但新项目真的不建议再用了构建速度比 Vite 慢维护也趋于停滞。以后遇到老项目会碰到理解它是怎么回事就行。三种方式我用一张表对比过很直观方式上手成本适合场景CDN 引入最低小工具、单页演示、入门体验Vite 脚手架中日常开发、独立搭建正式项目vue-cli中维护既有老项目、兼容旧构建生态1.2 VSCode 和 DevTools写 Vue 的标配工具环境不只是装个脚手架编辑器配置直接决定你的开发体验。我现在用的 VSCode 插件组合是Volar、ESLint、Prettier。Volar 是 Vue 官方推荐的语法高亮和类型提示工具注意它和旧插件 Vetur 是冲突的同时装的话会出现“标签没法跳转”“代码提示失灵”之类的问题。很多人反馈“在 VSCode 里点 Vue 文件中的标签没有跳转”十有八九就是这个问题要么 Vetur 没禁用要么 Volar 没装。另一个很常见的跳转问题跟路径别名有关。项目里我们用/components/xxx这种写法编辑器默认并不知道指向哪里。需要在项目根目录建一个jsconfig.json{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } }, exclude: [node_modules, dist] }配好之后按住 Ctrl 点击组件标签才能真正跳转到对应文件。浏览器端的 Vue DevTools 插件也建议尽早装。它能直接查看组件树、当前路由、Pinia 或 Vuex 里的状态还能逐层查看 props 和 emit。调试复杂交互时光靠 console.log 打半天不如打开 DevTools 看一眼组件数据。装好之后如果发现侧边栏不显示 Vue 图标先检查是不是在一个已运行的 Vue 页面上再检查插件是不是被浏览器拦截了。1.3 第一个页面“跑起来”的认知检查清单我之前带过几个新人发现他们跟着教程把页面跑起来后其实并不知道自己做了什么。所以我习惯让他们对照这个清单自查模板里写的{{ message }}是从哪个对象里取值的createApp挂载到了哪个 DOM 节点页面里有没有对应的div idapp修改data里的值页面为什么自动变了如果这三个问题都能答清楚说明你对 Vue 最核心的“响应式”有了感性的认识。如果只是复制粘贴跑通那可以再慢一点把return里的数据改一改看页面变化多试几次。第一个页面跑通的意义不是“我成功了”而是理解了模板、脚本、数据三者之间的关系。这一步站稳了后面组件化才不至于悬空。2. 组件化思维从“写死数据”到“搭积木”的核心转变能跑通 Hello World 之后很多人会陷入一个误区所有页面内容都写在一个巨型组件里数据、弹窗、列表、表单全堆在一起。页面刚写起来很爽但超过几百行就开始失控。这段时间我最大的教训是组件化不是 Vue 的语法要求而是人的需求——拆得好不好决定项目能不能维护下去。2.1 先理解组件是什么组件就是把一段可以复用的界面和它对应的逻辑封装在一起。你可以把它理解成乐高积木基础积木就那几款但拼法千变万化。在 Vue 里一个组件就是一份.vue文件里面有三块template负责结构script setup负责数据和逻辑style负责外观。组件有局部注册和全局注册。局部注册就是在一个组件里import进来再使用推荐日常都这么写依赖关系清晰。全局注册用app.component(Xxx, Xxx)适合那些真的会到处用到的基础组件比如自定义的按钮、弹窗。少全局注册多局部引入这是我坚持的原则。全局组件一多代码可读性会变差而且明明只在一个页面用到的组件也被打进了全局首屏反而更重。2.2 props、emit 和插槽组件之间的三种对话方式组件拆出来了组件之间怎么通信这是新手最容易卡住的点。最简单的是父组件给子组件传数据用props。子组件声明自己需要什么template div classitem{{ title }}/div /template script setup defineProps({ title: String }); /script父组件里这样用template Item title示例标题 / /template子组件要通知父组件做事情用emit。比如子组件里的删除按钮被点击了script setup const emit defineEmits([delete]); const handleClick () emit(delete, id); /script父组件监听template Item deletehandleDelete / /template除了数据传递还有一种需求是“把内容传进去”。比如写一个卡片组件边框、标题、按钮都由父组件决定子组件只负责排版位置。这时候就该用插槽slot了。插槽分三种。默认插槽最直接子组件写slot /父组件写在标签里的内容就会被塞进去。具名插槽适合一个组件有多个坑位的情形比如卡片头部和底部template div classcard headerslot nameheader //header mainslot //main /div /template父组件对应Card template #header标题/template 正文内容 /Card还有作用域插槽子组件把自己的数据暴露给父组件来渲染。比如表格组件父组件想要自定义某一列的展示格式子组件可以通过slot :rowrow /把当前行数据传出去父组件在插槽里用{ row }解构拿到。我个人的经验是props emit 适合“双方有明确协议”的场景比如选日期组件父组件给初始日期子组件选完回传结果插槽适合“子组件负责框架父组件负责填肉”的场景比如弹窗、卡片、表格列。把握住这个区分拆组件的时候就不会纠结。2.3 样式隔离为什么我的样式总是“打架”组件化之后样式问题很快会浮出来。默认情况下Vue 组件的style是全局的如果你在 A 组件里写了.title { color: red }B 组件里也可能被影响。解决办法是给 style 加上scopedstyle scoped .title { color: red; } /style加了scoped后Vue 会给当前组件的元素加一个>:deep(.el-button) { background: red; }:deep()意思是把这一段选择器穿透到子组件内部去。我见过不少同事在 scoped 里试图覆盖组件库样式失败最后要么放弃要么在全局样式里硬写一个类名。说实话最干净的方案就是:deep()优先用它。2.4 把常见交互变成组件折叠展开这类需求组件化不只是拆页面区块交互也应该组件化。比如“内容折叠展开”几乎每个管理系统里都有。我早先是每写一个页面就复制一遍 v-show 加过渡动画的模板后来发现太蠢了直接抽成一个基础组件效果就是这段代码的核心template transition namefade div v-showisOpen classcollapse-body slot / /div /transition /template script setup defineProps({ isOpen: Boolean }); /script这样一来所有需要展开内容的页面只需要控制isOpen这一个状态。注意如果用 v-if 控制组件内部状态会被销毁重建v-show 只是隐藏适合频繁切换的场景。折叠动画要平滑的话一般配合transition组件或者给容器加 max-height 过渡。这个细节看起来小实际做起来很影响体验。组件化的心态转变我总结成一句话写代码前先问自己这段东西会不会在别的页面复用如果会现在就拆如果只是这个页面独有的逻辑也别硬拆成组件否则反而增加阅读负担。3. 路由配置与权限管理单页应用的“导航系统”脱离页面跳转谈 Vue 是不完整的。和传统多页面网站不同Vue 应用是一个单页应用SPA所有内容都在同一个 HTML 里靠路由切换显示不同组件。路由不只是“从一个页面跳到另一个页面”它还承担着传参、权限控制、页面缓存这些任务。3.1 路由基础配置一个页面跳转装路由一般用 Vue Router 4对应 Vue 3npm install vue-router4然后在src/router/index.js里创建路由实例import { createRouter, createWebHistory } from vue-router; import Home from ../views/Home.vue; const routes [ { path: /, name: Home, component: Home }, { path: /about, name: About, component: () import(../views/About.vue) } ]; const router createRouter({ history: createWebHistory(), routes }); export default router;在入口文件里app.use(router)然后在根组件App.vue里放一个router-view /路由组件就会渲染在这里。页面跳转不需要再写a href直接router.push(/about); // 或者带名字 router.push({ name: About });这里要重点区分 hash 历史和 HTML5 历史模式。createWebHashHistory会在地址栏带一个#号兼容性好任何静态服务器都不会有问题createWebHistory地址干净但没有服务端配合刷新某个深层路由会直接 404。我最初独立搭项目时用 history 模式部署到服务器一刷新就白屏折腾了好久才发现需要后端在 404 时回退到 index.html。如果是纯静态托管没条件配置回退老老实实用 hash 模式更省心。3.2 动态路由与路由参数让 URL 承载信息实际页面都需要带参数。比如用户详情页/user/123这个123就是动态部分。Vue Router 支持路径参数{ path: /user/:id, name: UserDetail, component: UserDetail }组件里通过route.params.id拿到参数。此外还有两种常见传参方式query 参数类似?id123适合筛选条件、回跳地址等非结构化信息命名路由 params通过router.push({ name: UserDetail, params: { id: 123 } })跳转三者各自有场景。路径参数适合资源对象的 IDquery 参数适合更多附带条件命名路由 params 的好处是不依赖路径里的字符串重构路由表时不容易散落硬编码。动态路由还有一个更深层的用途权限相关。比如不同角色能看到不同页面管理员登录后系统动态地把管理后台的路由加进来router.addRoute({ path: /admin, name: Admin, component: () import(../views/Admin.vue) });这个操作方案很常见但要特别注意一点动态添加的路由不会自动持久化页面刷新后路由表会重置。所以用户在登录后要重新拉取权限再次 addRoute。我遇到过刷新后路由先跳 404 再跳回首页的奇怪现象最后定位到就是路由还没加回来匹配不到地址。解决思路是在路由守卫里判断当前路由是否存在如果存在就正常放行不存在就动态加路由后重定向一次。顺序很重要先补路由再放行不然还是 404。3.3 meta 与按钮权限前端权限控制的两板斧路由的meta字段是个好东西什么信息都能往里放。最常见的用法是定义页面的访问权限{ path: /user, name: User, component: () import(../views/User.vue), meta: { title: 用户管理, requiresAuth: true, permission: user:list } }然后在全局前置守卫router.beforeEach里做拦截router.beforeEach((to, from, next) { if (to.meta.requiresAuth !isLogin()) { next(/login); } else { next(); } });登录校验一般是第一步。再细一点菜单也要按权限渲染这时用meta.permission配合权限判断函数有权限才放入菜单。这样“路由权限”和“菜单权限”是同一份配置不会出现菜单看得见、页面进不去这种尴尬。按钮权限是另一个高频需求。很多人不知道怎么做就在每个按钮外面套 v-if判断一串冗长的条件。稍微好一点的做法是抽成自定义指令import { hasPermission } from /utils/permission; app.directive(permission, { mounted(el, binding) { if (!hasPermission(binding.value)) { el.parentNode?.removeChild(el); } } });模板里就可以这样写template button v-permissionuser:add新增用户/button /template权限码哪里来一般后端登录接口会返回当前用户的权限标识数组前端存到状态管理里。指令的核心就是判断当前用户是否拥有这个标识没有就移除元素。要提醒的是指令移除只是视觉上的控制真正的安全必须依赖后端接口校验前端只管体验。3.4 页面缓存与打开新标签页管理后台里列表页做了筛选后跳去详情再返回列表希望筛选状态还在。这个需求靠keep-alive缓存组件实现template router-view v-slot{ Component } keep-alive :includecachedViews component :isComponent / /keep-alive /router-view /templatecachedViews是需要缓存的页面 name 列表可以从路由 meta 里动态维护。遇到某些页面不希望被缓存比如一些详情页每次进入都要重新请求可以通过meta.noCache标记在 include 列表里排除它。网上搜“vue router meta nocache”相关的问题基本都是在问这个机制。至于“打开新 tab”Vue 本身是单页应用路由跳转通常不会开新的浏览器标签页。真需要的话直接由业务代码发起window.open(/detail/123);如果项目用的是 history 模式要注意拼接完整 URL别丢掉 base 路径。更常见的“打开新 tab”其实是项目内部用 tabbar 组件模拟的多标签类似浏览器标签的效果这时候要自己维护一个 tab 列表和当前激活状态再配合 keep-alive 缓存对应组件。4. 数据请求与前后端联调从 Mock 到真实接口页面骨架搭好之后下一步就是接数据。这里我会一次性讲清楚请求封装、Mock 的使用、跨域联调和特殊的数据传输方式。这几个问题几乎贯穿所有 Vue 项目是“能独立搭建页面”和“只会写静态页面”的分水岭。4.1 请求封装与拦截器为什么 axios 要包一层我见过不少项目直接在组件里axios.get(...)、axios.post(...)页面多了之后代码到处都是重复的 token 注入和错误处理。成熟的习惯是先封装一个统一的请求实例import axios from axios; const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { // 统一处理业务错误 return Promise.reject(new Error(res.message || 请求失败)); } return res; }, error { if (error.response?.status 401) { // 跳转登录页 router.push(/login); } return Promise.reject(error); } ); export default service;使用的时候只负责业务部分const res await get(/user/list, { page: 1 });为什么一定要封装因为我踩过太多重复处理的坑。接口没权限时如果没有统一拦截每个页面都要自己判断 401 然后跳登录写三五个页面还能忍写三五十个页面就开始失控。拦截器本质上是把横切逻辑收拢到一处后续团队里任何新人写接口只要按封装好的方式调就不会遗漏 token 和错误处理。4.2 Mock 数据的体现增删改查是怎么“假”出来的前端开发经常遇到后端接口还没写完、或者本地开发环境不想依赖真实服务的情况。这时候 Mock 就非常有用。很多人对 Mock 的理解模糊看到网上说“mock 版本增加修改删除”不知道具体体现为什么。其实核心就是拦截前端的请求假装是后端返回数据。最常见的方式是用 mockjs 拦截 XMLHttpRequestimport Mock from mockjs; Mock.mock(/api/user/list, get, { code: 200, data: { list|10: [ { id: id, name: cname } ] } });这样前端axios.get(/api/user/list)发出去mockjs 直接在浏览器端把请求拦截下来返回构造好的假数据。增删改查怎么做在 mock 配置里写对应的 handler模拟真实的逻辑let users Mock.mock({ data|5: [{ id: id, name: cname }] }).data; Mock.mock(/api/user/delete, delete, ({ body }) { const { id } JSON.parse(body); users users.filter(item item.id ! id); return { code: 200, data: users }; });新增就 push 一条修改就 map 替换删除就 filter 过滤。有一点要知道Mock 数据是内存态的刷新页面后会重置因为它是前端模拟出来的不是真实持久化。我的建议是Mock 开关最好走环境变量控制if (import.meta.env.VITE_USE_MOCK true) { import(./mock); }开发环境开启联调环境关闭。不要写死在业务代码里否则后端接口好了你还得去翻代码一个个删 Mock效率极低。4.3 前后端分离联调跨域、代理、接口约定以 Java 的 SpringBoot 后端和 Vue 前端为例前后端分离意味着两个服务分别跑在不同端口。如果前端直接请求后端接口浏览器跨域策略会拦下一堆请求。解决跨域有两个思路后端开启 CORS或者前端开发环境配代理。配代理是更推荐的前端做法。在vite.config.js里这样写export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };这样前端请求/api/user/listVite 的开发服务器会转发到http://localhost:8080/api/user/list。浏览器以为请求是同源的就不会有跨域问题。如果后端接口路径没有/api前缀还可以用 rewrite 把路径重写掉。生产环境一般交给 Nginx 做同样的代理这个配置思路和 Vite 的 proxy 非常像理解了开发环境生产环境也顺理成章。联调阶段除了跨域沟通成本也要提前控制。我习惯先和后端敲定几个约定成功响应和失败响应的结构、错误码段含义、分页字段名pageNum/pageSize还是page/limit、时间字段的格式。这些约定如果开始不一致后期光字段映射就要写一瘸子代码。我记得有个项目后端返回code: 200另一个接口返回code: 0统一请求层每次都要接一遍特别痛苦。早一点建立规范后边能省一半加班时间。4.4 SSE 等特殊数据交互服务端推送怎么接常规接口都是“前端请求后端响应”但有些场景是后端有新的进展主动推送给前端比如任务进度、日志输出、在线人数统计、AI 生成过程等。这时候除了 WebSocket还有一个轻量方案叫 SSEServer-Sent Events。SSE 用起来非常简单浏览器原生EventSource就能订阅一个消息流不需要额外依赖。我在 Vue 里一般包成一个 hookfunction useSSE(url, onMessage) { const source new EventSource(url); source.onmessage e { onMessage(JSON.parse(e.data)); }; source.onerror () { source.close(); setTimeout(() { source new EventSource(url); }, 3000); }; return () source.close(); }用的时候在组件里const close useSSE(/api/task/progress, data { progress.value data.percent; }); onUnmounted(close);关键点有两个。一个是组件销毁时必须关闭连接不然页面切走之后连接还在控制台会一直报错后端也会认为客户端还在线。另一个是断线重连要写服务端重启或者网络抖动会导致连接断开不做重连用户体验会很差。另外要记得EventSource不支持自定义请求头如果后端要求 token 走 header就要降级成 query 参数传递或者改用 fetch 的流式读取来模拟 SSE。这个坑我在真实项目里踩过第一次接的时候后端非要用 Authorization 头折腾了半天才换成 query。5. 状态管理与代码组织组合式 API 和 Pinia 的非玄学理解组件之间通信用 props 和 emit 可以搞定但页面一复杂就会遇到“数据放在哪个组件都不对”的情况。登录用户信息、权限列表、全局配置这些东西几乎每个页面都要用总不能每个页面发一次请求。这时候就需要一个全局的数据仓库也就是状态管理。5.1 什么时候需要状态管理先想清楚不是所有共享数据都需要 Pinia。我见过有人为了共享一个弹窗的开关状态引入了一个完整的状态库完全没必要。用 props 向上传递、用 v-model 在父子组件间同步就能解决的场景别上重型武器。真正需要状态管理的信号是数据在多个层级很深的组件里都要用或者多个不相关的组件要保持同步。比如用户登录后顶栏显示用户昵称、侧边栏根据权限渲染菜单、个人信息页展示完整资料如果这些页面都自己拉一次接口数据会不一致而且难维护。这时候全局存一份用户 store登录后写入所有组件从同一处读取就简洁得多。如果你只是希望组件里能直接“拿到最近的祖先提供的数据”还有一个轻量选择是provide/inject。它适合组件库这种深层次通信但它是响应式插入式的不是完整的状态中枢没有 DevTools 调试、没有持久化插件跨页面共享级别还是算了。5.2 组合式 API 和选项式 API怎么选、能不能混Vue 3 之前的写法是选项式组件里的逻辑按照data、computed、methods、watch这些“选项”分门别类放。Vue 3 引入了组合式 API核心变化是逻辑可以在script setup里按业务顺序自由组织。选项式的典型形态export default { data() { return { count: 0 }; }, methods: { inc() { this.count; } } };组合式的形态import { ref } from vue; const count ref(0); const inc () count.value;写出组合式最大的好处是可以把相关逻辑抽出来变成函数多个组件复用一套业务逻辑。比如“获取用户信息并维护加载状态”这个逻辑选项式只能写进mounted或者created不同组件之间很难复用组合式可以直接写一个useUserInfo()函数谁需要谁就调用。团队老代码和新代码同时存在时选项式和组合式是可以混合在一个项目里的Vue 3 保持了对旧写法的兼容。但我不建议在同一个组件里混合写一会儿用setup一会儿用options维护的人会精神分裂。新组件统一用组合式老组件在重构时再迁移这是最稳妥的路线。5.3 用 Pinia 管理一个真实的用户状态Pinia 现在是 Vue 生态里最主流的状态管理库相比 Vuex它去掉了很多繁琐概念TypeScript 支持也好写起来更接近日常直觉。定义一个用户 storeimport { defineStore } from pinia; export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , name: , permissions: [] }), actions: { setLoginInfo({ token, name, permissions }) { this.token token; this.name name; this.permissions permissions; localStorage.setItem(token, token); }, logout() { this.token ; this.name ; this.permissions []; localStorage.removeItem(token); } } });组件里使用import { useUserStore } from /stores/user; const userStore useUserStore(); const hasPermission code userStore.permissions.includes(code);Pinia 的 state 是响应式的一处修改所有引用它的组件视图都会更新。它对调试也很友好DevTools 里可以直接看到状态变化的时间线。如果项目里还没上状态管理新项目直接选 Pinia不用犹豫。等需要持久化时再配一个pinia-plugin-persistedstate几行配置把 token 和用户信息存进 localStorage刷新不丢比我手写一堆localStorage.setItem要省事得多。6. 实战需求处理播放器、地图、文件上传这些绕不开的场景独立搭建页面总有一天会遇到特殊需求。这些东西语法上都简单但坑全在细节里我把几个典型的场景拿出来讲一下。6.1 m3u8 流媒体播放免安装是怎么做到的m3u8 是 HLS 流媒体协议使用的索引文件直播、监控回放、视频网站常常用。你打开一个.m3u8地址里面不是真正的视频数据而是一串 ts 分片文件的地址列表。浏览器原生并不支持直接播放 m3u8Safari 部分例外所以要借助 hls.js 来做解析和播放。“免安装”的意思是用户不用再装 Flash 之类的插件。我们在 Vue 项目里的做法是封装一个视频播放组件import Hls from hls.js; const videoEl ref(null); const play url { if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(url); hls.attachMedia(videoEl.value); } else if (videoEl.value.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 videoEl.value.src url; } };模板里放一个video refvideoEl muted playsinline controls /就行。实际项目里有几个问题要提前处理。第一个是跨域hls.js 需要访问 m3u8 和 ts 文件如果服务端没放开 CORS播放会失败这个要找后端配合和普通接口跨域一样处理。第二个是自动播放策略浏览器普遍禁止带声音的自动播放所以要么组件放出来时用户手动点一下播放按钮要么先muted挂上再让用户自主取消静音。第三个是切换清晰度多码率的 m3u8 要用hls.loadSource重新加载并指定 level否则切换不生效。组件封装好了以后接手直播需求会轻松很多。6.2 地图与外部 SDK腾讯地图集成地图是另一个典型场景。以腾讯地图为例官方提供了 JS SDK通常通过 CDN 的 script 标签引入。Vue 项目里集成的基本思路是在index.html里引入 SDK然后在组件挂载后初始化地图template div refmapContainer classmap-container / /template script setup import { ref, onMounted, onUnmounted } from vue; const mapContainer ref(null); let map null; onMounted(() { map new window.TMap.Map(mapContainer.value, { center: new window.TMap.LatLng(39.9, 116.4), zoom: 12 }); }); onUnmounted(() { map null; }); /script注意onMounted之后再初始化因为这时候 DOM 节点才真正渲染出来。尺寸问题也很常见容器要有明确高度否则地图区域显示一片空白。容器宽度和高度变化时要记得调用地图实例的resize方法不然地图会错位。这个方法可以推广到所有外部 SDK比如“在 Vue 里用 CDN 引入企业微信 JS-SDK”这类场景。SDK 挂载在window上不是标准的 ES module所以要么像上面一样直接操作window.xxx要么封装一个小模块把初始化逻辑包起来。重点是想清楚时机DOM 渲染完成后再调用依赖 DOM 的 SDK组件销毁时把事件监听器和实例清理掉避免内存泄漏和重复初始化。6.3 多文件上传进度、限制、并发细节文件上传在后台管理系统里出现频率极高尤其是“多文件上传”。最基础的做法是input支持 multiple拿到文件列表后逐个或异步并发上传template input typefile multiple changehandleChange / /template script setup const handleChange e { const files Array.from(e.target.files); files.forEach(file { const formData new FormData(); formData.append(file, file); axios.post(/api/upload, formData, { onUploadProgress: e { const percent Math.round((e.loaded / e.total) * 100); // 更新进度条 } }); }); }; /script有几个细节新手容易漏。第一上传前要校验类型和大小常见做法是维护一个允许扩展名数组超出就拦截第二多文件并发如果数量很大后端和网络都受不了最好做一个并发限制比如同时最多 3 个其余的排队第三取消上传和失败重试也要考虑至少失败时要给用户明确提示别只是控制台一条红字。进度条用onUploadProgress最好它能拿到真实的传输字节数比自己模拟进度靠谱得多。多说一句上传组件的拆分价值很高。文件选择、校验、进度展示、删除已传文件、上传结果列表这些逻辑一旦封装成通用组件几乎每个业务模块都能直接用。我的习惯是交互稍微复杂一点的文件上传宁可多花半天做成组件也不要在业务页面里堆原生逻辑。6.4 图片、PDF 这些特殊内容的展示有人问过“Vue 里 image 能显示 PDF 吗”。答案是明确的不能。img标签只支持图片格式把 PDF 地址塞进去只会得到一片空白或左下角的错误图标。PDF 展示常见的方案有几种我列个表方便看方案优点缺点iframe :srcpdfUrl实现最简单浏览器自带渲染样式和工具栏不可控部分浏览器会下载而不是预览pdf.js 自定义渲染功能强可控性高开发成本高需要管理加载进度和 canvas 绘制第三方 Vue PDF 组件上手快示例多部分组件维护不够活跃版本兼容需注意临时展示、预览要求不高的场景用 iframe 就够。如果产品对预览的交互要求比较高比如要显示页码、放大缩小、跳转指定页那建议直接学 pdf.js虽然前期投入大但后面可定制程度完全不同。选型不用过度纠结按需求紧急度和可控需求来定。7. 项目适配与发布浏览器兼容、移动端、桌面端与小程序页面写完了离“独立搭建页面”还差最后一步让项目在真实环境里跑起来。这一步涉及的坑比写业务代码还要多我单独列一章。7.1 浏览器兼容360 浏览器和其他“换壳”产品有个高频问题Vue 项目怎么适配 360 浏览器。首先要理解 360 浏览器其实是双内核的极速模式基于 Chromium用 Vue 3 完全没问题兼容模式基于旧版 IE 内核Vue 3 根本不支持。所以正确的做法不是想办法兼容 IE而是引导用户使用极速模式。在index.html的 head 里加一行meta namerenderer contentwebkit /这行配置是给 360 浏览器看的告诉它当前页面优先用极速模式渲染。另外也可以在加载失败时检测一下环境给出一个“请升级浏览器或切换极速模式”的提示页。如果项目客户里确实有大量旧内核浏览器那大概率需要考虑降级使用 Vue 2并配合合适的 babel 配置和 polyfill。但我个人很不推荐为了旧内核浏览器牺牲整个项目的现代性成本太高效果还不好。时代已经变了移动端都普遍支持现代浏览器桌面端没必要倒车。7.2 移动端适配与“Vue 做手机软件”很多人搜“vscode vue 怎么制作手机软件”——这里要先厘清概念。Vue 做出来的是 Web 页面不是原生 App。所谓“用 Vue 做手机软件”其实有两种理解一是做一个移动端 H5 网站二是把 H5 包一层壳变成可安装的 App。移动端 H5 首先要解决适配问题。我通常用rem结合视口宽度来做单位换算配合移动端 UI 库比如 Vant能很快搭出能看的手机界面。调试时可以在vite.config.js里配置host: true启动后局域网内的手机就能访问电脑上的开发地址这个真机调试能力比模拟器靠谱得多。如果最后确实要上架应用商店常见路线有几种用 uni-app 跨端写小程序和 App用 Capacitor 把现有 H5 项目套壳成 Android/iOS 应用或者用原生 WebView 加载 H5。线上项目用 uni-app 的情况更常见因为它能一套代码同时出 H5、小程序和 App但代价是组件体系不能完全用 Vue 生态的东西要跟着 uni-app 的规范来。7.3 Electron 打包 Vue主进程、渲染进程和 IPCElectron 是桌面端的方向。它把浏览器装进桌面应用里主进程负责和操作系统打交道渲染进程就是你的页面。Vue 应用运行在渲染进程里承担界面交互Electron 主进程用 Node.js 的能力去操作用户文件、读取系统信息、创建窗口。常看到的问题“Electron 主渲染进程 IPC 通信和 Vue 有关系吗”答案是IPC 是 Electron 自身的机制和 Vue 本身没有直接关系但 Vue 可以很方便地封装它。主进程监听const { ipcMain } require(electron); ipcMain.handle(get-version, () app.getVersion());渲染进程里Vue 组件这样调用const version await window.electronAPI.getVersion();在渲染进程里不能直接访问 Node 环境所以要通过contextBridge把方法暴露给页面再做封装不要在组件里到处散落ipcRenderer调用。打包通常用 electron-builder配置项里有几个坑安装 Electron 依赖选择好的源能省不少时间安装包体积需要裁剪别把node_modules一整包塞进去Windows 签名问题没有证书时只能打未签名包安装时会有风险提示提前跟用户讲清楚。7.4 小程序、SSR 与开源脚手架Vue 生态的延伸有句话我常对新人说Vue 不是一个框架而是一整套生态。独立搭建页面的下一步往往就是接触这些生态周边。“Vue 项目如何发布微信小程序”是很多人会问的问题。直接答案是现有 Vue 项目不能直接转成小程序。小程序有自己的渲染层和逻辑层不识别 DOM 和 Vue 运行时。现实的做法是用 uni-app 或 Taro 这类跨端框架写一套代码同时输出 H5 和小程序。如果你已经用 Vue 写好了业务逻辑换成 uni-app 时组件写法会有变化但数据驱动视图的心智模型是一致的学习迁移成本不算高。Nuxt 则是另一个方向。Vue 是客户端渲染搜索引擎爬虫抓不到内容做 SEO 很被动。Nuxt 在 Vue 之上做了服务端渲染页面在服务端生成好 HTML 再返回。它的项目组织方式和普通 Vite Vue 项目有明显区别需要单独学习但做内容型站点基本绕不开。两者不是竞争关系Nuxt 是 Vue 的一个应用框架解决的问题完全不同。开源脚手架也值得提一下。像 ruoyi-vue 这类完整的管理后台项目把登录、权限、菜单、字典、代码生成都串好了对初次独立搭建页面的人来说是很好的参考仓库。我在新人阶段干过一个很笨但很有效的事把这类项目的路由、状态管理、请求封装从头到尾读一遍对照官方文档看每一处为什么这么写然后自己重新搭一个精简版。比只看教程记 API 有营养得多。网上还有“ruoyi vue 去掉验证码”这种搜索词其实就是在实践过程中改需求验证码一般在登录页的组件代码和后端接口处都有校验从前端删掉图形码控件再注释掉后端的校验逻辑两步就能去掉。这种改动前先看懂了人家为什么加验证码再决定要不要拆比一上来就删代码要稳妥。最后再聊一点我的习惯。我从入门到现在最受益的做法不是记住了多少 API而是维护了一份“踩坑笔记”。每解决一个奇怪的问题就花十分钟把原因和排查过程记下来。时间久了你会发现很多所谓疑难杂症无非是环境、版本、缓存、时序这四类问题排列组合。能独立搭建页面的人不一定是最聪明的那一个但一定是在坑里爬出来之后还顺手把坑标在了地图上的人。希望这篇总结也能成为你用来标坑的一份地图。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑