vue3教程精选教程 - 完整解决方案与实战教程

在当下前端招聘与业务迭代中,Vue3 已成为中大型项目的默认选型,但很多开发者从“能跑起来”到“能上线稳定运行”之间,往往卡在响应式丢失、组合式函数复用混乱、打包体积失控这三个深水区。尤其是搜索“vue3教程精选教程”的开发者,通常已经看过基础 API,却在真实业务中遇到 ref 解构后视图不更新、reactive 对象被整体替换后失去响应、以及生产环境白屏但本地正常 等高频问题。本文不堆砌 API 文档,而是以一个“商品列表 + 筛选 + 分页”的典型业务模块为线索,把 Vue3 实战中最容易踩的坑一次讲透,并给出可直接落地的完整代码与排查清单。

问题现象:本地开发一切正常,上线后交互失效

一个非常典型的场景是:你写了一个 useProductList 组合式函数,内部用 reactive 管理筛选条件,用 ref 管理列表数据。本地 npm run dev 时点击筛选、翻页都正常,但执行 npm run build 后部署到 Nginx,发现:

这类问题最消耗排查时间,因为它不是语法错误,而是响应式链路在某个环节被切断,且开发环境的热更新(HMR)会掩盖部分问题。

原因分析:三个高频“响应式断链”点

结合大量真实项目复盘,问题通常集中在以下三处:

  1. 解构 reactive 对象const { keyword, page } = reactive({...}) 之后,keywordpage 变成普通变量,失去响应式。这是 Vue3 教程中最常被一笔带过、但业务里最常犯的错误。
  2. 整体替换 reactive 对象state.list = newList 可以,但 state = newState 会切断引用,模板仍指向旧对象。
  3. 组合式函数返回值未保持响应式:在 useProductList 中返回 reactive 对象的解构结果,或返回 ref 时误加 .value,导致父组件拿到的是快照值。

此外,生产环境与开发环境的差异还会放大问题:Vite 生产构建会进行 Tree-shaking 和压缩,若代码中存在副作用依赖顺序问题,开发环境不报错,生产环境可能直接白屏。

解决方案(附完整代码):用“引用传递 + toRefs + 显式类型”重建响应式链路

下面给出一个可直接复用的 useProductList 实现,重点展示如何正确保持响应式,并附带分页与筛选逻辑。代码基于 Vue3 <script setup> + TypeScript + Axios。

// composables/useProductList.ts
import { ref, reactive, toRefs, computed } from 'vue'
import axios from 'axios'

// 定义筛选条件类型,避免 any 导致的响应式误判
interface Filters {
  keyword: string
  category: string
  page: number
  pageSize: number
}

// 定义商品项类型
interface Product {
  id: number
  name: string
  price: number
}

export function useProductList() {
  // 1. 用 reactive 管理筛选条件,但不要直接解构返回
  const filters = reactive<Filters>({
    keyword: '',
    category: '',
    page: 1,
    pageSize: 10
  })

  // 2. 列表数据用 ref,保持整体替换时的响应式
  const list = ref<Product[]>([])
  const total = ref(0)
  const loading = ref(false)

  // 3. 请求方法:注意这里读取的是 filters 的属性,保持响应式追踪
  const fetchList = async () => {
    loading.value = true
    try {
      const { data } = await axios.get('/api/products', {
        params: {
          keyword: filters.keyword,
          category: filters.category,
          page: filters.page,
          pageSize: filters.pageSize
        }
      })
      // 整体替换 ref.value,触发视图更新
      list.value = data.items
      total.value = data.total
    } finally {
      loading.value = false
    }
  }

  // 4. 分页切换:直接修改 filters 的属性,而非替换整个对象
  const changePage = (newPage: number) => {
    filters.page = newPage
    fetchList()
  }

  // 5. 关键:用 toRefs 将 reactive 转为 ref 集合,解构后仍保持响应式
  const filtersRefs = toRefs(filters)

  // 6. 计算属性示例:总页数
  const totalPages = computed(() => Math.ceil(total.value / filters.pageSize))

  return {
    // 返回 ref 和 toRefs 后的引用,父组件解构也不会丢失响应式
    ...filtersRefs,
    list,
    total,
    loading,
    totalPages,
    fetchList,
    changePage
  }
}

在组件中使用时,务必注意解构方式:

<script setup lang="ts">
import { onMounted } from 'vue'
import { useProductList } from '@/composables/useProductList'

// 正确:从 useProductList 返回的已经是 ref 或 toRefs 后的引用
const {
  keyword,
  category,
  page,
  list,
  total,
  loading,
  totalPages,
  fetchList,
  changePage
} = useProductList()

onMounted(() => {
  fetchList()
})
</script>

<template>
  <div>
    <input v-model="keyword" placeholder="搜索商品" />
    <select v-model="category">
      <option value="">全部</option>
      <option value="digital">数码</option>
    </select>

    <div v-if="loading">加载中...</div>
    <ul v-else>
      <li v-for="item in list" :key="item.id">
        {{ item.name }} - ¥{{ item.price }}
      </li>
    </ul>

    <button :disabled="page <= 1" @click="changePage(page - 1)">上一页</button>
    <span>{{ page }} / {{ totalPages }}</span>
    <button :disabled="page >= totalPages" @click="changePage(page + 1)">下一页</button>
  </div>
</template>

生产环境排查清单:按顺序执行,避免盲目试错

如果你已经按上述方式重构,但生产环境仍有问题,请按以下步骤排查:

  1. 执行 npm run build 后,用 npx serve dist 本地预览生产包,确认是否为构建差异导致。
  2. 打开 Vue Devtools,检查目标组件的 setup 状态中,listpage 等是否为 RefReactive,若显示为普通值则响应式已丢失。
  3. 检查 vite.config.ts 中是否误配置了 optimizeDepsbuild.rollupOptions 导致 Vue 被重复打包,可通过 npm ls vue 确认版本唯一。
  4. 若使用 Pinia,确认 storeToRefs 是否用于解构 state,直接解构 store 同样会丢失响应式。
  5. main.ts 中确认 app.mount('#app') 的挂载点与 index.html 一致,避免生产环境挂载失败导致白屏。

最后提醒一点:Vue3 的响应式基于 Proxy,任何“把响应式对象当普通对象传递或解构”的操作都是高危动作。把 toRefsstoreToRefs 和“保持引用传递”变成肌肉记忆,你就能避开 90% 的“本地正常、线上失效”类问题。这篇实战排坑教程如果对你有帮助,建议收藏并在团队内做一次代码 Review 清单,比反复搜索“vue3教程精选教程”更有效。