虽然 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中的布局)。它负责处理以下内容:
这些只是这个组件可以做的事情的一些例子。你可能会设想出一个更复杂的组件,不过这里的这些已经足够本文举例说明了。为了便于阅读,我只用了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>
如你所见,每个选项都包含所有功能的其中一部分。它们之间没有明确的分隔,这使代码很难阅读。如果代码并不是你写的,你还是第一次看到它,那么读起来会更费劲,很难分清具体哪种功能使用的是哪种方法。
我们再来看一下,但这次用注释把逻辑上的关注点标识出来。这些关注点包括:
<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.无需与任何特定组件紧密耦合也能独立运行的一般性功能
2.可在多个组件中使用的可复用功能
3.组件内的代码组织
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/