JavaScript 开发者快速上手 Vue 3 教程

JavaScript 开发者快速上手 Vue 3 教程

基于 Vue 官方当前文档:Vue 3 推荐使用单文件组件 SFC<script setup>Composition API。项目脚手架推荐使用create-vue,它基于 Vite。

参考文档:

  • Vue Quick Start
  • Vue Single-File Components
  • Vue Composition API FAQ

1. 你应该怎么理解 Vue

如果你熟悉 JavaScript,可以把 Vue 理解成:

HTML 模板 + 响应式状态 + 组件化 + 自动 DOM 更新

你不再手动写:

document.querySelector('#count').textContent=count

而是写:

<script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <button @click="count++"> Count: {{ count }} </button> </template>

count变化时,Vue 自动更新页面。


2. 创建 Vue 项目

官方推荐脚手架:

npmcreate vue@latest

然后按提示选择:

Project name: my-vue-app Add TypeScript? 建议先选 No,熟悉后再用 Add JSX? No Add Vue Router? Yes,做 SPA 建议选 Add Pinia? Yes,做中大型项目建议选 Add Vitest? 可选 Add ESLint? Yes Add Prettier? Yes

进入项目:

cdmy-vue-appnpminstallnpmrun dev

典型目录:

src/ main.js App.vue components/ views/ router/ stores/

3. Vue 单文件组件.vue

Vue 最常见的组件格式是 SFC,Single-File Component:

<script setup> const title = 'Hello Vue' </script> <template> <h1>{{ title }}</h1> </template> <style scoped> h1 { color: #42b883; } </style>

一个.vue文件通常分三块:

<script setup> 写 JS 逻辑 <template> 写 HTML 结构 <style scoped> 写当前组件样式

scoped表示样式只影响当前组件。


4.ref:最常用的响应式状态

<script setup> import { ref } from 'vue' const count = ref(0) function increment() { count.value++ } </script> <template> <button @click="increment"> {{ count }} </button> </template>

注意:

count.value++

<script setup>里访问ref要用.value

但在模板里不用:

{{ count }}

Vue 会自动解包。


5.reactive:对象响应式

<script setup> import { reactive } from 'vue' const user = reactive({ name: 'Alice', age: 18 }) function grow() { user.age++ } </script> <template> <p>{{ user.name }} - {{ user.age }}</p> <button @click="grow">Grow</button> </template>

简单选择:

基本类型:ref 对象/嵌套对象:reactive 或 ref({ ... })

实际项目里,ref更通用,很多团队会优先用ref


6. 模板语法

文本插值

<p>{{ message }}</p>

动态属性

<img :src="imageUrl" :alt="title" />

:srcv-bind:src的简写。

条件渲染

<p v-if="isLoggedIn">欢迎回来</p> <p v-else>请登录</p>

列表渲染

<ul> <li v-for="todo in todos" :key="todo.id"> {{ todo.text }} </li> </ul>

一定要写:key

事件绑定

<button @click="save">保存</button>

@clickv-on:click的简写。


7. 表单绑定v-model

<script setup> import { ref } from 'vue' const username = ref('') </script> <template> <input v-model="username" /> <p>你输入的是:{{ username }}</p> </template>

相当于自动处理:

value + input 事件

常见用法:

<input v-model="text" /> <textarea v-model="content" /> <input type="checkbox" v-model="checked" /> <select v-model="selected">...</select>

8.computed:派生状态

如果一个值可以从现有状态计算出来,用computed

<script setup> import { ref, computed } from 'vue' const firstName = ref('Ada') const lastName = ref('Lovelace') const fullName = computed(() => { return `${firstName.value} ${lastName.value}` }) </script> <template> <p>{{ fullName }}</p> </template>

不要这样写:

<p>{{ firstName + ' ' + lastName }}</p>

简单可以,但复杂逻辑应该放进computed


9.watch:监听变化执行副作用

computed用于算值,watch用于做事。

例如搜索框防抖、请求接口、本地存储同步。

<script setup> import { ref, watch } from 'vue' const keyword = ref('') watch(keyword, (newValue, oldValue) => { console.log('搜索关键词变化:', oldValue, '=>', newValue) }) </script> <template> <input v-model="keyword" /> </template>

监听多个值:

watch([page,keyword],([newPage,newKeyword])=>{fetchData(newPage,newKeyword)})

10. 生命周期

最常用的是onMounted

<script setup> import { ref, onMounted } from 'vue' const users = ref([]) onMounted(async () => { const res = await fetch('/api/users') users.value = await res.json() }) </script>

常见生命周期:

onMounted 组件挂载后 onUpdated DOM 更新后 onUnmounted 组件卸载后

如果你熟悉 React:

onMounted ≈ useEffect(() => {}, []) onUnmounted ≈ useEffect 的 cleanup computed ≈ useMemo 的一部分场景 ref/reactive ≈ useState 但自动响应式

11. 组件拆分

父组件:

<script setup> import UserCard from './components/UserCard.vue' const user = { id: 1, name: 'Alice', role: 'Admin' } </script> <template> <UserCard :user="user" /> </template>

子组件UserCard.vue

<script setup> defineProps({ user: { type: Object, required: true } }) </script> <template> <article> <h2>{{ user.name }}</h2> <p>{{ user.role }}</p> </article> </template>

defineProps<script setup>中不需要 import。


12. 子组件向父组件通信emit

子组件:

<script setup> const emit = defineEmits(['delete']) function handleDelete() { emit('delete') } </script> <template> <button @click="handleDelete">删除</button> </template>

父组件:

<UserCard @delete="removeUser" />

带参数:

emit('delete',user.id)

父组件接收:

<UserCard @delete="removeUser" />
functionremoveUser(id){console.log(id)}

13. 一个完整小例子:Todo List

App.vue

<script setup> import { ref, computed } from 'vue' const newTodo = ref('') const todos = ref([ { id: 1, text: '学习 Vue 模板语法', done: true }, { id: 2, text: '学习组件通信', done: false } ]) const unfinishedCount = computed(() => { return todos.value.filter(todo => !todo.done).length }) function addTodo() { const text = newTodo.value.trim() if (!text) return todos.value.push({ id: Date.now(), text, done: false }) newTodo.value = '' } function removeTodo(id) { todos.value = todos.value.filter(todo => todo.id !== id) } </script> <template> <main> <h1>Todo List</h1> <form @submit.prevent="addTodo"> <input v-model="newTodo" placeholder="输入任务" /> <button type="submit">添加</button> </form> <p>未完成:{{ unfinishedCount }}</p> <ul> <li v-for="todo in todos" :key="todo.id"> <label> <input type="checkbox" v-model="todo.done" /> <span :class="{ done: todo.done }"> {{ todo.text }} </span> </label> <button @click="removeTodo(todo.id)"> 删除 </button> </li> </ul> </main> </template> <style scoped> .done { text-decoration: line-through; color: #888; } </style>

这里覆盖了:

ref computed v-model v-for :key @click @submit.prevent 动态 class 数组更新

14. Vue Router:页面路由

如果创建项目时选择了 Router,会有:

src/router/index.js src/views/

基本配置:

import{createRouter,createWebHistory}from'vue-router'importHomeViewfrom'../views/HomeView.vue'importAboutViewfrom'../views/AboutView.vue'constrouter=createRouter({history:createWebHistory(import.meta.env.BASE_URL),routes:[{path:'/',name:'home',component:HomeView},{path:'/about',name:'about',component:AboutView}]})exportdefaultrouter

入口main.js

import{createApp}from'vue'importAppfrom'./App.vue'importrouterfrom'./router'createApp(App).use(router).mount('#app')

页面出口:

<RouterView />

导航:

<RouterLink to="/">首页</RouterLink> <RouterLink to="/about">关于</RouterLink>

编程式跳转:

<script setup> import { useRouter } from 'vue-router' const router = useRouter() function goHome() { router.push('/') } </script>

15. Pinia:状态管理

如果多个组件共享状态,用 Pinia。

src/stores/counter.js

import{defineStore}from'pinia'import{ref,computed}from'vue'exportconstuseCounterStore=defineStore('counter',()=>{constcount=ref(0)constdoubleCount=computed(()=>count.value*2)functionincrement(){count.value++}return{count,doubleCount,increment}})

组件里使用:

<script setup> import { useCounterStore } from '@/stores/counter' const counter = useCounterStore() </script> <template> <p>{{ counter.count }}</p> <p>{{ counter.doubleCount }}</p> <button @click="counter.increment"> +1 </button> </template>

什么时候用 Pinia:

多个页面共享用户信息 购物车 权限信息 主题设置 复杂筛选条件 需要跨组件复用的业务状态

什么时候不用:

只在一个组件内部使用的状态 父子组件之间简单传值

16. Vue 和 React / 原生 JS 的思维差异

你不要再频繁操作 DOM

少写:

document.querySelector('.modal').classList.add('open')

多写:

<script setup> import { ref } from 'vue' const isOpen = ref(false) </script> <template> <Modal v-if="isOpen" /> </template>

状态决定 UI。


模板不是字符串拼接

不要这样:

list.innerHTML=todos.map(...).join('')

Vue 里这样:

<li v-for="todo in todos" :key="todo.id"> {{ todo.text }} </li>

数据流一般是单向的

父组件 -> props -> 子组件 子组件 -> emit -> 父组件 共享状态 -> Pinia

不要让子组件直接修改父组件对象,除非你明确知道这样做的影响。


17. 常用指令速查

<p v-if="visible">显示</p> <p v-else>否则显示</p> <li v-for="item in list" :key="item.id"> {{ item.name }} </li> <input v-model="name" /> <button @click="submit">提交</button> <img :src="url" /> <div :class="{ active: isActive }"></div> <form @submit.prevent="submit"></form>

常见缩写:

v-bind:src -> :src v-on:click -> @click

18. 常见修饰符

事件修饰符

<form @submit.prevent="submit">

阻止默认提交。

<button @click.stop="handleClick">

阻止事件冒泡。

键盘修饰符

<input @keyup.enter="submit" />

v-model 修饰符

<input v-model.trim="name" /> <input v-model.number="age" />

19. 推荐学习路径

第 1 天:基础语法

掌握:

create-vue .vue 文件结构 ref 模板插值 @click v-if v-for v-model

练习:做一个 Todo List。


第 2 天:组件化

掌握:

props emit slot 组件拆分 父子通信

练习:把 Todo List 拆成:

App.vue TodoInput.vue TodoList.vue TodoItem.vue

第 3 天:副作用和请求

掌握:

onMounted watch computed fetch/axios loading/error 状态

练习:做一个用户列表页面,支持搜索和加载状态。


第 4 天:路由

掌握:

Vue Router RouterLink RouterView 动态路由 编程式跳转

练习:做一个三页应用:

首页 用户列表 用户详情

第 5 天:状态管理

掌握:

Pinia store 共享状态 持久化

练习:做登录状态或购物车状态。


20. 一个小型项目结构建议

src/ assets/ components/ AppHeader.vue AppButton.vue views/ HomeView.vue UserListView.vue UserDetailView.vue router/ index.js stores/ user.js services/ userService.js App.vue main.js

services/userService.js

exportasyncfunctionfetchUsers(){constres=awaitfetch('/api/users')if(!res.ok){thrownewError('Failed to fetch users')}returnres.json()}

组件中:

<script setup> import { ref, onMounted } from 'vue' import { fetchUsers } from '@/services/userService' const users = ref([]) const loading = ref(false) const error = ref(null) onMounted(async () => { loading.value = true try { users.value = await fetchUsers() } catch (err) { error.value = err } finally { loading.value = false } }) </script>

21. 最容易踩的坑

1. 在 JS 里忘记.value

错误:

count++

正确:

count.value++

2.v-for不写key

错误:

<li v-for="item in list">

正确:

<li v-for="item in list" :key="item.id">

3. 过度使用watch

如果你只是要计算一个值,用computed

consttotal=computed(()=>price.value*count.value)

不要用watch手动同步。


4. 子组件直接改 props

不推荐:

props.user.name='Bob'

推荐:

emit('update-user',newUser)

或者把共享状态放到 Pinia。


5. 把所有状态都放 Pinia

Pinia 不是全局变量垃圾桶。

局部状态放组件里,共享状态才放 store。


22. 你可以按这个顺序写第一个 Vue 项目

目标:用户管理小应用。

功能:

用户列表 新增用户 删除用户 搜索用户 用户详情页 全局登录用户状态

涉及知识点:

组件 props emit ref computed watch Router Pinia API 请求

推荐文件:

src/ views/ UserListView.vue UserDetailView.vue components/ UserForm.vue UserTable.vue stores/ auth.js services/ userService.js

23. 总结

你熟悉 JavaScript,上手 Vue 的关键不是语法难度,而是转变思路:

用状态描述 UI 用组件拆分页面 用 props/emit 处理父子通信 用 computed 表达派生数据 用 watch 处理副作用 用 Router 管页面 用 Pinia 管共享状态

最快路线:

npmcreate vue@latest

然后从一个 Todo List 开始,把它逐步拆成组件,再加路由和 Pinia。这样学 Vue 会比单纯看语法快很多。