前言
在这篇文章中,我将展示一个我们可以应用 Vue 3 的新 Composition API 解决一个特定挑战的模式。我不会介绍所有的基础知识,因此熟悉这个 新 API 的基础知识 将对您有所帮助。
重要提示: Composition API 是一个添加剂,是一个新特性,它没有也不会取代 Vue 1 和 2 中你所了解和喜爱的很好的旧 “Options API”。只要把这个新的 API 看作你工具箱中另一个工具,那么在使用 Options API 解决有些笨拙的情况下,它可能会派上用场。
我喜欢 Vue 的新 Composition API。对我来说,Vue 的反应系统似乎已经摆脱了组件的约束,现在我可以使用它来编写任何我想要的反应代码。
根据我的经验,一旦您对它有所了解,它就是一个创建灵活、可重用代码的极好的方法,这些代码可以很好地组合,并让你看到组件的所有部分和特性是如何交互的。
我将以一个小工具开始这篇文章,你可以用它更容易地使用组件和 v-model
。
概述: v-model
指令
如果你使用过 Vue,你就知道 v-model
指令:
<input type="text" v-model="localStateProperty">
这是一个非常赞的快捷方式,以避免我们输入复杂的模板标记,像这样:
<input
type="text"
:value="localStateProperty"
@change="localStateProperty = $event.target.value"
>
最棒的是,我们还可以在组件上使用它:
<message-editor v-model="message">
这相当于做以下事情:
<message-editor
:modelValue="message"
@update:modelValue="message = $event"
>
但是,为了实现属性(prop)和事件(event)的约定,我们的 <message-editor>
组件必须看起来像这样:
<template>
<label>
<input type="text" :value="modelValue", @change="(event) => $emit('update:modelValue', event.target.value)" >
<label>
</template>
<script> export default {
props: {
'modelValue': String,
}
}
</script>
然而,这似乎相当冗长。🧐
我们必须这样做,因为无法直接写到属性。我们必须发出正确的事件,并将其留给父组件来决定如何处理我们传递的更新,因为它是父组件的数据,而不是 <message-editor>
组件的数据。因此,在这种情况下,我们不能在 input 上使用 v-model
。烦人。
你可能从 Vue 2 了解到 Options API 中有一些处理这个问题的模式,但是今天我想看看使用 Composition API 提供的工具如何以一种简洁的方式解决这个问题。
挑战: 减少样板
我们想要实现的是一个抽象,它允许我们在输入中使用相同的 v-model
快捷方式,即使我们实际上不想写入本地状态,而是想发出正确的事件。这是我们希望模板完成后的样子:
<template>
<label>
<input
type="text"
v-model="message"
/>
<label>
</template>
那么,让我们使用 Composition API 来实现这个:
import { computed } from 'vue'
export default {
props: {
'modelValue': String,
},
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
return {
message,
}
}
}
好吧,这是一段新的代码,也许有点异形。让我们来分解一下:
import { computed } from 'vue'
首先,我们导入 computed() 函数,该函数返回一个用于计算属性的引用(ref) —— 一个用于从其他反应性数据(例如 props)派生出值的包装器。
const message = computed({
get: () => props.modelValue,
set: (event) => emit('update:modelValue')
})
在 setup 中,我们创建了这样一个计算属性,但是是一个特殊的属性:我们计算属性有一个 getter
和 setter
,因此我们实际上可以读取它的派生值并为它赋一个新值。
这是我们计算属性在 Javascript 中使用时的行为:
message.value
// => '这返回一个字符串'
message.value = 'This will be emitted up'
// => 调用 emit('onUpdate:ModelValue', 'This will be fired up')
通过从 setup() 函数返回这个计算属性,我们将它暴露给模板。现在,我们可以将它和 v-model 一起使用,得到一个干净漂亮的模板:
<template>
<label>
<input
type="text"
v-model="message"
>
<label>
</template>
<script>
import { computed } from 'vue'
export default {
props: {
'modelValue': String,
},
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
return {
message,
}
}
}
</script>
现在模板非常干净。但与此同时,我们必须在 setup() 中编写一堆样板文件来实现这一点。看起来我们只是将样板文件从模板移到了 setup 函数中。
因此,让我们将这个逻辑提取到它自己的函数中——一个 composition 函数——或者简称为 “composable”。
将其转换为 composable
composable 只是一个函数,我们使用它从 setup 函数中抽象出一些代码。可组合性(Composables)是这个新 Composition API 的优势,也是允许更好的代码抽象和组合的核心原则。
这就是我们的目标:
📄 modelWrapper.js
import { computed } from 'vue'
export function useModelWrapper(props) {
/* 暂时不讨论实现 */
}
📄 MessageEditor.vue
import { useModelWrapper } from '../utils/modelWrapper'
export default {
props: {
'modelValue': String,
}
setup(props, { emit }) {
return {
message: useModelWrapper(props),
}
}
}
注意,我们的 setup()
函数中的代码是如何简化为一行代码的(如果我们假设我们在一个很好的编辑器中工作,它可以为我们自动添加 useModelWrapper
的导入,比如 VSCode)。
我们是怎么做到的呢?实际上,我们所要做的就是将 setup
中的代码复制粘贴到这个新函数中!这是它的样子:
📄 modelWrapper.js
import { computed } from 'vue'
export function useModelWrapper(props) {
return computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
}
好吧,这很简单,但我们能做得更好吗? 是的,我们可以!
不过为了理解我们可以用什么方式,我们将绕一小圈回到 v-model
如何在组件上工作……
在 Vue 3 中,v-model
可以使用额外的参数将其应用到 modelValue
以外的属性上。如果组件想要公开多个属性作为 v-model
的目标,这是非常有用的。
v-model:argument
<message-editor
v-model="message"
v-model:draft="isDraft"
/>
第二个 v-model 可以这样实现:
<input
type="checkbox"
:checked="draft",
@change="(event) => $emit('update:modelValue', event.target.checked)"
>
再重复一遍冗长的模板代码。是时候调整新的 composition 函数,这样我们也可以在这个例子中重用它。
要实现这一点,我们需要向函数添加第二个参数,该函数指定我们实际想要封装的属性名称。由于 modelValue
是 v-model
的默认属性名称,我们也可以将它作为包装器的第二个参数的默认名称:
📄 modelWrapper.js
import { computed } from 'vue'
export function useModelWrapper(props, name = 'modelValue') {
return computed({
get: () => props[name],
set: (value) => emit(`update:${name}`, value)
})
}
就是这样。现在我们可以对任何 v-model
属性使用这个包装器。
所以最终的组件看起来是这样的:
<template>
<label>
<input type="text" v-model="message" >
<label> <label>
<input type="checkbox" v-model="isDraft"> Draft
</label>
</template>
<script>
import { useModelWrapper } from '../utils/modelWrapper'
export default {
props: {
modelValue: String,
draft: Boolean
},
setup(props, { emit }) {
return {
message: useModelWrapper(props, 'modelValue'),
isDraft: useModelWrapper(props, 'draft')
}
}
}
</script>
下一步
这种可组合性不仅在我们希望将 modelValue
属性映射到模板中的输入时有用,还可以使用它将计算属性引用传递给其他需要引用的可赋值组合。
通过像上面那样,首先包装 modelValue
属性,其次组合函数可以不知道我们实际上没有处理局部状态的事实。我们在可组合的小 useModelWrapper
中抽象了实现细节,因此其他可组合的可以将其视为本地状态。
“快速输入,否则丢失”
作为一个公认的愚蠢示例,我们有一个名为 useMessageReset
的可组合组件。当你停止输入 5 秒后,它会将你的信息重置为空字符串。它是这样的:
function useMessageReset(message) {
let timeoutId
const reset = () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => (message.value = ''), 5000)
watch(message, () => message.value !== '' && reset())
}
}
此可组合使用 watch(),这是 Composition API 的另一个功能。
- 每当消息更改时,第二个参数中的回调函数就会运行。
- 如果消息不是空的,它将(重新)启动超时,5 秒后将消息重置为空值。
注意,这个函数期望接收一个它可以监视的引用并为其赋值。
我们在使用 modelValue
属性时会遇到问题,因为我们不能直接写入它。但使用 useModelWrapper
,我们可以提供一个可写的计算属性引用到这个组合:
import { useModelWrapper } from '../utils/modelWrapper'
import { useMessageReset } from '../utils/messageReset'
export default {
props: { modelValue: Boolean },
setup(props, { emit }) {
const message = useModelWrapper(props)
useMessageReset(message)
return { message }
}
}
注意,这个可组合组件是如何不知道 message
实际上是为父组件的 v-model
发出一个事件的。就像我们通过普通的 ref() 一样,它只能分配给 .value
。
还要注意,我们的其余功能是如何不受此影响的。我们仍然可以在模板中使用 message
,或者以其他方式使用它。
最后的感想
对于每个想要使用它的 Vue 开发人员来说,composition API 是一个伟大的、灵活的工具。上面显示的只是冰山一角。