很多刚开始学前端的朋友都会跟我当年一样,陷入同一种尴尬:教程看了一堆,文档翻了好几遍,跟着视频能敲出代码,可一旦关掉教程,让自己独立写一个网站,脑子立刻一片空白。这种感觉不是你不努力,而是学习路径出了问题。今天这篇东西,就是围绕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' }" @click="toggleLike"> 点赞 {{ 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-if;v-for是列表渲染,配合key使用,这个后面细讲;v-model是双向绑定的语法糖,本质上是:value和@input的合体。刚开始理解不透没关系,但你得知道它"替你做了一次手动绑定";v-bind是属性绑定,缩写是冒号,:href="url"的意思是把url这个数据变量的值绑到href属性上;v-on是事件监听,缩写是@,@click="fn"的意思是点击时执行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风格的选项式API(Options API),你肯定会注意到:组件里的data必须是一个返回对象的函数,而不能直接写成一个对象。为什么?
因为组件是用来复用的。如果data直接是一个对象,那这个对象在内存里只有一份,所有组件实例引用的都是同一个对象。你在一个组件实例里改了数据,另一个组件实例的数据也跟着变了——因为它们是同一份内存。把它们变成函数之后,每个实例在创建时都会调用一次这个函数,拿到一份全新的数据副本。
打个比方:对象是"公用的饮水机",函数是"每个人自带水杯"。你以为每个组件在用自己那杯水,结果用的都是同一个水龙头里流出的同一杯。在Vue 3的组合式API(Composition 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。我用一个表格给你列一下区别:
| 特性 | methods | computed | watch |
|---|---|---|---|
| 是否缓存结果 | 不缓存 | 有缓存 | 不缓存,但机制不同 |
| 适合场景 | 事件处理、临时调用 | 派生数据 | 数据变化后的副作用处理 |
| 是否必须写返回值 | 视情况 | 必须返回表达式的结果 | 不需要返回 |
| 模板中是否可访问 | 可调用 | 可直接当变量用 | 不直接在模板中使用 |
记住这个区分方式,你写代码时的思路会清晰很多。
4. 独立搭建第一个网站:从项目规划到部署上线的完整过程
4.1 定需求:一个页面也不写,先把网站想清楚
学完上面的基础内容之后,我开始做真正独立的完整项目。我选择的题材是一个"个人作品集网站"——展示自己做的几个小作品,附带个人介绍和联系方式。为什么选这个?因为它的功能边界很清晰:一个首页、一个作品列表页、一个作品详情页,可能加一个关于我的页面。复杂度刚好。
动工之前我给自己列了一份需求清单,这份清单在整个开发过程中帮我省了太多力气:
- 页面结构:导航栏、底部信息栏、四个页面;
- 作品列表:从本地JSON文件读取数据,支持按分类筛选;
- 作品详情:根据路由参数显示对应内容;
- 一个简单的"联系我"表单,前端做格式校验,不接后端;
- 部署要求:静态部署,支持刷新页面时路由不404。
我特别想强调"路由刷新不404"这条。很多纯前端项目部署到Nginx后,直接访问根路径没问题,但一刷新子路径比如/detail/1就报404。这是因为前端路由是浏览器的历史模式,跳转时并没有真的向后端请求那个路径的文件,但刷新时浏览器会按真实路径请求一次,服务器没有对应的文件或重写规则就404了。这个问题我在部署时专门处理过,后面细说。
4.2 工程化选型:Vite + Vue 3 + Vue Router + Pinia
现在做新项目,我的选型基本固定在这一套:Vite作为构建工具,Vue 3(组合式API+script setup语法)作为框架,Vue Router负责路由,Pinia负责全局状态管理。没有选Vue CLI的原因很简单:Vite在开发服务器的启动速度和热更新体验上明显更快,而且它就是现在官方推荐的默认方案。
创建项目的命令极简:
npm create vite@latest 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-for="item in list" v-if="item.published" :key="item.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-for="item in publishedList" :key="item.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这种插件自动按需引入很方便。
第三,长列表的虚拟滚动。我这个项目列表量不大,还用不上。但在另一个后台管理系统里,一个表格有上万条数据,直接渲染会导致页面卡顿。那里我用了虚拟滚动,思路很简单:只渲染当前可视区域内的那部分数据,滚动时动态替换。你可以把虚拟滚动理解成"你坐火车时透过窗看到的那几根电线杆"——你不可能看到整条铁路沿线几千根电线杆,但画面依然连续。这个类比虽然糙,但原理就是这个方向。
第四,图片懒加载。作品集网站里有很多图片。用loading="lazy"属性可以延迟加载屏幕外的图片,对首屏性能提升很明显。我还在图片外层包了一层占位容器,设置了固定宽高比,避免图片加载时页面布局来回跳动。
做完整套优化之后,整个包的体积从接近1MB降到了大概250KB,首屏加载速度明显变快。优化不是炫技,是让真正访问你网站的人有更好的体验。这一步做下来,我对"上线"的概念才算有了真正的理解。
结尾:学习Vue这件事,最值得投资的是一遍完整的循环
如果让我回头看这段从入门到独立搭建网站的路,我最大的感受是:真正让你成长的不是背熟了多少API,而是完整地走完"想清楚需求→写代码→遇到bug→查资料→修复→上线→被用户反馈→继续优化"这一整个循环。每走完一遍循环,你对Vue、对前端开发的理解都会上一个台阶。
最后再分享两个实用的小建议。一个是一定要养成写"踩坑笔记"的习惯,不用多正式,随便一个文档或者备忘录就行。很多坑你当时查一两个小时才解决,过两个月又会遇到,翻笔记三分钟就能搞定。另一个是多看看别人项目的源码,尤其是那些star很多的开源项目。你不一定能看懂全部,但哪怕从读一个组件的写法开始,都会发现很多自己没想到的代码组织方式。
如果你正在学Vue的路上,别怕慢,也别怕出错。出错的次数越多,你积累的经验就越扎实。动手写吧。