我最近有机会在一个真实项目中尝试新的 Vue 组合 API,以查看它在哪些地方可能有用,以及我们将来如何使用它。
到目前为止,当我们创建一个新的组件时,我们一直在使用 选项 API。该 API 强制我们按选项分离组件代码,这意味着我们需要将所有反应式数据放在一个地方 (data
)、所有计算属性放在一个地方 (computed
)、所有方法放在一个地方 (methods
),等等。
对于较小的组件,它很方便且易于阅读,但当组件变得更加复杂并处理多个功能时,它就会变得很痛苦。通常,与特定功能相关的逻辑包含一些反应式数据、计算属性、一个或多个方法;有时它还涉及使用组件生命周期钩子。当处理单个逻辑问题时,这会让你不断地在代码中的不同选项之间跳转。
在使用 Vue 工作时,你可能遇到的另一个问题是如何提取可以被多个组件重用的通用逻辑。Vue 已经提供了一些选项来做到这一点,但它们都存在各自的缺点(例如,mixins 和 scoped-slots)。
组合 API 带来了一种新的创建组件、分离代码和提取可重用代码段的方式。
让我们从组件内的代码组合开始。
代码组合
假设你有一个主组件,它为你的整个 Vue 应用程序设置了一些东西(比如 Nuxt 中的布局)。它处理以下内容
- 设置语言环境
- 检查用户是否仍处于身份验证状态,如果未验证则重定向
- 防止用户多次重新加载应用程序
- 跟踪用户活动并在用户在特定时间段内处于非活动状态时做出反应
- 使用 EventBus(或 window 对象事件)监听事件
这些只是组件可以执行的少数几件事。你可能可以想象一个更复杂的组件,但这将满足本示例的目的。为了可读性,我仅使用道具名称而不使用实际实现。
以下是使用选项 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>
如你所见,每个选项都包含来自所有功能的部分。它们之间没有明确的分隔,这使得代码难以阅读,尤其是如果你不是编写代码的人,并且第一次看到它。很难找到哪个方法由哪个功能使用。
让我们再次查看它,但将逻辑关注点标识为注释。这些将是
- 活动跟踪器
- 重新加载阻止程序
- 身份验证检查
- 语言环境
- 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>
看看解开所有这些有多难?🙂
现在想象一下,你需要在一个功能中进行更改(例如活动跟踪逻辑)。你不仅需要知道哪些元素与该逻辑相关,而且即使你知道,你仍然需要在不同的组件选项之间来回跳转。
让我们使用组合 API 按逻辑关注点分离代码。为此,我们为每个与特定功能相关的逻辑创建一个单独的函数。这就是我们所说的组合函数。
// 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
)、计算属性、方法(普通函数)、观察者 (watch
) 和生命周期钩子 (onMounted
/ onUnmounted
)。基本上你在组件中通常使用的一切。
在代码存放位置方面,我们有两个选择。我们可以将其保留在组件内部,也可以将其提取到单独的文件中。由于组合 API 尚未正式推出,因此还没有关于如何处理它的最佳实践或规则。在我看来,如果逻辑与特定组件紧密耦合(即它不会在其他任何地方重用),并且它本身无法独立存在,我建议将其保留在组件内部。另一方面,如果它是可能被重用的通用功能,我建议将其提取到单独的文件中。但是,如果我们想将其保留在单独的文件中,我们需要记住从文件中导出函数并在我们的组件中导入它。
以下是使用新创建的组合函数的组件外观
<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
函数中调用相关的组合函数。
再次想象一下,你需要在活动跟踪逻辑中进行一些更改。与该功能相关的所有内容都位于 useActivityTracker
函数中。现在,你立即知道在哪里查看,并跳到正确的位置以查看所有相关的代码段。太棒了!
提取可重用代码段
在我们的案例中,EventBus 监听器注册看起来像是我们可以用于任何监听 EventBus 事件的组件的代码段。
如前所述,我们可以将与特定功能相关的逻辑保留在单独的文件中。让我们将我们的 EventBus 监听器设置移动到单独的文件中。
// 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>
就这样!我们现在可以在任何需要的组件中使用它。
总结
关于组合 API,正在进行着讨论。这篇文章无意宣传任何一方的观点。它更多地是关于展示它可能在哪些地方有用以及它在哪些情况下带来额外价值。
我认为,在像上面这样的真实示例中,更容易理解这个概念。还有更多用例,你使用新的 API 越多,你就会看到越多的模式。这篇文章仅仅是几个基本的模式,可以帮助你入门。
让我们再次回顾一下介绍的用例,并看看组合 API 在哪些地方可能有用
可以独立存在而无需与任何特定组件紧密耦合的通用功能
- 与特定功能相关的所有逻辑都位于一个文件中
- 将其保存在
@/composables/*.js
中并在组件中导入 - 示例:活动跟踪器、重新加载阻止程序和语言环境
在多个组件中使用的可重用功能
- 与特定功能相关的所有逻辑都位于一个文件中
- 将其保存在
@/composables/*.js
中并在组件中导入 - 示例:EventBus 监听器注册、窗口事件注册、通用动画逻辑、通用库使用
组件内的代码组织
- 与特定功能相关的所有逻辑都位于一个函数中
- 将代码保存在组件内的组合函数中
- 与相同逻辑关注点相关的代码位于同一个地方(即无需在数据、计算、方法、生命周期钩子等之间跳转)
记住:这一切都处于开发阶段!
Vue 组合 API 目前处于开发阶段,可能会在将来发生变化。上面示例中提到的任何内容都不确定,语法和用例都可能发生变化。它旨在与 Vue 3.0 版本一起发布。在此期间,你可以查看 view-use-web,它包含一系列组合函数,这些函数预计将包含在 Vue 3 中,但可以在 Vue 2 中与组合 API 一起使用。
如果你想尝试新的 API,可以使用 @vue/composition 库。
很棒的文章,并很好地举例说明了组合 API 在哪些地方非常有用。
不过我有一个问题:在组合示例中,你调用了相同函数,然后声明它们,为什么这样做呢?
以下是解释
– 声明函数相当于使用选项 API 将其放在 methods 属性中
– 直接调用方法 setup 等同于在 beforeCreate 钩子之后和 before created 钩子之前调用方法
因此,我们使用函数声明方法,然后在组件实例化时调用它。
在本文的这一部分,我们还没有导入任何内容,一切都在组件内部。你可以从外部文件导入它,然后在 setup 中调用它,就像文章最后使用 useEventBusListener 一样。
它们没有被导入,因此必须声明。能够将它们移动到不同的文件,而不会出现 mixin 的弊端,这正是它如此强大的原因。
啊……我以为这些函数在单独的文件中,但它们只是存在于组件中。
谢谢!这有道理!
这是我见过的关于组合 API 的最佳示例。这是我对组合 API 的 A-HA 点。
这篇文章清楚地阐述了我至今在 Vue 体验中感到不安的一件事——感谢你。我现在真的很期待使用这个组合 API。
谢谢,你让我爱上了新的组合 API。
这个例子非常有用,而且 100% 的主题:删除了很多代码并将其拆分到多个文件,能够在 Vue 组件中使用代码。
是的,现在我认为它是必不可少的。
再次感谢。