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" />:src是v-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>@click是v-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 -> @click18. 常见修饰符
事件修饰符
<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.jsservices/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.js23. 总结
你熟悉 JavaScript,上手 Vue 的关键不是语法难度,而是转变思路:
用状态描述 UI 用组件拆分页面 用 props/emit 处理父子通信 用 computed 表达派生数据 用 watch 处理副作用 用 Router 管页面 用 Pinia 管共享状态最快路线:
npmcreate vue@latest然后从一个 Todo List 开始,把它逐步拆成组件,再加路由和 Pinia。这样学 Vue 会比单纯看语法快很多。
