Post

从Vue2到Vue3

从Vue2到Vue3

Vue3的重大变化概览

Vue3相比Vue2有了诸多改进:

  • Composition API:全新的组件逻辑组织方式
  • 性能提升:更快的渲染和更小的包体积
  • TypeScript支持:原生TypeScript重写,更好的类型推断
  • 新特性:Teleport、Fragments、Suspense等
  • Tree-shaking支持:按需引入,减少包体积

Composition API详解

为什么需要Composition API?

在Vue2中,我们使用Options API来组织组件逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Vue2 Options API
export default {
  data() {
    return {
      count: 0,
      loading: false,
      users: []
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    async fetchUsers() {
      this.loading = true
      try {
        const response = await api.getUsers()
        this.users = response.data
      } finally {
        this.loading = false
      }
    }
  },
  mounted() {
    this.fetchUsers()
  }
}

但在复杂组件中,相关的逻辑会分散在不同的选项中,难以维护。

Composition API的优势

Vue3的Composition API让我们可以按逻辑功能组织代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Vue3 Composition API
import { ref, computed, onMounted } from 'vue'
import { useUsers } from '@/composables/useUsers'

export default {
  setup() {
    // 计数器逻辑
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    const increment = () => count.value++

    // 用户数据逻辑
    const { users, loading, fetchUsers } = useUsers()

    onMounted(() => {
      fetchUsers()
    })

    return {
      count,
      doubleCount,
      increment,
      users,
      loading,
      fetchUsers
    }
  }
}

核心API详解

1. ref和reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { ref, reactive, toRefs } from 'vue'

export default {
  setup() {
    // ref用于基本类型
    const count = ref(0)
    const message = ref('Hello Vue3')
    
    // reactive用于对象类型
    const state = reactive({
      user: {
        name: 'John',
        age: 25
      },
      posts: []
    })
    
    // 使用toRefs解构reactive对象
    const { user, posts } = toRefs(state)
    
    const updateUser = () => {
      // 修改ref需要.value
      count.value++
      
      // 直接修改reactive对象
      state.user.name = 'Jane'
      state.user.age++
    }
    
    return {
      count,
      message,
      user,
      posts,
      updateUser
    }
  }
}

2. computed和watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { ref, computed, watch, watchEffect } from 'vue'

export default {
  setup() {
    const firstName = ref('John')
    const lastName = ref('Doe')
    
    // computed
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`
    })
    
    // 可写的computed
    const fullNameWritable = computed({
      get: () => `${firstName.value} ${lastName.value}`,
      set: (value) => {
        const names = value.split(' ')
        firstName.value = names[0]
        lastName.value = names[1] || ''
      }
    })
    
    // watch单个源
    watch(firstName, (newVal, oldVal) => {
      console.log(`firstName changed: ${oldVal} -> ${newVal}`)
    })
    
    // watch多个源
    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log('Name changed')
    })
    
    // watchEffect 自动追踪依赖
    watchEffect(() => {
      console.log(`Full name: ${fullName.value}`)
    })
    
    return {
      firstName,
      lastName,
      fullName,
      fullNameWritable
    }
  }
}

3. 生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated,
  onBeforeUnmount, 
  onUnmounted 
} from 'vue'

export default {
  setup() {
    console.log('setup - 相当于 beforeCreate/created')
    
    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    
    onMounted(() => {
      console.log('onMounted')
    })
    
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate')
    })
    
    onUpdated(() => {
      console.log('onUpdated')
    })
    
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
    })
    
    onUnmounted(() => {
      console.log('onUnmounted')
    })
  }
}

Composables - 逻辑复用的新方式

在Vue2中,我们使用mixins来复用逻辑,但mixins有命名冲突和来源不明的问题。Vue3的composables提供了更好的解决方案:

创建一个用户管理的composable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// composables/useUsers.js
import { ref, reactive } from 'vue'
import { userApi } from '@/api/users'

export function useUsers() {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  const fetchUsers = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await userApi.getUsers()
      users.value = response.data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  const addUser = async (userData) => {
    try {
      const response = await userApi.createUser(userData)
      users.value.push(response.data)
      return response.data
    } catch (err) {
      error.value = err.message
      throw err
    }
  }
  
  const deleteUser = async (userId) => {
    try {
      await userApi.deleteUser(userId)
      users.value = users.value.filter(user => user.id !== userId)
    } catch (err) {
      error.value = err.message
      throw err
    }
  }
  
  return {
    users: readonly(users),
    loading: readonly(loading),
    error: readonly(error),
    fetchUsers,
    addUser,
    deleteUser
  }
}

在组件中使用composable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// components/UserList.vue
import { onMounted } from 'vue'
import { useUsers } from '@/composables/useUsers'

export default {
  setup() {
    const { 
      users, 
      loading, 
      error, 
      fetchUsers, 
      deleteUser 
    } = useUsers()
    
    const handleDelete = async (userId) => {
      if (confirm('确定要删除这个用户吗?')) {
        try {
          await deleteUser(userId)
        } catch (err) {
          alert('删除失败')
        }
      }
    }
    
    onMounted(() => {
      fetchUsers()
    })
    
    return {
      users,
      loading,
      error,
      handleDelete
    }
  }
}

新特性实战

1. Teleport - 传送门

Teleport让我们可以将组件的一部分渲染到DOM的其他位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- Modal.vue -->
<template>
  <teleport to="body">
    <div v-if="show" class="modal-overlay" @click="close">
      <div class="modal-content" @click.stop>
        <slot></slot>
        <button @click="close">关闭</button>
      </div>
    </div>
  </teleport>
</template>

<script>
export default {
  props: ['show'],
  emits: ['close'],
  setup(props, { emit }) {
    const close = () => emit('close')
    return { close }
  }
}
</script>

<style>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
}
</style>

2. Fragments - 多个根元素

Vue3支持组件有多个根元素:

1
2
3
4
5
6
<template>
  <!-- Vue2中这样会报错,Vue3中完全OK -->
  <header>头部内容</header>
  <main>主要内容</main>
  <footer>底部内容</footer>
</template>

3. Suspense - 异步组件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- App.vue -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() => 
  import('./components/HeavyComponent.vue')
)

export default {
  components: {
    AsyncComponent
  }
}
</script>

项目迁移实战

1. 渐进式迁移策略

不需要一次性重写整个项目,可以逐步迁移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在Vue3项目中使用Vue2风格的组件
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
  // Vue3完全兼容这种写法
}

2. 混合使用Options API和Composition API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default {
  // 传统的props定义
  props: ['title'],
  
  // 使用Composition API
  setup(props) {
    const count = ref(0)
    const increment = () => count.value++
    
    return {
      count,
      increment
    }
  },
  
  // 传统的computed
  computed: {
    displayTitle() {
      return this.title.toUpperCase()
    }
  },
  
  // 传统的methods
  methods: {
    handleClick() {
      this.increment()
      console.log('Clicked!')
    }
  }
}

3. 常见迁移问题及解决方案

问题1:this.$refs的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Vue2
this.$refs.myInput.focus()

// Vue3 Composition API
import { ref } from 'vue'

setup() {
  const myInput = ref(null)
  
  const focusInput = () => {
    myInput.value.focus()
  }
  
  return {
    myInput,
    focusInput
  }
}

问题2:全局API的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Vue2
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false
Vue.use(SomePlugin)

new Vue({
  render: h => h(App)
}).$mount('#app')

// Vue3
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.use(SomePlugin)
app.mount('#app')

问题3:事件总线的替代方案

1
2
3
4
5
6
7
8
9
10
11
// Vue2使用事件总线
// eventBus.js
import Vue from 'vue'
export default new Vue()

// Vue3推荐使用mitt
// eventBus.js
import mitt from 'mitt'
export default mitt()

// 或者使用Pinia/Vuex进行状态管理

性能优化建议

1. 使用ref vs reactive的选择

1
2
3
4
5
6
7
8
9
10
11
12
13
// 基本类型使用ref
const count = ref(0)
const message = ref('hello')

// 复杂对象使用reactive
const state = reactive({
  user: { name: 'John', age: 25 },
  posts: [],
  loading: false
})

// 避免reactive包装基本类型
const badExample = reactive({ count: 0 }) // 不推荐

2. 合理使用readonly

1
2
3
4
5
6
7
8
9
10
11
12
import { readonly } from 'vue'

// 在composable中暴露只读状态
export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  
  return {
    count: readonly(count), // 防止外部直接修改
    increment
  }
}

3. 懒加载和代码分割

1
2
3
4
5
6
7
8
9
10
11
12
// 路由懒加载
const routes = [
  {
    path: '/users',
    component: () => import('./views/Users.vue')
  }
]

// 组件懒加载
const HeavyComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

与TypeScript的完美结合

Vue3对TypeScript的支持非常出色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 定义Props类型
interface Props {
  title: string
  count?: number
}

// 使用TypeScript的setup
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  setup(props: Props) {
    const internalCount = ref(props.count)
    
    const displayTitle = computed(() => 
      `${props.title} (${internalCount.value})`
    )
    
    const increment = (): void => {
      internalCount.value++
    }
    
    return {
      internalCount,
      displayTitle,
      increment
    }
  }
})

生态系统迁移

1. 路由迁移 (Vue Router 4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Vue2 + Vue Router 3
import VueRouter from 'vue-router'
Vue.use(VueRouter)

const router = new VueRouter({
  routes: [...]
})

// Vue3 + Vue Router 4
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})

2. 状态管理迁移 (Vuex 4 / Pinia)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Vuex 4
import { createStore } from 'vuex'

const store = createStore({
  state: { count: 0 },
  mutations: {
    increment(state) { state.count++ }
  }
})

// 或使用Pinia(推荐)
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const increment = () => count.value++
  
  return { count, increment }
})

实际项目经验总结

优势

  1. 更好的性能:重写的响应式系统更高效
  2. 代码组织:Composition API让逻辑复用更简单
  3. TypeScript支持:类型推断和检查更准确
  4. 包体积:支持tree-shaking,按需引入
  5. 开发体验:更好的IDE支持和调试体验

挑战

  1. 学习成本:需要学习新的API和概念
  2. 生态系统:部分Vue2插件需要更新
  3. 团队迁移:需要团队统一学习和规范

迁移建议

  1. 新项目直接使用Vue3
  2. 现有项目可以渐进式迁移
  3. 充分利用Composition API的优势
  4. 建立团队编码规范
This post is licensed under CC BY 4.0 by the author.