设计模式
数据提供者模式
在之前的一篇 文章 中,我们了解了无渲染组件如何将组件的逻辑与其呈现方式分离。当我们需要创建可重用的逻辑以应用于不同的 UI 实现时,这将非常有用。
无渲染组件还允许我们利用另一个称为 **数据提供者模式** 的有用模式。
数据提供者模式
数据提供者模式是一种设计模式,它通过专注于为组件提供数据和状态管理功能来补充 Vue 中的无渲染组件模式,而无需关注数据如何渲染或显示。
在数据提供者模式中,数据提供者组件封装了从子组件获取、管理和公开数据的逻辑。然后,子组件可以消费这些数据并在自己的渲染或行为中使用它。
这种模式促进了关注点分离,因为数据提供者组件负责与数据相关的任务,而子组件可以专注于呈现和交互。
让我们通过一个例子来说明数据提供者模式。考虑一个简单的应用程序,它显示一个有趣笑话的设置,然后是它的笑点。为了帮助我们随机显示不同的笑话,我们将使用免费的公共 API 端点 https://official-joke-api.appspot.com/random_joke,它以 JSON 格式返回一个随机笑话。
# 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
的数据提供者组件,它将负责从 API 获取笑话。在组件的 <script>
部分,我们将从 Vue 库导入 ref()
和 reactive()
函数,将端点 URL 值分配给一个常量,并设置 data
和 loading
反应式属性来捕获我们的 API 请求的数据和加载状态。
<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()
的异步函数,负责从指定的 API 端点获取笑话。该函数将
- 首先将
loading
反应式值设置为true
,表示正在获取笑话。 - 使用原生浏览器 fetch() 函数向 API 端点发送 GET 请求。
- 使用
response.json()
方法将 API 的响应转换为 JSON 格式。 - 从获得的请求数据中提取
setup
和punchline
值,并将其分配给data
对象中的相应属性。 - 最后,将
loading
值设置回false
,表示笑话已获取。
有了这些更改,我们的 fetchJoke()
函数将如下所示
<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
组件的消费者中可用。为此,我们可以将这些属性传递给我们将放在 <template>
部分中的 <slot>
元素。
<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>
现在我们的无渲染数据提供者组件已完成,我们可以在应用程序中使用它。在父应用程序组件中,我们将导入 DataProvider
组件并将其放在模板中。
<template>
<DataProvider v-slot="{ data, loading }">
<!-- ... -->
</DataProvider>
</template>
<script setup>
import DataProvider from "./components/DataProvider.vue";
</script>
通过简单地渲染 <DataProvider>
组件,我们向端点发出请求以获取笑话,并且可以借助 v-slot
指令访问请求的 data
和 loading
值。
在 <DataProvider>
组件声明中,我们可以创建 UI,如果请求处于加载状态,则显示加载消息,或者在数据可用时显示笑话设置和笑点。
<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 中,我们现在将笑话的笑点放在一个披露元素中,借助 HTML <details>
和 <summary>
元素。
使用数据提供者模式,我们能够以解耦和可重用的方式管理数据并将其提供给不同的元素/组件。通过将 API 获取逻辑抽象到一个无渲染组件中,我们可以在各种情况下重用 API 数据的请求,而无需重复代码。
1<template>2 <slot :data="data" :loading="loading"></slot>3</template>45<script setup>6import { ref, reactive } from "vue";78const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";910const data = reactive({11 setup: null,12 punchline: null,13});14const loading = ref(false);1516const fetchJoke = async () => {17 loading.value = true;1819 const response = await fetch(API_ENDPOINT_URL);20 const responseData = await response.json();2122 data.setup = responseData.setup;23 data.punchline = responseData.punchline;24 loading.value = false;25};2627fetchJoke();28</script>
我们能否改用组合式 API?
可以!我们可以不使用数据提供者模式,而是利用组合式 API 将获取逻辑提取到一个可重用的函数中。
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>
我们的应用程序现在将像以前使用数据提供者示例一样工作。
1<template>2 <div class="joke-section">3 <p v-if="loading">Joke is loading...</p>4 <p v-if="!loading">{{ data.setup }}</p>5 <p v-if="!loading">{{ data.punchline }}</p>6 </div>7</template>89<script setup>10import { useGetJoke } from "./composables/useGetJoke";1112const { data, loading } = useGetJoke();13</script>
数据提供者模式通过让父组件根据无渲染组件公开的数据和行为渲染适当的 UI,帮助将组件的逻辑与其呈现方式分离。但是,由于在 Vue 3 中能够创建可重用的组合式函数,因此组合式函数可以同样适用于大多数可以使用数据提供者模式的情况。
在考虑使用数据提供者模式还是组合式函数时,我们建议尽可能使用组合式函数,因为它避免了每次需要进行数据获取时都必须渲染组件实例(这会导致 性能开销)。
此外,如果您使用像 Pinia 这样的状态管理工具来管理数据如何提供给组件,您很可能会在商店的 actions() 中进行 API 请求。有了这种现有的状态管理模式,使用数据提供者组件模式的必要性就不那么重要了。