数据提供者模式
在之前的 文章 中,我们学习了无渲染组件是如何帮助分离逻辑和表现的。这在我们创建在不同 UI 中复用的程序逻辑时十分有用。
无渲染组件同样允许我们使用另一种有用的模式,叫做 数据提供者模式。
数据提供者模式
数据提供者模式是一种设计模式,它为 Vue 中的无渲染模式提供了补充,它专注于为组件提供数据和状态管理,而不关心数据如何渲染或是展示。
在数据提供者模式中,一个数据提供者组件为它的子组件封装了数据的获取、管理、暴露的逻辑。子组件可以在它们自己的渲染或具体行为中使用这份数据。
这种模式促进了关注点分离,因为数据提供者组件负责数据相关的任务,子组件则专注于表现和交互。
我们用一个例子来简述数据提供者模式。一起思考一个简单的程序,它先显示一个笑话的铺垫,然后显示这则笑话的笑点。要随机显示不同的笑话,我们可以使用免费的公共 API https://official-joke-api.appspot.com/random_joke,它以 JSON 格式返回一个随机笑话。
译注
他们的笑话有一个铺垫 setup
和一个笑点 punchline
,可以简单理解为类似歇后语。前半句让人摸不着头脑,后半句让人恍然大悟会心一笑。
# https://official-joke-api.appspot.com/random_joke
{
"type": "general",
"setup": "How good are you at Power Point?",
"punchline": "I Excel at it.",
"id": 129
}
我们先创建一个数据提供者组件,叫作 DataProvider
,它负责从接口中获取笑话数据。在组件的 <script>
部分,我们要从 Vue 的库中导入 ref()
和 reactive()
,将接口地址赋值给一个常量,然后设置 data
和 loading
两个响应式变量来接收接口获取到的数据和正在请求的加载状态。
<script setup>
import { ref, reactive } from "vue";
const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";
const data = reactive({
setup: null,
punchline: null,
});
const loading = ref(false);
</script>
我们随后创建一个异步函数 fetchJoke()
,负责从接口中获取笑话数据。
设置响应式变量
loading
为true
,表示笑话已经在获取了。使用浏览器原生方法
fetch()
向接口发送一个 GET 请求。使用
response.json()
方法,将接口请求的结果转换为 JSON 格式。从请求结果中获取笑话的铺垫和笑点,并赋值给
data
中的响应式状态。最后,将
loading
设置回false
,表示笑话获取完毕。
经过这番修改,我们的 fetchJock()
函数应该看上去像下面这样:
<script setup>
import { ref, reactive } from "vue";
const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";
const data = reactive({
setup: null,
punchline: null,
});
const loading = ref(false);
const fetchJoke = async () => {
loading.value = true;
const response = await fetch(API_ENDPOINT_URL);
const responseData = await response.json();
data.setup = responseData.setup;
data.punchline = responseData.punchline;
loading.value = false;
};
fetchJoke();
</script>
是不是注意到了我们在 <script>
部分的末尾触发了 fetchJoke()
函数?这确保了在 DataProvider
组件渲染后立即获取笑话数据。
还剩最后一件事,就是让 data
和 loading
属性在使用 DataProvider
的组件中可用。为此,我们将这些属性传递给一个 <slot>
元素,这个 <slot>
将被放置在 <template>
部分中。
<template>
<slot :checkbox="checkbox" :toggleCheckbox="toggleCheckbox"></slot>
</template>
<script setup>
import { ref, reactive } from "vue";
const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";
const data = reactive({
setup: null,
punchline: null,
});
const loading = ref(false);
const fetchJoke = async () => {
loading.value = true;
const response = await fetch(API_ENDPOINT_URL);
const responseData = await response.json();
data.setup = responseData.setup;
data.punchline = responseData.punchline;
loading.value = false;
};
fetchJoke();
</script>
这个无渲染的数据提供者组件完成了,我们可以在应用中使用它。在 app 组件中,我们导入 DataProvider
组件,把它放在模板中。
<template>
<DataProvider v-slot="{ data, loading }">
<!-- ... -->
</DataProvider>
</template>
<script setup>
import DataProvider from "./components/DataProvider.vue";
</script>
通过渲染 <DataProvider>
组件,我们向接口发送了一个请求来获取笑话数据,并通过 v-slot
指令获取到请求的数据 data
和 加载状态 loading
。
我们可以使用 <DataProvider>
创建创建用户界面。当请求处于发送中时,显示加载信息;当请求结束并成功获取数据后,显示笑话的铺垫和笑点。
<template>
<DataProvider v-slot="{ data, loading }">
<div class="joke-section">
<p v-if="loading">Joke is loading...</p>
<p v-if="!loading">{{ data.setup }}</p>
<p v-if="!loading">{{ data.punchline }}</p>
</div>
</DataProvider>
</template>
<script setup>
import DataProvider from "./components/DataProvider.vue";
</script>
保存好代码后,我们将看到一条加载信息,随后会看到一条随机的笑话。
如果我们想要再渲染一个笑话出来,我们可以使用不同的模板。重新使用 <DataProvider>
组件,按我们想要的效果重新创建内部的子元素。
<template>
<DataProvider v-slot="{ data, loading }">
<div class="joke-section">
<p v-if="loading">Joke is loading...</p>
<p v-if="!loading">{{ data.setup }}</p>
<p v-if="!loading">{{ data.punchline }}</p>
</div>
</DataProvider>
<DataProvider v-slot="{ data, loading }">
<p v-if="loading">Hold on one sec...</p>
<div v-else class="joke-section">
<details>
<summary>{{ data.setup }}</summary>
<p>{{ data.punchline }}</p>
</details>
</div>
</DataProvider>
</template>
<script setup>
import DataProvider from "./components/DataProvider.vue";
</script>
在我们新写的 UI 中,我们使用 <details>
和 <summary>
元素,将笑点藏在一个可折叠的样式中。
通过使用数据提供者模式,我们可以在不同的组件间管理和提供数据,并且这是解耦的且可复用的。通过将发送请求的相关逻辑提取到一个无渲染的组件中,我们可以在各种场景中复用请求逻辑,而不会产生重复的代码。
在演练场中查看代码我们就不能用组合式吗?
确实,如果不使用数据提供者模式,我们也可以使用组合式,将获取数据的逻辑提取到可复用的函数中。
import { ref, reactive } from "vue";
const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";
export function useGetJoke() {
const data = reactive({
setup: null,
punchline: null,
});
const loading = ref(false);
const fetchJoke = async () => {
loading.value = true;
const response = await fetch(API_ENDPOINT_URL);
const responseData = await response.json();
data.setup = responseData.setup;
data.punchline = responseData.punchline;
loading.value = false;
};
fetchJoke();
return { data, loading };
}
在我们的组件实例中,我们可以导入并使用这个组合式函数,获取请求的数据 data
和加载状态 loading
。
<template>
<div class="joke-section">
<p v-if="loading">Joke is loading...</p>
<p v-if="!loading">{{ data.setup }}</p>
<p v-if="!loading">{{ data.punchline }}</p>
</div>
</template>
<script setup>
import { useGetJoke } from "./composables/useGetJoke";
const { data, loading } = useGetJoke();
</script>
我们的程序将正常运行,和之前数据提供者模式的例子中别无二致。
在演练场中查看代码数据提供者模式有助于分离组件的逻辑和样式,其方法是让父组件使用无渲染组件的数据,并渲染页面样式。然而,从 Vue 3 中提出了组合式函数的概念后,人们发现组合式函数可以覆盖大多数需要使用数据提供者模式的场景。
在思考数据提供者模式和组合式函数之间的取舍时,我们的建议是尽可能使用组合式函数。因为数据提供者模式在每次需要获取数据的时候都渲染一个组件,这会可能会导致额外的 性能开销,而组合式函数不需要担心这个问题。
另外,如果你正在使用状态管理插件(例如 Pinia)来管理组件所使用的数据,你可能会更加喜欢在 store 中使用 actions() 来获取数据。有了这种状态管理模式,数据提供者的组件模式就变得不那么重要了。