前言
在 web 开发上,我们都对数据采用分页加载的机制,一种变形就是在页面采用循环加载的机制,拉到页面最下方有个加载更多的按钮。问题在于,当不同的数据要展示时,就要写很多这种列表,但是其中的逻辑都是相似的。
维护一组数据
加载更多数据
将数据用对应的组件显示出来
处理加载状态等
那有没有这么一个组件,来完成这一切相同的逻辑呢?
需求
需要有这么一个 InfiniteList 组件,它负责管理相关数据的加载和维护,然后以列表的形式显示出来,而列表项必须是由调用方决定的组件。
HOC
高阶组件的概念,是 React 里面经常提到的,类似于高阶函数。高阶函数:(fn) => otherFn 高阶组件:component => otherComponent 高阶组件用是代码复用的优秀工具,主要在处理逻辑方面和普适性上,有着奇效。
所以我决定用 HOC 来实现这个需求
参考文章:http://hcysun.me/2018/01/05/%… 良心博客
本文涉及的知识
vue
vue 的 render 函数
实现
0
我使用的是 vue 和 iview UI 库
1
先弄出 UI 框架先,我用一个 vue 文件来构建整个组件的基本框架。源代码地址
html 部分
<template>
<div class=”wrapper”>
<div class=”content-wrapper”>
<slot></slot>
</div>
<div class=”load-wrapper”>
<Button
:icon=”tipIcon”
type=”text”
v-bind:disabled=”!hasMore”
v-bind:style=”{color: tipColor}”
v-bind:loading=”loading”
v-on:click=”handleClickLoad”>
{{loadButtonText}}
</Button>
</div>
</div>
</template>
用一个 slot 来分发要循环渲染的项目
js 部分
一些 UI 有关的数据(不是很重要)
props: {
loadTip: {
type: String,
default: “ 加载更多 ”
}
…
},
computed: {
loadButtonText() {},
tipIcon() {}
}
这部分比较重要的只有一个事件发射,将点按钮的行为转换为 请求加载数据
handleClickLoad() {
// 发射 请求加载数据的 事件
this.$emit(“on-load”);
}
css 部分略
2
接下来就是最重要的部分,编写 HOC 首先要明白,Vue 中的组件,到底是什么。像我们写一个 Vue 文件,export 出的是一个对象,所以我们现在写 HOC,其实也是要最后返回一个对象。所以我写了下面的函数来生成 HOC
/**
* 使用高阶组件的办法实现了一个无限加载列表
* 可以根据数据循环渲染出特定的组件,并且管理加载状态
* @param component 具体项的组件 {props: {data}}
*/
function InfiniteList(listItem) {
return {
props:…
data(){}
…
}
}
而我们如果渲染呢,当然是用 Vue 的 render 函数
render(h) {
return h(component, data, children);
}
我们使用组合的方式,最外层需要用到我们第 1 步写到的模板,于是导入它,并注册它
import InfiniteListTemplate from “./InfiniteListTemplate”;
function InfiniteList(listItem) {
return {
…
components: {
InfiniteListTemplate // 列表框架的模板,这个模板里面只有 ui 表现
},
…
}
}
render 函数对于熟悉 React 的程序员来说应该是不难的,官网也有很详细的介绍。
render(h) {
const self = this;
// 根据 data 的 dataList 循环渲染子组件
const listItems = …
return h(InfiniteListTemplate, {
props: {
…self.$props, // 传递所有参数
hasMore: self.hasMore, // 另外的 hasMore 和 loading 是这个 HOC 的 state
loading: self.loading
},
attrs: self.$attrs,
on: {
// 监听加载按钮事件
“on-load”: () => self.handleLoadData()
}
}, listItems);
},
这里在最外层渲染我们的模板(且称为模板组件),并将当前 HOC 的 props,attrs 传递给模板组件。这里提到了 HOC 的 data,非常简单,就是两个状态和一个数据数组
data() {
return {
hasMore: true,
loading: false,
dataList: []
}
}
然后呢,循环渲染在哪?别急,render 中的 listItems 就是我们循环渲染出来的组件,这里使用了 map,相信使用 React 的人非常熟悉这种风格
const listItems = this.dataList.map(item => h(component, {
props: {
data: item
}
})
);
最终返回的就是
return h(InfiniteListTemplate, {options}, listItems);
在哪里维护数据呢?当然是要传入一个加载数据的函数来进行管理,我们在 HOC 的 props 里面定义
props: {
tipColor,
loadTip,
loadingTip,
// 上面的数据都是为了传给模板(组件)
offset: {
type: Number,
default: 5
},
// 数据加载的函数,需要的是一个 (index, offset) => Promise<[]>
loadDataFunc: {
type: Function,
default() {
return (index, offset) => Promise.resolve(new Array(offset).map((o, i) => index + i));
}
}
},
然后我们还记得模板函数发射了个 on-load 事件么?我们需要在 HOC 里监听它并且处理逻辑
render(h) {
return h(InfiniteListTemplate, {
…
on: {
‘on-load’: () => self.handleLoadData()
}
}, listItems);
},
methods: {
/**
* 监听模板点出了加载按钮时的操作
* 调用数据加载函数加载数据
* @return {Promise<void>}
*/
async handleLoadData() {
try {
this.loading = true;
let res = await this.loadDataFunc(this.dataList.length, this.offset);
if (res && res.length) {
this.dataList = this.dataList.concat(res);
this.$Message.success(` 成功获取到 ${res.length} 条新数据 `);
} else {
this.$Message.info(` 已经获取了全部数据了 `);
this.hasMore = false;
}
} catch (e) {
this.$Message.error(“ 加载失败 ” + e.message);
} finally {
this.loading = false;
}
}
},
完整 InfiniteList.js 代码
3
接下来使用一遍
<script>
import MyComponent from “./components/MyComponent”;
import InfiniteList from “./components/hoc/InfiniteList”;
const InfiniteListComponent = InfiniteList(MyComponent);
…
data() {
loadDataFunc: (index, offset) => Promise<[]>
}
</script>
<template>
<div id=”app”>
<InfiniteListComponent
v-if=”loadDataFunc”
v-bind:load-data-func=”loadDataFunc”>
</InfiniteListComponent>
</div>
</template>
MyComponent.vue 是个非常简单的组件
<template>
<div>Hello</div>
</template>
<script>
export default {
name: “MyComponent”,
props: {
data: {
type: String
}
}
}
</script>
效果图如下
总结
在前端开发过程中,HOC 是代码利用的利器,但是对抽象的要求高。我觉得自己爱上了 React…Vue 实现这个 HOC 烦死了