基于Hoc封装业务FlatList

Hoc是什么?

HOC(High Order Component) 是react提出的一种设计模式,接收一个组件作为参数,并返回一个新的组件。

业务FlatList封装

起因

FlatList作为ReactNative开发中最长使用到的组件之一,其官方已经封装很完善了,我们的列表页面只需要简单引入即可,但是,一般app中会有多个页面都是列表展示,并且没有太复杂的操作,而我们则需要复制多个FlatList,包括头部、底部、间隙等组件的配置,更重要的是在处理FlatList的data列表数据时,我们需要下拉刷新、上拉加载更多,这些工作都是重复的,因此基于此我们考虑到能否采用Hoc来实现列表页面都只需要关注这两个点:renderItem、网络请求的函数

设计思路

  1. 每个列表都需要一个组件来承载列表中每条数据的呈现,因此需要将这个itemCell组件作为Hoc的入参
  2. 我们让Hoc返回一个能正确装载itemCell组件的FlatList
  3. 如果我们的列表仅是作为展示,不需要对dataList的单条数据进行操作,那么可以将新的组件作为dataList的容器(将其设置为state.dataList)
  4. 如果我们的列表页面还需要对单条数据操作(修改属性、删除数据等),那么可以将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)

实现代码

以下三个模块可以忽略

  1. CommonUtil-项目中的一个工具类,用CommonUtil.debounce可以进行防抖 ,说明链接
  2. BaseNet是我们业务中的网络请求相关,这里支持是Promise方式获取请求结果
  3. 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,
  },
})

猜你喜欢

转载自blog.csdn.net/u010899138/article/details/110928950