前言
React 和 Redux 是目前前端领域非常流行的技术栈,在大型应用中也越来越普遍地被应用。因此,在本篇文章中,我们将探讨如何使用 React 和 Redux 来构建一个高性能的音乐播放器,同时分享一些关于 React 和 Redux 的实践经验和一些指导意义。
技术栈
功能介绍
- 搜索功能:根据关键词搜索音乐。
- 播放功能:支持音乐的播放、停止、上一首、下一首、随机播放、单曲循环等操作。
- 歌词滚动:根据歌曲进度滚动歌词。
- 播放列表:展示当前的播放队列。
- 歌曲下载:支持歌曲下载。
进入实战
在开始之前,我们需要准备好开发所需的环境。本篇文章将采用 create-react-app 来搭建项目,同时使用 NeteaseCloudMusicApi 提供的接口来获取音乐数据。
目录结构
我们先来看一下整个项目的目录结构:
// javascriptcn.com 代码示例 ├── src │ ├── api │ ├── components │ ├── containers │ ├── reducers │ ├── store │ ├── styles │ └── utils ├── public └── package.json
- api:封装了请求相关的工具函数。
- components:存放 React 的可复用 UI 组件。
- containers:存放各个页面的容器组件。
- reducers:存放 Redux 的 store。
- store:负责组合所有 reducer,创建 store,并导出供页面使用。
- styles:存放公共的样式文件。
- utils:存放一些工具函数。
- public:用于存放静态资源。
环境配置
我们需要先确保我们已经安装了 Node.js 和 Git,接下来我们通过 create-react-app 来创建项目:
npx create-react-app react-redux-music-player
下一步,我们需要安装一些需要的依赖:
cd react-redux-music-player npm i redux react-redux redux-thunk react-router-dom immutable --save npm i axios classnames --save npm i eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y --save-dev
ESLint 配置
为了保证代码风格的一致性,我们需要为项目配置 ESLint,同时采用 Airbnb 的代码风格规范。根据项目需要,我们对 .eslintrc.js 中的配置进行了一些改动,完整的配置如下:
// javascriptcn.com 代码示例 module.exports = { env: { browser: true, es2021: true, }, extends: ['airbnb'], parser: '@babel/eslint-parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react'], settings: { 'import/resolver': { node: { paths: ['src'], }, }, }, rules: { 'no-console': 'off', 'no-unused-vars': 'warn', 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }], 'react/jsx-props-no-spreading': 'off', 'react/forbid-prop-types': 'off', 'react/require-default-props': 'off', 'react/destructuring-assignment': 'off', 'import/prefer-default-export': 'off', 'import/no-unresolved': 'off', 'import/extensions': 'off', }, };
开始编码
API 封装
我们需要封装请求相关的工具函数,这里我们采用 axios:
// javascriptcn.com 代码示例 import axios from 'axios'; export const get = (url, params) => { return axios.get(url, { params }) .then((response) => response.data) .catch((err) => console.log(err)); }; export const post = (url, data) => { return axios.post(url, data) .then((response) => response.data) .catch((err) => console.log(err)); };
路由配置
我们需要在 src/routes/index.js 中进行路由的配置:
// javascriptcn.com 代码示例 import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import React from 'react'; import Layout from '../containers/Layout'; import Home from '../containers/Home'; import Playlists from '../containers/Playlists'; import PlaylistDetail from '../containers/PlaylistDetail'; import Search from '../containers/Search'; import Play from '../containers/Play'; const AppRoutes = () => ( <Router> <Layout> <Switch> <Route exact path="/" component={Home} /> <Route path="/playlists" component={Playlists} /> <Route path="/playlist/:id" component={PlaylistDetail} /> <Route path="/search" component={Search} /> <Route path="/play/:id" component={Play} /> </Switch> </Layout> </Router> ); export default AppRoutes;
组件封装
我们需要先分析页面所需的组件,设计出组件之间的父子关系,进而封装出高复用性的组件,这里我们展示一下对 Header 和 Footer 组件的封装:
// javascriptcn.com 代码示例 import React from 'react'; import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; import styles from './index.module.css'; const Header = () => ( <header className={styles.header}> <nav> <ul> <li> <NavLink activeClassName={styles.active} exact to="/"> 首页 </NavLink> </li> <li> <NavLink activeClassName={styles.active} to="/playlists"> 歌单 </NavLink> </li> <li> <NavLink activeClassName={styles.active} to="/search"> 搜索 </NavLink> </li> </ul> </nav> </header> ); const Footer = () => ( <footer className={styles.footer}> <small>© 2021 React+Redux 高性能音乐播放器</small> </footer> ); export { Header, Footer }; Header.propTypes = {}; Footer.propTypes = {};
Redux 配置
借助于 Redux-Thunk,我们可以编写异步的 Action,并将 Action 和 Reducer 串联在一起,进而创建一个 Store,这里展示一下我们对播放列表所做的操作:
// javascriptcn.com 代码示例 import { fromJS } from 'immutable'; import * as actionTypes from '../actionTypes'; const defaultState = fromJS({ playingList: [], playIndex: 0, }); const reducer = (state = defaultState, action) => { switch (action.type) { case actionTypes.SET_PLAYING_LIST: return state.set('playingList', fromJS(action.playingList)); case actionTypes.SET_PLAY_INDEX: return state.set('playIndex', action.playIndex); case actionTypes.ADD_TO_PLAYING_LIST: return state.update('playingList', (playingList) => playingList.push(fromJS(action.song))); case actionTypes.REMOVE_FROM_PLAYING_LIST: return state.update('playingList', (playingList) => playingList.delete(action.index)); case actionTypes.CLEAR_PLAYING_LIST: return state.set('playingList', fromJS([])).set('playIndex', 0); default: return state; } }; export default reducer;
Immutable.js 的使用
对于 Immutable 的优势和使用方法,如何优雅地使用 Redux 和 Immutable 数据结构的响应式 UI 等问题,可以参考这篇 React+Redux 实战小结。
开始绘制 UI
经过功能分析、路由设计、组件封装以及 Redux 配置之后,我们终于可以着手进行 UI 的开发了,这里我们简单罗列一下 HomePage 的代码,其他页面的代码略作省略:
// javascriptcn.com 代码示例 import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { fetchBanner } from '../../actions/home'; import Header from '../../components/Header'; import Footer from '../../components/Footer'; import Carousel from '../../components/Carousel'; import styles from './index.module.css'; const HomePage = ({ banner, actions: { fetchBanner } }) => { useEffect(() => { fetchBanner(); }, []); return ( <> <Header /> <div className={styles.poster}> <Carousel banners={banner} /> </div> <Footer /> </> ); }; HomePage.propTypes = { banner: PropTypes.object.isRequired, actions: PropTypes.object.isRequired, }; const mapStateToProps = (state) => ({ banner: state.home.get('banner'), }); const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators({ fetchBanner, }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
完善播放器功能
在 UI 布局方面,我们暂且不提。下面我们来详细讲解一下如何实现播放器的功能。我们使用了 react-h5-audio-player 这个第三方组件库,但是在基础展现方面却与此无任何关系,所以关于 UI 的代码部分略作省略:
// javascriptcn.com 代码示例 import React, { useRef, memo } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { ToastContainer, toast } from 'react-toastify'; import { Howl } from 'howler'; import { removeSongFromList, clearPlayingList, setPlayIndex } from '../../actions/player'; import { setLyric, setLyricIndex } from '../../actions/lyric'; import { getSongUrl, getLyric } from '../../api/song'; import { formatTime } from '../../utils'; import PlayerProgressBar from '../../components/PlayerProgressBar'; import styles from './index.module.css'; const Player = memo(({ playingList, playIndex, removeSongFromList, clearPlayingList, setPlayIndex, setLyric, setLyricIndex, }) => { const playerRef = useRef(null); const howlCurrent = useRef(null); const playSong = async (index) => { const song = playingList[index]; const url = await getSongUrl(song.get('id')); if (!url) { toast.warn('无法播放该歌曲'); return; } const howlOptions = { src: [url], html5: true, format: ['mp3', 'wav'], }; const howl = new Howl(howlOptions); howlCurrent.current = howl; howl.on('play', () => { const lyricId = song.get('id'); getLyric(lyricId).then((data) => { setLyric(data.lyric); setLyricIndex(0); }); }); howl.play(); }; const handleEnded = () => { if (playIndex === playingList.size - 1) { howlCurrent.current.stop(); } else { setPlayIndex(playIndex + 1); playSong(playIndex + 1); } }; const handleDelete = (index) => { if (playingList.size === 1) { howlCurrent.current.stop(); clearPlayingList(); } else { if (PlayIndex === index) { if (playingList.size === (index + 1)) { setPlayIndex(index - 1); } else { setPlayIndex(index + 1); } howlCurrent.current.stop(); playSong(playIndex); } removeSongFromList(index); } }; const handlePlayIndex = (index) => { if (index === playIndex) return; setPlayIndex(index); howlCurrent.current.stop(); playSong(index); }; return ( <> <audio ref={playerRef} onEnded={handleEnded} /> {/* 进度条,播放/暂停,歌曲名等UI元素 */} <div className={styles.list}> <button className={styles.clear} onClick={() => clearPlayingList()}> <i className="fa fa-fw fa-trash" /> 清空列表 </button> <ul> {playingList.map((song, index) => ( <li key={song.get('id')} className={playIndex === index ? styles.active : ''}> <div className={styles.index}> {playIndex === index ? <i className="fa fa-fw fa-volume-up" /> : <span>{index + 1}</span>} </div> <div className={styles.info}> <span className={styles.name}>{song.get('name')}</span> <span className={styles.artist}>{song.get('artistName')}</span> </div> <button className={styles.delete} onClick={() => handleDelete(index)}> <i className="fa fa-fw fa-trash" /> </button> <button className={styles.play} onClick={() => handlePlayIndex(index)}> {playIndex === index ? ( <i className="fa fa-fw fa-pause" /> ) : ( <i className="fa fa-fw fa-play" /> )} </button> </li> ))} </ul> </div> <PlayerProgressBar currentTime={howlCurrent.current ? formatTime(howlCurrent.current.seek()) : '00:00'} totalTime={playingList.size ? formatTime(playingList.getIn([playIndex, 'dt']) / 1000) : '00:00'} currentPercent={howlCurrent.current ? howlCurrent.current.seek() / howlCurrent.current.duration() : 0} /> <ToastContainer /> </> ); }); Player.propTypes = { playingList: PropTypes.object.isRequired, playIndex: PropTypes.number.isRequired, removeSongFromList: PropTypes.func.isRequired, clearPlayingList: PropTypes.func.isRequired, setPlayIndex: PropTypes.func.isRequired, setLyric: PropTypes.func.isRequired, setLyricIndex: PropTypes.func.isRequired, }; const mapStateToProps = (state) => ({ playingList: state.player.get('playingList'), playIndex: state.player.get('playIndex'), }); const mapDispatchToProps = (dispatch) => ({ removeSongFromList: bindActionCreators(removeSongFromList, dispatch), clearPlayingList: bindActionCreators(clearPlayingList, dispatch), setPlayIndex: bindActionCreators(setPlayIndex, dispatch), setLyric: bindActionCreators(setLyric, dispatch), setLyricIndex: bindActionCreators(setLyricIndex, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(Player);
总结
既然您已经看到这里,那就代表我们已经一起完成了这个高性能的音乐播放器项目。它不仅展现了 React 和 Redux 的一些最佳实践,还向我们展示了如何编写高性能、易维护和易扩展的代码。总之,希望这篇文章可以对您有所帮助,如果有考虑使用 React 和 Redux 构建类似项目的话,可以参考一下。
来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/652d298c7d4982a6ebe97621