欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

综合分析大神三元网易云音乐React+钩子

最编程 2024-04-21 07:06:23
...

吧啦吧啦

     自从弄懂了神三元大神的作品《云音悦》,整个人神清气爽,飘飘欲仙~,本着学习应该共同进步的精神,我将三元项目的一个页面流程进行了一个细致的讲解。是的,你没有听错,是流程。我相信有很多人拿到一个开源项目希望能够弄懂,但是一个项目肯定是很大的,不知从何下手,每个组件与每个组件之间有着千丝万缕的联系,于是,想想还是算了吧。

     在这里我将从零开始搭建这个项目,一步一步的将项目的真面目露出来,需要说明的是,我在这里仅仅只是对三元项目的代码进行了 cv大战(粘贴复制),当然cv不是说每个文件一次性全部复制,而是有选择性的将流程能够走通。

     如下图就是我们要完成的一个页面,其他的页面也是类似,完成这个页面其实能说明很多问题了,这个页面有从后端请求数据,有redux管理数据,有轮播图。。。 不多说了,进入正题吧!

技术栈

  • react
  • styled.components 使得css在js文件可以书写
  • hooks react新特性
  • redux 状态管理
  • immutable 优化性能
  • better-scroll 一个比较好的滚动下拉组件
  • react-router-config 将路由全部统一管理
注意: 这些知识点你都要掌握(至少要了解他们是什么),不然食起来可能会有点痛苦哦~
## 项目目录结构
目录

src

  • api 前后端数据接口 以及一些配置文件
  • application 页面组件以及每个组件的store
  • assets 静态资源目录,主要放iconfonts以及全局样式
  • baseUI 功能性组件 如 加载组件 滚动条等
  • components 公共组件 如轮播图
  • layouts 涉及到路由,组件嵌套
  • routes 路由,使用react-router-config
  • store 全局 redux 仓库

开始工作

1. 配置路由

src下创建 routes 文件夹 建index.js文件

index.js

    import React, { lazy, Suspense } from "react";
    import { Redirect } from "react-router-dom";
    import HomeLayout from "../layouts/HomeLayout";
    import BlankLayout from "../layouts/BlankLayout";

// 路由懒加载
const SuspenseComponent = Component => props => {
  return (
    <Suspense fallback={null}>
      <Component {...props}></Component>
    </Suspense>
  )
}
// 组件懒加载
const RecommendComponent = lazy(() => import("../application/Recommend/"));

export default [
  {
    component: BlankLayout,
    routes: [
      {
        path: "/",
        component: HomeLayout,
        routes: [
          {
            path: "/",
            exact: true,
            render: () => <Redirect to={"/recommend"} />,
          },
          {
            path: "/recommend",
            component:  SuspenseComponent(RecommendComponent),
          },
        ],
      },
    ],
  },
];


这种配置路由的方式与之前普通的区别可能有点大,不了解的建议充个电(react-router-config)
在这里BlankLayout是全局组件,在它下面放置所有路由级别组件,HomeLayout下所有路由级别组件也都归HomeLayout管理,这里我们看到的只有recommend组件,这也就是逐级嵌套。所有的路由都放在这里统一管理,之前普通路由是哪个组件需要路由才到哪个组件下面写,管理不方便。

2. 创建layouts文件夹

建 BlankLayout.js 以及 HomeLayout.js
   BlankLayout.js下是全局组件,HomeLayout下放的是recommend singers,rank 三大组件,由于整个项目过多,这里我们只介绍recommend页面,当然,如果recommend页面会写了,那么其他页面其实都是类似的操作

BlankLayout.js

  import React from "react";
  import { renderRoutes } from "react-router-config";
  // 从我们刚刚配置的路由可知 这个组件是最外层组件 所有路由级别组件都在这
  const Layout = ({ route }) => <>{renderRoutes(route.routes)};
  export default Layout

HomeLayout.js

```js
  import React from "react";
  import { renderRoutes } from "react-router-config";

  function Home(props) {
    const { route } = props;
    return (
      

        {/* 将HomeLayout的子组件都放到这里 */}
        {renderRoutes(route.routes)}
      

    )
  }
  export default React.memo(Home);
  ```
可以看到这样用到了 renderRoutes,根据 routes 里面的内容,HomeLayout 是 BlankLayout 子路由,并且在 BlankLayout 下面有子路由 recommend, 所以将renderRoutes放在 HomeLayout 下面。(route)

3. 创建application目录

创建Recommend子目录,Recomend下创建index.js

index.js

现在还不能运行,因为在app.js中没有导入路由(由于div会被编辑器吃掉????,某些地方只能上图片)

4. 来到App.js

App.js

    ```js
    import React from "react";
    import routes from "./routes/index.js";
    import { renderRoutes } from "react-router-config";
    import { HashRouter } from "react-router-dom";

function App() {
  return (
      <HashRouter>
        {renderRoutes(routes)}
      </HashRouter>
  );
}

export default App;
```	


我们将 routes里面的文件导入到了renderRoutes,renderRoutes就可以根据routes里面的组件在其他组件下放入其他组件了(就问你绕不绕,此其他非彼其他????)。这个理解了其实用起来感觉会很方便。 到这我们安装两个包

yarn add react-router-config
yarn add react-router-dom

这个时候项目就可以运行起来了 运行结果如下

5. 配置 global-style.js 文件 以及全局样式

src下创建 assets目录,创建该文件,该文件是通用样式文件(不要与全局样式混淆),比如,该app的主题颜色(你懂的颜色)在许多地方都会用到

global-style.js

    ```js
    const extendClick = () => {
      return `
        position: relative;
        &:before{
          content: '';
          position: absolute;
          top: -10px; bottom: -10px; left: -10px; right: -10px;
        };
      `;
    };

const noWrap = () => {
  return `
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  `;
};

const bgFull = () => {
  return `
    background-position: 50%;
    background-size: contain;
    background-repeat: no-repeat;
  `
};

export default {
  "theme-color": "#d44439", //今天有绿吗
  "theme-color-shadow": "rgba(212, 68, 57, .5)",
  "font-color-light": "#f1f1f1",
  "font-color-light-shadow": "rgba(241, 241, 241, 0.6)",//略淡
  "font-color-desc": "#2E3030",
  "font-color-desc-v2": "#bba8a8", //略淡
  "font-size-ss": "10px",
  "font-size-s": "12px",
  "font-size-m": "14px",
  "font-size-l": "16px",
  "font-size-ll": "18px",
  "border-color": "#e4e4e4",
  "border-color-v2": "rgba(228, 228, 228, 0.1)",
  "background-color": "#f2f3f4",
  "background-color-shadow": "rgba(0, 0, 0, 0.3)",
  "highlight-background-color": "#fff",
  "official-red": "#E82001",
  extendClick,
  noWrap,
  bgFull
};
```	


全局样式,这个样式不仅仅在这里可以用到,其他的项目同样可以引入
style.js

    ```js
    import { createGlobalStyle } from 'styled-components'

export const GlobalStyle = createGlobalStyle`
    html, body, div, span, applet, object, iframe,
    h1, h2, h3, h4, h5, h6, p, blockquote, pre,
    a, abbr, acronym, address, big, cite, code,
    del, dfn, em, img, ins, kbd, q, s, samp,
    small, strike, strong, sub, sup, tt, var,
    b, u, i, center,
    dl, dt, dd, ol, ul, li,
    fieldset, form, label, legend,
    table, caption, tbody, tfoot, thead, tr, th, td,
    article, aside, canvas, details, embed, 
    figure, figcaption, footer, header, hgroup, 
    menu, nav, output, ruby, section, summary,
    time, mark, audio, video {
        margin: 0;
        padding: 0;
        border: 0;
        font-size: 100%;
        font: inherit;
        vertical-align: baseline;
    }
    /* HTML5 display-role reset for older browsers */
    article, aside, details, figcaption, figure, 
    footer, header, hgroup, menu, nav, section {
        display: block;
    }
    body {
        line-height: 1;
    }
    html, body{
        background: #f2f3f4;;
    }
    ol, ul {
        list-style: none;
    }
    blockquote, q {
        quotes: none;
    }
    blockquote:before, blockquote:after,
    q:before, q:after {
        content: '';
        content: none;
    }
    table {
        border-collapse: collapse;
        border-spacing: 0;
    }
    a{
        text-decoration: none;
        color: #fff;
    }
`
```


6. 开始写头部

这里我们将用到 比较新的知识styled.components以及需要iconfont , 来到HomeLayout下,

HomeLayout.js

这里我们导入了HomeLayout.style.js(styled.componets)以及(iconfont),未加iconfont类似于‘口’,因此我们需要创建它们。styled.components就类似于在js文件里面写样式,并且该容器的class会随机生成。 从如下代码我们可以看到Top就是一个容器,它的子容器的样式也可以在这里写,然后导入上面的HomeLayout.js.

7. 在同级目录下创建HomeLayout.style.js

HomeLayout.style.js

     ```js
    import styled from "styled-components";
    import style from "../assets/global-style";

export const Top = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 5px 10px;
  background: ${style["theme-color"]};
  & > span {
    line-height: 40px;
    color: #f1f1f1;
    font-size: 20px;
    &.iconfont {
      font-size: 25px;
    }
  }
`
```	


将这段代码引入之后就可以看到效果了 此时运行结果如下 因为还没有配置iconfont,所以是如上效果

8. 配置iconfont

可以到icontfont官网下载所需要的文件,放在assets目录下,目录如下 在全局App.js引入icontfont文件,加上这几行代码

import { IconStyle } from "./assets/iconfont/iconfont";
<HashRouter>
   <IconStyle></IconStyle>
   {renderRoutes(routes)}
</HashRouter>

运行效果如下,图标出来了

9. 接着在 HomeLayout.js 写布局

在原来的的基础上加上如下代码下面写

HomeLayout.js

这里我们用NavLink指向了三个路由地址,我们目前只做recommend

上面引入了 ,在HomeLayout.style.js写

HomeLayout.style.js

    ```js
    export const Tab = styled.div`
      height: 44px;
      display: flex;
      flex-direction: row;
      justify-content: space-around;
      background: ${style["theme-color"]};
      a {
        flex: 1;
        padding: 2px 0;
        font-size: 14px;
        color: #e4e4e4;
        &.selected {
          span {
            padding: 3px 0;
            font-weight: 700;
            color: #f1f1f1;
            border-bottom: 2px solid #f1f1f1;
          }
        }
      }
    `;
    export const TabItem = styled.div`
      height: 100%;
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;
    `;
    ```

页面结果如下

10. 来到了难点重点部分。

前面的都是一些基础的不需要从后端获取数据就可以完成的,但是接下来的内容轮播图,以及列表都是需要从后端接口请求数据渲染到页面。 数据拿到了也需要人来管理,那么谁呢,就是你又爱又恨的redux
接下来首先得准备好接口数据 src下创建api文件夹,在该文件夹下配置config.js文件, 该文件一般是用来配置默认后端请求接口路径

config.js

    ```js
    import axios from "axios";

export const baseUrl = "http://neteasecloudmusicapi.zhaoboy.com";

// 创建一个axios的实例及拦截器配置
const axiosInstance = axios.create({
  baseURL: baseUrl
});
// 响应拦截器,在获取响应之前处理数据
axiosInstance.interceptors.response.use(
  res => res.data,
  err => {
    console.log(err, "网络错误");
  }
);

export { axiosInstance };
```	


11. 同路径api下创建request.js

该文件用来封装 请求接口,通过调用该函数可以请求数据

import { axiosInstance } from "./config";

export const getBannerRequest = () => {
  return axiosInstance.get("/banner");
};

12. 创建redux 仓库

需要说明的是,神三元项目中每个对应的路由组件都有一个store,然后将所有的store(该store中没有store文件,一个项目只应该有一个仓库)全部引入到最外面的store,这样写的理由是逻辑清晰。如果不这样写很多action放到一起,很多reducer放到一起,文件数量可能会较大,头都大了。
那么用redux怎么管理数据呢?简单说就是dispatch(action)向store请求数据,store又找reducer,reducer对请求的数据进行一定的操作,存到store,在组件中connect一下,再通过props获取数据 我们在Recommend下创建store文件夹,分别创建
constants.js actionCreators.js reducer.js index.js

12.1 constants.js

该文件只有一行代码,作用是区分不同的actions

export const CHANGE_BANNER = 'home/recommend/CHANGE_BANNER';

12.2 actionCreators.js

该文件中有 changeBannerList 用来改变状态,也就是传进来什么数据就修改成什么数据,但是为了支持异步,我们需要通过下面一个getBannerList来获取数据

actionCreators.js

     ```js
    import * as actionTypes from './constants';
    import { fromJS } from 'immutable';
    import { getBannerRequest } from '../../../api/request';

export const changeBannerList = (data) => ({
  type: actionTypes.CHANGE_BANNER,
  data: fromJS(data)
});
// 获取 轮播图数据
export const getBannerList = () => {
  return (dispatch) => {
    getBannerRequest().then(data => {
      const action = changeBannerList(data.banners);
      dispatch(action);
    }).catch(() => {
      console.log("轮播图数据传输错误");
    }) 
  }
};
```	


注意,我们在这里使用了immutable,以后的文件会大量用到immutable,简单介绍下作用: 使用旧数据创建新数据时可以保证新数据不变,并且创建新数据的过程中使用了结构共享,如下图所示 在上面的代码中我们导入getBannerRequest的请求,getBannerList 返回的是一个方法,我们需要在store导入redux-thunk包,这是后面写到的。

12.3 reducer.js

reducer.js 首先初始化bannerList的数据,然后switch一顿狂操作

reducer.js

    ```js
    import * as actionTypes from './constants';
    import { fromJS } from 'immutable';

const defaultState = fromJS({
  bannerList: [],
})
export default (state = defaultState, action) => {
  switch(action.type) {
    case actionTypes.CHANGE_BANNER:
      return state.set('bannerList', action.data);
    default:
      return state;
  }
}
```	


12.4 index.js

import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants'

export { reducer, actionCreators, constants };

将三个文件整合,并导出 以上是Recommend 里面的store,(再次说明: 里面的store是没有仓库的,统一放到外面的仓库管理),我们需要创建 外面最大的store

13. 在src 创建 store文件夹(最外面的仓库)

store 下创建 reducer.js 集合所有reducer, index.js 仓库

reducer.js

import { combineReducers } from "redux-immutable";
import { reducer as recommendReducer } from "../application/Recommend/store/index";

export default combineReducers({
  recommend: recommendReducer,
});

index.js, 这里我们安装了 redux-thunk,支持action返回方法

import { createStore, compose, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
  1. 在App.js 引入store,使之成为全局仓库 现在App.js文件长这样
代码

    ```js
    import React from "react";
    import routes from "./routes/index.js";
    import { renderRoutes } from "react-router-config";
    import { HashRouter } from "react-router-dom";
    import { IconStyle } from "./assets/iconfont/iconfont";
    import { Provider } from "react-redux";
    import store from "./store/index";

function App() {
  return (
    <Provider store={store}>
      <HashRouter>
        <IconStyle></IconStyle>
        {renderRoutes(routes)}
      </HashRouter>
    </Provider>
  );
}

export default App;
```	


需要导入一个Provider,使得全局都可以使用store里面的数据 到这里我们终于把Recommend的数据管理给搞定了,下载必要的包 yarn add axios redux react-reudx immutable redux-immutable redux-thunk (可能不全) 接下里就是如何将数据渲染到页面了。

15. 来到HomeLayout的子组件Recommend下的 index.js 中

首先我们先通过connect拿到数据

index.js

connect 之后怎么拿数据呢? 通过props可以获取数据,接下来我们来写如何通过props获取数据

index.js

```js
    function Recommend(props) {
      const { bannerList} = props;
      const { getBannerDataDispatch } = props;
      useEffect(() => {
        if(!bannerList.size){
          getBannerDataDispatch();
          console.log(bannerList);
        }
      }, []);
      return (
        

          Recommend
        

      )
    }
    ```

只要connect之后,我们就可以通过props拿到数据了,props之后解构刚刚上面的bannerList数据和getBannerDataDispatch方法,调用该方法之后就可以获取数据,所以我们把它放到useEffect,中,判断bannerList是否为空,不用每次执行都请求一次数据。打印之后我们可以看到如下结果 数据拿到了,现在要做轮播图,因为轮播图可能多个地方都会用到,所以我们将它放到components目录下

16. 创建slider

在src下创建components文件夹,接着创建slider文件夹,在里面创建 index.js和style.js

style.js, 这是给swiper加的样式,引入了Global-style里面的主题颜色

style.js

    ```js
    import styled from 'styled-components';
    import style from '../../assets/global-style';

export const SliderContainer = styled.div`
  position: relative;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  margin: auto;
  background: white;
  .before{
    position: absolute;
    top: -300px;
    height: 400px;
    width: 100%;
    background: ${style["theme-color"]};
    z-index: 1;
  }
  .slider-container{
    position: relative;
    width: 98%;
    height: 160px;
    overflow: hidden;
    margin: auto;
    border-radius: 6px;
    .slider-nav{
      position: absolute;
      display: block;
      width: 100%;
      height: 100%;
    }
    .swiper-pagination-bullet-active{
      background: ${style["theme-color"]};
    }
  }
`
```


index.js
代码

    ```js
    import React, { useEffect, useState } from 'react';
    import "swiper/swiper-bundle.min.css"; //这里引用的方式已经发生了变化
    import Swiper from "swiper";
    import { SliderContainer } from './style';
    function Slider(props) {
      const [sliderSwiper, setSliderSwiper] = useState(null);
      const { bannerList } = props;
      useEffect(() => {
        if(bannerList.length && !sliderSwiper){
          let sliderSwiper = new Swiper(".slider-container", {
            loop: true,
            autoplay: {
              delay: 3000,
              disableOnInteraction: false,
            },
            pagination: {el:'.swiper-pagination'},
          });
          setSliderSwiper(sliderSwiper);
        }

  }, [bannerList.length, sliderSwiper])
  return (
    <SliderContainer>
       <div className="before"></div>
       <div className="slider-container">
       <div className="swiper-wrapper">
         {
           bannerList.map(slider => {
             return (
              <div className="swiper-slide" key={slider.imageUrl}>
                 <div className="slider-nav">
                 <img src={slider.imageUrl} width="100%" height="100%" alt="推荐" />
                 </div>
                </div>
             )
           })
         }
       </div>
       <div className="swiper-pagination"></div>
       </div>
    </SliderContainer>
  )
}
export default React.memo(Slider);
```


这里主要是使用了swiper api, 具体用法可以看swiper官网有详细介绍

17. 导入slider

swiper 写完了之后,来到,recommend 下的index.js, 首先为index.js写个style.js

style.js

    ```js
    import styled from 'styled-components';

export const Content = styled.div`
    position: fixed;
    top: 94px;
    left: 0;
    bottom: ${props => props.play > 0?"60px": 0};
    width: 100%;
`
```


在index.js中导入Slider

import { Content } from "./style";
import Slider from "../../components/slider/";
 const bannerListJS = bannerList ? bannerList.toJS() : [];
  return (
    <Content >
      <div><Slider bannerList={bannerListJS}></Slider></div>
    </Content>
  );
}

在 index.js加上上面这几句代码之后就可以看到效果了,但是你会发现header被覆盖了,这里其实是 Slider组件里面的before搞的鬼, 如果要改过来也很简单,在它的父容器加个overflow:hidden就可以解决问题,但是现在因为我们要做滚动(需要上下可以滚动),滚动会做slider的父容器,也会有overflow:hidden,所以这里就先不处理。 但是我们发现这个轮播图是不能滚动的,因为我们现在用的swiper 是swiper6最新版本,具体的可以看官网如何使用

18. loading组件

讲解scroll组件又会牵扯到 loading 组件(加载)和 debounce函数(防抖),所以得先建loading组件和防抖函数 baseUI文件下分别创建loading和loading-v2文件夹

loading中的文件

index.js

    ```js
    import React from 'react';
    import styled, { keyframes } from 'styled-components';
    import style from '../../assets/global-style';

const loading = keyframes`
  0%, 100% {
    transform: scale(0.0);
  }
  50% {
    transform: scale(1.0);
  }
`
const LoadingWrapper = styled.div`
    >div {
      position: absolute;
      top: 0; left: 0; right: 0; bottom: 0;
      margin: auto;
      width: 60px;
      height: 60px;
      opacity: 0.6;
      border-radius: 50%;
      background-color: ${style["theme-color"]};
      animation: ${loading} 1.4s infinite ease-in;
    }
    >div:nth-child(2) {
      animation-delay: -0.7s;
    }
`
function Loading()  {
  return (
    <LoadingWrapper>
      <div></div>
      <div></div>
    </LoadingWrapper>
  );
}

export default React.memo(Loading);
```


loading-v2中的文件
index.js

    ```js
    import React from 'react';
    import styled, {keyframes} from 'styled-components';
    import style from '../../assets/global-style'

const dance = keyframes`
    0%, 40%, 100%{
      transform: scaleY(0.4);
      transform-origin: center 100%;
    }
    20%{
      transform: scaleY(1);
    }
`
const Loading = styled.div`
    height: 10px;
    width: 100%;
    margin: auto;
    text-align: center;
    font-size: 10px;
    >div{
      display: inline-block;
      background-color: ${style["theme-color"]};
      height: 100%;
      width: 1px;
      margin-right:2px;
      animation: ${dance} 1s infinite;
    }
    >div:nth-child(2) {
      animation-delay: -0.4s;
    }
    >div:nth-child(3) {
      animation-delay: -0.6s;
    }
    >div:nth-child(4) {
      animation-delay: -0.5s;
    }
    >div:nth-child(5) {
      animation-delay: -0.2s;
    } 
`
function LoadingV2() {
  return (
    <Loading>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <span>拼命加载中...</span>
    </Loading>
  );
}

export default React.memo(LoadingV2);
```


这两个文件都是用animation做的加载动画,具体就不介绍了,篇幅过长~

19. debounce 函数

防抖函数建在 api 的 utils.js 中 什么是防抖

***举个栗子:***有个scroll事件,滚动会不停触发回滚动事件,从而调用绑定的回调函数,我们希望当我们停止滚动的时,才触发一次回调,这时可以使用函数防抖。

代码

```js
    const debounce = (func, delay) => {
      let timer;
      return function (...args) {
        if(timer) {
          clearTimeout(timer);
        }
        timer = setTimeout(() => {
          func.apply(this, args);
          clearTimeout(timer);
        }, delay);
      };
    };
    export { debounce }
```
这是三元写的原生js防抖函数,防抖原理就是第一次触发某个函数,在规定时间内如果再次触发就会重新计时。

20. scroll

好了,预备工作完成了,就可以写了,这里再说明一下,我现在是尽可能的帮助大家梳理如何能够看懂一个比较好的项目,实现可以自己动手实践

index.js

    ```js
    import React, {
      forwardRef,
      useState,
      useEffect,
      useRef,
      useImperativeHandle,
      useMemo,
    } from "react";
    import PropTypes from "prop-types";
    import BScroll from "better-scroll";
    import styled from "styled-components";
    import { debounce } from "../../api/utils";
    import Loading from '../loading/index';
    import Loading2 from '../loading-v2/index';

const ScrollContainer = styled.div`
  width: 100%;
  height: 100%;
  overflow: hidden;
`;

const PullUpLoading = styled.div`
  position: absolute;
  left: 0;
  right: 0;
  bottom: 5px;
  width: 60px;
  height: 60px;
  margin: auto;
  z-index: 100;
`;

export const PullDownLoading = styled.div`
  position: absolute;
  left: 0;
  right: 0;
  top: 0px;
  height: 30px;
  margin: auto;
  z-index: 100;
`;
const Scroll = forwardRef((props, ref) => {
  const [bScroll, setBScroll] = useState();
  const scrollContaninerRef = useRef();
  const {
    direction,
    click,
    refresh,
    pullUpLoading,
    pullDownLoading,
    bounceTop,
    bounceBottom,
  } = props;
  const { pullUp, pullDown, onScroll } = props;
  let pullUpDebounce = useMemo(() => {
    return debounce(pullUp, 500);
  }, [pullUp]);

  let pullDownDebounce = useMemo(() => {
    return debounce(pullDown, 500);
  }, [pullDown]);

  useEffect(() => {
    const scroll = new BScroll(scrollContaninerRef.current, {
      scrollX: direction === "horizental",
      scrollY: direction === "vertical",
      probeType: 3,
      click: click,
      bounce: {
        top: bounceTop,
        bottom: bounceBottom,
      },
    });
    setBScroll(scroll);
    return () => {
      setBScroll(null);
    };
    // eslint-disable-next-line
  }, []);

  useEffect(() => {
    if (!bScroll || !onScroll) return;
    bScroll.on("scroll", onScroll);
    return () => {
      bScroll.off("scroll", onScroll);
    };
  }, [onScroll, bScroll]);

  useEffect(() => {
    if (!bScroll || !pullUp) return;
    const handlePullUp = () => {
      //判断是否滑动到了底部
      if (bScroll.y <= bScroll.maxScrollY + 100) {
        pullUpDebounce();
      }
    };
    bScroll.on("scrollEnd", handlePullUp);
    return () => {
      bScroll.off("scrollEnd", handlePullUp);
    };
  }, [pullUp, pullUpDebounce, bScroll]);

  useEffect(() => {
    if (!bScroll || !pullDown) return;
    const handlePullDown = (pos) => {
      //判断用户的下拉动作
      if (pos.y > 50) {
        pullDownDebounce();
      }
    };
    bScroll.on("touchEnd", handlePullDown);
    return () => {
      bScroll.off("touchEnd", handlePullDown);
    };
  }, [pullDown, pullDownDebounce, bScroll]);

  useEffect(() => {
    if (refresh && bScroll) {
      bScroll.refresh();
    }
  });

  useImperativeHandle(ref, () => ({
    refresh() {
      if (bScroll) {
        bScroll.refresh();
        bScroll.scrollTo(0, 0);
      }
    },
    getBScroll() {
      if (bScroll) {
        return bScroll;
      }
    },
  }));
  const PullUpdisplayStyle = pullUpLoading
    ? { display: "" }
    : { display: "none" };
  const PullDowndisplayStyle = pullDownLoading
    ? { display: "" }
    : { display: "none" }<