一步搞定Vue3开发

313 阅读4分钟

前言

Vue3已经发布一段时间了,前不久在项目中使用了Vue3,借这篇文章来讲述一下Vue3的实践方法,以及分享一下开发过程中遇到的一些问题,希望能对大家有一些帮助。

一、为什么重写Vue2.X

尤雨溪的回答是两个关键因素:

  • 主流浏览器对新的JavaScript语言特性的普遍支持
  • 当前Vue代码库随着时间的推移而暴露出来的设计和体系架构问题

二、安装Vue3

  • 通过脚手架Vite
npm init vite hello-vue3 | yarn create vite hello-vue3
  • 通过脚手架vue-cli
npm install -g @vue/cli | yarn global add @vue/cli
vue create hello-vue3
# 选择 vue 3 preset

三、组件基本结构分析

下面以helloWorld.vue组件为例讲述

//dom 里的东西基本与Vue2相同
<template>
  <h1>{{ msg }}</h1>
  <button @click="increment">
    count: {{ state.count }}, double: {{ state.double }},three:{{ three }},refnum:{{refnum}}
  </button>
</template>

<script>
// 这里就是Vue3的组合Api了,需要的api要手动import引入
import {ref, reactive, computed ,watchEffect,watch} from "vue"

export default {
  name: "HelloWorld",
  
  props: {
    msg: {
      type: String,
      default: ''
    }
  },
  
  // 这里的setup相当于Vue2的beforeCreate 和created,简单理解就是初始化
  setup() { 
    // 这里通过reactive使state成为响应状态
    const state = reactive({
      count: 0,
      // 计算属性computed可以直接在state中使用,更灵活了
      double: computed(() => state.count * 2),
    })
    
    //computed也可以单独拿出来使用
    const three = computed(() => state.count * 3)
    
    //ref跟reactive作用一样都是用来数据响应的,ref的颗粒度更小
    const refnum = ref()
    
    //这里的watchEffect只要里面的变量发生了改变就会执行,并且第一次渲染会立即执行,无法获取到变化前的值
    watchEffect(() => {
      refnum.value = state.count
      console.log(state, "watchEffect")
    })
    
    //watch的使用方法与Vue2相同,第一个参数是监听需要的变量,第二个是执行的回调函数,
    watch(refnum,(a,b)=>{
      console.log(a,b,'watch,a,b')
    })
    
    //所有的方法里再也不需要用this了
    function increment() {
      state.count++
    }
    
    //组中模板中需要的变量,都要通过return给暴露出去
    return {
      state,
      increment,
      three,
      refnum
    };
  },
};
</script>

四、Vue3新特性

1. 组合式API

WeChat640a9cbb3bfbb24ff29a4d43ecb37e52.png setup是Vue3新增的一个组件选项,在组件被创建之前,props被解析之后执行,是组合式API的入口

setup参数

  • props 组件传入的属性

props是响应式的,会及时被更新,由于是响应式的,所以不可以使用ES6解构,解构会消除他的响应式

  • context

由于setup中不能访问Vue2中最常用的this对象,所以context提供了this中最常用的三个属性:attrsslotemit,分别对应Vue2中的$attr属性、slot插槽和$emit发射事件

script setup 语法糖

setup script是Vue3新出的一个语法糖,使用方法就是在书写script标签的时候在后面加一个setup修饰,这与普通的script只在组件被首次引入的时候执行一次不同,<script setup>中代码会在每次组件实例被创建的时候执行。 例如:

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<script setup>中需要使用definePropsdefineEmits来声明propsemits,他们会提升到模块范围,所以引用时直接使用变量名即可:

<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
</script>

<script setup>默认是关闭的,通过ref或者$parent链不能获取组件内的属性,想要获取组件内的属性需要使用defineExpose来暴露出可以访问的属性:

<script setup>
import { ref, defineExpose } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

<script setup>中需要使用useSlotsuseAttrs来调用slotsattrs

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

<script setup>可以和普通的<script>一起使用,例如inheritAttrs或通过插件启用的自定义选项、声明命名导出、运行副作用或者创建只需要执行一次的对象等情况,必须用到<script>,但setup只有一个可以生效(后面的会覆盖前面的)

<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()

// 声明额外的选项
export default {
  inheritAttrs: false,
  customOptions: {}
}
</script>

<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>

使用语法糖优点:

  1. 自动注册组件;
  2. 属性和方法无需返回

响应式变量的定义 ref、toRef、reactive、toRefs

看到这些,大家可能很疑惑,无从下手,下面我们就先来看一下它们的使用方式

<template>
   <div>哈哈哈</div>
   <HelloWorld ref='helloWorldRef'></HelloWorld>
</template>

<script setup>
import { ref, toRef, reactive, toRefs } from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'

// 为子组件或html元素绑定ref,函数中使用通过helloWorldRef.value访问
const helloWorldRef = ref()
// 初始化响应式变量tor,函数中取值通过tor.value
let tor = ref(0)

const obj = { age: 12 }
// 将对象的某个属性转化为响应式,并设置key值,函数中取值通过toR.value
let toR = toRef(obj, 'age')

// 初始化响应式变量,传入的值需为引用类型的值,例如数组、对象等
// reactive内部可以使用计算属性等各种方法,跟ref混合使用时可以用isRef判断类型
// 在模板中可以使用state.num访问,通过toRefs转换后在模板中可以直接使用num访问
const state = reactive({
   num: 1,
   name: '张三'
})

return {
   helloWorldRef,
   tor,
   toR,
   // toRefs可以将使用了reactive的响应式对象拆分成多个响应式ref,外界可以读取到响应式的所有属性
   ...toRefs(state)
}
</script>

来总结一下:

  • ref可以将一个基本数据类型的属性转换为响应式,使用在对象的某个属性时,是对该属性的拷贝,但拷贝后的值改变不会原对象属性的值
  • toRef可以将对象的某个基本数据类型的属性转换为响应式,是对该属性的引用,所以值改变后会影响原对象的值
  • toRefs是原对象数据的引用,值改变后会影响原对象的值,但必须要和reactive连用
  • reactive可以将引用类型的数据转换为响应式,访问不用加.value

watch、watchEffect

看名字就知道两者都是用来监听的,但是又存在着一些区别,下面我们先来看一下watch的使用方式

watch(source, callback, [options])
// watch的使用方式和Vue2相同,可接收三个参数
// source:需要监听的数据源,支持基本数据类型、object、function、array
// callback:执行的回调函数
// options:支持deep、immediate和flush选项

watch监听不同的数据源,使用方式略有不同

<script setup>
import { ref, toRef, reactive, toRefs } from 'vue'

let tor = ref(0)

const obj = { age: 12 }
let toR = toRef(obj, 'toR')

const state = reactive({
   num: 1,
   name: '张三',
   room: {
      id: 1,
      attrs: {
         size: 100,
         type: '两室一厅'
      }
   }
})

// 监听reactive定义的数据
watch(
   () => state.name,
   (newValue, oldValue) => {
      console.log("新值:", newValue, "老值:", oldValue)
   }
)

// 监听ref定义的数据
watch(
   tor,
   (newValue, oldValue) => {
      console.log("新值:", newValue, "老值:", oldValue)
   }
)

// 监听多个数据
watch(
   [() => state.name, tor],
   ([newName, newTor], [oldName, oldTor]) => {
      console.log("新值:", newName, "老值:", oldName)
      console.log("新值:", newTor, "老值:", oldTor)
   }
)

// 监听深度嵌套的对象
watch(
   () => state.room,
   (newValue, oldValue) => {
      console.log("新值:", newValue, "老值:", oldValue)
   },
   { deep: true }
)

// stop停止监听,组件中创建的watch监听会在销毁时自动停止,但如果我们想在组件销毁之前停止监听,可以通过调用`watch()`函数的返回值
const stopWatch = watch(
   () => state.name,
   (newValue, oldValue) => {
      console.log("新值:", newValue, "老值:", oldValue)
   }
)
setTimeout(() => {
   stopWatch()
}, 3000)
</script>

上面介绍了watch的使用方法,基本上已经满足我们需要的所有监听需求了,下面再来看一下watchEffect的使用方式吧

<script setup>
import { ref, toRef, reactive, toRefs } from 'vue'

let tor = ref(0)

const obj = { age: 12 }
let toR = toRef(obj, 'toR')

const state = reactive({
   num: 1,
   name: '张三'
})

// watchEffect使用方式
watchEffect(() => {
   console.log(state, tor)
})
</script>

最后总结一下二者的区别

  • watch需要传入需要监听的属性;watchEffect不需要传入参数
  • watch是惰性的,只有在监听属性变化时才会执行,可通过传入immediate参数使其在初始化时执行;watchEffect第一次会立即执行
  • wacth的回调方法会返回变化前后的值;watchEffect只可以拿到变化后的值
  • watch可以reactive绑定的对象,watchEffect不可以(reactive的值要具体到内部属性),只会执行一次

生命周期

我们从生命周期图来看一下生命周期的变化 image.png 最后来总结一下各声明周期钩子在setup中的名称

选项式 API调用时机setup
beforeCreate在实例初始化之后、进行数据侦听和事件/侦听器的配置之前同步调用不需要
created在实例创建完成后被立即同步调用不需要
beforeMount在挂载开始之前被调用onBeforeMount
mounted在实例挂载完成后被调用onMounted
beforeUpdate在数据发生改变后,DOM 被更新之前被调用onBeforeUpdate
updated在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用onUpdated
beforeUnmount在卸载组件实例之前调用onBeforeUnmount
unmounted卸载组件实例后调用onUnmounted
errorCaptured在捕获一个来自后代组件的错误时被调用onErrorCaptured
renderTracked跟踪虚拟 DOM 重新渲染时调用onRenderTracked
renderTriggered当虚拟 DOM 重新渲染被触发时调用onRenderTriggered
activated被 keep-alive 缓存的组件激活时调用onActivated
deactivated被 keep-alive 缓存的组件失活时调用onDeactivated

整体来看其实变化不大,使用setup代替了之前的beforeCreatecreated,其他生命周期名字有些变化,功能都是没有变化的;另外除了beforeCreatecreated,其他的生命周期钩子还是可以在setup外调用的,setup中调用的生命周期钩子函数可以重复调用,有助于代码的集中管理

2. Teleport

teleport是Vue3新推出的功能,这个词翻译过来是传送的意思,意为将模板传动到DOMVue app之外的其他位置。

下面来看一下teleport的使用方式:

<!-- index.html -->
<body>
  <div id="app"></div>
  <!-- modal需要渲染的位置 -->
  <div id="modal"></div>
</body>

然后我们再来看一下modal组件的具体定义

<template>
  <!-- 传送到index.html文件中id为modal的dom下 -->
  <teleport to="#modal">
    <div class="modal" v-if="show">
      <!-- 模态框内容 -->
      <slot></slot>
    </div>
  </teleport>
</template>

最后我们来看一下在helloWorld组件中引用model组件

<template>
  <div class="hello-world">
    <p class="text">Hello World!</p>
    <el-button type="primary" size="small" @click="handleShowModal">展示Modal</el-button>

    <modal :show="show" @handleCloseModal="handleCloseModal">
      <div class="modal-content">
        <p class="icon-close"  @click="handleCloseModal">X</p>
        <p class="modal-content-text">这里是modal</p>
      </div>
    </modal>
  </div>
</template>

<script>
import { ref } from 'vue'
import modal from './components/modal.vue'
export default {
  name: 'helloWorld',

  components: {
    [modal.name]: modal
  },

  setup() {
    let show = ref(false)

    const handleShowModal = () => {
      show.value = true
    }

    const handleCloseModal = () => {
      show.value = false
    }

    return {
      show,
      handleShowModal,
      handleCloseModal
    }
  }
}
</script>

我们来看一下效果

WeChat04161229b141f74e23b906813775b202.png 可以看到我们虽然是在helloWorld组件中引用的modal组件,但是modal却渲染在了和app同级的modal中,但是modal的显示却是由helloWorld组件控制的。

最后来总结一下:teleport可以将包裹的内容传送到index.html文件中除<div id="app"></div>之外的其他位置

使用场景:像 modal,toast 等这样的元素,需要使用到 Vue 组件的状态(data 或者 props)的值,但是又想在在 Vue 应用的范围之外渲染它

原因在于如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难

3. 片段

在Vue2中,组件内只被允许有一个根节点,所以许多组件都被包裹在一个<div>中,像这样:

<template>
  <div>
    <header></header>
    <main></main>
    <footer></footer>
  </div>
</template>

现在Vue3中,组件允许你定义多个根节点,这也是Vue3的一个新特性,像这样:

<template>
  <header></header>
  <main></main>
  <footer></footer>
</template>

4. 触发组件选项

这一部分主要从父子组件发射事件与绑定属性说明

  • 事件名

与Vue2用法类似,可自动转换大小写,即子组件中触发一个以驼峰式命名的事件,可以在父组件中添加一个短横线分割命名的监听器。

// 子组件
this.$emit('myEvent')

// 父组件
<my-component @my-event="doSomething"></my-component>
  • 1)定义自定义事件 在Vue3中子组件发射的事件需要集中在emits中定义,类似于props的用法
emits: ['inFocus', 'submit']

事件与props的类型验证类似,也可以验证抛出的事件,使用对象语法而不是数组语法定义发出的事件,就可以对它进行验证

emits: {
  // 没有验证
  click: null,

  // 验证 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
},
methods: {
  submitForm(email, password) {
    context.emit('submit', { email, password })
  }
}
  • 2)v-model参数

v-model在Vue2中也可以传参给子组件,并且子组件可以修改父组件的值,但是一个组件只可定义一个v-model且子组件接收只能通过value;若子组件想修改多个父组件的值,需要使用.sync属性,但Vue3中删除了.sync属性,Vue3中子组件想修改父组件的变量值,可以通过v-model来实现

默认情况下,父组件上的v-model使用modelValue作为prop,子组件中的update:modelValue作为事件

// 父组件
<my-component v-model="bookTitle"></my-component>

// 子组件
<script>
import { ref, toRef, reactive, toRefs } from 'vue'

export default {
   props: {
     modelValue: {
       type: String,
       defaule: ''
     }
   },
   
   emits: ['update:modelValue'],
   
   setup(props, context) {
     context.emit('update:modelValue', 'newValue')
   }
}
</script>

我们也可以通过向v-model传递参数来修改这些名称

// 父组件
<my-component v-model:title="bookTitle"></my-component>

// 子组件
<script>
import { ref, toRef, reactive, toRefs } from 'vue'

export default {
   props: {
     title: {
       type: String,
       defaule: ''
     }
   },
   
   emits: ['update:title'],
   
   setup(props, context) {
     context.emit('update:title', 'newValue')
   }
}
</script>

我们也可以绑定多个v-modal,每个v-model将同步到不同的prop

// 父组件
<my-component v-model:title="bookTitle" v-model:content="bookContent"></my-component>

// 子组件
<script>
import { ref, toRef, reactive, toRefs } from 'vue'

export default {
   props: {
     title: {
       type: String,
       defaule: ''
     },
     
     content: {
       type: String,
       defaule: ''
     }
   },
   
   emits: ['update:title', 'update:content'],
   
   setup(props, context) {
     context.emit('update:title', 'newTitle')
     context.emit('update:content', 'newContent')
   }
}
</script>

5. 单文件组件状态驱动的css变量

单文件组件的<style>标签可以通过v-bind这一CSS函数将CSS的值关联到动态的组件状态上:

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

<style>
.text {
  color: v-bind(color);
}
</style>

<script setup>中需要这样使用:

<script setup>
const theme = {
  color: 'red'
}
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

6. Suspense

Suspense是Vue3中的新增特性,当前处于试验阶段,生产环境不建议使用,下面我们就简单来看一下它的作用

背景:前端页面的展示内容通常依赖于异步接口返回的结果,在此之前为了交互友好我们通常会添加一个loading或者骨架屏,等拿到数据中再进行展示,在Vue2中我们是通过v-if判断来控制的。

Suspense就是为了解决这一问题的,它提供了两个插槽,他们都只接收一个子节点,当default中的节点不能展示时,会展示fallback插槽里的节点,只到default中的节点可以正常展示为止

<template>
  <suspense>
    <template #default>
      <todo-list />
    </template>
    <template #fallback>
      <div>
        Loading...
      </div>
    </template>
  </suspense>
</template>

<script>
export default {
  components: {
    TodoList: defineAsyncComponent(() => import('./TodoList.vue'))
  }
}
</script>

注意:使用Suspense,需要返回一个promise

五、变更

这里主要说一下插槽、自定义指令、异步组件的变更,其他变更建议查看官方文档

1. slot

在Vue2中我们这样使用具名插槽:

<!--  子组件中:--> 
<slot name="title"></slot>

在父组件中调用:

<template slot="title"> 
  <p>title</p> 
 <template>

在Vue2中我们这样使用作用域插槽:

// 子组件 
<slot name="content" :data="content"></slot> 

export default { 
  data(){
    return {
      content: 'content'
    }
  }
}

在父组件中调用:

<template slot="content" slot-scope="scoped"> 
  <p>{{ scoped.data }}</p> 
<template>

在Vue3中将slotslot-scope进行了合并,统一通过v-slot来使用:

<!-- 父组件中使用 -->
<template v-slot:content="scoped"> 
  <p>{{ scoped.data }}</p> 
<template>
 
<!-- 也可以简写成: --> 
<template #content="{data}"> 
  <p>{{ data }}</p> 
</template>

2. 自定义指令

还记得在Vue2中我们如何实现一个自定义指令吗

// 注册一个全局的v-highlight指令
Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value
  }
})

Vue3将指令的钩子函数重新命名,现与组件的生命周期保持一致,expression字符串不再作为binding对象的一部分被传入

Vue2Vue3备注
created新增,在元素的attributte或事件监听器被应用之前调用
bindbeforeMount指令绑定到元素后调用,只调用一次
insertedmounted元素插入父DOM后调用
beforeUpdate新增,在元素本身被更新之前调用,与组件的生命周期钩子十分相似
update移除
componentUpdatedupdated一旦组件和子级被更新,就会调用这个钩子
beforeUnmount新增,与组件的生命周期钩子类似,它将在元素被卸载之前调用
unbingunmounted一旦指令被移除,就会调用这个钩子,只调用一次

在Vue3我们这样来自定义指令

const app = Vue.createApp({})

app.directive('highlight', {
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value	
  }
})

3. 异步组件

Vue3中使用defineAsyncComponent来定义异步组件,配置选项component替换为loaderloader函数不在接收resolvereject参数,且必须返回一个Promise

<template>
  <!-- 异步组件的使用 -->
  <AsyncPage />
</tempate>

<script>
import { defineAsyncComponent } from "vue";

export default {
  components: {
    // 无配置项异步组件
    AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),

    // 有配置项异步组件
    AsyncPageWithOptions: defineAsyncComponent({
      loader: () => import(".NextPage.vue"),
      delay: 200,
      timeout: 3000,
      errorComponent: () => import("./ErrorComponent.vue"),
      loadingComponent: () => import("./LoadingComponent.vue"),
    })
  },
}
</script>

注意:

  • defineAsyncComponentsuspense一起用时,defineAsyncComponent配置的延迟、超时、错误、加载选项都将被忽略
  • 实践中发现子组件返回promise并使用defineAsyncComponent同时使用时,子组件不显示,必须与suspense同时使用才可以

4. 过滤器

vue3中去掉了filter,官方建议使用computed或者method来代替

  • 使用computed
computed: {
    computedText() {
      // 计算属性要return一个函数接收参数
      return function (state) {
        switch (state) {
          case "1":
            return "待发货";
            break;
          case "2":
            return "已发货";
            break;
          case "3":
            return "运输中";
            break;
          case "4":
            return "派件中";
            break;
          case "5":
            return "已收货";
            break;
          default:
            return "快递信息丢失";
            break;
        }
      };
    },
  },
  • 使用method
methods: {
    methodsText(state) {
      switch (state) {
        case "1":
          return "待发货";
          break;
        case "2":
          return "已发货";
          break;
        case "3":
          return "运输中";
          break;
        case "4":
          return "派件中";
          break;
        case "5":
          return "已收货";
          break;
        default:
          return "快递信息丢失";
          break;
      }
    },
  },