高阶组件(HOC)是使用模板声明性地向您的应用程序添加某些功能的组件。我相信即使引入了Composition API,它们仍将保持非常重要的关联。
HOC始终无法充分发挥其功能的全部功能,并且由于它们在大多数Vue应用程序中并不常见,因此它们的设计通常很差,可能会带来限制。这是因为模板就是这样-模板或一种您表达某种逻辑的受约束的语言。但是,在JavaScript或JSX环境中,表达逻辑要容易得多,因为您可以使用所有的JavaScript。
Vue 3带给桌面的是能够使用Composition API和声明性易用的模板无缝地混合和匹配JavaScript的表达能力。
我在为各种逻辑(如网络,动画,UI和样式,实用程序和开源库)构建的应用程序中积极使用HOC。我有一些技巧可以分享如何构建HOC,尤其是即将发布的Vue 3 Composition API。
让我们假设以下fetch组件。在研究如何实现这样的组件之前,您应该考虑如何使用组件。然后,您需要决定如何实现它。这与TDD类似,但没有经过测试-更像是在尝试该概念之前对其进行了研究。
理想情况下,该组件将使用一个端点并将其结果作为范围限定的插槽属性返回:
<!-显示响应数据->
现在,尽管此API的基本目的是通过网络获取一些数据并显示它们,但仍有许多丢失的东西很有用。
让我们从错误处理开始。理想情况下,我们希望能够检测到是否抛出了网络错误或响应错误,并向用户显示了一些指示。让我们将其草绘到我们的用法片段中:
<!-显示响应数据->
{{ error.message }}
到目前为止,一切都很好。但是加载状态呢?如果我们走同样的路,我们最终会得到这样的结果:
{{ error.message }}
Loading....
凉。现在,假设我们需要分页支持:
{{ error.message }}
Loading....
虽然我们拥有的字符数基本相同,但是从某种意义上说,它在组件的不同操作周期中使用多个插槽来显示不同的UI时,这要干净得多。它甚至允许我们按每个插槽而不是整个组件公开更多数据。
当然,这里还有改进的空间。但是,让我们确定这些是您想要该组件的功能。
什么都没有呢。您仍然必须实施实际代码,以使其正常工作。
从模板开始,我们只有3个使用来显示的slot v-if:
v-if与多个插槽一起使用是一种抽象,因此该组件的使用者不必有条件地呈现其UI。这是一个方便的功能。
组合API提供了构建更好的HOC的独特机会,这是本文的重点。
有了模板,第一个天真的实现将在一个setup函数中:
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
};
}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
};
}
这是该setup功能的概述。为了完成它,我们可以实现如下fetchData功能:
function fetchData(page = 1) {
loading.value = true;
// I prefer to use fetch
// you cause use axis as an alternative
return fetch(`${endpoint}?page=${page}`, {
// maybe add a prop to control this
method: 'get',
headers: {
'content-type': 'application/json'
}
})
.then(res => {
// a non-200 response code
if (!res.ok) {
// create error instance with HTTP status text
const error = new Error(res.statusText);
error.json = res.json();
throw error;
}
return res.json();
})
.then(json => {
// set the response data
data.value = json;
// set the current page value
currentPage.value = page;
})
.catch(err => {
error.value = err;
// incase a custom JSON error response was provided
if (err.json) {
return err.json.then(json => {
// set the JSON response message
error.value.message = json.message;
});
}
})
.then(() => {
// turn off the loading state
loading.value = false;
});
}
在所有这些都准备就绪之后,就可以使用该组件了。您可以在这里找到它的工作示例。
但是,此HOC组件与Vue 2中的组件相似。您只能使用composition API重新编写它,尽管它很简洁,但几乎没有用。
我发现,要为Vue 3构建更好的HOC组件(尤其是像这样的面向逻辑的组件),最好以“ Composition-API-first”的方式构建它。即使您仅打算运送HOC。
您会发现我们已经做到了。该fetch组件的setup功能,也能提取到其自身的功能,这就是所谓useFetch:
export function useFetch(endpoint) {
// same code as the setup function
}
And instead our component will look like this:
import { useFetch } from '@/fetch';
export default {
props: {
// ...
},
setup({ endpoint }) {
const api = useFetch(endpoint);
return api;
}
}
让我们通过将分页逻辑提取为其自身的功能来阐明这一点。问题就变成了:如何将分页逻辑与获取逻辑分开?两者似乎交织在一起。
您可以通过关注分页逻辑的功能来弄清楚。解决它的一种有趣方法是将其拿走并检查您消除的代码。
当前,它的作用是endpoint通过附加page查询参数来修改,并currentPage在暴露next和previous起作用时保持状态的状态。从字面上看,这就是在上一次迭代中所做的。
通过创建一个usePagin
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我们已经覆盖了。
与其期望endpoint参数是常规字符串,不如让我们将其作为反应性值。这使我们能够观看它,并且每当分页页面更改时,它将产生新的端点值,从而触发重新获取。
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我们已经覆盖了。
与其期望endpoint参数是常规字符串,不如让我们将其作为反应性值。这使我们能够观看它,并且每当分页页面更改时,它将产生新的端点值,从而触发重新获取。
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构建为Composition API。然后,将逻辑部分尽可能地分解为较小的可组合函数。将它们全都放在您的HOC中以暴露最终结果。
通过这种方法,您可以构建组件的变体,甚至可以构建各种变体而又不会脆弱且难以维护。通过以composition-api-first的心态进行构建,您可以自己编写与UI无关的独立代码部分。通过这种方式,您可以让HOC成为盲目的JavaScript和无功能的UI之间的桥梁。