Vue 3 Composition API实战前瞻

虽然 Vue3.0 尚未发布,但前段时间,Vue 发布了关于 Composition API 的官方插件,使广大用户可以在 Vue2.x 中享受 Function Base 带来的新体验。本文作者通过实际试用,对 Composition API 与当前的 Options API 作出了一番对比,并对 Composition API 的特征与用途进行了通俗易懂的总结。一起来看看!

我最近得到了机会,在一个真实的项目中试用了Vue中全新的Composition API,进而对它可能的用途以及未来的用法探索了一番。

现在,当我们创建一个新组件时使用的是Options API。使用这个API时,我们必须通过选项将组件的代码分离开来,这意味着我们需要将所有响应性数据放在一个地方,所有计算的属性放在一个位置,所有方法也都放在一个位置,依此类推。

这个API在处理较小的组件时比较易读和顺手,而当组件变得更加复杂,需要处理多种功能时,它用起来就很痛苦了。一般来说,与一个特定功能相关的逻辑会包含一些响应性数据、一些计算属性,还有一种或一些方法。有时还会用到组件生命周期hooks。于是在处理单个逻辑问题时,需要不断在代码中的不同选项之间来回切换。

使用Vue时,可能遇到的另一个问题是设法提取可被多个组件复用的通用逻辑。Vue已经提供了一些方案可供选择,但它们都有各自的缺点(例如mixins和作用域插槽)。而新的Composition API带来了一种创建组件、分离代码和提取可复用代码段的全新方式。

首先来看组件内的代码构成。

代码构成

假设你有一个核心组件,为整个Vue应用设置了一些内容(就像Nuxt中的布局)。它负责处理以下内容:

  • 设定区域;
  • 检查用户是否处于登录状态,如果没有,则将其重定向;
  • 防止用户重新加载应用程序太多次数;
  • 跟踪用户活动,并在用户静默一段时间后做出反应;
  • 使用EventBus监听事件(或窗口对象事件)。

这些只是这个组件可以做的事情的一些例子。你可能会设想出一个更复杂的组件,不过这里的这些已经足够本文举例说明了。为了便于阅读,我只用了props的名称,而没有实际实现。

下面是使用Options API时组件的样子:

<template>
  <div id="app">
    ...
  </div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      userActivityTimeout: null,
      lastUserActivityAt: null,
      reloadCount: 0
    }
  },

  computed: {
    isAuthenticated() {...}
    locale() {...}
  },

  watch: {
    locale(value) {...},
    isAuthenticated(value) {...}
  },

  async created() {
    const initialLocale = localStorage.getItem('locale')
    await this.loadLocaleAsync(initialLocale)
  },

  mounted() {
    EventBus.$on(MY_EVENT, this.handleMyEvent)

    this.setReloadCount()
    this.blockReload()

    this.activateActivityTracker()
    this.resetActivityTimeout()
  },

  beforeDestroy() {
    this.deactivateActivityTracker()
    clearTimeout(this.userActivityTimeout)
    EventBus.$off(MY_EVENT, this.handleMyEvent)
  },

  methods: {
    activateActivityTracker() {...},
    blockReload() {...},
    deactivateActivityTracker() {...},
    handleMyEvent() {...},
    async loadLocaleAsync(selectedLocale) {...}
    redirectUser() {...}
    resetActivityTimeout() {...},
    setI18nLocale(locale) {...},
    setReloadCount() {...},
    userActivityThrottler() {...},
  }
}
</script>

如你所见,每个选项都包含所有功能的其中一部分。它们之间没有明确的分隔,这使代码很难阅读。如果代码并不是你写的,你还是第一次看到它,那么读起来会更费劲,很难分清具体哪种功能使用的是哪种方法。

我们再来看一下,但这次用注释把逻辑上的关注点标识出来。这些关注点包括:

  • Activity Tracker(活动追踪);
  • Reload Blocker(阻止重新加载);
  • Authentication check(登录状态检查);
  • Locale(区域选项);
  • Event Bus Registration(EventBus注册)。
<template>
  <div id="app">
    ...
  </div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      userActivityTimeout: null, // Activity tracker
      lastUserActivityAt: null, // Activity tracker
      reloadCount: 0 // Reload blocker
    }
  },

  computed: {
    isAuthenticated() {...} // Authentication check
    locale() {...} // Locale
  },

  watch: {
    locale(value) {...},
    isAuthenticated(value) {...} // Authentication check
  },

  async created() {
    const initialLocale = localStorage.getItem('locale') // Locale
    await this.loadLocaleAsync(initialLocale) // Locale
  },

  mounted() {
    EventBus.$on(MY_EVENT, this.handleMyEvent) // Event Bus registration

    this.setReloadCount() // Reload blocker
    this.blockReload() // Reload blocker

    this.activateActivityTracker() // Activity tracker
    this.resetActivityTimeout() // Activity tracker
  },

  beforeDestroy() {
    this.deactivateActivityTracker() // Activity tracker
    clearTimeout(this.userActivityTimeout) // Activity tracker
    EventBus.$off(MY_EVENT, this.handleMyEvent) // Event Bus registration
  },

  methods: {
    activateActivityTracker() {...}, // Activity tracker
    blockReload() {...}, // Reload blocker
    deactivateActivityTracker() {...}, // Activity tracker
    handleMyEvent() {...}, // Event Bus registration
    async loadLocaleAsync(selectedLocale) {...} // Locale
    redirectUser() {...} // Authentication check
    resetActivityTimeout() {...}, // Activity tracker
    setI18nLocale(locale) {...}, // Locale
    setReloadCount() {...}, // Reload blocker
    userActivityThrottler() {...}, // Activity tracker
  }
}
</script>

由此可见,解开这团乱麻有多复杂。????

现在假设你需要更改一种功能(例如活动追踪逻辑)。你不仅需要知道有哪些元素与该逻辑相关,而且就算你知道了,也需要在不同的组件选项之间跳来跳去。

下面我们使用Composition API,通过逻辑关注点来分离代码。为此,我们为每个与特定功能相关的逻辑创建一个函数。这就是我们所说的composition函数

// Activity tracking logic
function useActivityTracker() {
  const userActivityTimeout = ref(null)
  const lastUserActivityAt = ref(null)

  function activateActivityTracker() {...}
  function deactivateActivityTracker() {...}
  function resetActivityTimeout() {...}
  function userActivityThrottler() {...}

  onBeforeMount(() => {
    activateActivityTracker()
    resetActivityTimeout()
  })

  onUnmounted(() => {
    deactivateActivityTracker()
    clearTimeout(userActivityTimeout.value)
  })
}
// Reload blocking logic
function useReloadBlocker(context) {
  const reloadCount = ref(null)

  function blockReload() {...}
  function setReloadCount() {...}

  onMounted(() => {
    setReloadCount()
    blockReload()
  })
}
// Locale logic
function useLocale(context) {
  async function loadLocaleAsync(selectedLocale) {...}
  function setI18nLocale(locale) {...}

  watch(() => {
    const locale = ...
    loadLocaleAsync(locale)
  })

  // No need for a 'created' hook, all logic that runs in setup function is placed between beforeCreate and created hooks
  const initialLocale = localStorage.getItem('locale')
  loadLocaleAsync(initialLocale)
}
// Event bus listener registration
import EventBus from '@/event-bus'

function useEventBusListener(eventName, handler) {
  onMounted(() => EventBus.$on(eventName, handler))
  onUnmounted(() => EventBus.$off(eventName, handler))
}

如你所见,我们可以声明响应性数据(ref/reactive)、计算的props、方法(纯函数)、观察者(watch)和生命周期hooks(onMounted/onUnmount)。基本上你平时在组件中使用的所有内容都能声明。

关于保存代码的位置,我们有两个选择。我们可以将其保留在组件中,或提取到单独的文件中。由于Composition API尚未正式发布,因此还没有关于如何使用它的最佳实践或规则。我的看法是,如果逻辑与特定组件紧密耦合(即不会在其他任何地方复用),并且逻辑离开了组件就无法生存,我建议将其保留在组件中。另一方面,如果是可能会被复用的一般性功能,则建议将其提取到单独的文件中。但如果我们要将其保存在单独的文件中,则需要从文件中导出函数并将其导入到组件中。

这是使用新创建的composition函数后,我们组件的样子:

<template>
  <div id="app">
      
  </div>
</template>

<script>
export default {
  name: 'App',

  setup(props, context) {
    useEventBusListener(MY_EVENT, handleMyEvent)
    useActivityTracker()
    useReloadBlocker(context)
    useLocale(context)

    const isAuthenticated = computed(() => ...)

    watch(() => {
      if (!isAuthenticated) {...}
    })

    function handleMyEvent() {...},

    function useLocale() {...}
    function useActivityTracker() {...}
    function useEventBusListener() {...}
    function useReloadBlocker() {...}
  }
}
</script>

这里每个逻辑关注点都有了一个函数。如果要使用某一个关注点,则需要在新的setup函数中调用相关的composition函数。

再设想一下,你需要对活动跟踪逻辑作一些更改。与该功能相关的所有内容都放在useActivityTracker函数中。现在你就能立刻找出并跳转到正确的位置,查看所有相关的代码段了。非常漂亮!

提取可复用的代码段

在我们的例子中,事件总线侦听器注册(Event Bus listener registrations)看来是一段代码,如果有组件需要侦听事件总线上的事件,我们就可以用这段代码来实现。

如前所述,我们可以将与特定功能相关的逻辑保存在单独的文件中。下面我们将事件总线侦听器设置转移到一个单独的文件中。

// composables/useEventBusListener.js
import EventBus from '@/event-bus'

export function useEventBusListener(eventName, handler) {
  onMounted(() => EventBus.$on(eventName, handler))
  onUnmounted(() => EventBus.$off(eventName, handler))
}

要在组件中使用它,我们需要导出函数(命名或默认),并将其导入组件中。

<template>
  <div id="app">
    ...
  </div>
</template>

<script>
import { useEventBusListener } from '@/composables/useEventBusListener'

export default {
  name: 'MyComponent',

  setup(props, context) {
    useEventBusListener(MY_EVENT, myEventHandled)
    useEventBusListener(ANOTHER_EVENT, myAnotherHandled)
  }
}
</script>

完事了!现在我们能在任何组件中使用它了。

小结

关于Composition API的讨论还在进行中。这篇文章无意在讨论中站队,更关心的是新API可能的用途,以及它能在哪些情况下带来附加价值。

我认为在现实案例中理解概念总是比较容易的,就像上面这个例子一样。用例越多,使用新API的次数越多,我们也就能发现更多的模式。这篇文章只涉及了一些基本的模式,供抛砖引玉。

我们再来看一遍上面这些用例,看看Composition API的用途有哪些:

1.无需与任何特定组件紧密耦合也能独立运行的一般性功能

  • 与一个特定功能相关的所有逻辑都放在一个文件中;
  • 将其保存在@/composables/*.js,并将其导入组件中;
  • 示例:活动跟踪、阻止重新加载和区域设置。

2.可在多个组件中使用的可复用功能

  • 与一个特定功能相关的所有逻辑都放在一个文件中;
  • 将其保存在@/composables/*.js,并将其导入组件中;
  • 示例:事件总线侦听器注册、窗口事件注册、通用动画逻辑、通用库的使用。

3.组件内的代码组织

  • 与一个特定功能相关的所有逻辑都放在一个函数中;
  • 将代码保留在组件内的composition函数中;
  • 与同一逻辑关注点相关的代码位于同一位置。也就是说,无需在数据、计算属性、方法、生命周期hooks等内容之间来回跳转)。

记住:这些都尚在开发中!

Vue Composition API目前尚处于开发阶段,未来还可能出现更改。上面示例中提到的任何内容、语法和用例都可能出现变化。这个API计划将随Vue 3.0一起推出。另外,你可以在view-use-web上查看一组composition函数的信息,这些函数预计会包含在Vue 3中,但也能用在Vue 2中的Composition API上。

如果你想尝试新的API,可以使用@vue/composition库

原文链接https://css-tricks.com/an-early-look-at-the-vue-3-composition-api-in-the-wild/

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/RZvgaitrsG20Xk7qqtWF
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券