ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue项目实战:HTML与JavaScript是基石,掌握这些技巧少走弯路

Vue项目实战:HTML与JavaScript是基石,掌握这些技巧少走弯路 带新人的这些年我最常被问的一句话就是“我是不是得先把html和javascript学完才能开始学vue”问这句话的人有的是刚接触前端的大学生有的是半路转行做开发的还有的是在公司被临时拉去写vue项目的后端同事。我的回答一直很一致vue项目从来不是替代html和javascript而是站在两者肩膀上干活。你写的每一行vue模板最终都会被编译成html结构你的数据逻辑、事件处理、接口请求全部都是javascript在跑。甚至可以说vue项目里的html和javascript比你想象中承担的责任还要多。这篇文章没有什么高深的理论我就是把vue项目里那些跟html、javascript强相关的核心场景拆开讲一遍包括数据类型判断、函数封装、路由配置、插槽复用、依赖安装、打包部署这些绕不开的实操环节。不管你是刚入门想搭第一个vue项目还是已经有项目但经常被报错卡住这篇都能给你一份可以直接照着干的地图。我会尽量用“当时我遇到这个问题是怎么处理的”这种方式来写而不是给你复制一份官方文档。1. 先搞懂vue、html、javascript三者的分工1.1 html在vue里其实叫模板不是静态页面很多初学者最大的认知误区就是把vue项目里的html理解成“以前那种写死的网页”。传统html里一个按钮和一段文本都是写死在文件里的页面打开是什么样就是什么样。但在vue里html这部分进化成了“模板”它的核心作用是描述页面结构同时留出变量和指令的口子让数据能钻进去。举个例子传统html写一个用户姓名展示可能是这样p张三/p但vue里你通常会这么写p{{ userName }}/p这里{{ userName }}就是模板语法它是一个插槽位真正显示什么内容由javascript里的数据决定。不仅仅是文本标签的样式、属性、显示隐藏也都可以通过vue指令动态控制。比如v-if控制元素存在与否、v-for循环生成列表、:class动态绑定样式类名。还有一件事我每次带新人都会专门提一下就是模板顶部的文档声明和字符编码那一堆东西。很多人觉得!doctype html、html langzh-cn、meta charsetutf-8是无意义模板直接删掉也无所谓。实际上langzh-cn会影响浏览器的语言解析和翻译建议charsetutf-8则是中文不乱码的底线。我之前接过一个历史项目某个页面没有声明utf-8结果接口里返回的中文全部变成乱码最后排查半天才发现是页面编码和接口返回编码不一致导致的。vue项目里虽然大部分页面是组件拼出来的但入口的index.html该有的声明一个都不能少。1.2 javascript从“操作dom”变成“定义数据”原生javascript时代前端干活的基本流程是三步走找到dom元素、绑定事件、修改内容或样式。比如做一个点击按钮数字加一的计数器原生写法大概是这样的let count 0; const btn document.getElementById(btn); const num document.getElementById(num); btn.addEventListener(click, function () { count; num.textContent count; });这套写法你仔细体会一下会发现一个问题程序员的精力大量消耗在“告诉浏览器怎么改页面”上而不是“业务逻辑本身”。尤其在项目变大之后手动维护dom更新顺序和状态一致性是非常痛苦的事情今天改了这里忘了那里页面数据就对不上了。vue的核心思路是把这一步接管过来。在vue里你只需要维护一个count变量模板里写p{{ count }}/p当你修改count的值页面上的显示自动跟着更新。这个机制靠的是vue的响应式系统它用javascript的Proxy对象去监听数据的读取和赋值一旦发现数据变化就在内部记录哪些组件依赖了这条数据然后找到对应dom节点做最小粒度的更新而不是整页刷新。但这不代表javascript基础就不重要了恰恰相反在vue项目里javascript的重要性不仅没有降低反而体现在更复杂的地方。vue帮你处理的是“视图同步”这一层但业务逻辑还得你自己写比如处理接口返回的数据、判断复杂条件、组织函数封装、处理异步流程这些全是要靠原生javascript功底的。我见过不少只会写v-if和v-for的vue开发者一到写一个稍微复杂的搜索过滤函数就卡壳这就是javascript基础没跟上。2. javascript基本功在vue里怎么用三个绕不开的场景2.1 判断数据类型别再用typeof一条路走到黑在vue项目里接口返回数据往往是不稳定的有时候后端返回的是一个对象有时候可能是个数组甚至可能是null。开局先判断数据类型是每个前端每天都要干的事。很多新人习惯一上来就typeof然后就会踩坑。typeof有个经典的坑它对null返回的是object对数组返回的也是object。你要是用typeof res object去过滤非空对象说不定就把数组和null也放进来了。我干活的时候更习惯用Object.prototype.toString.call()这套方案它能把类型判断做到很精确function getType(value) { return Object.prototype.toString.call(value); // 返回如 [object String]、[object Array] } function isPlainObject(value) { return Object.prototype.toString.call(value) [object Object]; } function isArray(value) { return Array.isArray(value); }Array.isArray()也值得单独记一下这是判断数组最可靠的方式比toString更简单直接。在vue项目里这种判断最常见的场景是接口返回的字段可能是一个对象也可能是对象数组你必须先判断完再决定是直接渲染还是用v-for遍历。另外一个场景是表单组件里处理默认值比如一个下拉选项的初始值可能是undefined、null、空字符串如果不做类型判断很容易在组件内部报错。我在实际带项目的时候还会要求团队成员把常用的类型判断函数统一收进src/utils/type.js里而不是到处各写一份。这样后面的人接了代码看到isPlainObject就知道是干什么的不用重新读一遍逻辑。2.2 保留两位小数和数值处理里的猫腻“javascript保留两位小数”这个搜索词的频率高得吓人主要原因是js的浮点数计算确实容易让人抓狂。比如0.1 0.2你在浏览器的console里按回车得到的是0.30000000000000004这不是bug而是二进制浮点数表示误差的必然结果。vue项目里处理金额、百分比、数据统计时这种问题几乎天天遇到。很多人的第一反应是num.toFixed(2)但这招有局限。首先是toFixed返回的是一个字符串如果你要传给后端的数字类型还得再parseFloat转一下。其次是四舍五入的规则在某些场景下会跟你预期不一样比如1.005.toFixed(2)在不少环境下得到的是1.00而不是1.01这就是二进制表示误差导致的。还有负数的情况(-1.005).toFixed(2)的结果可能也会让你意外。我这里建议一个相对稳妥的处理方式用Math.round加上系数换算或者封装一个专门的格式化函数function formatMoney(value) { const num Number(value); if (isNaN(num)) return 0.00; const rounded Math.round(num * 100) / 100; return rounded.toFixed(2); }Math.round(num * 100) / 100的思路是先把数字放大100倍取整再缩小回来这样能避开大部分浮点误差的坑。然后再用toFixed(2)补零保证小数位始终是两位。实际项目里我还会把这个函数放到全局公共工具里所有涉及金额展示的地方统一调用这样不会出现有的页面显示12.3、有的显示12.30这种不一致的尴尬。2.3 函数封装从methods到utils的出入口vue单文件组件里交互逻辑一般写在methods里这没什么问题。但如果你把一个复杂的格式化逻辑、防抖逻辑、数组去重逻辑直接写在某个组件里回头另一个组件也要用就得复制粘贴一遍了。复制粘贴的次数多了代码就成了“屎山”改一个地方漏了另一处bug就来了。我的做法是组件内只放跟这个组件的状态强相关的函数通用逻辑一律抽到src/utils/下面导出后组件里按需引入。比如把接口数据里的时间戳统一格式化比如把树形数据转换为扁平数组再比如节流防抖函数这些都属于通用逻辑。// utils/debounce.js export function debounce(fn, delay 300) { let timer null; return function (...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }在组件里这样用import { debounce } from /utils/debounce; export default { methods: { onSearch: debounce(function () { // 搜索逻辑 }, 500) } };这种封装还有一个额外好处就是单元测试变得容易写。纯函数丢给测试框架直接跑就行不用启动整个页面。我带团队的时候会定一条规矩同一个函数在项目中第三次被复制粘贴就必须提炼成公共方法。这条规矩看起来死板但执行下来项目的维护成本是真的肉眼可见地降低。3. 实操从零搭建并跑通一个带路由和插槽的vue项目3.1 环境安装与依赖管理装包失败的常见解法“vue安装及环境配置”是每个新人的第一关。我的建议是直接上最新稳定版的Node.js而不是老项目里那种老版本。因为新版Vue的脚手架和生态组件基本上都对最新的Node版本有要求你用老版本装新依赖大概率会遇到engine字段报错。安装Node的时候我个人比较推荐用nvm这类管理工具它允许你在机器上同时存在多个Node版本随时切换。真实项目中这个需求太常见了你自己开发的项目可能用Node 18公司老项目可能还在用Node 14没有版本管理工具你就只能在卸载重装之间反复折腾。macOS和Linux上用nvm很简单Windows上可以用nvm-windows网上教程很多这里不展开。装上Node之后用npm还是yarn还是pnpm我的建议是看项目的lockfile约定。项目里有package-lock.json就用npm有yarn.lock就用yarn有pnpm-lock.yaml就用pnpm。别混着用这是很多依赖问题的根源。npm和yarn混装的后果就是node_modules里的依赖树不一致最后表现出来的是同一份代码在不同人电脑上跑的结果不一样。“vue安装依赖”报错是高频问题常见的几类我列一下网络原因导致的下载超时可以用淘宝镜像源或者公司内部源解决版本冲突导致的依赖不兼容往往报ERESOLVE之类的错误这时可以试着升级包版本或者统一包管理工具还有node_modules残留导致的文件权限问题解决办法最直接——删掉整个node_modules和lockfile重新执行安装命令。我自己的习惯是遇到解读不了的装包报错第一时间看报错信息顶部的英文提示大多数情况下解决办法就写在里面根本不需要搜答案。项目本身用Vite还是Vue CLI我的观点比较直接新项目优先Vite启动速度快是实实在在的体验提升配置也更简单。但接手老项目就得跟着项目走除非你有充足理由升级否则别在业务交付期去动构建工具代价太大。3.2 路由配置普通路由、路由参数、动态路由一次说清vue项目里几乎没有不用vue-router的。路由配置的核心就三件事定义路由表、在模板里放路由出口、通过跳转函数或router-link导航。真正容易搞混的是路由传参。先看最基础的传参方式query方式传参在链接里以?keyvalue的形式出现刷新页面参数不会丢目标页面用this.$route.query获取。另一种是params方式配动态路由比如配置/user/:id跳转时传{ params: { id: 7 } }在目标页面用this.$route.params.id获取。这里有个细节params传参如果用router.push时只传了params没配路径模式页面刷新之后参数可能就变成undefined了原因是params走的是内存记录并不会体现在url上。要保住刷新不丢要么用query要么把参数拼在路径里。动态路由除了/user/:id这种路径参数还有一类是“根据权限决定哪些路由对用户可见”。这个场景在后台管理系统里特别常见。做法是登录后获取用户的菜单权限列表然后用router.addRoute动态添加路由项路由表里不提前写死所有页面入口。这样做的意义很实际没有权限的用户根本访问不到对应的路由地址从路由层面拦了一道。我做过的一个后台项目就是把菜单表、角色权限、路由表三张结构对齐权限配置直接是数据驱动的产品经理在后台改一行配置前端路由跟着变不用发版。3.3 插槽让组件留个位置把灵活性交给调用方vue插槽算是一个“只要理解了就再也不会忘记”的知识点。它的本质就是组件模板里先开一个“坑位”父组件往里填内容。默认插槽是最常见的形式在子组件里写slot/slot父组件在标签对里放什么它就被渲染到那个位置。具名插槽解决的是多个坑位的问题比如一个弹窗组件标题区一个坑、内容区一个坑、底部操作按钮区一个坑。子组件分别写slot nametitle、slot namecontent、slot namefooter父组件用template v-slot:title这种写法一一对应填充。这套机制在封装业务通用组件时非常好用比如确认弹窗、页面头部、卡片容器谁用谁往里塞内容组件本身不用知道业务细节。作用域插槽稍微进阶一点它的适用场景是子组件有自己的数据但希望父组件来决定这些数据怎么展示。最典型的例子是表格组件。表格内部封装了分页和数据处理但每一列的渲染方式需要调用方自定义这时候子组件把当前行数据通过插槽props抛出来!-- 子组件 -- slot nameoperation :rowrowData/slot父组件这样接收并使用template v-slot:operation{ row } button clickhandleEdit(row.id)编辑/button button clickhandleDelete(row.id)删除/button /template我第一次拿到这种代码时也觉得绕但后来理解了它的价值它把“数据流向”和“渲染形式”拆开了子组件负责数据父组件负责形态双方都不用互相侵入。这在团队协作里太重要了我封装列表组件不需要知道运营团队要放多少按钮、按钮叫什么名字。3.4 样式和模板细节scoped、class绑定、返回顶部vue单文件组件的style默认是全局生效的如果每个组件的class都写得很普通比如.title、.content这种最后所有人的样式会互相打架。vue的scoped属性就是解决这个问题的加了scoped之后组件模板里的元素会自动带上一个类似>style scoped :deep(.el-dialog__body) { padding: 20px; } /style:deep()的意思是不限制这个选择器必须匹配当前组件的dom范围而是让它延续到子组件的内部结构里去。但这个能力要克制使用项目里如果到处是:deep()说明你对样式的规划出了问题外部组件改不到的时候就该想想是不是该用插槽或者样式变量来做了。顺带说一个高频小需求一键返回顶部。很多人会去搜“html一键返回顶部算法”其实核心就是用window.scrollTo控制滚动位置加个平滑过渡就是完整方案function scrollToTop() { window.scrollTo({ top: 0, behavior: smooth }); }放在vue组件里就是在按钮的click事件里调用这个函数。如果滚动容器不是整个页面而是某个内部div那就用这个容器元素的scrollTop属性手动归零。这里容易踩的坑是用window.scrollTo但滚动容器其实是某个div页面根本没动按钮像是坏了。问题不在代码而在客户端到底在滚动哪个容器用的时候多留个心眼。4. 项目做完了部署和扩展场景怎么处理4.1 打包放进springboot避开空白页和404vue项目开发完之后要部署上线常见的一种方式是把打包产物丢给后端工程让后端把vue当作静态资源托管起来。我做过不少这类对接最深的体会是打包就是一个“把vue运行时变成浏览器认识的htmljavascript”的过程而对接后端时痛点非常集中。先用命令构建项目Vite项目的默认输出目录是dist里面会生成index.html和一堆带哈希的js、css文件。要让这些文件被springboot正常访问第一步要改vue项目的base路径。默认情况下构建出的资源引用是以/开头的绝对路径但如果你的vue页面是被放在某个二级路径下访问比如localhost:8080/admin/资源路径就全错了页面白屏。解决办法是构建时设置base: ./让资源引用变成相对路径这样不管放在哪个目录下都能正确加载。第二步是处理路由模式。vue-router默认是history模式这种模式的路由路径是浏览器地址栏里的真实路径比如/user/7。在开发时一切正常但部署到springboot里后端如果没有配置任何路径都转发到index.html的规则用户一刷新/user/7这个地址后端就抛出404。处理方案有两种一是后端加转发规则把所有非静态资源的路径都指向index.html二是前端把路由改成hash模式地址变成/#/user/7这种模式本质上是页面内锚点定位不会触发后端的路径匹配刷新也不会404。对后端不熟悉、又追求快速上线的项目我用得最多的是hash模式省心。对路径美观有要求、又有后端同学配合的再用history模式。第三步是把dist目录里的内容拷贝到springboot的src/main/resources/static目录下启动springboot之后访问对应端口就能看到vue页面。需要注意的一点是如果vue页面要调后端的接口前端代码里写的接口地址不要固定写死一个域名应该用环境变量配置构建时按环境注入。否则同一套代码换个环境部署接口地址还得重新改一遍再打包。4.2 vue里播放m3u8视频流装上播放器就能跑“vue播放m3u8免安装”这个搜索词挺有意思的很多人想找一个不装任何第三方插件就能在vue里播放m3u8的方案。先解释一下m3u8是什么它是一种流媒体播放列表文件本质上是文本格式里面记录了一段视频被切分成的大量ts分片文件的地址。浏览器原生没有解析m3u8并连续播放的能力所以“免安装”不太现实但你可以用轻量级的javascript库来实现。我常用的方案是hls.js这个库专门在浏览器里实现HLS协议的解析。虽然它本身需要npm install但它打包进你的项目之后最终用户不需要任何额外安装浏览器上就可以直接播放。这大概就是“免安装”的真正含义免的是用户端的安装。用hls.js播放m3u8的标准流程先用npm install hls.js装进项目然后在组件里这样写import Hls from hls.js; export default { data() { return { videoUrl: https://example.com/stream.m3u8 }; }, mounted() { const video this.$refs.video; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(this.videoUrl); hls.attachMedia(video); this.hls hls; } else { // 兜底有些浏览器支持原生HLS比如Safari video.src this.videoUrl; } }, beforeUnmount() { if (this.hls) { this.hls.destroy(); this.hls null; } } };模板里对应一个普通video refvideo controls/video就行了。这段代码里有几个细节值得注意一是要把hls实例存到组件实例上在组件销毁时destroy()掉否则播放器会一直占着资源页面复杂之后会出现莫名其妙的卡顿和内存膨胀二是要判断Hls.isSupported()是否为真兼容Safari这类原生支持HLS的浏览器两套方案都要写好。如果你是追求省事的做法也可以用video.js这个全功能播放器它封装了更多UI和交互细节配一个type: application/x-mpegURL的参数就能播放m3u8。代价是包体积比hls.js大不少看你项目对体积的敏感程度。我自己一般推荐只要播放器界面需求简单就用hls.js加原生video元素可控性最高。4.3 地图、canvas和低代码平台扩展背后的共同逻辑vue项目里经常要集成一些“浏览器能力型”的东西比如地图、canvas画布、文件上传预览。很多新人一遇到这些就慌觉得“vue里怎么放地图”其实它们的底层逻辑非常统一你先在模板里准备好一个普通的html容器元素然后在javascript里初始化这个容器对应的实例。以接入Mapbox为例div refmapContainer stylewidth: 100%; height: 400px;/divimport mapboxgl from mapbox-gl; import mapbox-gl/dist/mapbox-gl.css; mounted() { this.map new mapboxgl.Map({ container: this.$refs.mapContainer, style: mapbox://styles/mapbox/streets-v11, center: [116.397, 39.908], zoom: 12 }); }核心就一句话container参数接收的是一个dom节点跟是不是vue没有任何关系。只要你在正确的时机拿到了这个dom节点初始化就是一样的。vue里正确的时机通常选mounted因为只有组件挂载完成之后ref对应的dom才真实存在于页面中。你要是写在created或者beforeMount里拿到的是undefined地图自然渲染不出来。同理canvas画布在vue里的用法也是在mounted后通过ref拿到canvas节点再getContext(2d)。顺带聊聊“vue 低代码平台”这个概念。低代码平台的核心思想是用一份json或者一组配置数据来描述一个页面应该长什么样、有哪些组件、组件之间如何联动。为什么vue这类框架适合做低代码因为vue本身就是数据驱动视图的你把配置数据塞给一个动态渲染组件它内部用v-for循环生成组件用v-bind动态绑定props用v-on动态绑定事件页面就出来了。这本质上还是我开头说的那件事结构是html模板逻辑是javascript解析配置vue在这中间做了数据到视图的桥梁。理解到这一层你就不会觉得低代码平台有多玄乎了。5. 常见问题速查表与调试心得实录5.1 高频问题排查速查表我整理了这些年带项目过程中出现频率最高的几个问题放在表格里遇到可以直接对着查。现象常见原因处理思路打包后页面空白资源路径用的是绝对路径/部署在二级路径下导致js/css加载失败构建时设置base: ./重新打包刷新路由页面404history路由模式后端未配置转发到index.html改hash模式或让后端把所有路由转发到index.html安装依赖报错包管理器混用、镜像源不稳定、版本冲突删node_modules和lockfile后用同一包管理器重装必要时切镜像源样式不生效没有scoped导致样式冲突或者用了scoped但改不到第三方组件内部公共样式统一规划改第三方组件用:deep()组件销毁后仍有定时器/播放器在跑定时器、hls实例、地图实例没有在生命周期钩子里清理在beforeUnmount里clear定时器和destroy实例接口数据是数组却用对象的方法处理没有提前判断数据类型统一使用类型判断工具函数先判断再处理刷新页面params参数丢失params传参未体现在url路径中改用query传参或者把参数拼进动态路由路径第三方库初始化报错“container is not defined”在created阶段就尝试初始化dom还不存在移到mounted阶段初始化用ref拿到容器这张表不是标准答案但它代表了一种排查问题的路径先看报错信息再看代码执行时机再看路径和数据格式。大多数vue项目的问题都绕不开这三个维度。5.2 项目源码怎么发给别人才是专业做法“vue项目源码怎么发给别人”这个问题我为什么会专门拿出来说因为在团队协作里我见过太多直接把整个项目文件夹用压缩包丢过来的情况里面有node_modules、有本地的临时配置、甚至还有.env文件里写死的本地接口地址。对方一解压光安装依赖就装了半天还因为node版本和包管理器不一致跑不起来。正确的做法是发源码之前先删掉node_modules目录这个文件夹动辄几百MB发过去没有任何意义再把.git目录清理掉除非你故意要带历史提交记录然后检查.gitignore文件确认node_modules、dist这些生成物不会被误传。如果项目里有环境相关配置应该提供一份示例文件比如.env.example里面写上每个配置项的含义让接收方自己复制成.env再填内容。最后一定要写一个简单的README说明以下三件事用的包管理器是什么npm/pnpm/yarn、Node版本要求是多少、启动命令和构建命令分别是什么。接收方拿到源码后第一件事不是急着看代码而是先看README然后按说明装依赖。如果安装过程报错再回头看包管理器版本和Node版本是否匹配。我教新人的时候常说一句玩笑话能让人家三分钟跑起来的源码才叫源码让人家折腾一下午的叫代码考古。5.3 “屏蔽高负载javascript”的正确解法是优化不是屏蔽最后一个话题关于浏览器里“屏蔽高负载javascript”的提示。很多用户遇到页面卡顿之后会在浏览器设置里找屏蔽脚本的选项网页开发者在做性能优化时也会搜索这个关键词。我的观点很明确真正的高负载javascript是你自己代码里那些长期霸占主线程的逻辑屏蔽它是治标不治本而且会直接破坏页面的正常功能。vue项目里常见的高负载javascript场景有一次性渲染上千条数据的列表、组件里频繁触发的搜索请求没有防抖、定时器里做了大量重复计算、路由切换时大组件同步加载。对应的优化手段我也直接列出来大数据列表用虚拟滚动比如vue-virtual-scroller只渲染可视区域内的dom而不是一次渲染全部搜索输入加防抖用户在停止输入300毫秒后才真正发起请求长任务拆成小任务可以用requestIdleCallback或者把大数组分片处理分帧执行路由组件用懒加载() import(/views/BigPage.vue)这种形式首屏只加载当前页面需要的内容。处理性能问题和处理业务bug的思路不一样它不是某个功能“错了”而是整体运行时的效率问题。我的排查习惯是先用浏览器DevTools的Performance面板录制一段操作看主线程里哪些任务占的时间最长再针对性优化。只看代码猜性能瓶颈通常猜不准。我带项目这些年最深的一个感受是vue、html、javascript这三样东西从来不是对立关系。你被vue的某个bug卡住的时候表面上在查vue的问题实际上八成是在跟原生javascript的基础较劲你调整页面布局的时候看着是vue模板实际上还是html结构的功底在起作用。很多人学vue学不下去不是因为vue难而是因为地基里的javascript和html还有窟窿没补。反过来讲只要这两样地基足够扎实vue就只是一层包装得很顺手的壳学起来是很快的。最后分享一个我平时带新人用的笨办法遇到任何vue报错先在脑子里把问题拆成三块——是不是html模板写法的问题是不是javascript逻辑的问题是不是vue数据联动的问题。大多数情况下你能靠这个分类直接定位到问题根源。这个习惯养成之后debug速度会快很多你也会越来越清楚自己真正缺的是哪一块的知识。
返回列表