vue3 常用编码习惯总结

vue
2023-11-21 21:47:10

函数式计算属性

通常写计算属性的一般写法如下

typescript 复制代码
const numberA = ref(0)
const numberB = ref(0)
const result = computed(() => numberA.value + numberB.value)

函数式可以这样写。这样写的理由是,add可以封装一次多处复用,而不是像上边那种写法,每次都需要书写一遍

typescript 复制代码
// 函数传入a、b两个Ref类型的参数,返回计算属性
function add(a: Ref<number>, b: Ref<number>) {
  return computed(() => a.value + b.value)
}

// 无论修改numberA还是numberB,result都会自动改变。也可以将numberA、numberB设置一个常量一个Ref。
const numberA = ref(0)
const numberB = ref(0)
const result = add(numberA, numberB)

ref 自动解包

ref整合到reactive,而不用在.value去调用

typescript 复制代码
const numberA = ref(0)
const numberB = ref(0)
const numberC = 0 

const result = reactive({
  numberA,
  numberB,
  numberC,
})

// result.numberA
// result.numberB
// result.numberC

上边这种类比比较基础,通畅在封装一个use函数,往往有许多ref返回的类型,可以直接整合起来

typescript 复制代码
// 自定义的hook
function useAdd(a: Ref<number>, b: Ref<number>) {
  return {
    a,
    b,
    result: computed(() => a.value + b.value)
  }
}

// 可以整合到reactive
const result = reactive(useAdd(ref(0), ref(0)))

result.a
result.b
result.result

重用ref

如果不太想关心传入的参数是不是ref,可以直接用ref去接收一个ref,像这个例子:这里 isRef(a) ? a.value 写法 和ref(a).value完全等价

typescript 复制代码
function add(a: Ref<number> | number, b: Ref<number>) {
  // 这里不清楚传入的a 是不是ref
  // 一般是这样做
  // return computed(() => isRef(a) ? a.value : a + b.value)
  // 这里 isRef(a) ? a.value 写法 和 ref(a).value 完全等价
  return computed(() => ref(a).value + b.value)
}

利用组合式函数和计算属性异步转同步

项目中最频繁的使用是发送后端的请求,以常见的axios请求为例,通常会这样写。

typescript 复制代码
const data = ref([]) // 需要和页面交互的数据

function loadList() {
  axios.get("xxxx").then(res => {
    data.value = res.data
  })
}

这样的使用很不符合组合api的思维,这里简单封装一下

typescript 复制代码
function useAxios(config: AxiosRequestConfig) {
  const data = ref({})
  function load() {
    axios(config).then(res => {
      data.value = res.data
    })
  }
  return {
    load,
    data
  }
}

在使用的时候就很方便了

typescript 复制代码
const { load: load1, data: data1 } = useAxios({ url: "xxxxx1", method: "get" })
const { load: load2, data: data2 } = useAxios({ url: "xxxxx2", method: "post" })

这样一来,只需要调用load函数,data数据就会自动响应到页面了。当然一般而言,我们响应的数据还要再处理下。

typescript 复制代码
// 后端返回的可能是这个样子: {msg:"成功",data:[],code: 0},这样在取值的时候可以根据情况处理,然后再在页面上渲染。这样一来,就将一个异步代码
const result = computed(()=> data1.code === 0 ? data.value?.data : [])

这种情况挺多的例如setIntervalsetTimeout。这类封装可参考https://vueuse.org/shared/usetimeout/#usetimeout,axios封装的比较完善的代码可参考:https://github.com/biancangming/howuse/blob/main/src/axios/index.ts

清除副作用

组合api最大的好处就是封装更高阶的函数,这样就自动请求一些副作用,就像上边的useAxios,组件卸载的时候可以直接取消正在进行的请求。就像axios正在请求时,或者路由离开这个页面,当页面onUnmounted时,需要清理内存中残留的状态。比如中断这个连接,清理data中的数据,以此来提高页面的性能,而且封装一次,以后也不必关心这个重复的写法。当然也可以不这么做,老板也不懂

typescript 复制代码
function useAxios(config: AxiosRequestConfig) {
  const cancelToken = axios.CancelToken.source()
  
  const data = ref({})
  function load() {
    axios({ ...config, cancelToken: cancelToken.token }).then(res => {
      data.value = res.data
    })
  }

  onUnmounted(() => {
    cancelToken.cancel() // 离开页面时,取消请求
    data.value = [] // 离开页面,清除不必要的数据
  })
  return {
    load,
    data
  }
}

或者是像常见的addEventListenersetInterval,都可以这样封装,路由跳转的时候removeEventListenerclearInterval,消除副作用。

vue组件在销毁的时候会自动清除一些副作用,但是有时候,想要一些动作,达到一定目的之后就不再执行。vue3 有个优秀的清除副作用的辅助函数,可以参考这里:https://cn.vuejs.org/api/reactivity-advanced.html#effectscope,刚好这个函数就可以达到这种目的

typescript 复制代码
const scope = effectScope() // 定义一个scope

const counter = ref(1) // counter 初始值 1

scope.run(() => {
  const doubled = computed(() => counter.value * 2)
  watch(doubled, () => console.log(doubled.value))
})

// 这个定时任务用于counter自增,当counter自增到10的时候,让 doubled 和 watch 失去作用
const countPlus = setInterval(() => {
  counter.value++
  if (counter.value > 10) {
    scope.stop() // 让 doubled 和 watch 失去作用,这样的好处就是及时回收没有必要的副作用。即便定时任务停止,再修改counter,对应的计算属性、监听都不再产生作用
    clearTimeout(countPlus)// 清除定时任务
  }
}, 1000)

依赖注入可靠类型注解

依赖注入经常在用,尤其针对项目的全局配置很有用,但是类型提示很头疼,InjectionKey这个辅助函数很有用,可以将注入的对象单独写在一个公用文件。

复制代码
import { InjectionKey, Ref } from 'vue';
export const injectKey: InjectionKey<Ref<string>> = Symbol()

提供注入的函数

复制代码
import { injectKey } from "xx.ts"
const username = ref()
provide(injectKey, username)

使用注入

复制代码
import { injectKey } from "xx.ts"
const username = inject(injectKey)
username.value // 得到正确的代码提示

状态共享

在进行项目开发的时候,很多时候需要多个组件之间数据交互,父子组件可以propsemit,兄弟组件多使用eventBus或者mitt,也有好多人直接使用vuex或者pinia。在大多需求中,这些方式要么不方便、要么消耗过多、或者是代码不好组织。vue3这种灵活的api,可以随意使用。就像官方说的那样,vue3的组合api,可以直接被拎出来使用。

复制代码
// example/store.ts
import { ref } from "@vue/reactivity" // 这个包是响应式核心依赖包,vue当中也是依赖这个包
//或者
// import { ref } from "vue"

export const count = ref(0)

在组件当中使用的时候可以这样, A组件和B组件得到共享,同样的,需要注意在页面销毁时清除count,否则残留在内存当中,因为他脱离了vue的周期

复制代码
// 组件A
import { count } from "example/store"
count.value ++
复制代码
// 组件B
import { count } from "example/store"
console.log(count.value) // 1

useSlots()

vue3 废除了 $slots,可以用useSlots代替

复制代码
const slots = useSlots() // slots获得的值,等价于原来的`$slots`

继承父级slots,封装高阶组件时,经常要继承别的组件。

html 复制代码
<template v-for="(_,name) in slots" #[name]>
   <slot :name="name"></slot>
</template>
typescript 复制代码
const slots = useSlots()

判断是否传入插槽,有时候需要在没有传入插槽内容的时候,隐藏对应插槽样式

html 复制代码
<div :class="`${prefixCls}__footer`" v-if="isShowSlot('footer')">
    <slot name="footer"></slot>
</div>
typescript 复制代码
const slots = useSlots()
const isShowSlot = (name: string) => !!slots[name];

useAttrs()

vue3 废除了 $attrs以及$listeners,这两个api都合并到useAttrs,对应vue3来说@符号相当于:on加上事件名首字母大写,

这就是为什么可以通过useAttrs这么一个api或者全部的事件以及属性。

例如:

html 复制代码
<template>
  <button @click="handle1">按钮1</button>
  <button :onClick="handle2">按钮2</button>
</template>
<script lang="ts" setup>
const handle1 = () => { alert(1) }
const handle2 = () => { alert(2) }
</script>

这样,在封装高阶组件的时候很有用,许多crud的组件都是这么干的。例如:这块有个增删改查的组件。https://biancangming.github.io/howuse/#/crudAntd

源码可以在这里看看:https://github.com/biancangming/howuse/tree/main/src/crud/antd

父子组件快速双向绑定

传统模式需要定义propsemit, 通常子组件的代码如下,比较繁琐。

ts 复制代码
const content = ref("")

const props = defineProps({
    content: ""
})

const emit = defineEmits<{
    (e: "update:content", content: string): void
}>()

emit("update:content", unref(content))

vue 3.3 + 增加了一个宏函数,defineModel, 实现上述任务只需要一行代码

ts 复制代码
const content = defineModel("content", { type: String, default: () => "" })

父组件

html 复制代码
<MingHtmlEditor v-model:content="content"/>

由于这个目前还是实验性特性,需要开启vue相关配置,vite.config.ts中配置如下

ts 复制代码
 plugins: [
    vue({
      script: {
        defineModel: true,
      }
    }),
]