第一章:Vue 3 简介与环境搭建
1.1 什么是 Vue 3?
Vue 3 是一个渐进式 JavaScript 框架,用于构建用户界面。它的特点包括:
- 🚀 性能更好(比 Vue 2 快 2 倍)
- 📦 包体积更小
- 🎯 更好的 TypeScript 支持
- 🎨 Composition API
- 📱 更好的移动端支持
Vue3 路由 动态路由 嵌套路由 懒加载 导航守卫 编程式导航与滚动行为 脚本语法 组合式API 响应式语法 生命周期 组件通信 工程创建 Vue CLI 脚手架 create-vue 脚手架 项目结构 基本指令 v-model v-show v-if v-for v-on v-html {{message}}
项目结构概览:
my-vue-app/
├── node_modules/ # 项目依赖
├── public/ # 静态资源
│ └── favicon.ico
├── src/ # 源代码
│ ├── assets/ # 资源文件
│ ├── components/ # 组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── index.html # 入口HTML
├── package.json # 项目配置
└── vite.config.js # Vite配置

1. package.json讲解
2. vite配置
这里采用js详细讲解 点击vite配置详细讲解
如果想要了解ts和js区别,点击js和ts区别
1.2 环境搭建
方式一:CDN 引入(适合学习)
html
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">{{ message }}</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue 3!'
}
}
}).mount('#app')
</script>
</body>
</html>
方式二:使用 Vite(推荐)
注意这里有两大脚手架,本文一creat-vue详细简介

项目创建流程:
安装Node.js npm create vue@latest 选择项目配置 安装依赖 npm run dev 开始开发 TypeScript? Vue Router? Pinia? ESLint?
bash
# 安装 Node.js (版本 >= 16.0)
# 创建项目
npm create vue@latest my-vue-app
# 进入项目目录
cd my-vue-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev
安装 Node.js (版本 >= 16.0)

创建项目


作为初学者,建议按照以下方式选择:
🎯 初学者推荐配置
bash
请选择要包含的功能:(T/↓ 切换,空格选择,a 全选,回车确认)
[✓] TypeScript # ✅ 建议选择
[ ] JSX 支持 # ❌ 暂时不选
[✓] Router(单页面应用开发) # ✅ 建议选择
[✓] Pinia(状态管理) # ✅ 建议选择
[ ] Vitest(单元测试) # ❌ 暂时不选
[ ] End-to-End 测试 # ❌ 暂时不选
[✓] ESLint(错误检防) # ✅ 建议选择
[ ] Prettier(代码格式化) # ❌ 暂时不选(避免配置冲突)
✅ 建议选择的:
-
TypeScript
- 提供类型检查,减少错误
- 更好的代码提示
- 大型项目必备技能
-
Router(路由)
- 多页面应用必需
- 学习单页应用开发
- 实际项目常用
-
Pinia(状态管理)
- Vue 3 官方推荐
- 比 Vuex 更简单
- 管理全局数据
-
ESLint
- 代码规范检查
- 发现潜在错误
- 养成好的编码习惯
❌ 暂时不选的:
- JSX - Vue 主要使用模板语法,JSX 较少用
- 测试工具 - 初学阶段先专注基础功能
- Prettier - 与 ESLint 可能冲突,先用 ESLint 就够了
或者直接idea新建一个vue.js

下载并启动
bash
# 进入项目目录
cd my-vue-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev

启动网址

第二章:Vue 3 基础语法
数据绑定流程:
数据 data 响应式处理 模板编译 虚拟DOM 真实DOM 用户交互 事件处理 更新数据
2.1 创建应用实例
javascript
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
2.2 模板语法
文本插值
vue
<template>
<div>
<p>{{ message }}</p>
<p>{{ count * 2 }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue!',
count: 10
}
}
}
</script>
属性绑定
vue
<template>
<div>
<!-- 完整语法 -->
<img v-bind:src="imageSrc" v-bind:alt="imageAlt">
<!-- 缩写语法 -->
<img :src="imageSrc" :alt="imageAlt">
<!-- 动态属性名 -->
<button :[key]="value">按钮</button>
<!-- 绑定多个属性 -->
<div v-bind="objectOfAttrs"></div>
</div>
</template>
<script>
export default {
data() {
return {
imageSrc: '/path/to/image.jpg',
imageAlt: '图片描述',
key: 'id',
value: 'my-button',
objectOfAttrs: {
id: 'container',
class: 'wrapper'
}
}
}
}
</script>
2.3 条件渲染
vue
<template>
<div>
<!-- v-if -->
<p v-if="score >= 90">优秀</p>
<p v-else-if="score >= 60">及格</p>
<p v-else>不及格</p>
<!-- v-show -->
<p v-show="isVisible">这段文字可以显示或隐藏</p>
<!-- template 上使用 v-if -->
<template v-if="ok">
<h1>标题</h1>
<p>段落 1</p>
<p>段落 2</p>
</template>
</div>
</template>
<script>
export default {
data() {
return {
score: 85,
isVisible: true,
ok: true
}
}
}
</script>
2.4 列表渲染
vue
<template>
<div>
<!-- 数组遍历 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.text }}
</li>
</ul>
<!-- 对象遍历 -->
<div v-for="(value, key, index) in object" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
<!-- 数字遍历 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
<!-- template 上使用 v-for -->
<template v-for="item in items" :key="item.id">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
</template>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: '学习 JavaScript' },
{ id: 2, text: '学习 Vue' },
{ id: 3, text: '构建项目' }
],
object: {
title: 'Vue 3 教程',
author: '张三',
publishedAt: '2024-01-01'
}
}
}
}
</script>
2.5 事件处理
vue
<template>
<div>
<!-- 内联事件处理 -->
<button @click="count++">Count: {{ count }}</button>
<!-- 方法事件处理 -->
<button @click="increment">增加</button>
<!-- 传递参数 -->
<button @click="say('hello')">Say hello</button>
<!-- 访问原生事件 -->
<button @click="warn('警告', $event)">警告</button>
<!-- 事件修饰符 -->
<form @submit.prevent="onSubmit">
<button type="submit">提交</button>
</form>
<!-- 按键修饰符 -->
<input @keyup.enter="submit" />
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
say(message) {
alert(message)
},
warn(message, event) {
if (event) {
event.preventDefault()
}
alert(message)
},
onSubmit() {
console.log('表单提交')
},
submit() {
console.log('回车键按下')
}
}
}
</script>
2.6 表单输入绑定
vue
<template>
<div>
<!-- 文本 -->
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }}</p>
<!-- 多行文本 -->
<textarea v-model="message"></textarea>
<!-- 复选框 -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<!-- 多个复选框 -->
<div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
</div>
<div>选中的名字: {{ checkedNames }}</div>
<!-- 单选按钮 -->
<div>
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
</div>
<div>选择: {{ picked }}</div>
<!-- 选择框 -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<div>选择: {{ selected }}</div>
<!-- 修饰符 -->
<input v-model.lazy="msg"> <!-- 在 change 时而非 input 时更新 -->
<input v-model.number="age"> <!-- 自动转为数字 -->
<input v-model.trim="msg"> <!-- 自动过滤首尾空白字符 -->
</div>
</template>
<script>
export default {
data() {
return {
message: '',
checked: false,
checkedNames: [],
picked: '',
selected: '',
msg: '',
age: 0
}
}
}
</script>
第三章:计算属性与侦听器
计算属性工作原理:
是 否 响应式数据 计算属性 是否有缓存? 返回缓存值 执行计算 缓存结果 数据变化 清除缓存
3.1 计算属性
vue
<template>
<div>
<p>原始消息: "{{ message }}"</p>
<p>反转消息: "{{ reversedMessage }}"</p>
<p>{{ fullName }}</p>
<button @click="changeNames">改变姓名</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue 3!',
firstName: '张',
lastName: '三'
}
},
computed: {
// 计算属性的 getter
reversedMessage() {
return this.message.split('').reverse().join('')
},
// 计算属性的 getter 和 setter
fullName: {
get() {
return this.firstName + ' ' + this.lastName
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
},
methods: {
changeNames() {
this.fullName = '李 四'
}
}
}
</script>
3.2 侦听器
vue
<template>
<div>
<p>
问题: <input v-model="question">
</p>
<p>{{ answer }}</p>
<p>
<button @click="obj.count++">obj.count: {{ obj.count }}</button>
</p>
</div>
</template>
<script>
export default {
data() {
return {
question: '',
answer: '问题通常包含问号。',
obj: {
count: 0
}
}
},
watch: {
// 监听 question 的变化
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
},
// 深度监听
obj: {
handler(newValue, oldValue) {
console.log('obj 变化了', newValue)
},
deep: true,
immediate: true // 立即执行
},
// 监听嵌套属性
'obj.count'(newCount) {
console.log(`count 变为 ${newCount}`)
}
},
methods: {
async getAnswer() {
this.answer = '思考中...'
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = '错误! 无法访问 API。' + error
}
}
}
}
</script>
Watch vs Computed 对比:
Watch Computed 变化 变化 手动指定依赖 监听数据 执行副作用 无返回值 自动收集依赖 依赖数据 缓存结果 同步返回
第四章:组件基础
组件通信方式:
兄弟通信 跨级通信 父子通信 Props Events Provide Inject Event Bus/Pinia 兄弟2 兄弟1 孙子组件 祖父组件 子组件 父组件
4.1 定义和使用组件
vue
<!-- ButtonCounter.vue -->
<template>
<button @click="count++">
你点击了 {{ count }} 次
</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<!-- 使用组件 -->
<template>
<div>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
</div>
</template>
<script>
import ButtonCounter from './ButtonCounter.vue'
export default {
components: {
ButtonCounter
}
}
</script>
4.2 Props 传递数据
vue
<!-- BlogPost.vue -->
<template>
<article>
<h3>{{ title }}</h3>
<p>作者: {{ author }}</p>
<p>点赞数: {{ likes }}</p>
<p v-if="isPublished">已发布</p>
<p v-else>草稿</p>
</article>
</template>
<script>
export default {
props: {
// 基础类型检查
title: String,
// 多种可能的类型
likes: [String, Number],
// 必填
author: {
type: String,
required: true
},
// 带有默认值
isPublished: {
type: Boolean,
default: false
},
// 带有默认值的对象
metadata: {
type: Object,
default() {
return {
views: 0,
comments: 0
}
}
},
// 自定义验证函数
rating: {
type: Number,
validator(value) {
return value >= 0 && value <= 5
}
}
}
}
</script>
<!-- 父组件使用 -->
<template>
<BlogPost
title="Vue 3 学习指南"
:likes="42"
author="张三"
:is-published="true"
:rating="4"
/>
</template>
4.3 自定义事件
vue
<!-- SearchBox.vue -->
<template>
<div>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
<button @click="search">搜索</button>
</div>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue', 'search'],
methods: {
search() {
this.$emit('search', this.modelValue)
}
}
}
</script>
<!-- 父组件 -->
<template>
<div>
<SearchBox
v-model="searchText"
@search="handleSearch"
/>
<p>搜索内容: {{ searchText }}</p>
</div>
</template>
<script>
import SearchBox from './SearchBox.vue'
export default {
components: { SearchBox },
data() {
return {
searchText: ''
}
},
methods: {
handleSearch(value) {
console.log('搜索:', value)
}
}
}
</script>
4.4 插槽(Slots)
vue
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">默认标题</slot>
</div>
<div class="card-body">
<slot>默认内容</slot>
</div>
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<!-- 使用插槽 -->
<template>
<Card>
<template #header>
<h1>卡片标题</h1>
</template>
<p>这是卡片的主要内容</p>
<p>可以放置任何内容</p>
<template #footer>
<button>确定</button>
<button>取消</button>
</template>
</Card>
</template>
<!-- 作用域插槽 -->
<!-- TodoList.vue -->
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<slot name="todo" :todo="todo" :index="index">
{{ todo.text }}
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
todos: [
{ id: 1, text: '学习 Vue', done: true },
{ id: 2, text: '学习 Composition API', done: false },
{ id: 3, text: '构建项目', done: false }
]
}
}
}
</script>
<!-- 使用作用域插槽 -->
<template>
<TodoList>
<template #todo="{ todo, index }">
<span :class="{ done: todo.done }">
{{ index + 1 }}. {{ todo.text }}
</span>
</template>
</TodoList>
</template>
第五章:Composition API
5.1 setup 函数
vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
import { ref, computed, watch, onMounted } from 'vue'
export default {
setup() {
// 响应式数据
const count = ref(0)
// 方法
const increment = () => {
count.value++
}
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 侦听器
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
// 生命周期
onMounted(() => {
console.log('组件已挂载')
})
// 返回给模板使用
return {
count,
increment,
doubleCount
}
}
}
</script>
5.2 <script setup> 语法糖
vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">增加</button>
<ChildComponent :msg="message" @custom-event="handleEvent" />
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 响应式数据
const count = ref(0)
const message = ref('Hello')
// 方法
const increment = () => {
count.value++
}
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 侦听器
watch(count, (newValue) => {
console.log(`count 现在是 ${newValue}`)
})
// 事件处理
const handleEvent = (payload) => {
console.log('接收到事件:', payload)
}
// 生命周期
onMounted(() => {
console.log('组件已挂载')
})
</script>
5.3 响应式 API
vue
<script setup>
import { ref, reactive, toRefs, readonly } from 'vue'
// ref - 用于基本类型
const count = ref(0)
const message = ref('Hello')
// reactive - 用于对象
const state = reactive({
user: {
name: '张三',
age: 25
},
todos: []
})
// toRefs - 解构 reactive 对象
const { user, todos } = toRefs(state)
// readonly - 创建只读副本
const readonlyState = readonly(state)
// 修改数据
count.value++
state.user.name = '李四'
todos.value.push({ text: '新任务' })
</script>
5.4 组合式函数(Composables)
javascript
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const doubleCount = computed(() => count.value * 2)
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
vue
<!-- 使用组合式函数 -->
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">重置</button>
<p>鼠标位置: {{ x }}, {{ y }}</p>
</div>
</template>
<script setup>
import { useCounter } from './composables/useCounter'
import { useMouse } from './composables/useMouse'
const { count, increment, decrement, reset } = useCounter(10)
const { x, y } = useMouse()
</script>
第六章:Vue Router
6.1 安装和配置
bash
npm install vue-router@4
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/user/:id',
name: 'User',
component: () => import('../views/User.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
6.2 使用路由
vue
<!-- App.vue -->
<template>
<div>
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link> |
<router-link :to="{ name: 'User', params: { id: 123 }}">
用户
</router-link>
</nav>
<router-view />
</div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 获取路由参数
console.log(route.params.id)
console.log(route.query.search)
// 编程式导航
router.push('/about')
router.push({ name: 'User', params: { id: 123 }})
router.replace('/about')
router.go(-1)
</script>
6.3 路由守卫
javascript
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查用户是否登录
const isAuthenticated = localStorage.getItem('token')
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
// 路由配置
const routes = [
{
path: '/admin',
component: Admin,
meta: { requiresAuth: true }
}
]
// 组件内守卫
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被确认前调用
next()
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
},
beforeRouteLeave(to, from) {
// 导航离开该组件的对应路由时调用
const answer = window.confirm('确定要离开吗?')
if (!answer) return false
}
}
第七章:Vuex/Pinia 状态管理
7.1 Pinia(推荐)
bash
npm install pinia
javascript
// main.js
import { createPinia } from 'pinia'
app.use(createPinia())
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
return { count, doubleCount, increment, decrement }
})
vue
<!-- 使用 Store -->
<template>
<div>
<p>{{ counter.count }}</p>
<p>{{ counter.doubleCount }}</p>
<button @click="counter,"">+</button>
<button @click="counter.decrement">-</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
7.2 复杂示例:用户 Store
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')
// Getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name || '游客')
// Actions
async function login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await response.json()
token.value = data.token
user.value = data.user
localStorage.setItem('token', data.token)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
function logout() {
user.value = null
token.value = ''
localStorage.removeItem('token')
}
async function fetchUserProfile() {
if (!token.value) return
try {
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token.value}`
}
})
user.value = await response.json()
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
return {
user,
token,
isLoggedIn,
userName,
login,
logout,
fetchUserProfile
}
})
第八章:实战项目 - 待办事项应用
8.1 项目结构
todo-app/
├── src/
│ ├── components/
│ │ ├── TodoItem.vue
│ │ ├── TodoList.vue
│ │ └── TodoForm.vue
│ ├── stores/
│ │ └── todos.js
│ ├── App.vue
│ └── main.js
8.2 实现代码
javascript
// stores/todos.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTodosStore = defineStore('todos', () => {
const todos = ref([])
const filter = ref('all') // all, active, completed
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
const remaining = computed(() =>
todos.value.filter(todo => !todo.completed).length
)
function addTodo(text) {
todos.value.push({
id: Date.now(),
text,
completed: false
})
}
function removeTodo(id) {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
function toggleTodo(id) {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function clearCompleted() {
todos.value = todos.value.filter(todo => !todo.completed)
}
function setFilter(newFilter) {
filter.value = newFilter
}
return {
todos,
filter,
filteredTodos,
remaining,
addTodo,
removeTodo,
toggleTodo,
clearCompleted,
setFilter
}
})
vue
<!-- components/TodoForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="todo-form">
<input
v-model="newTodo"
placeholder="添加新任务..."
class="todo-input"
autofocus
>
</form>
</template>
<script setup>
import { ref } from 'vue'
import { useTodosStore } from '@/stores/todos'
const todosStore = useTodosStore()
const newTodo = ref('')
function handleSubmit() {
const text = newTodo.value.trim()
if (text) {
todosStore.addTodo(text)
newTodo.value = ''
}
}
</script>
vue
<!-- components/TodoItem.vue -->
<template>
<li :class="{ completed: todo.completed }" class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="todosStore.toggleTodo(todo.id)"
class="toggle"
>
<label>{{ todo.text }}</label>
<button
@click="todosStore.removeTodo(todo.id)"
class="destroy"
>
×
</button>
</li>
</template>
<script setup>
import { useTodosStore } from '@/stores/todos'
defineProps({
todo: {
type: Object,
required: true
}
})
const todosStore = useTodosStore()
</script>
vue
<!-- components/TodoList.vue -->
<template>
<div class="todo-list-container">
<ul class="todo-list">
<TodoItem
v-for="todo in todosStore.filteredTodos"
:key="todo.id"
:todo="todo"
/>
</ul>
<footer class="footer" v-if="todosStore.todos.length">
<span class="todo-count">
<strong>{{ todosStore.remaining }}</strong>
{{ todosStore.remaining === 1 ? '项' : '项' }}待完成
</span>
<ul class="filters">
<li>
<a
:class="{ selected: todosStore.filter === 'all' }"
@click="todosStore.setFilter('all')"
>
全部
</a>
</li>
<li>
<a
:class="{ selected: todosStore.filter === 'active' }"
@click="todosStore.setFilter('active')"
>
未完成
</a>
</li>
<li>
<a
:class="{ selected: todosStore.filter === 'completed' }"
@click="todosStore.setFilter('completed')"
>
已完成
</a>
</li>
</ul>
<button
@click="todosStore.clearCompleted"
class="clear-completed"
v-show="todosStore.todos.length > todosStore.remaining"
>
清除已完成
</button>
</footer>
</div>
</template>
<script setup>
import { useTodosStore } from '@/stores/todos'
import TodoItem from './TodoItem.vue'
const todosStore = useTodosStore()
</script>
vue
<!-- App.vue -->
<template>
<div id="app">
<section class="todoapp">
<header class="header">
<h1>待办事项</h1>
<TodoForm />
</header>
<TodoList />
</section>
</div>
</template>
<script setup>
import TodoForm from './components/TodoForm.vue'
import TodoList from './components/TodoList.vue'
</script>
<style>
/* 添加样式美化界面 */
.todoapp {
background: #fff;
margin: 130px auto 40px auto;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todo-input {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
width: 100%;
font-size: 24px;
}
.todo-item {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
padding: 15px;
}
.todo-item.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
/* 更多样式... */
</style>
第九章:最佳实践与性能优化
9.1 组件设计原则
- 单一职责原则
vue
<!-- ❌ 不好的例子 - 组件做了太多事情 -->
<template>
<div>
<!-- 用户信息、商品列表、购物车都在一个组件 -->
</div>
</template>
<!-- ✅ 好的例子 - 拆分成多个组件 -->
<template>
<div>
<UserProfile :user="user" />
<ProductList :products="products" />
<ShoppingCart :items="cartItems" />
</div>
</template>
- Props 验证
javascript
// 始终为 props 定义详细的类型
props: {
status: {
type: String,
required: true,
validator: (value) => {
return ['pending', 'success', 'error'].includes(value)
}
}
}
9.2 性能优化技巧
- 使用 v-show vs v-if
vue
<!-- 频繁切换使用 v-show -->
<div v-show="isVisible">内容</div>
<!-- 条件很少改变使用 v-if -->
<div v-if="isLoggedIn">用户信息</div>
- 列表渲染优化
vue
<!-- 始终使用 key -->
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
<!-- 避免在 v-for 中使用 v-if -->
<!-- ❌ 不好 -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
<!-- ✅ 好 - 使用计算属性 -->
<template>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
<script setup>
const activeUsers = computed(() =>
users.filter(user => user.isActive)
)
</script>
- 懒加载组件
javascript
// 路由懒加载
const User = () => import('./views/User.vue')
// 异步组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
- 使用 shallowRef 和 shallowReactive
javascript
import { shallowRef, shallowReactive } from 'vue'
// 对于大型数据结构,如果只需要顶层响应式
const state = shallowReactive({
nested: {
count: 0 //该不会是响应式的
}
})
// 对于不需要深层响应式的 ref
const data = shallowRef({
// 大型数据对象
})
9.3 代码组织最佳实践
- 组合式函数提取逻辑
javascript
// composables/useApi.js
import { ref } from 'vue'
export function useApi(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function fetchData() {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
return { data, error, loading, fetchData }
}
- 类型安全(使用 TypeScript)
typescript
// 定义 props 类型
interface Props {
title: string
count?: number
}
// 定义 emits 类型
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: number): void
}
// 使用
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
第十章:部署与生产环境配置
10.1 构建生产版本
bash
# 构建
npm run build
# 预览生产构建
npm run preview
10.2 环境变量配置
bash
# .env.development
VITE_API_URL=http://localhost:3000
# .env.production
VITE_API_URL=https://api.example.com
javascript
// 使用环境变量
const apiUrl = import.meta.env.VITE_API_URL
10.3 部署选项
-
静态托管服务
- Netlify
- Vercel
- GitHub Pages
-
传统服务器
- Nginx 配置
nginx
server {
listen 80;
server_name example.com;
root /var/www/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
总结
通过这个教程,你已经学习了:
- ✅ Vue 3 基础语法和概念
- ✅ 组件开发和通信
- ✅ Composition API 的使用
- ✅ 路由和状态管理
- ✅ 实战项目开发
- ✅ 最佳实践和性能优化
- ✅ 部署和生产环境配置
下一步学习建议
- 深入学习 TypeScript 与 Vue 3 的结合
- 学习 Nuxt 3 进行 SSR 开发
- 掌握 Vue 3 生态系统工具(VueUse、Vite 等)
- 参与开源项目,阅读优秀源码
