vue3教程精选教程 - 完整解决方案与实战教程
在当下前端招聘与业务迭代中,Vue3 已成为中大型项目的默认选型,但很多开发者从“能跑起来”到“能上线稳定运行”之间,往往卡在响应式丢失、组合式函数复用混乱、打包体积失控这三个深水区。尤其是搜索“vue3教程精选教程”的开发者,通常已经看过基础 API,却在真实业务中遇到 ref 解构后视图不更新、reactive 对象被整体替换后失去响应、以及生产环境白屏但本地正常 等高频问题。本文不堆砌 API 文档,而是以一个“商品列表 + 筛选 + 分页”的典型业务模块为线索,把 Vue3 实战中最容易踩的坑一次讲透,并给出可直接落地的完整代码与排查清单。
问题现象:本地开发一切正常,上线后交互失效
一个非常典型的场景是:你写了一个 useProductList 组合式函数,内部用 reactive 管理筛选条件,用 ref 管理列表数据。本地 npm run dev 时点击筛选、翻页都正常,但执行 npm run build 后部署到 Nginx,发现:
- 点击“下一页”页码变了,但列表数据不刷新;
- 修改筛选条件后,请求发出去了,但页面上的选中状态没有同步;
- 控制台没有报错,或者只报一个模糊的
Cannot read properties of undefined; - 刷新页面后偶尔又能正常,问题呈现“偶现”特征。
这类问题最消耗排查时间,因为它不是语法错误,而是响应式链路在某个环节被切断,且开发环境的热更新(HMR)会掩盖部分问题。
原因分析:三个高频“响应式断链”点
结合大量真实项目复盘,问题通常集中在以下三处:
- 解构 reactive 对象:
const { keyword, page } = reactive({...})之后,keyword和page变成普通变量,失去响应式。这是 Vue3 教程中最常被一笔带过、但业务里最常犯的错误。 - 整体替换 reactive 对象:
state.list = newList可以,但state = newState会切断引用,模板仍指向旧对象。 - 组合式函数返回值未保持响应式:在
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>
生产环境排查清单:按顺序执行,避免盲目试错
如果你已经按上述方式重构,但生产环境仍有问题,请按以下步骤排查:
- 执行
npm run build后,用npx serve dist本地预览生产包,确认是否为构建差异导致。 - 打开 Vue Devtools,检查目标组件的
setup状态中,list、page等是否为Ref或Reactive,若显示为普通值则响应式已丢失。 - 检查
vite.config.ts中是否误配置了optimizeDeps或build.rollupOptions导致 Vue 被重复打包,可通过npm ls vue确认版本唯一。 - 若使用 Pinia,确认
storeToRefs是否用于解构 state,直接解构store同样会丢失响应式。 - 在
main.ts中确认app.mount('#app')的挂载点与 index.html 一致,避免生产环境挂载失败导致白屏。
最后提醒一点:Vue3 的响应式基于 Proxy,任何“把响应式对象当普通对象传递或解构”的操作都是高危动作。把 toRefs、storeToRefs 和“保持引用传递”变成肌肉记忆,你就能避开 90% 的“本地正常、线上失效”类问题。这篇实战排坑教程如果对你有帮助,建议收藏并在团队内做一次代码 Review 清单,比反复搜索“vue3教程精选教程”更有效。