本节代码对应 GitHub 分支: chapter5

仓库传送门 (opens new window)

# 数据层开发

# axios 请求代码

在 api/request.js 中,添加以下代码:

export const getRankListRequest = () => {
  return axiosInstance.get (`/toplist/detail`);
};

# redux 层开发

排行榜单可以说是整个应用中就数据层而言最简单的一个组件。因此 redux 的代码我们集中在一个文件中。

//rank/store/index.js
import { fromJS } from 'immutable';
import { getRankListRequest } from '../../../api/request';

//constants
export const CHANGE_RANK_LIST = 'home/rank/CHANGE_RANK_LIST';
export const CHANGE_LOADING = 'home/rank/CHANGE_LOADING';

//actionCrreator
const changeRankList = (data) => ({
  type: CHANGE_RANK_LIST,
  data: fromJS (data)
})

export const getRankList = () => {
  return dispatch => {
    getRankListRequest ().then (data => {
      let list = data && data.list;
      dispatch (changeRankList (list));
      dispatch (changeLoading (false));
    })
  }
}

const changeLoading = (data) => ({
  type: CHANGE_LOADING,
  data
})

//reducer
const defaultState = fromJS ({
  rankList: [],
  loading: true
})

const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case CHANGE_RANK_LIST:
      return state.set ('rankList', action.data);
    case CHANGE_LOADING:
      return state.set ('loading', action.data);
    default:
      return state;
  }
}

export { reducer };

# 组件连接 redux

先在全局 store 注册:

//src/store/reducer.js
import { combineReducers } from 'redux-immutable';
import { reducer as recommendReducer } from '../application/Recommend/store/index';
import { reducer as singersReducer } from '../application/Singers/store/index';
import { reducer as rankReducer } from '../application/Rank/store/index';

export default combineReducers ({
  // 之后开发具体功能模块的时候添加 reducer
  recommend: recommendReducer,
  singers: singersReducer ,
  rank: rankReducer
});

然后让 rank 组件连接 redux,

import React, { useEffect } from 'react';
import { connect } from 'react-redux';

function Rank (props) {

}

// 映射 Redux 全局的 state 到组件的 props 上
const mapStateToProps = (state) => ({
  rankList: state.getIn (['rank', 'rankList']),
  loading: state.getIn (['rank', 'loading']),
});
// 映射 dispatch 到 props 上
const mapDispatchToProps = (dispatch) => {
  return {
    getRankListDataDispatch () {
      dispatch (getRankList ());
    }
  }
};

export default connect (mapStateToProps, mapDispatchToProps)(React.memo (Rank));

已经熟悉了 redux 开发方式的你,是不是也觉得非常简单呢?废话不多说,我们马上进入 Rank 组件的开发。

# Rank 组件开发

首先初始化相应的 props。

(这里的引入模块的代码大家自行参考 GitHub 仓库的 chapter5 分支,也根据命令行报错提示依次引入)

const { rankList:list, loading } = props;

const { getRankListDataDispatch } = props;

let rankList = list ? list.toJS () : [];

didMount 的时候发送 Ajax 请求:

useEffect (() => {
  getRankListDataDispatch ();
}, []);

排行榜单分为两个部分,一部分是官方榜,另一部分是全球榜。

官方榜单数据有 tracks 数组,存放部分歌曲信息,而全球榜没有。

但是后端数据并没有将这两者分开,因此我们需要做一下数据的处理。

let globalStartIndex = filterIndex (rankList);
let officialList = rankList.slice (0, globalStartIndex);
let globalList = rankList.slice (globalStartIndex);

其中,filterIndex 从 api/utils.js 中导出,

// 处理数据,找出第一个没有歌名的排行榜的索引
export const filterIndex = rankList => {
  for (let i = 0; i < rankList.length - 1; i++) {
    if (rankList [i].tracks.length && !rankList [i + 1].tracks.length) {
      return i + 1;
    }
  }
};
// 记得引入这个方法
import { filterIndex } from '../../api/utils';
// 这是渲染榜单列表函数,传入 global 变量来区分不同的布局方式
const renderRankList = (list, global) => {
  return (
    <List globalRank={global}>
      {
      list.map ((item) => {
        return (
          <ListItem key={item.coverImgId} tracks={item.tracks} onClick={() => enterDetail (item.name)}>
            <div className="img_wrapper">
              <img src={item.coverImgUrl} alt=""/>
              <div className="decorate"></div>
              <span className="update_frequecy">{item.updateFrequency}</span>
            </div>
            { renderSongList (item.tracks)  }
          </ListItem>
        )
      })
    }
    </List>
  )
}

const renderSongList = (list) => {
  return list.length ? (
    <SongList>
      {
        list.map ((item, index) => {
          return <li key={index}>{index+1}. {item.first} - {item.second}</li>
        })
      }
    </SongList>
  ) : null;
}

// 榜单数据未加载出来之前都给隐藏
let displayStyle = loading ? {"display":"none"}:  {"display": ""};

return (
  <Container>
    <Scroll>
      <div>
        <h1 className="offical" style={displayStyle}> 官方榜 </h1>
          { renderRankList (officialList) }
        <h1 className="global" style={displayStyle}> 全球榜 </h1>
          { renderRankList (globalList, true) }
        { loading ? <EnterLoading><Loading></Loading></EnterLoading> : null }
      </div>
    </Scroll>
    {renderRoutes (props.route.routes)}
  </Container>
  );

style.js 中:

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

// Props 中的 globalRank 和 tracks.length 均代表是否为全球榜

export const Container = styled.div`
  position: fixed;
  top: 90px;
  bottom: 0;
  width: 100%;
  .offical,.global {
    margin: 10px 5px;
    padding-top: 15px;
    font-weight: 700;
    font-size: ${style ["font-size-m"]};
    color: ${style ["font-color-desc"]};
  }
`;
export const List = styled.ul`
  margin-top: 10px;
  padding: 0 5px;
  display: ${props => props.globalRank ? "flex": "" };
  flex-direction: row;
  justify-content: space-between;
  flex-wrap: wrap;
  background: ${style ["background-color"]};
  &::after {
    content:"";
    display:block;
    width: 32vw;
  }
`
export const ListItem = styled.li`
  display: ${props => props.tracks.length ? "flex": ""};
  padding: 3px 0;
  border-bottom: 1px solid ${style ["border-color"]};
  .img_wrapper {
    width:  ${props => props.tracks.length ? "27vw": "32vw"};
    height: ${props => props.tracks.length ? "27vw": "32vw"};
    border-radius: 3px;
    position: relative;
    .decorate {
      position: absolute;
      bottom: 0;
      width: 100%;
      height: 35px;
      border-radius: 3px;
      background: linear-gradient (hsla (0,0%,100%,0),hsla (0,0%,43%,.4));
    }
    img {
      width: 100%;
      height: 100%;
      border-radius: 3px;
    }
    .update_frequecy {
      position: absolute;
      left: 7px;
      bottom: 7px;
      font-size: ${style ["font-size-ss"]};
      color: ${style ["font-color-light"]};
    }
  }
`;
export const SongList = styled.ul`
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  padding: 10px 10px;
  >li {
    font-size: ${style ["font-size-s"]};
    color: grey;
  }
`;

然后在 index.js 中引入 CSS 组件即可,代码就不展示了。

在 image_wrapper 中,我们再次利用渐变效果实现了一层遮罩,达到衬托文字的效果。

其实布局都是非常常用的 flex 布局,我就不在这上面浪费时间了。值得注意的是,当 flex 布局一行填满三个元素,但是最后一行只有两个元素的时候,会出现一些问题,你会发现最后一个元素并不是在居中的位置,而是在最右边,中间留出了空白。我当时就遇到了这个问题,最后采用伪元素的方式才得以解决:

export const List = styled.ul`
  margin-top: 10px;
  padding: 0 5px;
  display: ${props => props.globalRank ? "flex": "" };
  flex-direction: row;
  justify-content: space-between;
  flex-wrap: wrap;
  background: ${style ["background-color"]};
  &::after {
    content:"";
    display:block;
    width: 32vw;
  }
`

现在的接口列表数据比之前少了一条,因此不再存在这个问题,但是希望大家能了解到这个细节。

阅读全文