关于前端:前端必备手把手教你实现下拉刷新上拉加载和搜索让列表功能变得更加简单易用

具体源码能够看这里
感觉不错的小伙伴记得给个star⭐️,谢谢反对,

在挪动端页面中,列表是一个很常见的性能,接下来手把手教你实现一个下拉刷新,上拉加载,带有搜寻性能的列表状态管理器

clean-js 应用办法

在此之前先阐明一下这个状态库如何应用

性能:

  1. 提供presenter的束缚,束缚视图状态和更新的形式;
  2. 提供视图devtool(redux-devtool/log)
  3. 提供适配器,适配react/vue/…
  4. 提供IOC容器,能够实现依赖注入
  5. 依据YAPI,swagger2,swagger3等api协定主动生成申请代码

实现:

  1. 所有的状态类都须要继承基类Presenter,须要在基类写入泛型 IViewState
  2. 在结构器函数中须要申明默认的state,类型为 IViewState
  3. 能够通过setState函数来设置state值,从而触发组件渲染

interface IViewState {
  loading: boolean;
  name: string
}

export class NamePresenter extends Presenter<IViewState> {
  constructor() {
    super();
    this.state = {
      loading: false,
      name: 'hahaha'
    }
  }


  changeName() {
    this.setState(s => {
        s.name = 'segmentfault'
    }); // api of set model state
  }
}

具体在react组件中应用的形式如下


const Name = () => {
  const { presenter, state } = usePresenter(NamePresenter);
  return (
    <div>
      name: {state.name}
      <button onClick={presenter.changeName}>change name</button>
    </div>
  );
};


export default Name;

此外还反对依赖注入,context,依据YAPI,swagger2,swagger3等api协定主动生成申请代码等多种性能

具体内容能够看文档形容

定义列表模型

首先装置一下本人写的状态库

npm install @clean-js/presenter @clean-js/react-presenter --save

接着定义列表的模型,通常来说咱们须要上面这些属性

  • loading: boolean; 加载中的状态
  • data: Row[]; 列表数据,申请每一页的数据
  • allData: Row[]; 列表数据,缓存所有的数据
  • params: Record<any, any>; 申请附带的参数,
  • pagination: IPagination; 分页相干的参数
export interface IPagination {
  current: number;
  pageSize: number;
  total: number;
}
interface IViewState<Row, OtherParams> {
  loading: boolean;
  data: Row[]; 
  allData: Row[]; 
  params: OtherParams; 
  pagination: IPagination;
}

有了这些属性,在组件中就能够失常的渲染列表了

定义通用办法

回到咱们的需要
接下来申明ListPresenter类,给他设置一些通用的办法

ListPresenter类中咱们申明了几个办法

  • fetchData 用来发动申请,他会承受params和pagination作为参数,并且返回约定后的接口,这个函数须要具体业务来实现
  • showLoading/hideLoading 切换loading状态
  • loadMore 调用fetchData来发动申请,申请实现后更新data,loading和分页数据
  • hasMore 判断是否有下一页
  • updateParams 更新申请参数,通常咱们列表都会随同搜寻框,筛选框,这之后就能够通过这个办法来更新对应的参数了,须要留神的是,在参数发生变化之后,分页会重置为第一页
  • resetParams 顾名思义,用来重置申请参数
  • updatePagination 分页参数无关的逻辑,具体能够看上面代码

有了这些办法,咱们的列表状态治理就实现了



import { Presenter } from '@clean-js/presenter';

// 三个固定参数
export interface IPagination {
  current: number;
  pageSize: number;
  total: number;
}
interface IViewState<Row, OtherParams> {
  loading: boolean;
  data: Row[]; // 申请每一页的数据
  allData: Row[]; // 缓存所有的数据
  params: OtherParams; // 额定申请参数
  pagination: IPagination;
}

const defaultState = () => ({
  loading: false,
  params: {} as Record<any, any>,
  pagination: { current:1, pageSize: 10, total: 0 },
  data: [],
  allData: [],
});

export class ListPresenter<
  Row = any,
  Params = Record<any, any>
> extends Presenter<IViewState<Row, Params>> {
  constructor(
  ) {
    super();
    this.state = defaultState();
  }

  loadingCount = 0

  showLoading() {
    this.loadingCount += 1
    if (this.loadingCount === 0) {
      this.setState(s => {
        s.loading = true;
      });
    }
  }

  hideLoading() {
    this.loadingCount -= 1
    if (this.loadingCount === 0) {
      this.setState(s => {
        s.loading = false;
      });
    }
  }

  fetchData(
    params: Partial<Params> & { current: number; pageSize: number },
  ): Promise<{ data: Row[]; current: number; pageSize: number; total: number }>{
    throw Error('请实现fetchTable');
  }


  /**
   * 加载更多
   * @returns
   */
  loadMore() {
    const params: Partial<Params> = {};
    Object.entries(this.state.params || {}).map(([k, v]) => {
      if (v !== undefined) {
        Object.assign(params, { [k]: v });
      }
    });
    this.showLoading();

    return this
      .fetchData({
        current: this.state.pagination.current + 1, 
        pageSize: this.state.pagination.pageSize,
        ...params,
      })
      .then(res => {
        this.setState(s => {
          s.pagination.current = res.current;
          s.pagination.pageSize = res.pageSize;
          s.pagination.total = res.total;
          s.data = res.data;
          s.allData = [...s.allData, ...res.data];
        });
        return res;
      })
      .finally(() => {
        this.hideLoading();
      });
  }

  hasMore() {
    const { current, pageSize, total } = this.state.pagination;
    return current * pageSize < total;
  }

  /**
   * 重置所有参数 刷新申请
   */
  refresh() {
    this.reset();
    return this.loadMore();
  }

  reset() {
    this.setState(defaultState());
  }

  /**
   * 重置data,allData,pagination
   */
  resetData() {
    this.setState(s => {
      s.data = [];
      s.allData = [];
      s.pagination = defaultState().pagination;
    });
  }

  /**
   * 更新每次申请的参数
   * @param params
   */
  updateParams(params: Partial<Params>) {
    const d: Partial<Params> = {};
    Object.entries(params).forEach(([k, v]) => {
      if (v !== undefined) {
        Object.assign(d, {
          [k]: v,
        });
      }
    });

    this.setState(s => {
      s.params = {
        ...s.params,
        ...d,
      };
    });
  }

  /**
   * 重置参数
   */
  resetParams() {
    this.setState(s => {
      s.params = {} as Record<any, any>;
    });
  }

  updatePagination(pagination: Partial<IPagination>) {
    this.setState(s => {
      s.pagination = {
        ...s.pagination,
        ...pagination,
      };
    });
  }
}

接着找一个罕用的组件库实现view层
在这里咱们实现一个最根底的下拉刷新,上拉加载的列表性能

import { PullToRefresh, InfiniteScroll, List } from 'antd-mobile'
const Name = () => {
  const { presenter, state } = usePresenter(ListPresenter);
  return (
     <PullToRefresh
      onRefresh={async () => {
         await presenter.refresh()
      }}
    >
     <List>
        {state.data.map((item, index) => (
          <List.Item key={index}>{item}</List.Item>
        ))}
      </List>
      <InfiniteScroll 
          loadMore={() => {
              presenter.loadMore()
          }} 
          hasMore={presenter.hasMore()}
       />
    </PullToRefresh>
  );
};


export default Name;

搜寻性能

接下来咱们增加一个搜寻性能

这里有个小优化,能够用防抖函数防止屡次申请


 search = debounce(this._search, 1000);

  _search(value: string) {
    this.updatePagination({current: 1})
    this.updateParams({
      searchText: value,
    });
    return this.updateData();
  }

至于其余工性能,比方筛选之类的就留给小伙伴们本人去实现啦

hook实现和class的比照

此外我还用hooks实现了一版

function useBaseList(
  fetchData: (
    params: Partial<ListState['params']> & {
      current: number;
      pageSize: number;
    },
  ) => Promise<{
    data: any[];
    current: number;
    pageSize: number;
    total: number;
  }>,
) {
  const [state, setState] = useState<ListState>({
    loading: false,
    data: [],
    params: {},
    pagination: {
      current: 1,
      pageSize: 10,
      total: 0,
    },
  });

  const showLoading = useCallback(() => {
    setState({
      ...state,
      loading: true,
    });
  }, [state]);

  const hideLoading = useCallback(() => {
    setState({
      ...state,
      loading: true,
    });
  }, [state]);

  const updateData = useCallback(() => {
    const params: Record<any, any> = {};
    Object.entries(state.params || {}).forEach(([k, v]) => {
      if (v !== undefined) {
        Object.assign(params, { [k]: v });
      }
    });
    showLoading();

    return fetchData({
      current: state.pagination.current || 1,
      pageSize: state.pagination.pageSize || 10,
      ...params,
    })
      .then((res) => {
        setState({
          ...state,
          pagination: {
            current: res.current,
            pageSize: res.pageSize,
            total: res.total,
          },
          data: res.data,
        });
        return res;
      })
      .finally(() => {
        hideLoading();
      });
  }, [fetchData, hideLoading, showLoading, state]);

  return {
    state,
    hideLoading,
    showLoading,
    updateData,
  };
}

function useNormalList(
  fetchData: (
    params: Partial<ListState['params']> & {
      current: number;
      pageSize: number;
    },
  ) => Promise<{
    data: any[];
    current: number;
    pageSize: number;
    total: number;
  }>,
) {
  const { state, hideLoading, showLoading, updateData } =
    useBaseList(fetchData);

  /**
   * 上拉加载
   * @returns
   */
  const loadMore = useCallback(() => {
    return updateData();
  }, [updateData]);

  return {
    state,
    hideLoading,
    showLoading,
    updateData,
    loadMore,
  };
}

大家能够发现,其实hook实现起来和用class一样也是用oop的形式来封装
只不过因为hooks函数的起因,你须要用到useCallback之类的api, 以及要特地留神useEffect依赖数组的依赖项,防止死循环

在hooks呈现之前,class components最大的问题就是没法很好的复用逻辑,不过通过clean-js咱们也能够实现class抽离出通用的逻辑达到复用的成果

比照一下hooks和clean-js的区别

  • 代码格调就看集体爱好了,clean-js偏差于传统的oop,更容易了解浏览;hooks能够用函数实现oop
  • hooks和react强绑定,无奈在vue或者其余框架应用,clean-js能够在vue中应用
  • 应用hooks的时候须要留神用useCallback,useMemo等api缓存,防止反复渲染;
  • 其余的就是clean-js还提供额定的性能,如dev-tool,IOC,代码生成等等

为什么还要弄一个clean-js

过后看完《架构整洁之道》就想在前端实现这样的架构,于是实现了上面这些性能,就有了这个库

  • 为了视图框架,状态解耦,实现依赖倒置,于是弄了Presenter,不依赖于框架,在react和vue中都能应用
  • Presenter 不依赖于具体的service, 于是退出了IOC性能,具体能够看这个例子,table能够注入任意的service。
  • 提供视图devtool(redux-devtool/log)便于debug
  • 申请代码生成器;依据YAPI,swagger2,swagger3等api协定主动生成申请代码

举荐架构

如下图所示,在前端利用中视图层(View)应该是最低的档次,也是最常变动的中央,它依赖于presenter(提供视图状态和办法),而Presenter依赖更外围的业务逻辑(service);

依赖倒置

依赖倒置准则(Dependency Inversion Principle,DIP)是软件工程中常见的一种设计准则

  • 高层模块不应该依赖于低层模块,两者都应该依赖于形象。
  • 形象不应该依赖于细节,细节应该依赖于形象。

在上图中,view依赖Presenter,如果要做齐全的依赖倒置咱们能够申明一个接口,view和presenter别离依赖这个接口来实现view和presenter的解耦,上面给个例子

申明ListPresenter接口

interface ListPresenter {
  state: ListState;
  onPageChange?(p: Pagination): void;
}

而咱们的BaseListPresenter实现这个接口

class BaseListPresenter implements ListPresenter {}

在view依赖的是ListPresenter接口

const Index = () => {
  const { presenter, state } = usePresenter<ListPresenter>(BaseListPresenter);
  return (
    <div>
      name: {state.name}
      <button onClick={presenter.changeName}>change name</button>
    </div>
  );
};

这样就能够让view和Presenter齐全解耦了

不过一般来说没有这个必要,Presenter这个类自身隐含着接口定义,只有接口定义不改就能够了,哪怕当前要切换到别的视图框架,只须要批改view的代码,依赖Presenter即可

再举个例子,Presenter通常的数据源是HTTP service,如果有一天咱们须要从缓存中获取,或者jsbridge获取,这时候就须要批改原来的HTTP service了,如果做了依赖倒置,就能够切换具体的service实现,而无需去批改Presenter

IOC

IOC(管制翻转)是一种设计模式,目标为了更好的解耦,实现依赖倒置,而DI(依赖注入)能够了解为IOC的一种实现形式

比方我有这个服务类NameService,须要在NamePresenter中应用,则须要在NamePresenter实例化
NameService,这样两个类就耦合在一起了,最直观的例子就是在咱们写单元测试的时候很难去mock NameService这个服务

export class NameService {
  getName() {
    // 假如从http申请获取名称
    return Promise.resolve('name')
  }
}

class NamePresenter {
    constructor() {
        this.nameService = new NameService()
    }
}

如果用IOC实现的话,就不须要在NamePresenter中实例化NameService了

export class NameService {
  getName() {
    // 假如从http申请获取名称
    return Promise.resolve('name')
  }
}

class NamePresenter {
    constructor(@inject('service') public nameService: NameService) {}
}

这样咱们写单元测试的时候,就能够随便切换NameService,比方上面的代码MockService是咱们用来mock NameService的服务

it('test', () => {
    container.register('service', { useClass: MockService });
    const presenter = container.resolve(NamePresenter);
    
    // presenter的nameService就会别切换为MockService
     
});

在clean-js中也提供了IOC的性能,更加具体的例子能够看这里,这个TablePresenter和咱们后面封装的ListPresenter一样,不过在组件运行的过程中咱们能够注入具体要用的服务类,来达到在不同页面都应用同一个Presenter的成果


const Page = () => {
  const { presenter } = usePresenter<TablePresenter<Row, Params>>(
    TablePresenter,
    {
      registry: [{ token: TableServiceToken, useClass: MyService }],
    },
  );
  return (
    <div>
      <h1>table state</h1>
      <p>{JSON.stringify(presenter.state, null, 4)}</p>


      <button
        onClick={() => {
          presenter.getTable();
        }}
      >
        fetch table
      </button>
    </div>
  );
};

具体源码能够看这里
感觉不错的小伙伴记得给个star⭐️,谢谢反对,

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理