React云音悦WebApp

React云音悦WebApp,第1张

该项目是我跟着神三元(抖音架构组)做的一款网易云音乐的 WebApp,原电子书链接主要技术栈:react hooks + redux + immutable.js后端部分:采用 github 上开源的 NodeJS 版 api 接口 NeteaseCloudMusicApi,提供音乐数据。本项目使用新版本的依赖进行构建(如:react-router v6),新版本依赖的使用可以参考本文,具体知识点的学习可以查看小册链接。参考我的github仓库代码:CloudMusic

目前项目完成了首页展示(其他页面类似)。

一、全局样式与路由配置 1.项目搭建 过程查看文档:creat-react-app 2. 全局样式

(1)下载依赖

这个项目利用的是 css in js,所以我们先安装:styled-components
yarn add styled-components
它的作用就是让我们能够使用 js 来书写 css 样式。如果是用 vsCode 写这个项目的话,可以搜索 vscode-styled-components 这个插件来辅助我们书写代码。

(2)全局样式

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;
	}
`
这段代码是用来定义全局样式的。所有 styled-components 的代码都需要通过引入 styled-components 插件后,通过 export const GlobalStyle = createGlobalStyle 的方式使用。在这里面我们可以书写 CSS 代码,代码的格式类似于 Less

(3)图标文件

srcassets 目录下,创建一个 iconfont 文件夹,里面放我们我们的图标文件。可以通过我的项目源码获取到。把 iconfont.css 文件改成 iconfont.js 文件在 iconfont.js 里面引入 styled-components保留 @font-face.iconfont 的内容,其他的删掉。

(4)导入

function App() {
  return (
    <div className="App">
      <GlobalStyle></GlobalStyle>
      <IconStyle></IconStyle>
      <div>组件</div>
    </div>
  );
}

export default App;
3. 路由配置

(1)下载依赖

react-routerreact-router-dom
yarn add react-router react-router-dom
我们利用 react-router-dom 来写路由。这里使用的是的react-router-dom v6,不需要下载react-router-config新版本的react-router-dom与老版本还是有比较多的区别的,可以查看官网进行了解。

(2)书写路由文件

第一个路由指定 '/' 是进入的主界面,同时二级路由显示的是 Recommend 组件也就是推荐组件的内容。exact 是精确匹配的意思,当通过 '/' 进入主界面的时候,会进行重定向 *** 作。
import React from "react";
import { useRoutes, Navigate } from 'react-router-dom';
import Home from '../application/Home';
import Recommend from '../application/Recommend';
import Singers from '../application/Singers';
import Rank from '../application/Rank';


function Routes() {
  const routes = useRoutes([
    { 
      path: '/',
      element: <Home />,
      children: [
        { path: '/', exact: true, element: <Navigate to='/recommend'/> },
        { path: '/recommend', element: <Recommend /> },
        { path: '/rank', element: <Rank /> },
        { path: '/singers', element: <Singers /> }
      ]
    },
  ])
  return routes
}

export default Routes;

(3)导入路由文件

import { GlobalStyle } from './style';
import { IconStyle } from './assets/iconfont/iconfont';
import { HashRouter } from 'react-router-dom';
import Routes from './routes';

function App() {
  return (
    <HashRouter>
      <GlobalStyle></GlobalStyle>
      <IconStyle></IconStyle>
      <Routes/>
    </HashRouter>
  );
}

export default App;

(4)编写组件

application 文件夹下新建 Home 文件夹,然后新建 index.js 文件
import React from 'react';
import { Outlet } from 'react-router';

function Home() {
  return (
      <div>Home</div>
  )
}

export default React.memo(Home);
React.memo 是用来控制组件渲染的,类似于 PureComponent类似的编写 RecommendSingersRank 组件。现在启动项目,可以看到Home,但是还需要看到recommend。也就是说,现在只渲染了一级路由。Home组件需要修改一下。
import React from 'react';
import { Outlet } from 'react-router';

function Home() {
  return (
      <div>
        Home
        <Outlet /> // 用于渲染二级路由
      </div>
  )
}

export default React.memo(Home);

到这里,我们就可以看到渲染出来的内容了,项目也就基本搭建好了。

二、头部和Tab栏 1. 全局样式

(1)下载依赖

使用 flexible.js
yarn add lib-flexible
srcApp.js 中导入 flexible
import 'lib-flexible'
flexible.js 来做适配,因为 flexible.js 是把设备划分为 `10份

(2)书写全局样式

assets 目录下新建 global-style.js 文件
// 这里定义的全局的通用属性

// 扩大可点击区域
const extendClick = () => {
    return`
        position: relative;
        &:before {
            content: '';
            position: absolute;
            top: -.266667rem;
            bottom: -.266667rem;
            left: -.266667rem;
            right: -.266667rem;
        }
    `
}

// 一行文字溢出部分用 …… 代替
const noWrap = () => {
    return`
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
    `
}

export default {
    'theme-color': '#d44439',  // 主题颜色——网易云红
    'theme-color-shadow': 'rgba(212, 68, 57, .5)',  // 主题颜色——暗色
    'font-color-light': '#f1f1f1',  // 字体颜色——高亮灰白色
    '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',  // 边框颜色——白灰色
    'background-color': '#f2f3f4',  // 背景颜色——银灰色
    'background-color-shadow': 'rgba(0, 0, 0, 0.3)',  // 背景颜色——深灰黑色
    'hightlight-background-color': '#fff',  // 背景颜色——白色
    extendClick,
    noWrap
}
这里使用rem布局,可以使用 cssrem 这个插件来帮助我们进行计算,只要记得把 Cssrem: Root Font Size 的大小设置为 75 就好了。具体的原因请自行百度。值得注意的是我们这里的字体大小并没有使用 rem,依然使用的是 px。但是我们使用的图标需要使用 rem,这里可能不会被注意到.因为我们不希望字体随屏幕变化,但是图标我们希望它是自适应的。 2. 顶部栏开发

(1)书写顶部栏样式

使用 styled-components 来做组件

Home文件夹下新建style.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: 1.066667rem;
    color: #f1f1f1;
    font-size: 20px;
    &.iconfont {
      font-size: .666667rem;
    }
  }
`

styled-componentsless 一样,允许我们使用变量。

我们导入了全局样式 global-style,并使用里面预先设定好的颜色:theme-color

这里是用 js 语法写的。所以变量的使用方法是:${style["theme-color"]},这里是对象解构的写法。

使用样式组件

import React from 'react';
import { Outlet } from 'react-router';
import { Top, Tab, TabItem } from './style';
import { NavLink } from 'react-router-dom';

function Home() {
  return (
      <div>
        <Top>
          <span className="iconfont menu">&#xe65c;</span>
          <span className="title">WebApp</span>
          <span className="iconfont search">&#xe62b;</span>
        </Top>
        <Outlet />
      </div>
  )
}

export default React.memo(Home);

现在就启动服务,就可以看见效果了。

3. Tab栏开发

(1)书写Tab栏样式

export const Tab = styled.div`
  height: 1.066667rem;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  background-color: ${style['theme-color']};
`;

export const TabItem = styled.div`
  height: 100%;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
`;

现在的 Tag 组件

<Tab>
    <TabItem><span>推荐span>TabItem>
    <TabItem><span>歌手span>TabItem>
    <TabItem><span>排行榜span>TabItem>
Tab>

(2)引入路由

import { NavLink } from 'react-router-dom';
修改tab
<Tab>
  <NavLink to="/recommend" activeClassName="selected"><TabItem><span>推荐span>TabItem>NavLink>
  <NavLink to="/singers" activeClassName="selected"><TabItem><span>歌手span>TabItem>NavLink>
  <NavLink to="/rank" activeClassName="selected"><TabItem><span>排行榜span>TabItem>NavLink>
Tab>
使用 组件,会变成 a 标签,修改原来的Tab的样式:
export const Tab = styled.div`
  height: 1.066667rem;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  background-color: ${style['theme-color']};
  a {
    flex: 1;
    padding: .053333rem 0;
    font-size: .373333rem;
    color: #e4e4e4;
    &.selected {
      span {
        padding: .08rem 0;
        font-weight: 700;
        color: #f1f1f1;
        border-bottom: .053333rem solid #f1f1f1;
      }
    }
  }
`;
我们给三个 都写了 activeClassName="selected",这样当我们选中哪一个路由时,哪一个路由就有了 selected 的样式。

启动项目,查看效果:

三、引入Redux

Redux 中文官网 - JavaScript 应用的状态容器,提供可预测的状态管理。 | Redux 中文官网

1. 安装对应的依赖
redux redux-thunk redux-immutable react-redux immutable

reudx-thunk 是中间件,类似的还有 saga,这里我们使用的是 thunk

immutableFacebook 开发一个 持久化数据结构,它是一经创建变不可修改的数据,普遍运用于 redux 中。

2. 合并各组件的reducer

srcstore 目录下,创建 index.jsreducer.js

reducer.js 用来整合其他仓库的 reducer 的数据的,而辅助函数 combineReducers 就用来帮我们完成这件事。

import { combineReducers } from 'redux-immutable';

export default combineReducers({
    
})
3. 创建store

这里使用了增强函数,在启用谷歌调试工具 redux 的基础上使用了中间件 thunk

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

// compose 做的只是让你在写深度嵌套的函数时,避免了代码的向右偏移
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// 生成数据
const store = createStore(reducer, composeEnhancers(
    applyMiddleware(thunk)
))
4. 引入store

使用 react-reduxProvider 方法传递store。

store能被所有被 包裹的组件使用。

import { GlobalStyle } from './style';
import { IconStyle } from './assets/iconfont/iconfont';
import { HashRouter } from 'react-router-dom';
import Routes from './routes';
import 'lib-flexible'
import { Provider } from 'react-redux';
import store from './store/index';

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

export default App;
四、轮播图和推荐列表 1. 轮播图组件

先制作轮播图组件:

import React from 'react';
import Slider from '../../components/slider';

function Recommend(props) {

    const bannerList = [1, 2, 3, 4].map(item => {
        return { imageUrl: "http://www.kaotop.com/file/tupian/20220523/109951164331219056.jpg" }
    })

    return (
        <div>
            <Slider bannerList={bannerList}></Slider>
        </div>
    )
}

export default React.memo(Recommend);
src/components 下新建一个 slilder 目录及其 index.js 文件夹:
import React from 'react';

function Slider(props) {
    const { bannerList } = props

    return (
        <div>
            {
                bannerList.map((slider, index) => {
                    return (
                        <img key={index} src={slider.imageUrl} width="100%" height="100%" alt="推荐" />
                    );
                })
            }
        </div>
    )
}

export default React.memo(Slider)
2. 使用 swiper 插件 这里使用新版本的swiper,和旧版有区别,可以查看官网swiper官网,或者下载旧版。
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay} from 'swiper';
// 引入swiper样式
import 'swiper/css';
import 'swiper/css/pagination';

function Slider(props) {
  const {bannerList} = props
  return (
      <Swiper 
      modules={[Pagination, Autoplay]}
      loop={true} // 开启环路
      pagination={{ // 分页器
        el: '.swiper-pagination',
        type: 'bullets' }}
      autoplay={{ // 自动播放
        delay: 3000,
        stopOnLastSlide: false,
        disableOnInteraction: false}}>
        {
          bannerList.map((item, index) => {
            return (
              <SwiperSlide key={index}>
                <img src={item.imageUrl} width="100%" height="100%" alt="推荐"/>
              </SwiperSlide>
            )
          })
        }
      </Swiper>
  )
}
export default React.memo(Slider)

调节 swiper 样式

// style.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%;
    // 背景色
    .before{
    position: absolute;
    top: 0;
    height: 60%;
    width: 100%;
    background: ${style["theme-color"]};
  }
  // 轮播图样式
    .swiper{
        position: relative;
        width: 98%;
        overflow: hidden;
        margin:auto;
        border-radius: .16rem;
    }
    
    // 自定义分页器样式
    .swiper-pagination-bullet-active{
      background: ${style["theme-color"]};
    }
`;

引入样式

import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay} from 'swiper';
import 'swiper/css';
import 'swiper/css/pagination';
import {SliderContainer} from './style'

function Slider(props) {
  const {bannerList} = props
  return (
    <SliderContainer>
     // 背景色
      <div className="before"></div>
      <Swiper 
      modules={[Pagination, Autoplay]}
      loop={true} // 开启环路
      pagination={{ 
        el: '.swiper-pagination',
        type: 'bullets' }}
      autoplay={{
        delay: 3000,
        stopOnLastSlide: false,
        disableOnInteraction: false}}>
        {
          bannerList.map((item, index) => {
            return (
              <SwiperSlide key={index}>
                <img src={item.imageUrl} width="100%" height="100%" alt="推荐"/>
              </SwiperSlide>
            )
          })
        }
      </Swiper>
      // 不是必须的,为了自定义分页器样式
      <div className="swiper-pagination"></div>
    </SliderContainer>
  )
}
export default React.memo(Slider)
3. 推荐列表 数据模拟(和轮播图类似):
import List from '../../components/list';

// 推荐列表数据
const recommendList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item, index) => {
  return {
      id: index,
      picUrl: "http://www.kaotop.com/file/tupian/20220523/18999560928537533.jpg",
      playCount: 17171122,
      name: "[洗澡时听的歌] 浴室里听歌吹泡泡o○o○o○"
  }
});

<List recommendList={recommendList}></List>
JSX:
import React from 'react';

function RecommendList(props) {
    const { recommendList } = props
    return (
        <div>
            {
                recommendList.map((item, index) => {
                    return (
                        <img key={index} src={item.picUrl} alt={item.name} />
                    )
                })
            }
        </div>
    )
}

export default React.memo(RecommendList)

样式

import { ListWrapper, ListTitle, List, ListItem} from './style';
<ListWrapper>
  <ListTitle>
      <div className='title'>推荐歌单</div>
      <div className='tag'>歌单广场</div>
  </ListTitle>
  <List>
      {
          recommendList.map((item, index) => {
              return (
                  <ListItem>
                      <img key={index} src={item.picUrl} alt={item.name} />
                  </ListItem>
              )
          })
      }
  </List>
</ListWrapper>

样式文件

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

export const ListWrapper = styled.div`
    position: relative;
    width: 100%;
`
export const ListTitle = styled.div`
    overflow: hidden;
    line-height: 1.066667rem;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;

    .title {
        font-size: .373333rem;
        font-weight: 700;
        padding-left: .16rem;
    }
    .tag {
        height: .266667rem;
        font-size: .266667rem;
        font-weight: 600;
        padding: .053333rem .16rem;
        margin-right: .16rem;
        line-height: .266667rem;
        color: #444;
        border: .026667rem solid rgb(211, 210, 210);
        border-radius: .213333rem;
    }
`

export const List = styled.div`
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
`

export const ListItem = styled.div`
    box-sizing: border-box;
    flex: 33.33%;
    padding: 0 .16rem .16rem .16rem;
    img {
        width: 100%;
    }
`

接下来,更改

ListItem 的布局样式

减小请求图片的大小

添加播放次数

修改key的位置

<ListItem key={item.id}>
	<div className="img_wrapper">
    	<div className="decorate"></div>
    	{/* 加此参数可以减小请求的图片资源大小 */}
        <img src={item.picUrl + "?param=300x300"} width="100%" height="100%" alt="music" />
        <div className="play_count">
			<i className="iconfont play">&#xe885;</i>
			<span className="count">{item.playCount}</span>
		</div>
	</div>
	<div className="desc">{item.name}</div>
</ListItem>

样式文件

export const ListItem = styled.div`
    position: relative;
    width: 32%;
    .img_wrapper{
        .decorate {
            position: absolute;
            top: 0;
            width: 100%;
            height: .933333rem;
            border-radius: .08rem;
            background: linear-gradient(hsla(0,0%,43%,.4),hsla(0,0%,100%,0));
        }
        position: relative;
        height: 0;
        padding-bottom: 100%;
        .play_count {
            position: absolute;
            right: .053333rem;
            top: .053333rem;
            font-size: .32rem;
            line-height: .4rem;
            color: ${style["font-color-light"]};
            .play{
                font-size: .426667rem;
                vertical-align: top;
            }
        }
        img {
            position: absolute;
            width: 100%;
            height: 100%;
            border-radius: .08rem;
        }
  }
  .desc {
        overflow: hidden;
        margin-top: .053333rem;
        padding: 0 .053333rem;
        height: 1.333333rem;
        text-align: left;
        font-size: .32rem;
        line-height: 1.4;
        color: ${style["font-color-desc"]};
    }
`

修改播放量数据格式

src/api 下面新建一个 utils.js 文件
export const getCount = (count) => {
    if (count < 0) return;
    if (count < 10000) {
        return count;
    } else if (Math.floor(count / 10000) < 10000) {
        return Math.floor(count / 1000) / 10 + "万";
    } else {
        return Math.floor(count / 10000000) / 10 + "亿";
    }
}

规范格式

import { getCount } from "../../api/utils";

<span className="count">{getCount(item.playCount)}</span>
五、Scroll 基础组件

better-scroll

之前页面展示没有问题了,但是滚动页面时,是整个页面滚动。现在我希望顶部固定,滑动下面歌曲内容的时候,只有这部分出现滚动,现在实现一下。 1. 引入scroll插件

实现:滑动页面、下拉刷新、上拉刷新

(1)下载依赖

yarn add better-scroll@next

(2)大致结构

baseUI 中新建一个 scroll 文件夹及其 index.js 文件,大致结构如下
import React,{ useEffect, useState, useEffect, useRef } from 'react';
import BScroll from "better-scroll";

const Scroll = forwardRef((props, ref) => {
    return (
      <ScrollContainer ref={scrollContaninerRef}>
        {props.children}
      </ScrollContainer>
    );
})

export default Scroll;

ScrollContainer 是样式组件

forwarRef可以使得在父组件中可以得到子组件中的 DOM 节点。这里的ScrollContainer是可以作为DOM原生被父组件获取到。

2. 配置scroll

滚动的方向

vertical, horizental

滚动的位置

probeType: 3:不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。

点击事件

click: true,默认是阻止的。

回d

bounce: { top: bounceTop, bottom: bounceBottom},滑动超过顶部和底部边界时回d效果
import React,{ useEffect, useState, useEffect, useRef } from 'react';
import BScroll from "better-scroll";

const Scroll = forwardRef((props, ref) => {
    // 初始化scroll实例对象,用于配置
    const [bScroll, setBScroll] = useState();
    // scrollContaninerRef.current指向初始化bs实例需要的DOM元素 
    const scrollContaninerRef = useRef();
    
     // 创建 better-scroll实例
    useEffect(() => {
        const scroll = new BScroll(scrollContaninerRef.current, {
            scrollX: direction === "horizental", 
            scrollY: direction === "vertical",  
            probeType: 3,
            click: true,
            bounce: {
                top: bounceTop,
                bottom: bounceBottom
            }
        });
        setBScroll(scroll);
        return () => { // 清除函数
            setBScroll(null);
        }
        //eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <ScrollContainer ref={scrollContaninerRef}>
            {props.children}
        </ScrollContainer>
    );
})

export default Scroll;

重新渲染

每次重新渲染都要刷新实例,防止无法滑动
useEffect(() => {
  if(refresh && bScroll){
    bScroll.refresh();
  }
});
3. 绑定 scroll 事件

在滑动时触发 onScroll,传入scroll实例

useEffect(() => {
  if(!bScroll || !onScroll) return;
  bScroll.on('scroll', (scroll) => {// 绑定
    onScroll(scroll);
  })
  return () => {
    bScroll.off('scroll');// 解除绑定
  }
}, [onScroll, bScroll]);
4. 上拉刷新

滑动超过底部边界时,触发pullup()回d

useEffect(() => {
  if(!bScroll || !pullUp) return;
  bScroll.on('scrollEnd', () => {
    //判断是否滑动到了底部
    if(bScroll.y <= bScroll.maxScrollY + 100){
      pullUp();
    }
  });
  return () => {
    bScroll.off('scrollEnd');
  }
}, [pullUp, bScroll]);
5. 下拉刷新

下拉超过顶部边界时,触发pullDown()回d。

useEffect(() => {
  if(!bScroll || !pullDown) return;
  bScroll.on('touchEnd', (pos) => {
    //判断用户的下拉动作
    if(pos.y > 50) {
      pullDown();
    }
  });
  return () => {
    bScroll.off('touchEnd');
  }
}, [pullDown, bScroll]);
6. 暴露方法 暴露给使用scroll组件地方调用的刷新方法使用的方案是 React Hooks 中的 useImperativeHandle
// 一般和forwardRef一起使用,ref已经在forWardRef中默认传入
useImperativeHandle(ref, () => ({
  //给外界暴露refresh方法
  refresh() {
    if(bScroll) {
      bScroll.refresh();
      bScroll.scrollTo(0, 0);
    }
  },
  //给外界暴露getBScroll方法, 提供bs实例
  getBScroll() {
    if(bScroll) {
      return bScroll;
    }
  }
}));
7. 设置默认props scroll组件中默认props
Scroll.defaultProps = {
  direction: "vertical",
  click: true,
  refresh: true,
  onScroll:null,
  pullUpLoading: false,
  pullDownLoading: false,
  pullUp: null,
  pullDown: null,
  bounceTop: true,
  bounceBottom: true
};
8. 类型检查
import PropTypes from "prop-types";

Scroll.propTypes = {
  direction: PropTypes.oneOf(['vertical', 'horizental']),
  refresh: PropTypes.bool,
  onScroll: PropTypes.func,
  pullUp: PropTypes.func,
  pullDown: PropTypes.func,
  pullUpLoading: PropTypes.bool,
  pullDownLoading: PropTypes.bool,
  bounceTop: PropTypes.bool,//是否支持向上吸顶
  bounceBottom: PropTypes.bool//是否支持向上吸顶
};

给组件添加样式

import styled from 'styled-components';

const ScrollContainer = styled.div`
  width: 100%;
  height: 100%;
  overflow: hidden;
`;
9. Recommend中使用

引入

import Scroll from '../../baseUI/scroll';

JSX代码

用scroll组件包裹前面的内容,滑动的时候,scroll组件没有变化,里面的内容出现滚动效果。
<Content>
  <Scroll className="list">
    <div>
      <Slider bannerList={bannerList}></Slider>
      <List recommendList={recommendList}></List>
    </div>
  </Scroll>
</Content>

content 是样式组件

import styled from 'styled-components';

export const Content = styled.div`
  position: fixed;
  top: 2.4rem;
  bottom: 0;
  width: 100%;
  max-width: 750px;
`

为什么给content加绝对定位

我们滑动时,滑动的不是 这个组件,而是它里面的内容。前面给了 组件一个高度和宽度,都是相对于父容器 Content100%。那是因为我们没有给我们的 Header 组件加绝对定位,如果不给 Recommend 加绝对定位的话,我们滑动时滑动的会是整个页面。由于 Recommend 脱离了文档流,有固定的宽高,所以我们才能看见滑动的功能。 10. 调整样式

顶部下拉过程中间会有一段空白,这是因为设置了背景色导致的。直接加大高度解决这个问题。

.before{
  position: absolute;
  top: -8rem;
  height: 10.666667rem;
  width: 100%;
  background: ${style["theme-color"]};
}
六、请求获取数据 前面内容都是模拟数据展示的,现在使用网络请求的方式获取数据。先去 GitHub 上面 clone 这个项目:GitHub网易云音乐接口,然后把它运行在其他端口上,保证不和当前前端服务端口冲突。(可能需要修改端口) 1. axios

安装依赖

yarn add axios

配置 axios

api 文件夹里,在这个文件夹下面创建 config.js 文件

主要两点:

在所有请求前方加上http://localhost:3300,即让所有数据从这个端口号请求响应拦截,请求失败处理

还有:

请求超时时间带cookie响应数据类型…等
import axios from 'axios';

export const baseUrl = 'http://localhost:4000';

// 创建axios的实例
const axiosInstance = axios.create({
  baseURL: baseUrl,
  timeout: 5000// 请求超时时间
  responseType: "json",
  withCredentials: true, // 带cookie
  headers: {
    "Content-Type": "application/json;charset=utf-8"
  }
});

// 响应拦截器【响应拦截器的作用是在接收到响应后进行一些 *** 作】
axiosInstance.interceptors.response.use(
  // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
  res => res.data,
  // 服务器状态码不是2开头的的情况
  err => {
    console.log(err, "网络错误");
  }
);

export {
  axiosInstance
};
2. 封装不同的网络请求

这里封装了需要的两个接口,到时候直接调用即可。

// request.js
import { axiosInstance } from "./config";

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

export const getRecommendListRequest = () => {
  return axiosInstance.get('/personalized');
}
3. reudx 开发 在 Recommend 目录下,新建 store 文件夹 (1)常量集合 常量集合,存放不同action的type值
//constants.js
export const CHANGE_BANNER = 'recommend/CHANGE_BANNER';

export const CHANGE_RECOMMEND_LIST = 'recommend/RECOMMEND_LIST';
(2)reducer

该组件的仓库

// reducer
// 获取常量
import * as actionTypes from './constants';
// 导入 immutable 的 frmoJS 方法
import { fromJS } from 'immutable';

// 这里用到fromJS把JS数据结构转化成immutable数据结构
const defaultState = fromJS({
    bannerList: [],
    recommendList: [],
});

export default (state = defaultState, action) => {
  switch(action.type) {
    case actionTypes.CHANGE_BANNER:
      return state.set('bannerList', action.data);
    case actionTypes.CHANGE_RECOMMEND_LIST:
      return state.set('recommendList', action.data);
    default:
      return state;
  }
}

导出分仓库

// index.js

// 导入仓库
import reducer from './reducer'
// 导入变量
import * as actionCreators from './actionCreators'

// 导出变量
export { reducer, actionCreators };

合并到主仓库

// src/reducer.js

// 合并 reducer 函数
import { combineReducers } from 'redux-immutable';
// 导入分仓库的 reducer
import { reducer as recommendReducer } from '../application/Recommend/store/index';

// 合并 reducer 函数为一个 obj
export default combineReducers({
    recommend: recommendReducer,
})
(3)获取仓库数据

将数据映射Redux全局的state到组件的props上

// Recommend/index.js

const mapStateToProps = (state) => ({
    // 不要再这里将数据toJS,不然每次diff比对props的时候都是不一样的引用,还是导致不必要的重渲染, 属于滥用immutable
    bannerList: state.getIn(['recommend', 'bannerList']),
    recommendList: state.getIn(['recommend','recommendList']),
})

把去掉的 mock 数据,通过prop获取

const { bannerList, recommendList } = props;
// 把 immutable 数据类型转换为对应的 js 数据类型
const bannerListJS = bannerList ? bannerList.toJS() : [];
const recommendListJS = recommendList ? recommendList.toJS() :[];

此时如果 redux 中有数据,我们就已经获取到了,可是现在 redux 中还没有数据,所以接下来我们用 axios 来获取数据。

(4)action

提供用于修改仓库数据的方法

// actionCreators.js
// 导入常量
import * as actionTypes from './constants';
// 将JS对象转换成immutable对象
import { fromJS } from 'immutable';
// 导入网络请求
import { getBannerRequest, getRecommendListRequest } from '../../../api/request';

export const changeBannerList = (data) => ({
    type: actionTypes.CHANGE_BANNER,
    data: fromJS(data)
});

export const changeRecommendList = (data) => ({
    type: actionTypes.CHANGE_RECOMMEND_LIST,
    data: fromJS(data)
});

// 获取轮播图数据
export const getBannerList = () => {
    return (dispatch) => {
    	// 发送请求
        getBannerRequest().then(data => {
            dispatch(changeBannerList(data.banners));
        }).catch(() => {
            console.log("轮播图数据传输错误");
        })
    }
};

// 获取推荐列表
export const getRecommendList = () => {
    return (dispatch) => {
        getRecommendListRequest().then(data => {
            dispatch(changeRecommendList(data.result));
        }).catch(() => {
            console.log("推荐歌单数据传输错误");
        });
    }
};
(5)更新仓库数据

将方法映射dispatch到props上

const mapDispatchToProps = (dispatch) => {
    return {
        getBannerDataDispatch() {
            dispatch(actionCreaters.getBannerList());
        },
        getRecommendListDataDispatch() {
            dispatch(actionCreaters.getRecommendList());
        },
    }
}

在组件初次渲染的时候调用

const { getBannerDataDispatch, getRecommendListDataDispatch } = props;

// 当传空数组([])时,只会在组件 mount 时执行内部方法。
useEffect(() => {
    getBannerDataDispatch();
    getRecommendListDataDispatch();
}, []);

现在,就能够看到通过请求获取到的数据了。

七、优化 1. 图片懒加载 我们使用一个成熟的图片懒加载库:react-lazyload 来做我们的图片懒加载。
yarn add react-lazyload
components/list/index.js 中导入
import LazyLoad from "react-lazyload";

(1)添加占位图片

img 标签进行改造:

//img标签外部包裹一层LazyLoad

  
LazyLoad>
img 外面包裹了一层 LazyLoad,它会有一个占位图片 music.png,这个png文件在list文件夹下。

图片懒加载的原理:

在大量图片加载的情况下,会造成页面空白甚至卡顿。我们只让视口内的图片显示即可,同时图片未显示的时候给它一张默认的图片进行占位。滑动到占位图片的位置时,会请求真正的图片。

(2)滑动加载图片

懒加载库提供了一个滑动加载图片的方法:forceCheck

Scroll 组件的 onScroll 传入 forceCheck

import {forceCheck} from 'react-lazyload'
// ...
<Scroll className="list" onScroll={forceCheck}>
2. 进场Loading效果 Ajax请求往往需要一定的时间,在这个时间内,页面会处于没有数据的状态,也就是空白状态。用户点击来的时候看见一片空白的时候心里是非常焦灼的,尤其是Ajax的请求时间长达几秒的时候。而loading效果便能减缓这种焦急的情绪,并且如果loading动画做的漂亮,还能够让人赏心悦目,让用户对App产生好感。

(1)loading效果制作

利用了CSS3的animation-lay特性,让两个圆交错变化,产生一个涟漪的效果。
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: fixed;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        margin: auto;
        width: 1.6rem;
        height: 1.6rem;
        /* 不透明级 */
        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 (
        
            
            
        
    );
}

export default React.memo(Loading)

Recommend 组件中使用

import Loading from '../../baseUI/loading/index';

<Content>
  ...
  <Loading></Loading>
<Content>

现在,就可以看到Loading效果了。但是Loading效果一直存在,挡住了页面。

(2)Loading的控制逻辑

Loading效果一开始时出现的,但是当页面请求获取到数据的时候,需要隐藏该效果。

使用enterLoading来进行控制,这里也使用redux。

// reducer.js
const defaultState = fromJS({
  // ...
  enterLoading: true
});

// ...
case actionTypes.CHANGE_ENTER_LOADING:
      return state.set('enterLoading', action.data);
//constants.js
...
export const CHANGE_ENTER_LOADING = 'recommend/CHANGE_ENTER_LOADING';

当获取到请求数据的时候,将enterLoading修改为false。

//actionCreators.js
//...
// 获取推荐歌单
export const getRecommendList = () => {
  return (dispatch) => {
    getRecommendListRequest().then(data => {
      dispatch(changeRecommendList(data.result));
      // 修改enterLoading
      dispatch(changeEnterLoading(false));
    }).catch(() => {
      console.log("推荐歌单数据传输错误");
    });
  }
};

Recommend/index.js 中使用

const { bannerList, recommendList, enterLoading } = props;

// ...
<Content>
  // ...
  { enterLoading ? <Loading></Loading> : null }
<Content>
// ...
// 获取store
const mapStateToProps = (state) => ({
  ...
  enterLoading: state.getIn(['recommend', 'enterLoading'])
});

到这里,Loading效果完成。

3. Redux数据缓存 切换到歌手页面,然后切回到推荐页的时候,页面重新发出请求,虽然在页面上没有区别(因为数据没有变化),但是这个请求是没有必要发出的,所以优化一下。很简单,加一个判断,如果如果页面有数据,就不发请求。
//Recommend/index.js
useEffect(() => {
  // 如果页面有数据,则不发请求,通过immutable数据结构中的长度属性size判断
  if(!bannerList.size){
    getBannerDataDispatch();
  }
  if(!recommendList.size){
    getRecommendListDataDispatch();
  }
  // eslint-disable-next-line
}, []);

Recommend 组件就彻底完成了。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1296790.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存