ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从HTML+JS到Vue项目:模板、数据驱动与工程化实战指南

从HTML+JS到Vue项目:模板、数据驱动与工程化实战指南 从会写 HTML 和被 CSS 调样式到真正能用 JavaScript 做出点交互很多人以为接下来学 Vue 就是“换一套语法”而已。结果真打开一个 vue 项目面对 src 目录、node_modules、单文件组件这些陌生东西时才会发现这里面的 HTML 不再是以前那个 HTMLJavaScript 也不完全是以前那个 JavaScript。这篇就结合我自己从纯 htmljavaScript 页面转到 Vue 项目后的实际体会把两者之间的差异、迁移思路和踩过的坑一次讲清楚。这篇内容适合两类人一类是刚学完 HTML/CSS/JS 基础准备入门 Vue 的前端新手另一类是做过一些静态页或工具脚本但还没系统做过组件化项目的朋友。看完之后你能明白为什么 Vue 要把 HTML 拆成模板、为什么你的 JS 要从“操作 DOM”改成“驱动数据”以及一个 vue 项目从环境配置到跑通核心到底要过哪几关。1. 从“改完刷新就能跑”到“启动构建才能看”Vue 项目在底层逻辑上改了哪些事先回忆一下传统 html 项目的开发体验新建一个.html文件里面写好link引 CSS、script引 JS双击文件浏览器直接打开就能运行。改一行代码刷新页面生效没有任何中间过程。这种模式的好处是上手容易坏处是当页面越来越大时你会发现自己陷入了一场灾难——全局变量互相污染、JS 和 HTML 纠缠在一起、每加一个功能都要小心翼翼避免把别的功能搞挂。Vue 项目从根本上改变了这套流程。首先你跑的不是一个.html文件而是一个工程代码写在单文件组件里.vue经过构建工具编译打包后才会产出浏览器真正能识别的 HTML、CSS、JS。开发时通过本地服务器预览改完代码浏览器自动更新热更新HMR。这种“编译—预览”的链路看起来比原来麻烦实际上是在帮你在开发阶段就把错误暴露出来而不是等用户打开页面才崩溃。具体到角色分工Vue 里的 HTML 和 JavaScript 都被重新定义了层面传统 HTMLJSVue 项目页面结构HTML 文件本身就是结构.vue文件里的template模板脚本逻辑JS 通过 DOM API 找元素、改内容JS 定义数据和方法模板自动渲染复用方式复制粘贴代码块组件封装、props 传参、插槽组合依赖管理按顺序加载script标签npm 安装、ES Module 导入导出运行环境浏览器直接运行Node.js 环境构建后运行这里要理解一个关键点Vue 项目并没有抛弃 HTML 和 JavaScript而是把 HTML 升级成了“模板系统”把 JavaScript 升级成了“数据驱动引擎”。你以前写的document.getElementById(xxx).innerHTML ...在 Vue 里变成了data中声明一个变量然后在模板里用插值表达式绑定它——数据一变视图自动更新。代码从命令式告诉浏览器每一步做什么转向声明式告诉页面“这个位置显示什么”。我从传统页面转过来的第一个感受是写 Vue 项目最重要的是转变思维而不是记住更多 API。不转变这个思维你会在模板里疯狂查 DOM在mounted里写一堆querySelector最后发现不但代码难维护还丢掉了 Vue 的核心优势。2. 模板里的“HTML”已经变了味插值、指令、动态绑定到底在做什么Vue 的template看着像 HTML实际上是一个带“魔法”的模板。你在里面写div{{ message }}/div这个盒子最终显示什么取决于组件中message这个数据成了什么。模板不再是静态结构而是“数据的外观映射”。2.1 插值与表达式不是简单的字符串拼接初学者最容易犯的一个问题在模板里什么复杂逻辑都往里塞。比如{{ user.name.split().reverse().join() (flag ? A : B) }}这样写能运行但非常糟糕。模板里的插值表达式本质上不是给 JavaScript 做复杂运算用的它的定位是“展示”。需要复杂计算时应该用计算属性computed或方法methods这样逻辑清晰、可测试也方便复用。记住一条原则模板里只保留简单的三元判断和直接的数据引用。2.2 指令v-if、v-for、v-model、v-on 的协作方式指令是 Vue 模板对 HTML 最大的扩展。v-if负责条件渲染v-for负责列表循环v-model负责表单元素和数据之间的双向绑定v-on负责事件监听。它们不再像原生 HTML 那样“默写标签”而是对应了数据层面的控制逻辑。实际开发中v-if和v-for不要写在同一元素上因为v-for的优先级高于v-if每次循环都会先渲染再判断白白浪费性能。应该在外层套一层template包住v-for在里面放v-if。指令简写也是必须掌握的技巧!-- v-bind 简写为冒号 -- img :srcavatar / !-- v-on 简写为 -- button clickhandleSubmit提交/button有些朋友从旧资料里学 v-bind/v-on 的全称写法在模板里写得又长又乱。其实:和是官方认可的简写项目中基本都会用简写你必须在一开始就养成习惯。2.3 样式绑定class 和 style 的动态化原生 HTML 里改样式通常是element.style.color red或者操作 classList。Vue 里动态绑定 class 有几种常见写法div :class{ active: isActive, text-danger: hasError }/div div :class[activeClass, errorClass]/div div :classisActive ? active : /div对象语法适合“开关型”的场景比如菜单项的激活状态数组语法适合需要叠加多个 class 的场景三元表达式适合二选一的场景。需要注意的是数组里如果某个 class 可能不存在数组元素不要直接写字符串常量最好绑定到数据或使用对象语法避免产生空字符串 class 这样的脏属性。2.4 插槽slot给组件模板开“天窗”再提一下热词里的“vue插槽”。插槽是 Vue 组件化体系里非常有味道的设计它解决的是“组件内部结构不完全确定”的问题。类比现实生活一台电脑主板上预留了内存插槽具体插多大的内存条由用户决定。组件用slot在模板中预留位置外层调用组件时传入自定义内容。默认插槽、具名插槽、作用域插槽三种最容易被忽略的是作用域插槽。它允许插槽内容访问子组件内部的数据比如表格组件把某一行数据传给插槽让外部自定义渲染。理解作用域插槽的关键在于数据属于谁谁才能从“自己的范围”往外传递。3. Vue 项目里的 JavaScript从“操作 DOM”变成“驱动状态”如果只用一句话概括 Vue 对 JavaScript 的要求那就是把关注点从 DOM 移到数据上。原生 JS 时代我们写的代码大量围绕 DOM 操作展开——查节点、改属性、监听事件、再更新节点。Vue 项目里这些底层操作框架都帮你做了你要负责的是定义数据、处理业务逻辑、在合适的时机发起请求。3.1 data、computed、methods、watch 各干各的这四个选项经常让新手纠结其实分工非常明确data()组件的数据源头响应式数据的起点。computed依赖其他数据计算出的新数据有缓存依赖不变不会重新计算。methods事件处理函数和普通业务方法每次调用都会执行。watch监听某个数据变化后执行副作用操作比如搜索框输入后调用接口。一个很容易踩的坑把计算逻辑写在 methods 里然后模板里{{ getTotal() }}这样用。表面没问题但每次页面重新渲染都会重新执行函数性能浪费严重。正确方式是computed定义total模板里直接{{ total }}。3.2 写 JS 时最常见的几个小问题保留小数、函数、报错我看热词里出现了“javascript保留两位小数”“javascript函数”“javascript运行时报错”这些都是从纯 JS 往 Vue 挪的过程中特别容易碰到的点。保留两位小数大家的第一反应是toFixed(2)。但toFixed有个坑它返回的是字符串不是数字。有时候你算完还想继续参与运算结果变成字符串拼接排查半天。所以实际项目中我一般这样处理// 方案一Math.round const rounded Math.round(num * 100) / 100; // 方案二统一用 Number 包一层 const result Number((num * 1.2).toFixed(2));另外toFixed还存在舍入误差问题比如(0.1 0.2).toFixed(2)得到的结果有些机器上会是0.30这是由于浮点数精度导致。涉及金额计算建议先用整数分计算完再转小数显示。JavaScript 函数的写法在 Vue 里也要格外注意模板里绑定的函数如果用箭头函数简写要注意this指向。Vue 实例方法中this指向组件实例但如果在生命周期钩子或事件回调里嵌套箭头函数你可能会丢失组件上下文。我建议所有模板里用到的方法都写成普通函数少在 methods 里写箭头函数。运行时报错还有个经典场景接口返回的数据结构和自己预期不一致模板渲染到obj.a.b时obj.a是 undefined直接报错。解决方法是模板中做可选链或兜底处理{{ user?.address?.city }}或者把数据默认值在data()里就声明好不要让页面一开始就去渲染空对象下的深层属性。3.3 生命周期JavaScript 代码在哪一刻该做什么Vue 组件从创建到销毁会经历一系列阶段每个阶段都有对应钩子。新手最常混淆的是created和mountedcreated实例创建完成数据已初始化但 DOM 还没渲染出来。适合在这里请求接口获取数据。mounted组件真实 DOM 已挂载可以操作this.$refs适合初始化第三方组件库、绑定原生事件。我把一条经验分享给你凡是不依赖 DOM 的初始化逻辑能放在 created 就放在 created。比如请求列表数据、设置初始计时器。如果放在 mounted会导致数据到达后 DOM 重新渲染视觉效果上容易出现闪烁。4. 一整个页面从原生 HTML 迁移到 Vue 组件一个完整的案例拆解理论讲太多容易飘我挑一个很实际的例子来走一遍原来有一个视频播放页面用的是原生 HTMLJavaScript页面里写死了video标签通过hls.js库支持 m3u8 格式流媒体播放再加上一个简单的进度条和播放按钮。4.1 原生写法的样子传统页面大致长这样div idplayer-area video idvideoElement controls/video button idplayBtn播放/button /div script srchttps://cdn.jsdelivr.net/npm/hls.js/script script const video document.getElementById(videoElement); if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(https://example.com/video.m3u8); hls.attachMedia(video); } document.getElementById(playBtn).addEventListener(click, function() { video.play(); }); /script这段代码逻辑本身没问题问题在于一旦页面里要处理多个视频源、多个交互状态、还要根据路由参数切换代码量会迅速膨胀ID 冲突的可能性越来越大。4.2 迁移成 Vue 组件后的结构第一步把页面结构拆成模板、脚本、样式三部分template div classplayer-wrapper video refvideoElement controls/video button clickhandlePlay{{ playing ? 暂停 : 播放 }}/button p v-ifsourceUrl{{ sourceUrl }}/p /div /template script import Hls from hls.js; export default { name: VideoPlayer, data() { return { sourceUrl: , playing: false, }; }, created() { // 路由参数决定播放哪个流 this.sourceUrl this.$route.query.url || ; }, mounted() { this.initPlayer(); }, methods: { initPlayer() { const video this.$refs.videoElement; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(this.sourceUrl); hls.attachMedia(video); this.hls hls; } }, handlePlay() { const video this.$refs.videoElement; if (video.paused) { video.play(); this.playing true; } else { video.pause(); this.playing false; } }, }, beforeUnmount() { // 离开页面时销毁播放器实例避免内存泄漏 if (this.hls) { this.hls.destroy(); } }, }; /script style scoped .player-wrapper { max-width: 960px; margin: 0 auto; } video { width: 100%; } /style对比一下就能看出差别原生代码的操作逻辑必须要手动“找元素—绑事件—写逻辑”而 Vue 版本里ref帮你拿到了元素引用click帮你绑定了事件playing状态直接驱动按钮文字变化路由参数在created里获取后注入播放器。整个代码量差不多但可维护性、可复用性完全不是一个级别。4.3 迁移过程中的几个关键决策点为什么播放器初始化放在mounted而不是created因为this.$refs.videoElement必须是真实 DOM 渲染完成后才能拿到created阶段还没有 DOM。为什么用ref而不是document.getElementByIdref是 Vue 提供的访问子组件或 DOM 元素的统一方式它不依赖全局唯一 ID子组件和父组件之间也能通过 ref 引用避免样式和逻辑相互污染。你完全可以在一个页面里渲染两个VideoPlayer组件每个组件都有自己的局部状态。为什么用beforeUnmount销毁 Hls 实例因为hls.js内部会创建网络请求和定时器组件销毁后不手动清理会继续占用资源。这在原生 HTML 页面里你基本不需要关心因为页面关了浏览器自己回收但 Vue 是单页应用页面跳转只是组件切换浏览器进程还活着内存泄漏就是这样一点点累积起来的。4.4 如果要在 Vue 项目里用其他 HTML 片段怎么办有时候我们手头有一段现成的 HTML比如百度首页天气模块的静态结构、日历组件、某个静态页面的片段想在 Vue 项目里直接用。最省事的办法是把HTML转成模板再贴进.vue文件的template里但要注意几件事原来的div idxxx这样的硬编码 ID 建议去掉改成 class 或直接用组件边界控制。原来script标签里嵌入的初始化逻辑要改写到mounted或者对应的生命周期里。原来style里的全局样式放到style scoped里避免影响其他组件。如果 HTML 片段需要动态渲染可以用v-html指令但要注意v-html的内容千万不要包含用户输入否则有 XSS 风险。真的有大量现成静态 HTML 想在原项目中使用而懒优化时也可以用iframe src./static.html把静态页面包进去但这样会失去组件间通信能力能不用尽量不用。5. 从零把一个 vue 项目跑起来的完整链路环境、依赖、路由既然标题里带“vue项目”就该聊聊“项目”这两个字背后的工程化内容。很多热词指向“vue安装及环境配置”“vue安装依赖”“idea开发vue项目”这说明很多人卡在了入口处。这部分我按实际操作顺序来讲。5.1 Node 环境与 npm 的安装验证Vue 项目离不开 Node.js 环境因为构建工具Vite、Webpack都是跑在 Node 上的。先去官网下载 LTS 版本安装包装完验证node -v npm -v确认两个命令都有输出环境就基本就绪了。不少新同学在这步栽跟头下载了最新版 Node结果公司项目用的是老版本依赖一跑就报错。建议用nvmNode Version Manager管理多版本 Node项目需要哪个版本就切哪个版本。5.2 创建项目与安装依赖创建 Vue 项目常用方式是 Vite 或 Vue CLI。Vite 是目前官方推荐的构建工具速度更快、配置更简洁npm create vitelatest my-vue-app -- --template vue cd my-vue-app npm install npm run devnpm install会把项目所有依赖下载到node_modules目录这个过程慢是正常的尤其是 Windows 环境。这里有个高频问题vue 项目源码怎么发给别人。你只需要把项目目录不含 node_modules打包发给对方对方拿到后运行npm install就会自动安装依赖。如果你把 node_modules 也打包发过去文件巨大不说还可能因为系统差异导致依赖失效。所以记住项目源码传前端文件依赖永远靠package.json描述来安装。5.3 用 IDEA 开发 Vue 项目的配置要点很多 Java 或者偏后端背景的开发者习惯用 IDEAIDEA 开发 Vue 项目不需要特殊插件就能打开项目结构但有几个点建议配置好安装 Vue.js 官方插件这样.vue文件才有语法高亮和智能提示。把node_modules目录标记为排除状态否则索引非常卡。在设置里配置好 npmIDEA 才能识别 package.json 的脚本命令在右上角直接一键运行dev、build。曾经有个朋友换了台电脑之后IDEA 里 dev 脚本一直启动失败排查半天发现是项目路径带中文导致 webpack 编译异常。所以项目目录尽量用纯英文命名能省掉很多玄学问题。5.4 路由参数与动态路由页面之间怎么传值Vue Router 是 vue 项目里的“页面导航系统”。热词里“vue路由”“vue路由参数”“vue动态路由”都比较高。我按实际场景说先放一个最常见的需求从列表页点击某条记录跳到详情页要在详情页知道“这条记录是哪一个”。可以用以下方式# 命令行跳转在组件方法里 this.$router.push({ path: /detail/123, query: { from: list } }) # 或导航式 router-link :to{ path: /detail/123 }查看详情/router-link在routes配置里声明动态参数const routes [ { path: /detail/:id, component: DetailPage } ];然后在详情页组件里读取参数this.$route.params.id值得提醒的是$router和$route是两个不同的对象。前者负责“跳转操作”后者负责“读取当前路径信息”。很多人把他们搞混在$route上调用push直接报错。动态路由适合那种“路径不同但页面结构相同”的场景。比如/user/1和/user/2都渲染同一个用户组件只是数字不同。动态路由参数获取之后还要注意组件复用的坑从/user/1跳到/user/2时组件实例被复用created不会重新执行需要在watch中监听$route.params.id变化来重新加载数据。5.5 依赖安装失败的排查思路npm install报错是每个 vue 开发者都遇到过的事。最常见的几类网络原因换个源比如国内源npm install --registryhttps://registry.npmmirror.com。版本冲突package.json里某个依赖版本和另一个依赖要求的语义化版本区间不兼容。查看错误日志中peerDependencies的提示手动调整版本。缓存问题npm cache clean --force之后重装。权限问题Windows 上关掉杀毒软件再试Mac/Linux 上避免用sudo npm。还有一个非常典型的错误Failed to load tsconfig vue/tsconfig/tsconfig.web.json: tsconfig not found。这通常出现在项目里引用了 Vue 官方 TS 配置但对应包没装。解决办法是安装npm install -D vue/tsconfig或者在项目的tsconfig.json里删掉这个extends引用改成自己显式配置的 compilerOptions。这类问题的排查思路我后面会单独展开。6. 实际开发中绕不过去的几个坑从样式冲突到运行时报错的完整排查链路写 Vue 项目时间长了你会慢慢形成一套“报错处理”的肌肉记忆。这里挑几个高频且容易让人血压升高的场景来讲。6.1 样式冲突与 scoped 的真相热词里有“vue样式冲突”。症状是明明我在 A 组件里写的样式结果 B 组件的元素也长变了。原因几乎都是样式没有隔离。Vue 的style scoped并不是真正把样式隔离了它只是通过给当前组件模板里的元素加一个随机>!-- 子组件 -- slot/slot !-- 父组件 -- Child div{{ item.name }}/div !-- item 根本不存在 -- /Child正确做法是子组件把数据传给插槽slot :itemitem/slot父组件通过作用域插槽接收Child template #default{ item } div{{ item.name }}/div /template /Child另一个失误是默认插槽想传多个结构时以为写两个slot就行。其实一个组件里只能有一个默认插槽多个内容需要用具名插槽区分。如果发现页面里“有两个插槽的内容叠在一起”八成是把默认插槽和具名插槽混用了。6.3 运行时报错 “cannot read property of undefined” 的排查链路这是 vue 项目里最高频的错误之一。以我的经验排查链路大致是这样第一步看报错信息里明确指出了哪个文件、哪个表达式。Vue 的报错信息一般会给出组件路径和问题代码行定位后先看表达式访问的路径是obj.a.b还是arr[i].name。第二步检查数据源。如果是接口返回的数据打印接口原始返回确认字段结构是否和预期一致。最常见的是后端没返回a字段或返回字段名是a而不是aaa。第三步检查默认值。在data()里把访问对象初始化为非空data() { return { user: { address: {} }, list: [], }; }这样模板哪怕先渲染几帧也不会访问 undefined 的深层属性而报错。第四步如果数据是异步到达的而你又必须在模板中立即渲染可以配合v-if或可选链。比如v-ifuser.address确保数据存在后再渲染那一块。6.4 动态 m3u8 播放页面与组件卸载的配合问题回到前面举的播放器例子我再补充一个实际坑很多用hls.js的同学写播放器时只想着“加载流、播放”完全忘了组件卸载后该清理资源。页面刚开始还好多切换几次路由之后视频画面不再出现、控制台却报TypeError: Cannot read properties of undefined (reading destroy)。这个问题的本质是组件被复用了但旧的 Hls 实例还挂在this上。解决办法就是在beforeUnmountVue3 中是onBeforeUnmount里销毁实例并置空而且初始化时也要注意把实例挂到组件实例上而不是一个全局变量。如果项目里多处都要用播放器建议封装成独立组件内部管理好生命周期外部只需要传src属性就行。6.5 npm 安装依赖时常碰到的 tsconfig 报错怎么处理针对前面说的Failed to load tsconfig vue/tsconfig/tsconfig.web.json: tsconfig not found完整排查链路第一步确认这个引用是从哪来的。通常是项目模板的tsconfig.json或tsconfig.app.json里extends了vue/tsconfig包。第二步查看node_modules/vue/tsconfig是否存在。如果不存在说明依赖没装上或者版本不匹配。执行npm install -D vue/tsconfig或者npm install全量安装。第三步如果装不上检查package.json里是否有相关配置以及 npm 源是否正常。也可以直接把extends一行删掉把需要的 compilerOptions 显式写在项目里。我个人在遇到这类配置报错时最开始的解决思路一定不是硬记“报错 装某个包”因为同一类报错背后的原因可能完全不同。更稳妥的排查顺序永远是读完整报错 → 找到涉及的文件 → 确认依赖是否安装 → 查版本匹配 → 搜索错误关键字 → 定位到具体修复方案。7. 从能跑通到写出好项目我在 vue 项目里沉淀下来的几条判断标准最后聊聊一些“段位”层面的事。很多人学会写 Vue 组件之后还是觉得自己的代码“哪里不对”其实问题不在语法而在工程判断。第一模板里不要出现太多逻辑。如果一个插值表达式里面嵌套了两层箭头函数、调用了三个方法八成应该提炼成计算属性。模板是给人读的不是给编译器炫技的。我给自己定的一个模糊标准模板插值表达式的长度不超过一行且不做超过两次的方法调用。第二慎用 v-html 和全局事件。v-html会把字符串当 HTML 解析渲染如果字符串里有用户输入等于把 XSS 大门敞开。全局事件比如在window上绑定scroll、resize在组件销毁时要记得移除否则切页面后监听器还在运行页面卡顿就从这里来。第三写数据请求时一定要考虑组件卸载后的状态。比如组件先发起请求页面还没返回结果时用户就切走了请求回来之后去更新一个已卸载组件的状态控制台会提示警告。现在的相关库大多会忽略这种操作但最佳实践还是在卸载时用标识位拦截或者在请求完成时判断组件是否仍然存在。第四也是一直以来强调的遇到“HTML 转 Vue”的需求时别直接用复制粘贴的思路把 HTML 变成 template而是先想清楚哪些部分是重复出现的、应该是组件哪些是数据驱动的、应该是变量哪些是纯静态展示、直接放模板没问题。当你能从“这个 HTML 块属于谁”的角度去拆代码时才算真正理解了组件化。从我自己的经历来说从纯 htmljavaScript 转 vue 项目最大的门槛从来不是语法而是“每次动手写之前先想数据模型”的习惯。原生 JS 告诉你操作什么Vue 告诉你表达什么。这套思维一旦转过来你会发现很多之前觉得“要写好多代码”的功能在 vue 项目里往往只需要一段模板和一个数据字段就能完事。如果这篇能帮你少走几步弯路那就够了。真的想快速上手 vue 项目建议自己动手把一个以前写的静态页面迁移成 Vue 组件——迁移过程中遇到的那些“为什么这里不生效”、“为什么这个 this 是 undefined”的问题才是你最宝贵的成长素材。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进