我直接讲结论:如果你正在纠结“Vue2 和 Vue3 到底先学哪个”,这篇文章就是给你写的。我会先用通俗的方式把 Vue2 和 Vue3 的核心区别讲透,再给出一条明确的学习路线,然后带着你从环境配置、组件实战、路由配置,一路做到在 Vue 项目里播放 m3u8 视频流。这些都是实际项目中高频使用的能力,也顺带覆盖了面试常问的 Vue 题目。整个教程不需要你有很深的前端基础,只要会 HTML、CSS 和一点 JavaScript,就能跟着走完。
很多新人一上来就搜“Vue3 教程”,结果看到组合式 API 直接懵了,因为网上大量旧教程还是 Vue2 写法的。要解决这个问题,不能只背区别,你要先理解 Vue2 和 Vue3 的设计思路差异,再决定从哪里入手。下面开始。
1. Vue2 和 Vue3 区别:先搞懂这 3 个核心差异
1.1 选项式 API 和组合式 API:代码组织方式的变革
Vue2 最经典的写法是选项式 API,所有功能按照 data、methods、computed、watch 这些选项分开存放。比如一个简单的用户列表,你可能要在 data 里定义 list 和 loading,在 methods 里写 fetchList 和 deleteUser,在 computed 里计算 totalCount。当组件越来越大,你会发现同一个业务逻辑的代码被拆散到各个选项里,改一个功能要上下翻无数次。
Vue3 的组合式 API 解决的是这个问题。它把同一个逻辑相关的代码放在一起,按照功能模块来组织。用生活里的话说,Vue2 的方法像超市货架,所有饮料放一排、所有零食放一排;Vue3 的方法像一个菜篮子,做一道菜需要的菜、肉、调料都放同一个篮子里。这样做的好处是,组件再大,你能按功能快速定位到相关代码,维护成本直线下降。
有人会问:组合式 API 是不是完全替代了选项式?并不是。Vue3 依然支持选项式写法,你可以在一个组件里混用,但官方推荐新项目用组合式,因为它在逻辑复用和代码组织上更有优势。我见过不少老项目从 Vue2 迁到 Vue3 时保留选项式写法,这也没问题,只是享受不到组合式带来的收益。
1.2 响应式原理:Object.defineProperty 换成 Proxy
这是 Vue2 和 Vue3 最底层的区别,也是面试必问题。Vue2 的响应式靠 Object.defineProperty 实现,它只能劫持对象已有属性的读取和修改。所以 Vue2 有个著名的坑:给对象动态新增属性时,页面不会更新,必须用 this.$set 才能触发响应式。数组也有类似限制,通过索引直接修改数组元素时无法被监听,需要特殊处理。
Vue3 改用了 Proxy,它是直接拦截整个对象,而不是针对单个属性。这意味着无论新增属性、删除属性还是修改数组下标,都能被实时监听,不再需要 $set。同时在获取属性时才触发依赖收集,而不是像 Vue2 那样一开始就递归遍历所有属性,所以大数据量场景下初始化更快、内存占用更小。
除此之外,Vue3 在编译层还做了很多优化。比如静态标记(PatchFlag),在模板编译阶段就把静态节点和动态节点区分开,更新的时只需要精确对比动态部分;还有事件缓存、Fragment 支持多根节点等。这些优化加在一起,让 Vue3 在大型应用里的性能明显优于 Vue2。实际测试中,同样的组件树,Vue3 的渲染更新速度通常比 Vue2 快一倍以上。
1.3 生命周期、模板语法和其他细碎差异
生命周期方面的变化很直观。Vue2 的 beforeDestroy 和 destroyed 在 Vue3 里改成了 beforeUnmount 和 unmounted,职责一样,但语义更准确。同时新增了 onRenderTracked、onRenderTriggered 这类调试钩子,方便追踪响应式依赖。
Vue3 的 v-model 也有变化。在 Vue2 中,v-model 默认绑定 value 属性并监听 input 事件;Vue3 中改为绑定 modelValue 并监听 update:modelValue,这意味着自定义组件的 v-model 更灵活,支持多个 v-model 绑定。
还有全局 API 的调整。Vue.use 变成了 app.use,Vue.component 变成了 app.component,new Vue() 变成了 createApp()。这些都和 Vue3 的“应用程序实例”概念强绑定。实际项目里,如果你从 Vue2 平移到 Vue3,会发现很多东西的调用方式都变了,但思想是一致的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 学前准备:Vue 安装与环境配置保姆级操作
2.1 Node.js 和包管理器版本选择
不管用 Vue2 还是 Vue3,第一步都是装 Node.js。建议直接安装 18 或 20 的 LTS 版本,别用太老的 14,否则后续装依赖容易报错。装完 Node.js 后,npm 会自动带上,所以你不需要单独安装 npm。
包管理器方面,npm、yarn、pnpm 都可以。我个人的建议是:新项目用 pnpm,因为它安装速度最快、磁盘占用少;老项目或者公司统一用 npm 时,就跟着用 npm。不需要纠结,核心是能安装依赖。
如果你机器上已经有 Node.js,可以用一条命令检查版本:
bash复制node -v
npm -v
如果 node -v 能输出版本号,说明环境 OK,可以继续。
2.2 用 Vite 创建 Vue3 项目:为什么官方推荐它
Vue3 官方现在默认推荐 Vite 作为构建工具,不再推荐 Vue CLI。原因很直接:Vite 基于原生 ES Module,开发服务器启动时不用像 webpack 那样把整个项目打包一遍,而是按需编译,所以冷启动基本在毫秒到秒级。热更新(HMR)也是精准到模块级别,改一个组件只刷新那一个组件,大项目里体验非常明显。
创建一个 Vue3 项目,最标准的方式是:
bash复制npm create vue@latest
这个命令会引导你进行交互式配置,比如是否使用 TypeScript、是否安装 Vue Router、是否使用 Pinia 等。如果你不想交互,也可以直接用 Vite 模板:
bash复制npm create vite@latest my-vue-app -- --template vue
然后进入项目目录,安装依赖,启动开发服务器:
bash复制cd my-vue-app
npm install
npm run dev
启动后浏览器访问终端里打印的地址,一般默认是 http://localhost:5173,能看到 Vue 的欢迎页就说明环境没问题。
2.3 用 Vue CLI 创建 Vue2 项目:老项目维护场景
如果你的目标是维护已有老项目,那么 Vue CLI 依然是主力。全局安装 Vue CLI:
bash复制npm install -g @vue/cli
然后创建 Vue2 项目:
bash复制vue create my-vue2-app
在交互选项里选择 “Default (Vue 2)” 即可。
需要注意,Vue CLI 4.x 之后已经比较长时间没有大版本更新,新项目不建议再用它,但老项目维护时你可能会接触到。我的建议是:Vue2 只需要能看懂、会改,不需要在新项目里花太多时间搭建环境。
2.4 环境变量的区别:VUE_APP_ 和 VITE_
很多时候项目会用到环境变量,比如 BASE_URL、接口地址。Vue2(Vue CLI)的环境变量必须以 VUE_APP_ 开头,在代码里通过 process.env.VUE_APP_XXX 读取。Vue3 + Vite 的环境变量必须以 VITE_ 开头,通过 import.meta.env.VITE_XXX 读取。这个区别不大,但很容易踩坑。我见过不少从 Vue2 迁到 Vue3 的同事,直接在 .env 里写 VUE_APP_API_URL,然后在 Vue3 里读不到,就是因为前缀不对。
实际上,import.meta.env 是 Vite 暴露给前端代码的全局对象,而 process.env 是 Node.js 的全局对象,Vue CLI 在编译时把 VUE_APP_ 前缀的变量注入进去了。理解这个背景后,你就能记住 Vue3 必须用 VITE_ 前缀。
3. Vue3 组合式 API 核心实操:从零手写一个计数器
3.1 setup 到底怎么用?
组合式 API 的核心入口是 setup。在 Vue3 的 <script setup> 语法中,你不需要显式写 setup() 函数,因为整个 <script setup> 块就是 setup 的语法糖。
先看最基础的计数器:
vue复制<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">加 1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
这段代码的逻辑很简单,但你要理解 ref 的本质。count = ref(0) 返回的是一个包装过的对象,对象里的值在 count.value 上。模板里自动解包,所以直接写 {{ count }} 就可以,不用写 count.value。但在 JavaScript 逻辑中,你必须用 count.value 修改值。
为什么 Vue3 要用 ref 包装基础类型?因为 Proxy 只能代理对象,不能代理一个 number 或 string,所以 Vue3 把基础类型放进一个 { value: xxx } 的对象里,再用 Proxy 包裹这个对象,这样就能实现响应式了。
如果是一个对象,推荐用 reactive:
vue复制<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
list: []
})
function add() {
state.count++
state.list.push(state.count)
}
</script>
用 reactive 时不需要 .value,直接 state.count 就行。但注意,reactive 只能用于对象,不能用于基础类型。如果你解构 reactive 对象里的字段,比如 const { count } = state,得到的 count 会失去响应式,正确做法是用 toRefs 把它转成 ref。这个坑特别常见,建议你多写几遍。
3.2 computed 和 watch 在 Vue3 中的写法
Vue2 的 computed 是一个配置项,Vue3 里它是一个 API。用法如下:
vue复制<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(2)
const total = computed(() => price.value * quantity.value)
</script>
computed 返回一个只读的 ref,所以模板里直接用 {{ total }},JS 里用 total.value。如果你需要给计算属性赋值,可以传入有 get 和 set 的对象,但大多数场景不需要。
watch 也有变化。Vue2 里通过 watch 配置项监听,Vue3 里可以用 watch 函数:
vue复制<script setup>
import { ref, watch } from 'vue'
const query = ref('')
watch(query, (newVal, oldVal) => {
console.log('搜索词从', oldVal, '变成了', newVal)
})
</script>
还有一个 watchEffect,它不需要指定监听谁,而是自动收集回调里用到的响应式依赖,依赖变化时重新执行。适合做调试、日志、异步请求这类副作用操作。
3.3 组件通信:defineProps、defineEmits 和 provide/inject
在 <script setup> 中,父组件给子组件传值,子组件用 defineProps 声明:
vue复制<!-- Child.vue -->
<script setup>
const props = defineProps({
title: {
type: String,
default: '默认标题'
}
})
</script>
<template>
<h2>{{ title }}</h2>
</template>
子组件给父组件发事件用 defineEmits:
vue复制<script setup>
const emit = defineEmits(['update'])
function handleClick() {
emit('update', '一些数据')
}
</script>
父组件这样接收:
vue复制<template>
<Child @update="handleUpdate" />
</template>
对于跨层级传递数据,Vue3 推荐用 provide 和 inject。顶层组件提供数据:
vue复制<script setup>
import { provide, ref } from 'vue'
const user = ref({ name: '张三' })
provide('user', user)
</script>
任何子组件都可以注入:
vue复制<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>
这个模式在写可复用的业务组件库里很常用。和 Vue2 的 $parent、$children 相比,provide/inject 的耦合度更低,也更清晰。
4. Vue3 路由实战:Vue Router 4 从安装到动态路由
4.1 安装与基础配置
Vue3 对应的是 Vue Router 4,安装命令:
bash复制npm install vue-router@4
然后在 src/router/index.ts 中创建路由实例:
ts复制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
注意几个关键点。createRouter 是 Vue3 的全局路由工厂函数,替代了 Vue2 的 new Router()。history: createWebHistory() 表示用 HTML5 History 模式,URL 里不带 #;如果你要用 hash 模式,改为 createWebHashHistory() 即可。
然后在入口文件 main.ts 里注册:
ts复制import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
模板里用 <router-view></router-view> 渲染当前路由对应的组件。
4.2 路由懒加载、命名视图和重定向
路由懒加载是最基本也最有效的性能优化手段。上面代码里 () => import('../views/About.vue') 就是懒加载,只有访问 /about 时才会加载这个组件对应的 JS 文件。我建议所有非首屏路由都用懒加载,尤其是后台管理系统,路由一多,打包包体大小差异非常大。
命名视图适合一个页面多个出口的场景。比如布局页有 Header、Sidebar、Main 三个区域,在某个路由下你可能想同时渲染多个组件:
ts复制const routes = [
{
path: '/dashboard',
components: {
default: () => import('../views/Dashboard.vue'),
header: () => import('../components/Header.vue'),
sidebar: () => import('../components/Sidebar.vue')
}
}
]
对应模板里:
html复制<router-view></router-view>
<router-view name="header"></router-view>
<router-view name="sidebar"></router-view>
重定向也很简单:
ts复制{ path: '/', redirect: '/home' }
这在调整站点结构时非常实用,旧地址保持可用,用户访问时就自动跳到新地址。
4.3 路由守卫:登录鉴权实战
路由守卫是后台系统里绕不开的需求。最常见的场景是:未登录用户访问需要权限的页面时,自动跳转到登录页。
在 Vue Router 4 里,全局前置守卫这样写:
ts复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
next()
}
})
然后在路由配置里给需要权限的路由加上 meta:
ts复制{
path: '/admin',
component: () => import('../views/Admin.vue'),
meta: { requiresAuth: true }
}
这样访问 /admin 时,如果本地没有 token,就会跳转到 /login?redirect=/admin,登录成功后可以再跳回原页面。这个模式在真实项目中非常常用,几乎每个后台系统都会用。
Vue3 的组合式 API 里还有组件内守卫,比如 onBeforeRouteLeave,可以在用户离开当前页面时做拦截,比如表单未保存时提示确认。这些在实际项目中也很实用。
5. Vue 播放 m3u8 视频流:HLS 播放器实战
5.1 m3u8 是什么?为什么浏览器不能直接播放
m3u8 是 HLS(HTTP Live Streaming)协议下的视频索引文件格式,最早是苹果提出的,现在广泛用于直播、点播和大视频文件分发。它的核心思路是:不把整个视频打包成一个大文件下发,而是把视频切成很多小片段(通常 3 到 10 秒一个 .ts 文件),通过 m3u8 索引文件按顺序播放。
但浏览器原生 <video> 标签不支持 m3u8 格式,尤其 Chrome、Firefox 都不直接支持 HLS。所以前端要播放 m3u8,必须借助 JavaScript 库,最常用的是 hls.js。它在浏览器里通过 Fetch 拉取 m3u8 索引和 ts 分片,再利用 Media Source Extensions(MSE)把视频流喂给 <video> 标签。
5.2 在 Vue3 中集成 hls.js 完整代码
先安装依赖:
bash复制npm install hls.js
然后写一个可复用的播放器组件:
vue复制<template>
<video ref="videoRef" controls autoplay muted></video>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Hls from 'hls.js'
const props = defineProps({
src: {
type: String,
required: true
}
})
const videoRef = ref(null)
let hls = null
function play() {
const video = videoRef.value
if (!video) return
if (Hls.isSupported()) {
hls = new Hls({
// 出现加载错误时自动降级重试
fragLoadingMaxRetry: 6,
manifestLoadingMaxRetry: 6
})
hls.loadSource(props.src)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(() => {})
})
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// 针对 Safari,它是原生支持 HLS 的
video.src = props.src
video.addEventListener('loadedmetadata', () => {
video.play().catch(() => {})
})
}
}
function destroyPlayer() {
if (hls) {
hls.destroy()
hls = null
}
}
onMounted(play)
onUnmounted(destroyPlayer)
</script>
使用方式:
vue复制<template>
<HlsPlayer src="https://example.com/live/stream.m3u8" />
</template>
<script setup>
import HlsPlayer from '@/components/HlsPlayer.vue'
</script>
这里有几个细节值得注意。第一,onUnmounted 里必须调用 hls.destroy(),否则播放器实例和网络请求会一直占用资源,造成内存泄漏。第二,直播流地址经常需要携带 token 参数,你可以在 loadSource 时直接把完整地址传进去。第三,如果视频自动播放被浏览器拦截,play() 返回的 Promise 会 reject,所以要加 .catch() 兜底,避免控制台报错。
5.3 Vue2 项目里播放 m3u8 的兼容写法
还是有不少项目在维护 Vue2,播放 m3u8 的写法类似,只是生命周期和响应式写法不同。常见做法是用 video.js 加 videojs-contrib-hls 插件,或者直接用 video.js 7 的默认 HLS 支持。
用 video.js 的基础代码:
vue复制<template>
<video
ref="video"
class="video-js vjs-big-play-centered"
controls
></video>
</template>
<script>
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
export default {
name: 'VideoPlayer',
props: {
src: { type: String, required: true }
},
data() {
return { player: null }
},
mounted() {
this.player = videojs(this.$refs.video, {
sources: [{ src: this.src, type: 'application/x-mpegURL' }]
})
},
beforeDestroy() {
if (this.player) {
this.player.dispose()
}
}
}
</script>
注意 Vue2 的销毁钩子叫 beforeDestroy,Vue3 改成了 beforeUnmount。很多老代码迁移时改个钩子名就能跑起来。
6. Vue 学习顺序规划与面试题解
6.1 到底先学 Vue2 还是直接学 Vue3?
这是标题里最核心的问题,我说下我的结论。
如果你是零基础、目标是找前端工作:直接学 Vue3,但务必先补一下 JavaScript 基础,尤其是 ES6 的模块化、解构赋值、箭头函数、Promise 这些。现在大部分新项目和招聘要求都围绕 Vue3 + TypeScript,你花太多时间在 Vue2 上反而耽误进度。
如果你已经会用 Vue2 或者正在维护老项目:先精学 Vue2 的选项式写法和生命周期,理解响应式原理、组件通信、路由守卫这些基础概念。然后学 Vue3 时,你会发现大部分思想是共通的,差别就是组合式 API 和构建工具。迁移成本并不高。
如果你时间充足(比如有一个月的完整学习期):我建议按“Vue2 基础概念入门 + Vue3 组合式 API 进阶 + 项目实战”的顺序走。原因很简单,Vue2 的选项式 API 对新手理解“数据、方法、模板之间的关系”更友好,Vue3 的组合式 API 写起来灵活,但对逻辑组织能力要求更高。先把基础打牢,再追求效率。
但千万要记住:学习顺序是为了让你更快理解,不是让你两套都精通。现实中大多数岗位不会要求你从零写 Vue2,但会要求你能维护旧代码,所以我的建议是看懂 Vue2、写熟 Vue3。
6.2 Vue 面试高频题整理与核心答案
结合我这个领域从业者看到最常见的面试题,挑几个核心的给你整理一下。这些题目不仅面试有用,也会逼你重新理解框架。
第一个是 Vue2 和 Vue3 响应式原理的区别。Vue2 用 Object.defineProperty,只能拦截已有属性,新增属性和数组下标修改要额外处理;Vue3 用 Proxy,可以监听整个对象的增删改,性能更好,还不用 $set。回答时能说到“依赖收集”和“静态标记”更加分。
第二个是 computed 和 watch 的区别。computed 是计算属性,适合根据已有数据派生新数据,有缓存,只有依赖变化时才重新计算;watch 是侦听器,适合在数据变化时执行异步操作或复杂逻辑,没有缓存,每次值变化都会触发回调。如果一个问题能用 computed 实现,优先用 computed。
第三个是 Vue 组件通信方式有哪些。常见有:props 父传子,emit 子传父,v-model 双向绑定,$refs 直接访问子组件实例,provide/inject 跨层级,EventBus(在 Vue3 里不再内置,要自己实现),以及 Vuex/Pinia 这类状态管理库。能说出每种方法的适用场景就很稳妥。
第四个是 Vue Router 的 hash 模式和 history 模式区别。hash 模式 URL 里带 #,无需服务端配置,兼容性最好;history 模式 URL 更美观,但需要服务端做 history 回退配置(比如 Nginx 的 try_files),否则刷新页面会 404。
第五个是 v-if 和 v-show 的区别。v-if 是条件渲染,不满足条件时元素根本不会渲染到 DOM 中,切换时会销毁和重建;v-show 是条件显示,元素始终渲染,只是通过 display:none 隐藏。频繁切换用 v-show,运行时很少切换用 v-if。这个问题能看出来新手是否真的理解 DOM 渲染机制。
第六个是 Vue3 的生命周期和 Vue2 有什么不同。Vue2 的 beforeDestroy/destroyed 变成了 beforeUnmount/unmounted,新增了 onRenderTracked 和 onRenderTriggered。在组合式 API 中生命周期函数前面要加 on 前缀,比如 onMounted。
6.3 学习资源与实操建议
官方的学习路线其实很简单:先看 Vue2 官网的“基础”部分,包括模板语法、计算属性、生命周期、组件;然后直接切换 Vue3 官网,重点看“组合式 API”和“响应式核心”两个板块。不要一上来就啃源码,先写代码。
我强烈建议你按顺序完成三个小项目。
第一个是待办事项列表。练习组件拆分、v-for 渲染、props/emit 父子通信。这个项目虽然简单,但能把 Vue 的基本功全部过一遍。
第二个是做一个电商后台的商品管理页。用 Vue Router 做多页面跳转,用路由守卫模拟登录鉴权,用 computed 计算总价、筛选商品,用 watch 监听搜索关键字并调用接口。这个项目能帮你建立真实项目的开发习惯。
第三个是做一个点播或直播页面。用本章节的 hls.js 播放器接入 m3u8 视频流,支持清晰度切换、控制栏定制、自动播放失败处理。这个项目既有挑战性,又能让你接触到多媒体前端,面试时拿出来非常有竞争力。
另外,TypeScript 也是 Vue3 绕不开的配套技能。你可以先用 JavaScript 学会 Vue3 逻辑,再逐步给组件加上类型标注。一个带类型的 Vue3 项目,和纯 JS 项目相比,维护体验完全是两个世界。
6.4 常见问题速查表:从 Vue2 转 Vue3 最容易踩的坑
根据我接触过的迁移项目,这里整理一份高频问题的速查表,你遇到类似报错时可以直接对照。
| 问题现象 | 原因 | 解决方法 |
|---|---|---|
this.$set 不存在 |
Vue3 不再需要 $set |
改用赋值或 ref/reactive 直接修改 |
this.$router 不存在 |
<script setup> 中没有 Vue2 的全局 this |
从 vue-router 导入 useRouter() 使用 |
Vue 3 中不能直接访问 this.xxx` |
组合式 API 与选项式 API 的差异 | 把变量声明为 ref 或 reactive |
process.env 报 undefined |
Vite 环境变量前缀不对 | 改成 import.meta.env.VITE_XXX |
| Vite 项目刷新后 404 | history 模式未配置服务端回退 | 开发环境用 createWebHashHistory() 或配置 Nginx |
beforeDestroy 不生效 |
Vue3 生命周期改名 | 改用 beforeUnmount |
JS 中解构 reactive 对象后失去响应式 |
reactive 解构得到的不是响应式数据 |
用 toRefs 转换或直接使用 ref 对象 |
这张表我建议你收藏,遇到问题先查一眼。很多报错其实都是上面这几类原因。
我最后再说一个实际操作中的体会:Vue 的学习曲线更像一条缓坡,而不是陡峭的悬崖。我在带团队时发现,真正拉开差距的从来不是会不会一句 ref、reactive 的语法,而是能不能在项目里把状态管理、路由守卫、组件复用这些工程问题组织得清晰。所以你在学完语法后,一定要自己完整地写完一个项目再收手,单靠看教程不可能形成真正的能力。趁着今天环境已经配好,从那个计数器开始,一行一行敲出来。
