Vue 3 Composition API 实战前瞻

阅读数:1664 2019 年 11 月 26 日 15:16

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/

评论

发布