Vue 3.2发布后,Composition API、script setup、Pinia、Vite等新特性和新工具,让Vue开发更高效、更优雅。

但新生态也带来了新的坑。我在项目中用Vue 3.2+生态开发了几个项目,踩了不少坑。本文分享这些踩坑经历,包括响应式、生命周期、状态管理、构建工具、类型支持、第三方库兼容等方面,每个坑都有问题描述、原因分析和解决方案。

一、响应式相关的坑

响应式是Vue的核心,但Vue 3的响应式和Vue 2有很大不同,坑也不少。

坑一:reactive解构后失去响应式

问题:

用reactive创建的对象,解构后,属性失去了响应式。

const state = reactive({ count: 0, name: 'test' })
const { count, name } = state // 解构后,count和name不是响应式的

修改state.count,视图不更新。

原因:

reactive是通过Proxy实现的,解构后,得到的是普通的值,失去了Proxy的代理。

解决方案:

toRefs把reactive的属性转成ref:

import { toRefs } from 'vue'
const state = reactive({ count: 0, name: 'test' })
const { count, name } = toRefs(state) // 现在count和name是响应式的

或者直接用ref:

const count = ref(0)
const name = ref('test')

教训: reactive解构一定要用toRefs,或者直接用ref。


坑二:ref在模板中自动解包,在JS中要.value

问题:

新手经常搞混ref的使用方式。在模板中,ref会自动解包,直接用变量名就行;但在JS中,必须用.value

const count = ref(0)

// 模板中
{{ count }} // 正确,自动解包

// JS中
console.log(count) // 错误,输出的是ref对象
console.log(count.value) // 正确,输出0

原因:

Vue的模板编译器会自动处理ref的解包,但JS中不会。

解决方案:

记住这个规则:模板中不用.value,JS中必须用.value。

如果嫌麻烦,可以用reactive代替ref(但要注意解构的问题)。


坑三:数组的响应式问题

问题:

在Vue 2中,直接修改数组的索引(arr[0] = newValue)不会触发更新。Vue 3中,reactive数组可以通过索引修改,但ref数组有时候会出问题。

const arr = ref([1, 2, 3])
arr.value[0] = 10 // 这个是可以的
arr.value.length = 0 // 这个也可以

但如果是reactive数组中的对象属性,有时候会有意外。

原因:

Vue 3用Proxy实现响应式,对数组的支持比Vue 2好很多。但在某些边界情况下,还是可能出问题。

解决方案:

  • 数组操作尽量用数组的方法(push、splice、filter等)
  • 如果是ref数组,操作时用arr.value.xxx()
  • 复杂操作后,可以用triggerRef强制触发更新

坑四:shallowRef和triggerRef

问题:

用shallowRef创建的对象,修改内部属性不会触发更新。

const state = shallowRef({ count: 0 })
state.value.count = 1 // 不会触发更新

原因:

shallowRef只对.value的替换做响应式,不对内部属性做深度响应式。

解决方案:

如果需要修改内部属性,用reactive,或者修改后调用triggerRef

state.value.count = 1
triggerRef(state) // 强制触发更新

shallowRef适合大对象的性能优化,但要注意使用方式。

二、生命周期和组合式API的坑

坑五:setup中没有this

问题:

从Vue 2转过来的开发者,经常在setup中用this,结果报错。

setup() {
  this.$router.push('/') // 错误,setup中没有this
}

原因:

Composition API的setup中,组件实例还没创建,没有this。

解决方案:

用对应的组合式函数:

  • useRoute()代替this.$route
  • useRouter()代替this.$router
  • useStore()(Pinia)代替this.$store
  • getCurrentInstance()获取组件实例(不推荐常用)

坑六:onMounted等生命周期的使用时机

问题:

在异步回调中调用生命周期钩子,不会生效。

setup() {
  setTimeout(() => {
    onMounted(() => {
      console.log('mounted') // 这个不会执行!
    })
  }, 1000)
}

原因:

生命周期钩子必须在setup的同步执行阶段注册,异步回调中注册的钩子,无法关联到当前组件。

解决方案:

在setup的顶层调用生命周期钩子:

setup() {
  onMounted(() => {
    console.log('mounted') // 正确
  })
}

如果需要在异步后执行逻辑,在生命周期钩子内部做异步:

onMounted(async () => {
  const data = await fetchData()
})

坑七:watch和watchEffect的区别

问题:

很多人搞不清watch和watchEffect的区别,用错了导致问题。

// watch:需要指定监听的源
watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal)
})

// watchEffect:自动收集依赖
watchEffect(() => {
  console.log(count.value) // count变化时自动执行
})

区别:

  • watch:需要明确指定监听对象,可以拿到旧值,懒执行(默认不立即执行)
  • watchEffect:自动收集依赖,拿不到旧值,立即执行

解决方案:

根据场景选择:

  • 需要旧值,或者需要条件性监听,用watch
  • 简单的副作用,用watchEffect
  • watch可以设置immediate: true立即执行

三、状态管理的坑(Pinia)

Vue 3推荐用Pinia代替Vuex,Pinia更简单,但也有坑。

坑八:Pinia的store解构后失去响应式

问题:

从Pinia store中解构state,会失去响应式。

const store = useStore()
const { count, name } = store // 解构后不是响应式的

原因:

Pinia的state是reactive的,解构后同样会失去响应式(和reactive解构一样的问题)。

解决方案:

storeToRefs

import { storeToRefs } from 'pinia'
const store = useStore()
const { count, name } = storeToRefs(store) // 现在是响应式的

注意:actions可以直接解构,不需要storeToRefs:

const { increment } = store // actions可以直接解构

坑九:Pinia中修改state的方式

问题:

在Vuex中,修改state必须通过mutation。在Pinia中,可以直接修改state,但有时候会出问题。

const store = useStore()
store.count = 10 // 可以直接修改
store.$patch({ count: 10 }) // 也可以用$patch

原因:

Pinia允许直接修改state,但直接修改多个属性时,每次修改都会触发更新,性能不好。

解决方案:

  • 单个属性修改:直接改
  • 多个属性修改:用$patch批量修改
  • 复杂修改:用$patch传函数
store.$patch((state) => {
  state.count = 10
  state.name = 'test'
})

坑十:Pinia的持久化插件

问题:

用pinia-plugin-persistedstate做持久化,有时候数据不更新,或者和预期不符。

原因:

  • 持久化的key配置不对
  • 没有指定要持久化的state,全部持久化导致数据量大
  • 恢复数据时,和初始值冲突

解决方案:

  • 仔细配置persist选项,指定key和paths
  • 只持久化需要持久化的state
  • 注意数据版本,结构变化时要做迁移

四、构建工具的坑(Vite)

Vue 3推荐用Vite,Vite很快,但也有坑。

坑十一:Vite和Webpack的差异

问题:

从Webpack转到Vite,很多东西不一样,经常出问题。

  • 环境变量:Vite用import.meta.env,Webpack用process.env
  • 静态资源:Vite用new URL('./xxx.png', import.meta.url).href
  • 全局变量:Vite需要在vite.config.js中配置define
  • CommonJS模块:Vite对CommonJS的支持不如Webpack

解决方案:

  • 仔细看Vite文档,了解和Webpack的差异
  • 环境变量用import.meta.env,并且要以VITE_开头
  • 静态资源用正确的引入方式
  • CommonJS模块尽量换成ESM版本

坑十二:Vite开发环境和生产环境不一致

问题:

开发环境正常,打包后出问题。

原因通常是:

  • 开发环境用的是ESM,生产环境打包后有差异
  • 某些库只在开发环境生效
  • 路径问题,开发环境和生产环境的base不同

解决方案:

  • 经常在生产环境测试,不要只在开发环境测
  • 注意base路径的配置
  • 遇到问题,用vite preview预览生产构建

坑十三:依赖预构建的问题

问题:

Vite会预构建依赖,但有时候预构建出问题,导致页面白屏或报错。

原因:

  • 依赖是CommonJS格式,预构建转换出问题
  • 依赖有副作用,预构建后行为变化
  • 新安装的依赖没有重新预构建

解决方案:

  • 删除node_modules/.vite目录,重新启动
  • 在vite.config.js中配置optimizeDeps,指定需要预构建的依赖
  • 遇到问题,看Vite的报错信息,针对性解决

五、TypeScript的坑

Vue 3对TypeScript的支持更好了,但也有坑。

坑十四:ref的类型推断

问题:

ref的类型推断,有时候不符合预期。

const count = ref(0) // 类型是Ref<number>
count.value = 'hello' // 报错,类型不匹配

const data = ref(null) // 类型是Ref<null>
data.value = { name: 'test' } // 报错

解决方案:

用泛型指定类型:

interface Data {
  name: string
}
const data = ref<Data | null>(null)
data.value = { name: 'test' } // 正确

坑十五:reactive的类型问题

问题:

reactive的类型推断,有时候会把类型变宽。

const state = reactive({
  count: 0,
  name: 'test'
})
// state的类型是 { count: number, name: string }

这个一般没问题,但如果是复杂的嵌套对象,类型推断可能不准确。

解决方案:

用接口定义类型:

interface State {
  count: number
  user: {
    name: string
    age: number
  }
}
const state = reactive<State>({
  count: 0,
  user: { name: 'test', age: 18 }
})

坑十六:props的类型定义

问题:

在script setup中定义props的类型,和Options API不一样。

<script setup lang="ts">
// 正确的方式
const props = defineProps<{
  title: string
  count?: number
}>()

// 带默认值
const props = withDefaults(defineProps<{
  title: string
  count?: number
}>(), {
  count: 0
})
</script>

注意: defineProps的类型参数中,不能用外部导入的类型(某些版本有限制),如果需要,用接口定义在同一个文件中。

六、第三方库兼容的坑

坑十七:Vue 2的插件不能直接用

问题:

很多Vue 2的插件,在Vue 3中不能直接用。

比如,Vue 2的vue-router@3vuex@3element-ui,在Vue 3中都需要用新版本:

  • vue-router@4
  • Pinia(代替Vuex)
  • Element Plus(代替Element UI)

解决方案:

  • 升级到支持Vue 3的版本
  • 如果没有Vue 3版本,找替代库
  • 实在不行,用vue-demi做兼容(库作者用的)

坑十八:全局属性的使用

问题:

在Vue 2中,通过Vue.prototype.xxx挂载全局属性。在Vue 3中,方式变了。

// Vue 2
Vue.prototype.$http = axios

// Vue 3
const app = createApp(App)
app.config.globalProperties.$http = axios

在setup中使用:

import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
proxy.$http.get('/api')

但更推荐的方式是用provide/inject,或者直接导入。

解决方案:

  • 简单的全局属性,用globalProperties
  • 复杂的,用provide/inject
  • 最好的方式是封装成组合式函数,直接导入使用

七、其他常见的坑

坑十九:v-for和v-if的优先级

问题:

在Vue 2中,v-for的优先级高于v-if。在Vue 3中,v-if的优先级高于v-for。

所以,在Vue 3中,同时用v-for和v-if,v-if会先执行,可能访问不到v-for的变量。

解决方案:

不要在同一个元素上同时用v-for和v-if。用computed先过滤数据,再v-for。

<!-- 不推荐 -->
<div v-for="item in list" v-if="item.active" :key="item.id">

<!-- 推荐 -->
<div v-for="item in activeList" :key="item.id">
const activeList = computed(() => list.filter(item => item.active))

坑二十:组件名的大小写

问题:

在Vue 3中,组件名的大小写,有时候会出问题。

比如,在script setup中,组件名是文件名的转换。如果文件名是MyComponent.vue,在模板中可以用<MyComponent><my-component>

但在某些情况下(比如动态组件、递归组件),需要明确指定组件名。

解决方案:

  • <script setup>时,如果需要指定组件名,用defineOptions
<script setup>
defineOptions({
  name: 'MyComponent'
})
</script>
  • 组件名尽量用PascalCase,避免大小写问题

坑二十一:teleport和transition的组合

问题:

teleport和transition一起用的时候,有时候动画不生效。

原因:

teleport把元素传送到了其他位置,transition的类名可能没有正确应用。

解决方案:

  • 确保transition在teleport内部
  • 检查CSS选择器是否正确
  • 看Vue文档中teleport和transition的配合方式

八、总结和建议

总结一下Vue 3.2+生态开发的建议:

1. 深入理解响应式原理

Vue 3的响应式是基于Proxy的,和Vue 2的Object.defineProperty不同。理解原理,才能避免各种响应式的坑。

2. 善用官方文档

Vue 3的文档写得很好,遇到问题先查文档。特别是迁移指南,详细说明了从Vue 2到Vue 3的变化。

3. 用Composition API和script setup

Composition API和script setup是Vue 3的推荐写法,更灵活,更适合复杂组件。虽然有学习成本,但一旦习惯了,效率会高很多。

4. 用Pinia和Vite

Pinia比Vuex更简单,Vite比Webpack更快。新生态有新的坑,但整体体验更好。

5. 用TypeScript

Vue 3对TypeScript的支持很好,用TypeScript可以在编译时发现很多错误,减少运行时的坑。

6. 多写多练

踩坑是学习的必经之路。多写项目,多踩坑,多总结,才能真正掌握Vue 3。

九、写在最后

Vue 3.2+生态,虽然有不少坑,但整体来说,比Vue 2更强大、更高效、更优雅。

这些坑,很多是因为从Vue 2转过来的思维惯性,或者对新特性不熟悉。只要理解了原理,注意了细节,大部分坑都是可以避免的。

我踩过的这些坑,希望能帮你少走弯路。但技术在发展,新的坑还会不断出现。保持学习,保持好奇,才能跟上技术的步伐。

2022年了,Vue 3已经很成熟了,生态也越来越完善。如果你还在用Vue 2,建议尽快升级到Vue 3。虽然有迁移成本,但长期来看,是值得的。

最后,用一句话总结:"Vue 3的坑,大部分是因为不熟悉新特性。理解原理,注意细节,多写多练,就能避开大部分坑。"

愿你在Vue 3的世界里,少踩坑,多写好代码。