Hoc是什么?
HOC(High Order Component) 是react提出的一种设计模式,接收一个组件作为参数,并返回一个新的组件。
业务FlatList封装
起因
FlatList作为ReactNative开发中最长使用到的组件之一,其官方已经封装很完善了,我们的列表页面只需要简单引入即可,但是,一般app中会有多个页面都是列表展示,并且没有太复杂的操作,而我们则需要复制多个FlatList,包括头部、底部、间隙等组件的配置,更重要的是在处理FlatList的data列表数据时,我们需要下拉刷新、上拉加载更多,这些工作都是重复的,因此基于此我们考虑到能否采用Hoc来实现列表页面都只需要关注这两个点:renderItem、网络请求的函数
设计思路
- 每个列表都需要一个组件来承载列表中每条数据的呈现,因此需要将这个itemCell组件作为Hoc的入参
- 我们让Hoc返回一个能正确装载itemCell组件的FlatList
- 如果我们的列表仅是作为展示,不需要对dataList的单条数据进行操作,那么可以将新的组件作为dataList的容器(将其设置为state.dataList)
- 如果我们的列表页面还需要对单条数据操作(修改属性、删除数据等),那么可以将dataList存储在父组件中(及通过props传递给Hoc组件)
定义类型
// 每一个item的值定义,item表示数据,index表示索引
export type ItemData = {
item: any, index: number }
export type ItemEvents = {
// item组件的事件,调用者可以在item组件中extends这个type,然后添加更多的事件
onPress?: (item: ItemData) => void, // 默认存在onPress事件,为了灵活性,此事件并不封装到这个组件中
}
export interface ItemProps extends ItemEvents {
item: ItemData,
}
type ListUseOutsideDataListProps = {
dataList: any[], // 当设定使用外部数据源的时,需要将dataList通过props传递
onInitDataList: (list: any[]) => void, // 初始化网络数据成功后,将列表数据回调给父组件
onAddMoreDataList: (list: any[]) => void, // 加载更多网络数据成功后,将列表数据回调给父组件
}
interface BaseFlatListProps {
outsideData?: ListUseOutsideDataListProps,//使用外部的数据源
netRequestParams?: NetRequestOptionParams | any,// 适用于列表搜索的场景
style?: StyleProp<ViewStyle>, // flatList的style
separatorStyle?: StyleProp<ViewStyle>, // item间隙的样式(当ItemSeparatorComponent设置后,此属性无效)
ListEmptyComponent?: ReactNode, // dataList为空时,列表展示的组件
ListFooterComponent?: ReactNode, // 列表底部组件
ItemSeparatorComponent?: ReactNode | null, // item间隙组件
horizontal?: boolean, // 是否横向
netFunction: (params: any) => Promise<NetResponseOptions>,
// 网络返回的list列表可能存在的属性 例如:1-response.data,可以不传,也可以使用'data'或者['data'] 2-response.data.list,采用['data','list]
netResponseListKey?: string | string[],
dataUniqueIdKeyName?: string | string[], // 每一条数据的唯一标识,(也可能是多字段组合成唯一标识)
pageSize?: number, // 分页条数(默认20条)
itemEvents?: ItemEvents | any, // item组件的事件集合,最基本的有onPress-点击,还可以传递更多事件
}
关键代码
/**
*
* 网路请求数据的FlatList
* @param ItemComponent
* @return ReactNode
*/
const useBaseFlatList = (ItemComponent: ReactNode) => {
// eslint-disable-next-line react/display-name
return class extends React.Component<BaseFlatListProps, any> {
hasMoreData: boolean = false
constructor(props: BaseFlatListProps) {
super(props)
this.state = {
dataList: [],
}
let outsideData: ListUseOutsideDataListProps | undefined = this.props.outsideData
if (!!outsideData) {
if (!outsideData.onInitDataList ||
!outsideData.onAddMoreDataList ||
!outsideData.dataList) {
throw new Error('当设定为使用外部数据源(outsideData)时,必须传递onInitDataList、onAddMoreDataList和dataList')
}
}
}
_renderItem = (item: {
item: any, index: number }) => {
return (
// @ts-ignore
<ItemComponent
{
...this.props.itemEvents}
item={
item}
/>
)
}
render(): React.ReactElement<any, string | React.JSXElementConstructor<any>> | string | number | {
} | React.ReactNodeArray | React.ReactPortal | boolean | null | undefined {
return (
<FlatList
style={
this.props?.style ? this.props?.style : BaseFlatListNetStyles.flatListStyle}
keyExtractor={
(item, index) => this.getKeyExtractor(item, index)}
data={
this.dataList}
renderItem={
this._renderItem}
refreshing={
false}
onRefresh={
() => this.initDataFromServer()}
onEndReachedThreshold={
0.1}
ListEmptyComponent={
this._ListEmptyComponent}
ListFooterComponent={
this._ListFooterComponent}
onEndReached={
() => this.addMoreFromSever()}
ItemSeparatorComponent={
this._ItemSeparatorComponent}
horizontal={
this.props.horizontal}
/>
)
}
_ListFooterComponent = () => {
// 列表底部组件
}
_ListEmptyComponent = () => {
// 列表为空时展示的组件
}
_ItemSeparatorComponent = () => {
// 列表每个item的间隙组件
}
componentDidMount(): void {
// 从服务端获取列表数据
this.initDataFromServer()
}
get useOutsideData() {
return !!this.props.outsideData
}
get dataList() {
if (this.useOutsideData) {
return this.props.outsideData?.dataList
}
return this.state.dataList
}
// 从服务端拉取
initDataFromServer() {
// 需要判断是否是采用外部数据源
// 如果采用外部数据源,则网络请求返回后,将list通过this.props.outsideData?.onInitDataList将列表回调给父组件,否则采用setData直接赋值给state
}
/**
* 从服务端拉取更多数据
*/
addMoreFromSever() {
// 和initDataFromServer的本质一样
}
}
}
export {
useBaseFlatList}
使用方式
item组件示例代码
interface UserCellProps extends ItemProps {
onLongPress: (item: ItemData) => void
}
class UserCell extends React.Component<UserCellProps, any> {
render(){
return (
<TouchableOpacity
onPress={
() => {
this.props.onPress && this.props.onPress(this.props.item)
}}
onLongPress={
() => {
this.props.onLongPress && this.props.onLongPress(this.props.item)
}}
>
<Text>{
this.props.item.item.name}</Text>
</TouchableOpacity>
)
}
}
const UserList = useBaseFlatList(UserCell)
方式1(将dataList存储在Hoc组件中)
适用场景:列表仅展示,不对dataList数据进行操作(不包括点击事件)
// 关键使用代码
export class BaseFlatListT1 extends React.Component {
render() {
return (
<View style={
{
flex:1}}>
<UserList
// NetApi.listV1 是我们的基于Promise的网络请求函数声明,传递函数声明即可,在Hoc组件中,会触发netFunction()
netFunction={
NetApi.listV1}
netRequestParams={
{
}}
itemEvents={
{
onPress: (item: ItemData) => {
DialogUtil.showDialog(item.item.name)
},
onLongPress: (item: ItemData) => {
DialogUtil.showDialog(item.index)
},
}}
/>
</View>
)
}
}
方式2(将dataList存放在父组件中)
适用场景:需要对列表数据进行操作,因此通过props将dataList传递给Hoc组件,同时需要实现onInitDataList、onAddMoreDataList用来接收Hoc组件中的列表请求结果(是否可以加载更多的判断在Hoc组件内部已经处理)
// 关键使用代码
export class BaseFlatListOutside extends React.Component<IBaseProps, {
dataList: any[] }> {
constructor(props: IBaseProps) {
super(props)
this.state = {
dataList: [],
}
}
render() {
return (
<View style={
FlatListNetT1Style.contain}>
<Text>FlatListNetT1</Text>
<UserList
netFunction={
NetApi.listT2Outside}
netResponseListKey={
['data', 'list']} // list的在response的data.list属性中
outsideData={
{
dataList: this.state.dataList,
onInitDataList: (list) => {
this.setState({
dataList: list,
})
},
onAddMoreDataList: (list) => {
this.setState({
dataList: this.state.dataList.concat(list),
})
},
}}
itemEvents={
{
onPressDone: (item: ItemData) => {
let value = item.item
value.count = value.count - 1
this.state.dataList.splice(item.index, 1, value)
this.setState({
dataList: this.state.dataList,
})
},
}
}
/>
</View>
)
}
}
interface UserCellProps extends ItemProps {
onPressDone: (item: ItemData) => void,
}
class UserCell extends React.Component<UserCellProps, any> {
render() {
return (
<TouchableOpacity
style={
{
height: 40,
backgroundColor: '#c2c2c2',
paddingHorizontal: 20,
justifyContent: 'space-between',
flexDirection: 'row',
}}
>
<Text>{
this.props.item.item.count}</Text>
<Text onPress={
() => this.props.onPressDone(this.props.item)}
>{
'完成事件'}</Text>
</TouchableOpacity>
)
}
}
const UserList = useBaseFlatList(UserCell)
实现代码
以下三个模块可以忽略
- CommonUtil-项目中的一个工具类,用CommonUtil.debounce可以进行防抖 ,说明链接
- BaseNet是我们业务中的网络请求相关,这里支持是Promise方式获取请求结果
- NetLoadingHelper是业务中对网络请求进行辅助处理的工具类
import React, {
ReactNode, Fragment} from 'react'
import {
View,
FlatList,
StyleSheet,
Text,
StyleProp,
ViewStyle,
} from 'react-native'
import {
CommonUtil} from '../../../common/utils/CommonUtil'
import {
netCheck, NetRequestOptionParams, NetResponseOptions} from '../../net/base/BaseNet'
import {
NetLoadingHelper} from '../../net/NetLoadingHelper'
import {
DialogUtil} from '../dialog/DialogUtil'
// 类型定义在定义类型中
/**
*
* 网路请求数据的FlatList
* @param ItemComponent
* @return ReactNode
*/
const useBaseFlatList = (ItemComponent: ReactNode) => {
// eslint-disable-next-line react/display-name
return class extends React.Component<BaseFlatListProps, any> {
hasMoreData: boolean = false
constructor(props: BaseFlatListProps) {
super(props)
this.state = {
dataList: [],
}
let outsideData: ListUseOutsideDataListProps | undefined = this.props.outsideData
if (!!outsideData) {
if (!outsideData.onInitDataList ||
!outsideData.onAddMoreDataList ||
!outsideData.dataList) {
throw new Error('当设定为使用外部数据源(outsideData)时,必须传递onInitDataList、onAddMoreDataList和dataList')
}
}
}
get useOutsideData() {
return !!this.props.outsideData
}
get dataList() {
if (this.useOutsideData) {
return this.props.outsideData?.dataList
}
return this.state.dataList
}
_renderItem = (item: {
item: any, index: number }) => {
return (
// @ts-ignore
<ItemComponent
{
...this.props.itemEvents}
item={
item}
/>
)
}
render(){
return (
<FlatList
style={
this.props?.style ? this.props?.style : BaseFlatListNetStyles.flatListStyle}
keyExtractor={
(item, index) => this.getKeyExtractor(item, index)}
data={
this.dataList}
renderItem={
this._renderItem}
refreshing={
false}
onRefresh={
() => this.initDataFromServer()}
onEndReachedThreshold={
0.1}
ListEmptyComponent={
this._ListEmptyComponent}
ListFooterComponent={
this._ListFooterComponent}
onEndReached={
() => this.addMoreFromSever()}
ItemSeparatorComponent={
this._ItemSeparatorComponent}
horizontal={
this.props.horizontal}
/>
)
}
_ListFooterComponent = () => {
if (!this.showMoreDataFooter) {
return null
}
return (
<Fragment>
{
this.props?.ListFooterComponent ?
this.props?.ListFooterComponent :
<View
style={
BaseFlatListNetStyles.listEmptyStyle}
>
<Text>{
this.moreDataText}</Text>
</View>
}
</Fragment>
)
}
get showMoreDataFooter() {
return this.length > 0
}
get moreDataText() {
if (this.hasMoreData) {
return '上拉加载更多'
}
return '没有更多数据'
}
_ListEmptyComponent = () => {
return (
<Fragment>
{
this.props?.ListEmptyComponent ?
this.props?.ListEmptyComponent :
<View
style={
BaseFlatListNetStyles.listEmptyStyle}
>
<Text>{
'没有数据?试试下拉刷新~'}</Text>
</View>
}
</Fragment>
)
}
_ItemSeparatorComponent = () => {
return (
<Fragment>
{
this.props?.ItemSeparatorComponent ?
this.props?.ItemSeparatorComponent :
<View
style={
this.props?.separatorStyle ?
this.props?.separatorStyle : BaseFlatListNetStyles.separatorStyle}
/>
}
</Fragment>
)
}
componentDidMount(): void {
this.initDataFromServer()
}
initDataFromServer() {
CommonUtil.debounce(() => {
this.props.netFunction({
...this.requestParams,
startNum: 0,
endNum: this.pageSize,
}).then((response) => {
if (netCheck(response)) {
let list = this.getDataListFromResponse(response)
// 当使用的是外部的数据源时,需要将列表数据回调给父组件
if (this.useOutsideData) {
this.props.outsideData?.onInitDataList(list)
} else {
this.setState({
dataList: list,
})
}
} else {
DialogUtil.showNetErrorDialog(response)
}
})
})
}
/**
* 从服务端拉取更多数据
*/
addMoreFromSever() {
if (!this.hasMoreData || NetLoadingHelper.loading) {
return
}
NetLoadingHelper.showLoading()
this.props.netFunction({
...this.requestParams,
startNum: this.length,
endNum: this.length + this.pageSize - 1,
}).then((response) => {
if (netCheck(response)) {
let list: any[] = this.getDataListFromResponse(response)
// 当使用的是外部的数据源时,需要将列表数据回调给父组件
if (this.useOutsideData) {
this.props.outsideData?.onAddMoreDataList(list)
} else {
// @ts-ignore
let dataList = [].concat(this.dataList, list)
this.setState({
dataList: dataList,
})
}
} else {
DialogUtil.showNetErrorDialog(response)
}
})
}
get requestParams() {
if (this.props.netRequestParams) {
return {
...this.props.netRequestParams,
}
}
return {
}
}
// 分页条数
get pageSize() {
if (this.props.pageSize) {
return this.props.pageSize
}
return 20
}
// 列表当前的数据量
get length() {
return this.dataList.length
}
get responseListKey() {
return this.props.netResponseListKey
}
/**
* 根据response提取出list,同时判断接口列表中是否还有分页数据,用以判断是否可以触发加载更多
* @param response
*/
getDataListFromResponse(response: NetResponseOptions | any): any[] {
if (!netCheck(response)) {
this.hasMoreData = false
return []
}
let list: any[] = []
if (this.responseListKey) {
if (typeof this.responseListKey === 'string') {
list = response[this.responseListKey]
} else {
let dataList = response
for (let i = 0; i < this.responseListKey.length; i++) {
dataList = dataList[this.responseListKey[i]]
}
list = dataList
}
} else if (response.data && response.data instanceof Array) {
// 当response.data 存在且是数组的时候,才将其赋值给到组件
list = response.data
}
this.checkHasMore(list)
return list
}
// 判断服务端是否还有更多数据,用于上拉加载更多
checkHasMore(list: any[]) {
this.hasMoreData = list ? list.length >= this.pageSize : false
}
get dataUniqueIdKeyName() {
return this.props.dataUniqueIdKeyName
}
// 给FlatList的每条cell设置key
getKeyExtractor(item: any, index: number) {
if (this.dataUniqueIdKeyName) {
if (typeof this.dataUniqueIdKeyName === 'string') {
return item[this.dataUniqueIdKeyName]
}
let key = ''
this.dataUniqueIdKeyName.map((value) => {
key += item[value]
})
return key
}
return index.toString()
}
}
}
export {
useBaseFlatList}
const BaseFlatListNetStyles = StyleSheet.create({
flatListStyle: {
backgroundColor: '#F6F6F6',
},
separatorStyle: {
height: 1,
},
listEmptyStyle: {
marginTop: 10,
alignItems: 'center',
},
listFooter: {
marginTop: 10,
alignItems: 'center',
},
listEmptyText: {
color: '#666',
fontSize: 16,
},
})