Vue 3 基础教程:从入门到精通

第一章: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讲解

详细讲解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(代码格式化) # ❌ 暂时不选(避免配置冲突)
✅ 建议选择的:
  1. TypeScript

    • 提供类型检查,减少错误
    • 更好的代码提示
    • 大型项目必备技能
  2. Router(路由)

    • 多页面应用必需
    • 学习单页应用开发
    • 实际项目常用
  3. Pinia(状态管理)

    • Vue 3 官方推荐
    • 比 Vuex 更简单
    • 管理全局数据
  4. ESLint

    • 代码规范检查
    • 发现潜在错误
    • 养成好的编码习惯
❌ 暂时不选的:
  1. JSX - Vue 主要使用模板语法,JSX 较少用
  2. 测试工具 - 初学阶段先专注基础功能
  3. 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 组件设计原则

  1. 单一职责原则
vue 复制代码
<!-- ❌ 不好的例子 - 组件做了太多事情 -->
<template>
  <div>
    <!-- 用户信息、商品列表、购物车都在一个组件 -->
  </div>
</template>

<!-- ✅ 好的例子 - 拆分成多个组件 -->
<template>
  <div>
    <UserProfile :user="user" />
    <ProductList :products="products" />
    <ShoppingCart :items="cartItems" />
  </div>
</template>
  1. Props 验证
javascript 复制代码
// 始终为 props 定义详细的类型
props: {
  status: {
    type: String,
    required: true,
    validator: (value) => {
      return ['pending', 'success', 'error'].includes(value)
    }
  }
}

9.2 性能优化技巧

  1. 使用 v-show vs v-if
vue 复制代码
<!-- 频繁切换使用 v-show -->
<div v-show="isVisible">内容</div>

<!-- 条件很少改变使用 v-if -->
<div v-if="isLoggedIn">用户信息</div>
  1. 列表渲染优化
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>
  1. 懒加载组件
javascript 复制代码
// 路由懒加载
const User = () => import('./views/User.vue')

// 异步组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)
  1. 使用 shallowRef 和 shallowReactive
javascript 复制代码
import { shallowRef, shallowReactive } from 'vue'

// 对于大型数据结构,如果只需要顶层响应式
const state = shallowReactive({
  nested: {
    count: 0 //该不会是响应式的
  }
})

// 对于不需要深层响应式的 ref
const data = shallowRef({
  // 大型数据对象
})

9.3 代码组织最佳实践

  1. 组合式函数提取逻辑
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 }
}
  1. 类型安全(使用 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 部署选项

  1. 静态托管服务

    • Netlify
    • Vercel
    • GitHub Pages
  2. 传统服务器

    • Nginx 配置
nginx 复制代码
server {
    listen 80;
    server_name example.com;
    root /var/www/dist;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
}

总结

通过这个教程,你已经学习了:

  1. ✅ Vue 3 基础语法和概念
  2. ✅ 组件开发和通信
  3. ✅ Composition API 的使用
  4. ✅ 路由和状态管理
  5. ✅ 实战项目开发
  6. ✅ 最佳实践和性能优化
  7. ✅ 部署和生产环境配置

下一步学习建议

  1. 深入学习 TypeScript 与 Vue 3 的结合
  2. 学习 Nuxt 3 进行 SSR 开发
  3. 掌握 Vue 3 生态系统工具(VueUse、Vite 等)
  4. 参与开源项目,阅读优秀源码
相关推荐
百锦再2 小时前
树形数据展示:树形表格与树形控件的深度对比(Vue实现)
javascript·vue.js·ecmascript·递归·tree·data·table
Sylvia33.2 小时前
体育数据API实战:用火星数据实现NBA赛事实时比分与状态同步
java·linux·开发语言·前端·python
码农阿豪2 小时前
Vue+Ant Design表格组件开发实战:从问题到优化的完整指南
前端·javascript·vue.js
QQ24391972 小时前
spring boot医院挂号就诊系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·vue.js·spring boot
Coder-coco2 小时前
家政服务管理系统|基于springboot + vue家政服务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·家政服务管理系统
用户69371750013842 小时前
OS级AI Agent:手机操作系统的下一个战场
android·前端·人工智能
百锦再2 小时前
Vue不是万能的:前后端不分离开发的优势
前端·javascript·vue.js·前端框架·vue
毕设源码_王学姐2 小时前
2026毕设ssm+vue民宿管理系统论文+程序
前端·vue.js·课程设计
程序员鱼皮2 小时前
万字干货 | OpenClaw 进阶玩法大全:技能 / 多 Agent / 省钱 / 安全,50+ 实战技巧一次学会
前端·后端·ai编程