likes
comments
collection
share

vue3学习小札之(三):逻辑复用

作者站长头像
站长
· 阅读数 17

引子

可以前往专栏阅读该系列其他文章:传送门 之前的文章包含了 vue3 的基础知识、组件的深入介绍。 本期文章,将主要介绍 vue3 中如何实现代码封装,逻辑复用。 P.S. vue3 取消了mixin,别问,问就是不好用:) 接下来我们将从三个方面(组合式函数、自定义指令、插件)学习如何在 vue3 中实现逻辑复用。

组合式函数

在实际开发中,我们都会封装工具类函数,但是这类封装一般都是无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。 相比之下,有状态逻辑,负责管理随时间而变化的状态。

在上一篇文章介绍作用域插槽的时候,说到一种场景:无渲染组件。 无渲染组件内部其实就是封装了有状态逻辑,然后将动态数据通过作用域插槽的方式,上抛给消费者组件。从而实现了,组件内部仅负责逻辑处理,而布局、样式等处理都交给了消费者组件。 这种方式固然可行,但是会带来额外的组件实例的性能开销

所以,组合式函数的出现就应运而生了。 “组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。 当我们无需考虑视图布局,只是纯逻辑复用时,推荐用组合式函数替代无渲染组件。

组合式函数的约定和最佳实践

在学习编写组合式函数前,先介绍下 vue3 对于组合式函数的一些规范和推荐

命名

组合式函数约定用驼峰命名法命名,并以“use”作为开头。 react自定义hooks:???

输入参数

组合式函数可接收 ref 参数,所以当我们在编写组合式函数时,最好在内部对入参做兼容 ref 处理unref() 工具函数会对此非常有帮助:

import { unref } from 'vue'

function useFeature(maybeRef) {
  // 若 maybeRef 确实是一个 ref,它的 .value 会被返回
  // 否则,maybeRef 会被原样返回
  const value = unref(maybeRef)
}

而当组合式函数在接收一个 ref 参数,并且会随之产生响应式 effect,那么我们就需要针对这个 ref 参数添加侦听器: 可以使用 watch() 显式地监听此 ref; 也可以在 watchEffect() 中调用 unref() (针对ref进行解包)来进行正确的追踪。

返回值

推荐组合式函数始终返回一个包含多个 ref 的普通的非响应式对象。 目的是为了外部调用组合式函数后,对返回值进行解构为 ref 之后仍可以保持响应性

// x 和 y 是两个 ref
const { x, y } = useMouse()

但是将返回值解构为 ref 之后,需要用 .value 获取值,所以我们可以针对组合式函数返回的非响应式对象返回值,做一次响应式处理,这样内部的 ref 就会自动进行解包,然后我们就可以通过对象属性的形式来使用组合式函数中返回的状态:

const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)

副作用

如果在组合式函数中执行副作用 (例如:添加 DOM 事件监听器或者请求数据),请注意: 确保在 onUnmounted() 时清理副作用。 如果是同步形式挂载的侦听器副作用,基本是不需要手动清除的, 如果是异步形式挂载的,就需要通过调用侦听器的返回值函数,进行手动清除。

使用限制

组合式函数在 <script setup> 或 setup() 钩子中,应始终被同步地调用。 在某些场景下,也可以在像 onMounted() 这样的生命周期钩子中使用他们。 这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例, 只有能确认当前组件实例,才能够: 将组合式函数内的生命周期钩子注册到该组件实例上; 将组合式函数内的计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。 P.S. 后续介绍组合式 API 的时候会讲到一个叫做 effect 作用域的知识点。里面会阐述响应式副作用 (即计算属性和侦听器)的一些细节,其中就涉及到了组件卸载时,停止监听的内容。

通过例子学习编写组合式函数

解下来通过官网的两个例子,学习下如何编写组合式函数。 具体的细节,都写在代码的注释部分。

例子一:鼠标跟踪器

// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  // 一个组合式函数可以调用一个或多个其他的组合式函数。
  useEventListener(window, 'mousemove', (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })

  return { x, y }
}
// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  // 如果你想的话,
  // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
  onMounted(() => target.addEventListener(event, callback))
  // 需要手动清除副作用
  onUnmounted(() => target.removeEventListener(event, callback))
}

组件中调用该组合式函数,如下

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

tip: 每一个调用 useMouse() 的组件实例会创建其独有的 xy 状态拷贝,因此他们不会互相影响

例子二:有异步状态的组合式函数

// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'

// 同时可以接收静态的 URL 字符串和 URL 字符串的 ref
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  function doFetch() {
    // 在请求之前重设状态...
    data.value = null
    error.value = null
    // unref() 解包可能为 ref 的值,针对入参做 ref 处理
    fetch(unref(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  // 通过 isRef()检测 URL 是否为一个动态 ref 
  if (isRef(url)) {
    // 若输入的 URL 是一个 ref,那么启动一个响应式的请求
    // 该 effect 会立刻执行一次,并在此过程中将 URL 的 ref 作为依赖进行跟踪
    // 当 URL 的 ref 发生改变时触发回调函数,数据就会被重置,并重新请求
    watchEffect(doFetch)
  } else {
    // 否则只请求一次
    // 避免监听器的额外开销
    doFetch()
  }

  return { data, error }
}

组件中调用:

<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

自定义指令

前面的文章,已经介绍了两种在 Vue 中重用代码的方式:组件组合式函数组件是主要的构建模块 组合式函数则侧重于有状态的逻辑

而这一章节要介绍的自定义指令,主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。 在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  // 类似组件生命周期钩子
  // 钩子函数会接收到指令所绑定元素作为其参数
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册

export default {
  setup() {
    /*...*/
  },
  directives: {
    // 在模板中启用 v-focus
    focus: {
      /* ... */
    }
  }
}

将一个自定义指令全局注册到应用层级

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
})

指令钩子

一个指令的定义对象可以提供几种钩子函数 (都是可选的):

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

实际开发中,我们一般只会用到mounted 和 updated这两个钩子函数。 并且当这两个钩子的处理逻辑一样时,vue 提供了一个简写方式

<div v-color="color"></div>

// 直接用一个函数来定义指令
app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

钩子参数

除了 el 外,其他参数都是只读的,不要更改它们。 若需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现 el 指令绑定到的元素,可以用于直接操作 DOM binding 一个对象,包含以下属性: value 传递给指令的值 oldValue 之前传递给指令的值 仅在 beforeUpdate 和 updated 中可用 arg 传递给指令的参数 (如果有的话) modifiers 一个包含修饰符的对象 (如果有的话) instance 使用该指令的组件实例 dir 指令的定义对象 vnode 代表绑定元素的底层 VNode prevNode 代表之前的渲染中,指令所绑定元素的 VNode 仅在 beforeUpdate 和 updated 钩子中可用

注意

当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传 attributes 类似。 但是不同于透传 attributes 在多根节点组件上的表现,自定义指令不能通过 v-bind="$attrs" 来传递给一个明确的根元素。 所以不推荐在组件上使用自定义指令。

插件

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。 vue实例方法 app.use() 源码内部会对传入的第一个参数进行类型判断。 app.use() 还可以接收第二个参数:一个对象形式的可选的选项 options。该参数会被传递给插件内部的安装函数。 安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项 options 作为参数。

// 定义一个插件
const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}

import { createApp } from 'vue'

const app = createApp({})

// 挂载插件
app.use(myPlugin, {
  /* 可选的选项 */
})

通过一个例子简单介绍插件的开发

一个简单的 i18n (国际化 (Internationalization) 的缩写) 插件。 插件的实现: 插件的安装函数内部主要是针对 app 实例,进行一些全局的注入

// plugins/i18n.js
export default {
  install: (app, options) => {
    // 在这里编写插件代码
    // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      // 获取 `options` 对象的深层属性
      // 使用 `key` 作为索引
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
    
    // 将插件接收到的 `options` 参数提供给整个应用,
    // 让任何组件都能使用这个翻译字典对象
    app.provide('i18n', options)
  }
}

插件的安装: 用于查找的翻译字典对象 应当在插件被安装时作为 app.use() 的额外参数被传入

import i18nPlugin from './plugins/i18n'

app.use(i18nPlugin, {
  greetings: {
    hello: 'Bonjour!'
  }
})

插件安装后在业务代码中的使用: 这样,表达式 $translate('greetings.hello') 就会在运行时被替换为 Bonjour! 了

<h1>{{ $translate('greetings.hello') }}</h1>

// 插件中的 provide 就会成为全局的依赖注入
<script setup>
import { inject } from 'vue'

const i18n = inject('i18n')

console.log(i18n.greetings.hello)
</script>

插件发挥作用的常见场景

通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令; 通过 app.provide() 使一个资源可被注入进整个应用; 向 app.config.globalProperties 中添加一些全局实例属性或方法。 例如 vue-router,就是一个官方提供的,包含上述三种功能的插件。

总结

本篇文章主要介绍了 vue3 中如何实现代码的逻辑的复用

组合式函数是利用 vue3 组合式 API 来封装和复用有状态逻辑,侧重于纯逻辑。 当纯逻辑无法满足场景,需要加入一些布局样式的实现时,无渲染组件更适合。 自定义指令是为了重用涉及普通元素的底层 DOM 访问的逻辑,一般是在需要通过直接的 DOM 操作才能实现功能的场景下,才会使用的一种技术。 插件侧重于全局的注入,这种注入包括了:全局组件、全局自定义指令、全局资源、全局的实例属性和方法

以上,就是 vue3 使用中会用到的逻辑复用的知识点,我们可以在实际开发中,按需使用。