
很多刚开始学前端的朋友都会跟我当年一样陷入同一种尴尬教程看了一堆文档翻了好几遍跟着视频能敲出代码可一旦关掉教程让自己独立写一个网站脑子立刻一片空白。这种感觉不是你不努力而是学习路径出了问题。今天这篇东西就是围绕Vue实战这条线把我从入门到能独立搭建网站的完整过程、踩过的坑、想明白的原理一次讲清楚。内容主线很简单先讲我在正式学Vue之前做的选择和准备再讲入门期最关键的思维转变然后是我认为真正卡住所有人的几个核心知识点接着用一个实际网站的搭建过程串起整个技能树最后复盘性能优化和那些高频出现的坑。初学的人照着这条路径走会省很多力气已经会写但没独立做过完整项目的人中间关于工程化和接口联调的部分也许能帮上忙。1. 学Vue之前我建议你先想明白这三件事1.1 为什么我最终选了Vue而不是别的框架先说实话我一开始在Vue和另一款流行框架之间犹豫了很久。那时候听到的声音大概分两派一边说Vue上手快、文档友好、适合中小型项目另一边说某框架生态更完整、大厂用得更多。我自己尝试过之后选Vue的理由非常朴实心智负担小能让我把注意力放在写出一个能用的东西上而不是耗在理解框架本身的抽象概念里。Vue的模板语法对从HTML/CSS/JavaScript转过来的人特别友好。它不是让你用JavaScript去拼一整棵页面结构而是在接近HTML的写法里插入数据绑定和指令。这种渐进增强的体验对第一门框架来说非常关键。我的体会是框架是工具不是信仰。第一门框架最重要的是让你快速形成我能独立做东西的正反馈循环。这个自信建立起来之后再横向学别的框架会发现很多概念都是相通的。1.2 入门资料怎么选文档、视频和玩具项目的正确比例这一点我想多说几句因为太多人栽在这上面。我见过不少朋友的做法是买一门上百集的视频课每天看三四集笔记做得工工整整但一动手就废。问题不在于视频不好而在于被动接收的比例太高了。我的用量大概是这样的官方文档过一遍核心概念不追求每行都记住知道哪个问题去哪个章节查就行视频只看那些带着做项目的实战讲解而且看完一个功能立刻自己敲一遍练手项目至少占一半时间。与其看完十集课程不如把一集里的例子自己改着玩两小时。我当时的第一个练手项目是一个极简记账本——输入金额、选择分类、展示列表、统计总和。这个项目小到当天就能跑通但又完整覆盖了数据绑定、事件处理、列表渲染、计算属性这些核心API。做完的那一刻比看完十集课都踏实。练手项目的目标不是炫技而是让你在最短时间内把输入→处理→输出这个闭环跑通。1.3 给第一阶段定一个能看到终点的目标没有目标的学习一定会变成无限期的准备中。我给自己定的第一阶段目标是这样一句话两周内独立做一个没有任何后端依赖的纯前端小工具并且部署到一个公网能访问的地址。这个目标有三层意思独立意味着不看付费课程的步骤遇到问题查文档、查搜索引擎没有后端依赖意味着把注意力全部集中在Vue本身先不碰接口联调部署到公网是很多人忽略的临门一脚——你以为写完代码就算会了其实能部署上线被别人用才是一个完整项目的终态。这种能看到终点的目标非常管用。你不再是学完某门课而是做完一个东西。心态完全不一样。2. 从操作页面到数据驱动入门期最难的思维转换2.1 同样是点赞功能jQuery和Vue的写法差在哪我学Vue之前有jQuery的使用经验最开始最大的障碍不是语法而是思维方式。在jQuery时代我们的习惯是页面上有一个按钮我就去找这个按钮给它绑事件然后手动修改它的文本和样式。一切围绕着操作DOM元素来思考。举个最简单的例子。一个点赞按钮点击后数字加一同时按钮变成红色。jQuery的写法大约是这样let count 0; $(#likeBtn).on(click, function () { count; $(#likeCount).text(count); $(this).css(color, red); });你发现没有每一步都在操心哪个元素要变成什么样。项目一复杂这种代码就会散落各处今天改这个按钮的逻辑明天可能就动了另一个地方的样式调试起来非常累。Vue的思路是反过来你不用管DOM怎么变你只维护一个数据状态。页面上的内容全部由数据投影出来。数据变了视图自动跟着变。script setup import { ref } from vue; const count ref(0); const liked ref(false); function toggleLike() { liked.value !liked.value; if (liked.value) count.value 1; } /script template button :style{ color: liked ? red : black } clicktoggleLike 点赞 {{ count }} /button /template两者的差别说白了就是jQuery是你指挥DOM干活Vue是你只管改数据DOM自己看着办。这个转换刚接触时会很不适应因为你会总觉得不亲手改一下DOM不放心。一旦适应过来你会发现代码里跟页面结构纠缠不清的逻辑大大减少了。2.2 模板语法和指令哪些值得背哪些用多了自然就懂Vue的指令数量其实不多v-if、v-for、v-model、v-bind、v-on、v-show、v-html、v-once加上几个修饰符基本就是日常高频使用的内容。我一开始总想着把每个指令的每种用法背下来后来发现完全没有必要。真正值得理解的是它们解决的问题v-if和v-show都是控制元素显示但一个是真的创建/销毁DOM一个是CSS的display切换。频繁切换用v-show条件基本不变用v-ifv-for是列表渲染配合key使用这个后面细讲v-model是双向绑定的语法糖本质上是:value和input的合体。刚开始理解不透没关系但你得知道它替你做了一次手动绑定v-bind是属性绑定缩写是冒号:hrefurl的意思是把url这个数据变量的值绑到href属性上v-on是事件监听缩写是clickfn的意思是点击时执行fn。这些内容用不着刻意背连续写两个小页面你就再也不会搞混:和了。真正需要下功夫的是理解模板里写的是表达式不是语句。我见过新手在模板里写let x 1这种然后一脸疑惑为什么报错。记住一点模板里适合做简单的展示逻辑复杂的计算应该放到computed或函数里去做。2.3 组件化不是把页面切碎而是找到稳定的边界组件化是Vue的核心思想之一但这也是新手特别容易走歪的地方。有些人一上来就把页面拆成十几个组件每个组件只有一小块结果组件之间通信传得晕头转向。有些人则完全不拆一个巨型组件写几千行维护起来同样痛苦。我的经验是组件化的本质不是拆得越多越好而是找到变化频率不同的稳定边界。一个东西如果能独立复用、有自己的内部状态、或者被多处使用那它就应该是一个组件。反之如果一块内容只出现一次并且和它所在页面的逻辑紧密耦合那留在页面里反而更清晰。举个实际例子。我后来做一个博客站时文章列表和文章详情是两个明显的组件。原因很简单列表会在首页、分类页、归档页多处出现详情页会被多篇文章复用。而页面上某个横幅广告只出现在首页那就没必要单独抽组件留在页面里反而直观。还有个实用的判断标准当你在写模板时开始频繁地复制粘贴某一块结构或者一个模板里的表达式变得越来越长这就是该拆组件的信号了。3. 真正卡住我的四个知识点响应式、生命周期、computed和组件通信3.1 响应式原理改数据为什么视图会自动更新这是很多初学者最想不通的问题凭什么我改了JavaScript里的一个变量页面就变了难道是页面一直在盯着这个变量看其实不是。Vue做了三件事第一追踪依赖。当你的模板里使用了某个响应式数据Vue就会把这个数据和模板的渲染函数关联起来建立一个依赖关系。第二监听变化。Vue在数据变化时能感知到。Vue 2时代用的是Object.defineProperty去劫持对象的属性读写Vue 3换成了Proxy可以直接拦截整个对象的读写、删除、新增等操作。这部分不需要你理解到源码级你只需要知道Vue知道你的数据什么时候变了。第三触发更新。数据一旦变化Vue会把依赖这个数据的组件标为待更新然后在下一个事件循环的时机重新执行渲染函数算出新的虚拟DOM跟旧的做对比diff最后把差异精准地更新到真实DOM上。这个机制我用一个不太精确但很好懂的类比跟你解释你在一家餐厅里点了一份套餐。厨房Vue记下了你点了什么依赖收集做好之后端上来渲染。如果你跟服务员说把可乐换成雪碧修改数据厨房不需要重新做整桌菜只需要把可乐那杯换成雪碧送上来精准更新。这就是响应式的本质。3.2 data为什么必须是一个函数如果你用的是Vue 2风格的选项式APIOptions API你肯定会注意到组件里的data必须是一个返回对象的函数而不能直接写成一个对象。为什么因为组件是用来复用的。如果data直接是一个对象那这个对象在内存里只有一份所有组件实例引用的都是同一个对象。你在一个组件实例里改了数据另一个组件实例的数据也跟着变了——因为它们是同一份内存。把它们变成函数之后每个实例在创建时都会调用一次这个函数拿到一份全新的数据副本。打个比方对象是公用的饮水机函数是每个人自带水杯。你以为每个组件在用自己那杯水结果用的都是同一个水龙头里流出的同一杯。在Vue 3的组合式APIComposition API里我们用ref和reactive创建响应式数据天然就是每个组件实例自己持有的这个坑就自然避免了。3.3 生命周期不是用来背的是来解决实际问题的在Vue 2里生命周期钩子有beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。到了Vue 3销毁钩子改名成beforeUnmount和unmounted并且用组合式API时你还会用带on前缀的函数比如onMounted、onUpdated。新手最容易犯的错是把这些钩子当八股文背考试能答出来但实际开发不知道怎么用。我的建议是你只需要重点关注三个时机。第一个时机组件创建后、渲染前。对应Vue 2的created或Vue 3的setup时期。这个阶段可以初始化一些非响应式的数据但DOM还没生成拿不到DOM节点。第二个时机组件挂载完成。对应mounted或onMounted。这个阶段DOM已经渲染完毕适合做需要访问DOM的初始化操作也适合发起接口请求。很多人会问接口请求到底放created还是mounted我实践下来的习惯是放在onMounted里。原因很简单请求发起的早晚不是关键关键是响应回来后你要更新的数据得已经进入响应式系统。放在onMounted里逻辑上清晰地表示组件已经准备好了可以去拿数据了。第三个时机组件销毁前。对应beforeUnmount或onBeforeUnmount。这里最重要的事是清理清除定时器、解绑事件监听、关闭WebSocket连接。我见过有同学在组件里开了个定时器组件销毁了定时器还跑着结果触发一堆报错。这个坑只要在销毁钩子里清一次就永远不会再踩。3.4 computed、watch、methods三选一还是混着用这三个东西刚学时容易混好像都能执行一段逻辑到底用哪个我说说我的使用规则。methods是普通的函数。它没有缓存每次在模板里被调用都会重新执行。适合做事件处理或者不需要缓存的临时计算。computed是计算属性。它有缓存只要它依赖的响应式数据没有变化反复访问它也不会重新计算。适合做从已有数据派生出新数据的场景。比如购物车里的总价是由商品列表和数量算出来的。这个场景用methods也能算出正确结果但每次渲染都要重复计算性能浪费不说代码也不够声明式。watch是侦听器。它侦听一个数据的变化然后做一件事。适合跟数据变化之后产生的副作用打交道。比如搜索框的值变了需要防抖地去请求后端接口再比如根据用户是否登录动态跳转路由。我总结成一句话能用computed解决的优先用computed解决不了的需要在数据变化时主动触发逻辑的用watch事件处理和模板里的临时逻辑用methods。我用一个表格给你列一下区别特性methodscomputedwatch是否缓存结果不缓存有缓存不缓存但机制不同适合场景事件处理、临时调用派生数据数据变化后的副作用处理是否必须写返回值视情况必须返回表达式的结果不需要返回模板中是否可访问可调用可直接当变量用不直接在模板中使用记住这个区分方式你写代码时的思路会清晰很多。4. 独立搭建第一个网站从项目规划到部署上线的完整过程4.1 定需求一个页面也不写先把网站想清楚学完上面的基础内容之后我开始做真正独立的完整项目。我选择的题材是一个个人作品集网站——展示自己做的几个小作品附带个人介绍和联系方式。为什么选这个因为它的功能边界很清晰一个首页、一个作品列表页、一个作品详情页可能加一个关于我的页面。复杂度刚好。动工之前我给自己列了一份需求清单这份清单在整个开发过程中帮我省了太多力气页面结构导航栏、底部信息栏、四个页面作品列表从本地JSON文件读取数据支持按分类筛选作品详情根据路由参数显示对应内容一个简单的联系我表单前端做格式校验不接后端部署要求静态部署支持刷新页面时路由不404。我特别想强调路由刷新不404这条。很多纯前端项目部署到Nginx后直接访问根路径没问题但一刷新子路径比如/detail/1就报404。这是因为前端路由是浏览器的历史模式跳转时并没有真的向后端请求那个路径的文件但刷新时浏览器会按真实路径请求一次服务器没有对应的文件或重写规则就404了。这个问题我在部署时专门处理过后面细说。4.2 工程化选型Vite Vue 3 Vue Router Pinia现在做新项目我的选型基本固定在这一套Vite作为构建工具Vue 3组合式APIscript setup语法作为框架Vue Router负责路由Pinia负责全局状态管理。没有选Vue CLI的原因很简单Vite在开发服务器的启动速度和热更新体验上明显更快而且它就是现在官方推荐的默认方案。创建项目的命令极简npm create vitelatest my-portfolio -- --template vue然后安装路由和状态管理npm install vue-router pinia这步之后我会把项目里预设的HelloWorld组件之类全删掉并调整目录结构。我的习惯是这样一个分层src/ api/ # 接口请求层 assets/ # 静态资源 components/ # 通用组件 router/ # 路由配置 stores/ # Pinia状态 views/ # 页面级组件 styles/ # 全局样式这套结构不是标准答案但它能让我一眼就定位到某个职责的代码。项目小的时候怎么放都无所谓但一开始就养成按职责分层的习惯后面项目变大时你会感谢自己。4.3 路由和组件拆分让项目不绕晕的关键这个项目里路由配置大概是这样的import { createRouter, createWebHistory } from vue-router; const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: () import(/views/HomeView.vue) }, { path: /works, name: works, component: () import(/views/WorksView.vue) }, { path: /work/:id, name: work-detail, component: () import(/views/WorkDetailView.vue) }, { path: /about, name: about, component: () import(/views/AboutView.vue) }, ], });这里用了路由懒加载() import(...)会让每个页面在访问时才被加载而不是打包成一个巨大的文件。对于作品集这种网站首屏加载速度是很重要的用户体验指标路由懒加载是性价比很高的一步优化。组件的拆分我是跟着页面走的每个视图页面是一个大组件页面内部有可复用的部分再拆成子组件。比如作品列表卡片是一个WorkCard组件因为它在列表页和详情页的相关推荐里都会出现。导航栏和底部信息是全局组件在根组件里引入一次所有页面共享。这里的关键是组件的拆分角度是复用性和职责边界而不是看起来应该分一下。4.4 对接后端接口axios封装和环境变量的那些事虽然这个项目的数据来自本地JSON文件但在后续另一个项目里我需要对接真实后端接口所以axios的封装经验也是那会儿一次性总结下来的。首先我会在api目录下放一个统一的请求实例import axios from axios; const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000, }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) config.headers.Authorization Bearer ${token}; return config; }); request.interceptors.response.use( (response) response.data, (error) { if (error.response error.response.status 401) { // 未登录或登录过期跳转登录页 } return Promise.reject(error); } );这里有两个细节值得新人注意。第一环境变量的使用方式。在Vite构建的项目里环境变量要写在.env文件中并且只有以VITE_开头的变量才会被暴露到前端代码中。所以我在开发环境和生产环境分别建了.env.development和.env.production里面配置不同的VITE_API_BASE_URL。第二统一拦截器的作用。如果你在每个页面里都手动axios.get一旦需要统一处理登录过期或者统一加token你就得改几十个地方。做了封装之后只需要改拦截器这一处。4.5 部署上线本地跑通了只是第一步网站开发完本地npm run dev一切正常这只是第一步。部署环节有很多门道。我选择的方案是构建静态文件放到一台服务器上用Nginx提供访问。构建命令非常简单npm run build产物在dist/目录下。但如果你直接把dist扔到Nginx的根目录刷新路由404的问题立刻就会出现。原因前面说过前端路由是浏览器端模拟的路径跳转服务器上并不存在对应的物理文件。解决办法是在Nginx配置里加一条try_files指令location / { try_files $uri $uri/ /index.html; }这条指令的意思是如果请求的路径能匹配到真实文件就返回文件否则一律返回index.html让前端路由接管。配好之后刷新子路径就不会404了。另外还有一个容易忽略的点如果你用到了createWebHistory打包后直接双击index.html打开是白屏的路由系统会找不到路径。这也是我刚部署时踩的坑。后来我查了资料才知道历史模式路由需要服务器配合。如果你没有服务器配置能力可以暂时用createWebHashHistory地址栏会多一个#号但刷新不会404。从用户体验角度我还是建议用历史模式正确的Nginx配置。5. 项目做完之后的复盘性能优化与高频踩坑清单5.1 v-if和v-for优先级问题Vue 3里反过来了这个坑我印象太深了。在Vue 2中v-for的优先级比v-if高所以在同一个元素上同时写这两个指令意味着v-if会在每一次循环里都被执行——这通常不是你想要的效果性能也有浪费。到了Vue 3优先级反过来了v-if的优先级比v-for高。这会导致一个更隐蔽的问题你在v-if里判断用到的变量可能根本还没被v-for创建出来直接报错。举个例子如果你想遍历一组数据只显示其中已发布的新手很容易这样写template div v-foritem in list v-ifitem.published :keyitem.id {{ item.title }} /div /template在Vue 3里v-if会先执行而此刻item根本不存在于是控制台直接报错。正确做法有两个一是把判断逻辑放进计算属性里先过滤数据再遍历script setup import { computed } from vue; const publishedList computed(() list.value.filter(item item.published)); /script template div v-foritem in publishedList :keyitem.id {{ item.title }} /div /template二是把v-if放在外层template标签上。但不管哪种最推荐的是第一种用computed先处理数据。模板里的逻辑越简单越好这也是上一章我们讲computed时总结过的原则。5.2 列表渲染的key不只是为了消除警告写v-for时不加key控制台会报警告很多人为了消除警告随便给个index。但我后来才真正理解key不是用来讨好控制台的它是Vue做diff时的身份标识。Vue在更新列表时会拿新旧虚拟DOM做对比。如果列表中的某一项位置变了、或者内容变了Vue需要知道这一项到底是不是原来那一项。用index做key的问题在于当你在列表中间插入一条数据后面所有项的index都变了。而Vue会认为每个index对应的组件其实是同一个组件于是复用了错误的内部状态还可能导致渲染错乱。最理想的key是一个在整个列表中稳定且唯一的标识比如数据的id字段。如果数据本身没有id退而求其次用内容拼接的唯一值也行但尽量别用index。这个我后面写动画或处理带输入框的列表时体会特别深。5.3 异步更新与nextTick数据改了DOM还没来得及变Vue的响应式更新不是同步的。你修改一个数据视图不会立刻更新而是会被放进一个更新队列等到当前事件循环的合适时机统一刷新。这么做是为了性能——如果一次连续改十个数据十个都立刻触发DOM更新就太浪费了合并成一次更新效率更高。但这也带来一个常见问题你改了数据紧接着代码里去获取某个DOM元素的内容拿到的还是旧值。我第一次遇到这个情况时一头雾水。解决办法就是nextTick。它能在DOM更新完成后执行回调script setup import { ref, nextTick } from vue; const message ref(); async function changeMessage() { message.value 新内容; // DOM还没更新 console.log(document.querySelector(.msg).textContent); // 旧内容 await nextTick(); // DOM已更新 console.log(document.querySelector(.msg).textContent); // 新内容 } /script知道这个机制之后很多奇怪的问题都有了解释。如果你在修改状态后马上依赖DOM状态去判断下一步逻辑一定记得等nextTick。5.4 长列表和懒加载让网站打开更快的小技巧项目上线后我有个朋友提醒我看看首屏加载体积。结果一测打出来的包居然有将近1MB。对于作品集这种轻量级网站来说太笨重了。后来我做了几个优化第一路由懒加载前面提到的按页面拆包。第二按需引入UI组件库。如果用了Element Plus这类库只注册用到的组件别一股脑全量引入。Vite的项目里配合unplugin-vue-components这种插件自动按需引入很方便。第三长列表的虚拟滚动。我这个项目列表量不大还用不上。但在另一个后台管理系统里一个表格有上万条数据直接渲染会导致页面卡顿。那里我用了虚拟滚动思路很简单只渲染当前可视区域内的那部分数据滚动时动态替换。你可以把虚拟滚动理解成你坐火车时透过窗看到的那几根电线杆——你不可能看到整条铁路沿线几千根电线杆但画面依然连续。这个类比虽然糙但原理就是这个方向。第四图片懒加载。作品集网站里有很多图片。用loadinglazy属性可以延迟加载屏幕外的图片对首屏性能提升很明显。我还在图片外层包了一层占位容器设置了固定宽高比避免图片加载时页面布局来回跳动。做完整套优化之后整个包的体积从接近1MB降到了大概250KB首屏加载速度明显变快。优化不是炫技是让真正访问你网站的人有更好的体验。这一步做下来我对上线的概念才算有了真正的理解。结尾学习Vue这件事最值得投资的是一遍完整的循环如果让我回头看这段从入门到独立搭建网站的路我最大的感受是真正让你成长的不是背熟了多少API而是完整地走完想清楚需求→写代码→遇到bug→查资料→修复→上线→被用户反馈→继续优化这一整个循环。每走完一遍循环你对Vue、对前端开发的理解都会上一个台阶。最后再分享两个实用的小建议。一个是一定要养成写踩坑笔记的习惯不用多正式随便一个文档或者备忘录就行。很多坑你当时查一两个小时才解决过两个月又会遇到翻笔记三分钟就能搞定。另一个是多看看别人项目的源码尤其是那些star很多的开源项目。你不一定能看懂全部但哪怕从读一个组件的写法开始都会发现很多自己没想到的代码组织方式。如果你正在学Vue的路上别怕慢也别怕出错。出错的次数越多你积累的经验就越扎实。动手写吧。