Vue 3 即将发布,通过引入Composition API 。 它具有许多更改和性能改进。
高阶组件(HOC)是使用模板声明性地向您的应用程序添加某些功能的组件。 我相信即使引入了Composition API,它们仍将保持非常重要的关联。
HOC始终无法充分发挥其功能的全部功能,并且由于它们在大多数Vue应用程序中并不常见,因此它们的设计通常很差,可能会带来限制。 这是因为模板就是这样----模板或一种您表达某种逻辑的受约束的语言。 但是,在JavaScript或JSX环境中,表达逻辑要容易得多,因为您可以使用全部JavaScript。
Vue 3 带给桌面的是能够使用Composition API和声明性易用的模板无缝地混合和匹配JavaScript的表达能力。
我正在为各种逻辑(如网络,动画,UI和样式,实用程序和开源库)构建的应用程序中积极使用HOC。 我有一些技巧可以分享如何构建HOC,尤其是即将发布的Vue 3 Composition API。
1 The template
让我们假设以下Fetch组件。 在探讨如何实现这样的组件之前,您应该考虑如何使用组件。 然后,您需要决定如何实现它。 这与TDD类似,但没有经过测试-更像是在构思之前先尝试一下。
理想情况下,该组件将使用一个端点并将其结果作为范围限定的插槽属性返回:
<fetch endpoint="/api/users" v-slot="{ data }">
<div v-if="data">
<!-- Show the response data -->
</div>
</fetch>
现在,尽管此API的基本目的是通过网络获取一些数据并显示它们,但仍有许多丢失的东西将是有用的。
让我们从错误处理开始。 理想情况下,我们希望能够检测到是否抛出了网络或响应错误,并向用户显示一些指示。 让我们将其运用到我们的代码中:
<fetch endpoint="/api/users" v-slot="{ data, error }">
<div v-if="data">
<!-- Show the response data -->
</div>
<div v-if="error">
{{ error.message }}
</div>
</fetch>
到目前为止,一切都很好。 但是加载状态呢? 如果我们走相同的道路,我们最终会得到这样的结果:
<fetch endpoint="/api/users" v-slot="{ data, error, loading }">
<div v-if="data">
<!-- Show the response data -->
</div>
<div v-if="error">
{{ error.message }}
</div>
<div v-if="loading">
Loading....
</div>
</fetch>
现在,假设我们需要分页支持:
<fetch endpoint="/api/users" v-slot="{ data, error, loading, nextPage, prevPage }">
<div v-if="data">
<!-- Show the response data -->
</div>
<div v-if="!loading">
<button @click="prevPage">Prev Page</button>
<button @click="nextPage">Next Page</button>
</div>
<div v-if="error">
{{ error.message }}
</div>
<div v-if="loading">
Loading....
</div>
</fetch>
你知道这是怎么回事吧? 我们在默认范围内的插槽中添加了太多属性。 相反,让我们将其细分为多个位置:
<fetch endpoint="/api/users">
<template #default="{ data }">
<!-- Show the response data -->
</template>
<template #pagination="{ prevPage, nextPage }">
<button @click="prevPage">Prev Page</button>
<button @click="nextPage">Next Page</button>
</template>
<template #error="{ message }">
<p>{{ message }}</p>
</div>
<template #loading>
Loading....
</template>
</fetch>
虽然我们拥有的字符数基本相同,但是从某种意义上说,它在组件的不同操作周期中使用多个插槽来显示不同的UI时,这要干净得多。 它甚至允许我们按每个插槽而不是整个组件公开更多数据。
当然,这里还有改进的空间。 但是,让我们确定这些是您想要的组件功能。
什么都没有呢。 您仍然必须实施实际代码,以使其正常工作。
从模板开始,我们只有3个使用v-if显示的插槽:
<template>
<div>
<slot v-if="data" :data="data" />
<slot v-if="!loading" name="pagination" v-bind="{ nextPage, prevPage }" />
<slot v-if="error" name="error" :message="error.message" />
<slot v-if="loading" name="loading" />
</div>
</template>
此处将v-if与多个插槽一起使用是一种抽象,因此该组件的使用者不必有条件地呈现其UI。 这是一项方便的功能。
Composition API为构建更好的HOC提供了独特的机会,这是本文的重点。
2 The JavaScript
有了模板,第一个简单的实现将在单个设置函数中:
import { ref, onMounted } from 'vue';
export default {
props: {
endpoint: {
type: String,
required: true,
}
},
setup({ endpoint }) {
const data = ref(null);
const loading = ref(true);
const error = ref(null);
const currentPage = ref(1);
function fetchData(page = 1) {
// ...
}
function nextPage() {
return fetchData(currentPage.value + 1);
}
function prevPage() {
if (currentPage.value <= 1) {
return;
}
fetchData(currentPage.value - 1);
}
onMounted(() => {
fetchData();
});
return {
data,
loading,
error,
nextPage,
prevPage
};
}
};
以上是设置功能的概述。 为了完成它,我们可以像下面这样实现fetchData函数:
import { ref, onMounted } from 'vue';
export default {
props: {
endpoint: {
type: String,
required: true,
}
},
setup({ endpoint }) {
const data = ref(null);
const loading = ref(true);
const error = ref(null);
const currentPage = ref(1);
function fetchData(page = 1) {
// ...
}
function nextPage() {
return fetchData(currentPage.value + 1);
}
function prevPage() {
if (currentPage.value <= 1) {
return;
}
fetchData(currentPage.value - 1);
}
onMounted(() => {
fetchData();
});
return {
data,
loading,
error,
nextPage,
prevPage
};
}
};
一切就绪后,即可使用该组件。 您可以在这里找到它的工作示例。
但是,此HOC组件与Vue 2中的组件相似。您仅使用composition API重新编写了它,尽管它很简洁,但几乎没有用。
我发现,要为Vue 3构建更好的HOC组件(尤其是像这样的面向逻辑的组件),最好以“ Composition-API-first”的方式构建它。 即使您仅打算运送HOC。
您会发现我们已经做到了。 Fetch组件的设置功能可以提取为自己的函数,称为useFetch:
export function useFetch(endpoint) {
// same code as the setup function
}
相反,我们的组件将如下所示:
import { useFetch } from '@/fetch';
export default {
props: {
// ...
},
setup({ endpoint }) {
const api = useFetch(endpoint);
return api;
}
}
这种方法提供了一些机会。 首先,它使我们可以在与UI完全隔离的同时考虑逻辑。 这使我们的逻辑可以完全用JavaScript表示。 以后可以将其挂接到UI,这是Fetch组件的责任。
其次,它允许我们的useFetch函数将其逻辑分解为较小的函数。 可以将其视为将相似的东西“分组”在一起,并可能通过包含和排除这些较小的功能来创建我们组件的变体。
3分解
让我们通过将分页逻辑提取为其自身的功能来阐明这一点。 问题就变成了:如何将分页逻辑与获取逻辑分开? 两者似乎交织在一起。
您可以通过关注分页逻辑的功能来弄清楚。 解决它的一种有趣方法是将其拿走并检查您消除的代码。
当前,它的作用是通过添加页面查询参数来修改端点,并在暴露下一个和上一个函数的同时保持currentPage状态的状态。 从字面上看,这就是在上一次迭代中所做的。
通过创建一个名为usePagination的函数,该函数仅执行我们需要的部分,您将获得以下内容:
import { ref, computed } from 'vue';
export function usePagination(endpoint) {
const currentPage = ref(1);
const paginatedEndpoint = computed(() => {
return `${endpoint}?page=${currentPage.value}`;
});
function nextPage() {
currentPage.value++;
}
function prevPage() {
if (currentPage.value <= 1) {
return;
}
currentPage.value--;
}
return {
endpoint: paginatedEndpoint,
nextPage,
prevPage
};
}
这样做的好处是我们隐藏了外部使用者的currentPage引用,这是我在Composition API中最喜欢的部分之一。 我们可以轻松地向API使用者隐藏不重要的细节。
更新useFetch以反映该页面很有趣,因为它似乎需要跟踪usePagination公开的新端点。 幸运的是,watch已经覆盖了我们。
可以不希望端点参数是常规字符串,而可以允许它是反应性值。 这使我们能够观看它,并且每当分页页面更改时,它将产生新的端点值,从而触发重新获取。
import { watch, isRef } from 'vue';
export function useFetch(endpoint) {
// ...
function fetchData() {
// ...
// If it's a ref, get its value
// otherwise use it directly
return fetch(isRef(endpoint) ? endpoint.value : endpoint, {
// Same fetch opts
}) // ...
}
// watch the endpoint if its a ref/computed value
if (isRef(endpoint)) {
watch(endpoint, () => {
// refetch the data again
fetchData();
});
}
return {
// ...
};
}
请注意,useFetch和usePagination完全不了解彼此,并且两者的实现就像彼此都不存在一样。 这为我们的HOC提供了更大的灵活性。
您还会注意到,首先通过构建Composition API,我们创建了无法识别您的UI的盲JavaScript。 以我的经验,这对于正确地对数据建模而无需考虑UI或让UI决定数据模型非常有帮助。
另一个很酷的事情是,我们可以创建HOC的两种不同变体:一种允许分页,而另一种则不允许。 这为我们节省了几千字节。
这是仅进行提取的示例:
import { useFetch } from '@/fetch';
export default {
setup({ endpoint }) {
return useFetch(endpoint);
}
};
这是同时做这两项的另一个:
import { useFetch, usePagination } from '@/fetch';
export default {
setup(props) {
const { endpoint, nextPage, prevPage } = usePagination(props.endpoint);
const api = useFetch(endpoint);
return {
...api,
nextPage,
prevPage
};
}
};
更好的是,您可以根据道具有条件地应用usePagination功能以获得更大的灵活性:
import { useFetch, usePagination } from '@/fetch';
export default {
props: {
endpoint: String,
paginate: Boolean
},
setup({ paginate, endpoint }) {
// an object to dump any conditional APIs we may have
let addonAPI = {};
// only use the pagination API if requested by a prop
if (paginate) {
const pagination = usePagination(endpoint);
endpoint = pagination.endpoint;
addonAPI = {
...addonAPI,
nextPage: pagination.nextPage,
prevPage: pagination.prevPage
};
}
const coreAPI = useFetch(endpoint);
// Merge both APIs
return {
...addonAPI,
...coreAPI,
};
}
};
对于您的需求而言,这可能太多了,但是它使您的HOC更加灵活。 否则,它们将是僵化的代码体,难以维护。 绝对也更适合单元测试。
这是行动的最终结果:
链接:代码展示地址
总结
总结起来,首先将您的HOC构建为Composition API。 然后,将逻辑部分尽可能地分解为较小的可组合函数。 将它们全都放在您的HOC中以暴露最终结果。
这种方法使您可以构建组件的变体,甚至可以完成所有这些变体而又不会脆弱且难以维护。 通过以composition-api-first的心态进行构建,您可以自己编写与UI无关的孤立的代码部分。 通过这种方式,您可以让HOC成为盲目的JavaScript和无功能的UI之间的桥梁。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: GoSolo 原文链接:https://juejin.im/post/6859571758362525704