vue2 => vue3

迁移到vue3

新特性

开始之前,先介绍一下vue3的新特性:

  • Composition API。一种更好的逻辑复用和代码组织方式
  • Teleport。提供一种简洁的方式可以指定它里面内容的父元素
  • Fragments。vue3中组件支持拥有多个根元素
  • Global API。vue3中将全局API抽取成为独立函数,已支持tree shaking
  • 全面的TypeScript支持
  • ...

更多特性以及使用方法请参考官方文档

本文介绍的迁移方案是基于vue的单文件组件(SFC),如果是jsx/tsx的迁移,请参考Jsx for vue3

改动点

我们之前的项目都是基于vue2.x,如果要迁移到vue3中,有一些需要注意的地方

  • template和style几乎不需要改动,vue3的style也有一些新的变化,详情可查看Style Features

  • vue3仍然向前兼容,如果是保留Options API方式的,组件中的js代码几乎不需要改动,有改动的地方如下(不完整,更多细节请参考向Vue2迁移):

    • 使用createApp()代替了new Vue()

      import { createApp } from 'vue';
      import App from './index.vue';
      const app = createApp(App);
      app.mount("#app");
    • vue3没有Vue全局变量,原先挂载到Vue.prototype上的属性,可以用app.config.globalProperties替代

      import { createApp } from 'vue';
      import axios from 'axios';
      import App from './index.vue';
      const app = createApp(App);
      app.config.globalProperties.$axios = axios; // Vue.prototype.$axios = axios;
      app.mount("#app");
    • vue3没有Vue全局变量,一些全局api的用法发生了改变

      import { createApp } from 'vue';
      import App from './index.vue';
      const app = createApp(App);
      app.component('component-a', ComponentA); // Vue.component
      app.directive('directive-a', directiveA); // Vue.directive
      app.mount("#app");
    • 处理静态资源的时候,vite不支持require,如果require引入了图片,可以使用new URL('xxx', import.meta.url).href,详情参考Static Asset Handling

      <template>
        <!-- vue2.x <img :src="require('xxx')" /> -->
        <img :src="getImageUrl('xxx')" />
      </template>
      
      <script>
        export default {
          methods: {
            getImageUrl(name) {
              return new URL(`@/assets/${name}.png`, import.meta.url).href;
            }
          }
        }
      </script>
    • 异步组件引用方式改变

      <script>
        import { defineAsyncComponent } from 'vue' 
      
        export default {
          components: {
            ComponentA: defineAsyncComponent(() => import('xxxx')); // vue2.x () => import('xxxx')
          }
        }
      </script>
    • v-for中的 Ref数组

      <template>
        <div v-for="item in list" :ref="setItemRef"></div>
      </template>
      <script>
        export default {
          data() {
            return {
              itemRefs: []
            }
          },
          methods: {
            setItemRef(el) {
              if (el) {
                this.itemRefs.push(el)
              }
            }
          },
          beforeUpdate() {
            this.itemRefs = []
          },
          updated() {
            console.log(this.itemRefs)
          }
        }
      </script>
    • 不再有$children这个属性,请使用$refs替换

    • Vue.prototype.$set已经被剔除,可以直接修改对象或者数组

      // vue2.x
      let obj = {};
      let arr = [];
      this.$set('name', 'jack');
      this.$set(arr, 0, 'hello');
      //vue3.x  
      obj['name'] = 'jack';  
      arr[0] = 'hello';
    • 使用defineComponent方式声明组件,最重要的是给组件正确的参数类型推断,类似于vite.config.js中的defineConfig。可选

      <script>
      import { defineComponent } from 'vue';
      export default defineComponent({
      data() {},
      props: {},
      methods: {},
      computed: {}
      watch: {}
      })
      </script>

Composition API

1. 基本结构

<template>
  <div class="count">{{count}}</div>
  <div class="double-count">{{doubleCount}}</div>
  <button class="add-count" @click="addCount">add count</button>
</template>  

<script>
  import { ref, computed, watch, watchEffect, onMounted, onUnMount } from 'vue';
  export default defineComponent({
    props: {} // 保留,同vue2
    components: {} // 保留,同vue2
    // vue3提供了setup方法,作为composition api 的入口
    // setup是在befroeCreated之前被调用的,this不能找到组件实例,所以避免在setup中使用this
    setup(props, {emit, ...args}) {
      // 声明响应式对象的方法有ref 或者 reactive,在注意事项中会详细说明
      let count = ref(0);
  
      const addCount = () => {
        count.value += 1;
        emit('fn', count.value);
        emit('gn', doubleCount.value);
      }
  
      // computed和watch,在vue3中提供了computed和watch等api,可以单独使用
      let doubleCount = computed(() => count.value * 2);
  
      watch(count, (newCount) => {
        console.log('count changed', newCount);
      })
  
      watchEffect(() => {
        console.log('count is watched', count.value);
      })
  
      // 生命周期,详情请见注意事项
      onMounted(() => {});
  
      onUnMount(() => {});
  
      // 注意,template用到的属性和方法一定要return出去
      return {
        count,
        doubleCount,
        addCount
      }
    }
  })
</script>
  • composition api是可以和options api混用的,但是只能在options api中引用composition api。因为this的行为不同,不建议这种写法

  • vue3提供了单独的生命周期api,但是没有beforeCreated和created,如果有需要使用这两个生命周期,直接写在setup中即可

  • ref、reactive、toRefs的区别和用法

    • reactive:

      • 参数必须是对象或者数组
      • 底层的本质是将数据包装成Proxy
        // reactive
        const obj = reactive({ count: 0 });
        obj.count += 1;
        console.log(obj.count); // 1
    • ref:

      • 参数可以是基础类型也可以是对象类型,如果参数是对象类型,本质还是reactive;
      • ref只能操作浅层次的数据,深层次的数据依赖于reactive;
      • 在template中访问,系统会自动添加.value;在js中需要手动.value;
      • 因为ref响应式原理是依赖于Object.defineProperty()的get 和 set,所以返回是一个对象,这个对象上只有一个value属性,指向该内部值
      // ref
      let count = ref(0);
      console.log(count.value); // 0
      count.value += 1;
      console.log(count.value); // 1
    • toRefs:

      • 接收一个响应式对象作为参数,返回的结果对象上的每个属性都是指向原始对象中相应属性的ref对象
      • 传递响应式而不是创建响应式(数据修改会影响到原始数据)
      // toRefs
      let user = reactive({
      name: 'jack',
      age: 18
      })
      let { name, age } = toRefs(user);
      console.log(name.value, age.value); // jack 18
      age.value += 1;
      console.log(user.age); // 19
  • ref引用子组件或者dom的方式

    <template> 
      <>
        <div ref="root">This is a root element</div>
        <div 
          v-for="i of 5" 
          :key="i"
          :ref="el => refList[i] = el">
          {{i}}
        </div>
      <>
    </template>
    
    <script>
      import { ref, onMounted } from 'vue'
    
      export default {
        setup() {
          // 常规引用
          const root = ref(null) // 变量名与ref一致
          // 循环引用
          const refList = ref([]);
    
          onMounted(() => {
            // DOM 元素将在初始渲染后分配给 ref
            console.log(root.value) // <div>This is a root element</div>
            console.log(refList.value[0]); // 
          })
    
          return {
            root,
            refList
          }
        }
      }
    </script>
    // 循环引用
  • template中用到的方法和属性,一定要在setup方法的最后return出去

  • 解构props不能直接用ES6的解构赋值,这样会丢失数据的响应式。需要...toRefs(props)的方式进行解构

2. setup语法糖

  • 2.1 基本结构

    <template>
      <div class="count">{{count}}</div>
      <div class="double-count">{{doubleCount}}</div>
      <button class="add-count" @click="addCount">add count</button>
      <Children />
    </template> 
    
    <script setup lang="ts">  
    import { ref, computed, watch, defineProps, defineEmits } from 'vue';
    
    // 组件引入,引入的组件可以直接在template中使用,不需要在components中进行注册
    import Children from 'xxx';
    
    // props和emit必须要用defineProps和defineEmits来声明。props不能解构赋值,不然会破坏响应式,props中的属性在template中使用不需要props.xx,直接使用xx
    const props = defineProps({
      a: {},
      b: {}
    })
    
    const emit = defineEmits(['fn', 'gn']);
    
    // 变量,函数声明,以及 import 引入的内容都能在模板中直接使用,不需要return
    let count = ref(0);
    const addCount = () => {
      count.value += 1;
      emit('fn', count.value);
      emit('gn', doubleCount.value);
    }
    let doubleCount = computed(() => count.value * 2);
    
    // 生命周期和defineComponent没有变化
    </script>
  • 2.2 注意事项

    • 通过ref或者$parent的方式访问组件中的属性,需要用defineExpose暴露出去

3. 适合使用setup的场景

  • 3.1 搭配typescript,更好的类型推断与代码检查

    <script setup lang="ts">  
    import { ref, computed, defineProps, defineEmits, withDefaults } from 'vue';
    import { Ref } from '@vue/runtime-dom';
    
    let count: Ref<number> = ref(0);
    
    const props = withDefaults(defineProps<{
      a: boolean
    }>(), {
      a: false,
    });
    
    const emit = defineEmits<{
      (e: 'fn', num: number): void,
      (e: 'gn', num: number): void,
    }>(['fn', 'gn']);
    
    const addCount = (count: Ref<number>): void => {
      count.value += 1;
      emit('fn', count.value);
      emit('gn', doubleCount.value);
    }
    </script>
  • 3.2 通用逻辑的抽离与组合

    // 新建一个useScroll.ts
    import { onMounted, onUnmounted } from 'vue';
    
    const useScroll = (callback: () => {}): void => {
      onMounted(() => {
        window.addEventListener('scroll', callback());
      });
    
      onUnmounted(() => {
        window.removeEventListener('scroll', callback());
      })
    };
    export default useScroll;
    // 在组件中使用
    <script setup lang="ts">
    import useScroll from 'useScroll.ts';
    
    useScroll(() => { console.log('页面滚动') });
    </script>
  • 3.3 优雅的组件逻辑分解,不依赖组件的情况去维护数据

    // 新建一个useCount.ts
    import { ref, compunted } from 'vue';
    
    const useCount = () => {
      let count = ref(0);
    
      const addCount = () => {
        count.value += 1;
      }
    
      let doubleCount = computed(() => count.value * 2);
    
      return {
        count,
        addCount,
        doubleCount
      }
    }
    
    export default useCount;
    // 在组件中使用
    <script setup>
      import useCount from 'useCount.ts';
    
      let { count, doubleCount, addCount } = useCount(); // 可以直接在template中使用
    </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值