本节代码对应 GitHub 分支: chapter8

仓库传送门 (opens new window)

终于,我们进入了最精彩的环节,也是最有挑战的模块 ———— 播放器开发。

之前在不断地重构、更新代码,经过组件拆分代码解耦,最后理想的版本终于打造完成。接下来给大家呈现的也是最后一版的代码,每个组件的代码尽量控制在了 300 行以内,而不是在第一版那样近千行代码挤在一个文件,非常不利于维护。

播放器是一个比较特别的组件,里面并没有涉及到 Ajax 的操作,反而全程都在依赖 store 里面的数据。因从,我们从数据层开始准备是一个比较明智的选择。

application 目录下新建 Player 文件夹,然后新建 store 目录,开始 redux 层的开发。

# 1. 声明初始化 state

//store/reducer.js
import * as actionTypes from './constants';
import {fromJS} from 'immutable';
import { playMode } from './../../../api/config';

const defaultState = fromJS ({
  fullScreen: false,// 播放器是否为全屏模式
  playing: false, // 当前歌曲是否播放
  sequencePlayList: [], // 顺序列表 (因为之后会有随机模式,列表会乱序,因从拿这个保存顺序列表)
  playList: [],
  mode: playMode.sequence,// 播放模式
  currentIndex: -1,// 当前歌曲在播放列表的索引位置
  showPlayList: false,// 是否展示播放列表
  currentSong: {}
});

注意 playMode 对象应该在 api/config.js 中定义,

// 播放模式
export const playMode = {
  sequence: 0,
  loop: 1,
  random: 2
};

# 2. 定义 constants

//store/constants.js
export const SET_CURRENT_SONG = 'player/SET_CURRENT_SONG';
export const SET_FULL_SCREEN = 'player/SET_FULL_SCREEN';
export const SET_PLAYING_STATE = 'player/SET_PLAYING_STATE';
export const SET_SEQUECE_PLAYLIST = 'player/SET_SEQUECE_PLAYLIST';
export const SET_PLAYLIST = 'player/SET_PLAYLIST';
export const SET_PLAY_MODE = 'player/SET_PLAY_MODE';
export const SET_CURRENT_INDEX = 'player/SET_CURRENT_INDEX';
export const SET_SHOW_PLAYLIST = 'player/SET_SHOW_PLAYLIST';

# 3. 定义 reducer 函数

//store/reducer.js
export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.SET_CURRENT_SONG:
      return state.set ('currentSong', action.data);
    case actionTypes.SET_FULL_SCREEN:
      return state.set ('fullScreen', action.data);
    case actionTypes.SET_PLAYING_STATE:
      return state.set ('playing', action.data);
    case actionTypes.SET_SEQUECE_PLAYLIST:
      return state.set ('sequencePlayList', action.data);
    case actionTypes.SET_PLAYLIST:
      return state.set ('playList', action.data);
    case actionTypes.SET_PLAY_MODE:
      return state.set ('mode', action.data);
    case actionTypes.SET_CURRENT_INDEX:
      return state.set ('currentIndex', action.data);
    case actionTypes.SET_SHOW_PLAYLIST:
      return state.set ('showPlayList', action.data);
    default:
      return state;
  }
}

# 4. 编写具体的 action

//store/actionCreators.js
import { SET_CURRENT_SONG, SET_FULL_SCREEN, SET_PLAYING_STATE, SET_SEQUECE_PLAYLIST, SET_PLAYLIST, SET_PLAY_MODE, SET_CURRENT_INDEX, SET_SHOW_PLAYLIST, DELETE_SONG, INSERT_SONG } from './constants';
import { fromJS } from 'immutable';

export const changeCurrentSong = (data) => ({
  type: SET_CURRENT_SONG,
  data: fromJS (data)
});

export const changeFullScreen =  (data) => ({
  type: SET_FULL_SCREEN,
  data
});

export const changePlayingState = (data) => ({
  type: SET_PLAYING_STATE,
  data
});

export const changeSequecePlayList = (data) => ({
  type: SET_SEQUECE_PLAYLIST,
  data: fromJS (data)
});

export const changePlayList  = (data) => ({
  type: SET_PLAYLIST,
  data: fromJS (data)
});

export const changePlayMode = (data) => ({
  type: SET_PLAY_MODE,
  data
});

export const changeCurrentIndex = (data) => ({
  type: SET_CURRENT_INDEX,
  data
});

export const changeShowPlayList = (data) => ({
  type: SET_SHOW_PLAYLIST,
  data
});

# 5. 将相关变量导出

//store/index.js
import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants'

export { reducer, actionCreators, constants };

然后在全局 store 注册:

//store/reducer.js
import { reducer as playerReducer } from "../application/Player/store/index";

export default combineReducers ({
  //...
  player: playerReducer
});

# 播放器组件连接数据

//Player/index.js
import React, { useRef, useState, useEffect } from "react";
import { connect } from "react-redux";
import {
  changePlayingState,
  changeShowPlayList,
  changeCurrentIndex,
  changeCurrentSong,
  changePlayList,
  changePlayMode,
  changeFullScreen
} from "./store/actionCreators";

function Player (props) {
  return (
    <div>Player</div>
  )
}

// 映射 Redux 全局的 state 到组件的 props 上
const mapStateToProps = state => ({
  fullScreen: state.getIn (["player", "fullScreen"]),
  playing: state.getIn (["player", "playing"]),
  currentSong: state.getIn (["player", "currentSong"]),
  showPlayList: state.getIn (["player", "showPlayList"]),
  mode: state.getIn (["player", "mode"]),
  currentIndex: state.getIn (["player", "currentIndex"]),
  playList: state.getIn (["player", "playList"]),
  sequencePlayList: state.getIn (["player", "sequencePlayList"])
});

// 映射 dispatch 到 props 上
const mapDispatchToProps = dispatch => {
  return {
    togglePlayingDispatch (data) {
      dispatch (changePlayingState (data));
    },
    toggleFullScreenDispatch (data) {
      dispatch (changeFullScreen (data));
    },
    togglePlayListDispatch (data) {
      dispatch (changeShowPlayList (data));
    },
    changeCurrentIndexDispatch (index) {
      dispatch (changeCurrentIndex (index));
    },
    changeCurrentDispatch (data) {
      dispatch (changeCurrentSong (data));
    },
    changeModeDispatch (data) {
      dispatch (changePlayMode (data));
    },
    changePlayListDispatch (data) {
      dispatch (changePlayList (data));
    }
  };
};

// 将 ui 组件包装成容器组件
export default connect (
  mapStateToProps,
  mapDispatchToProps
)(React.memo (Player));

如果现在还看不到这个组件,可不要感到奇怪,仅仅凭经验就知道这个组件还并没有注册到全局。这里播放器组件比较特殊,没有专门的路由,也就是说,它会作为一个全局性的组件存在。让我们在 Home 组件来引入:

import Player from '../Player';

return (
  //...
  //renderRoute 下面
  <Player></Player>
)

现在你如果进入到某个页面,比如排行榜页,就能看到 Player 组件了。内容已经出现,样式之后再调整。

img

接下来我们来把基础 UI 构建一波。

阅读全文