React+Redux环境音应用:全暂停后恢复原播放音频功能求助
问题描述
我正在开发一款包含多个环境音的React应用,需实现如下功能:
- 点击复选框时暂停所有正在播放的音频
- 取消勾选时仅恢复勾选前正在播放的音频
我使用React和useReducer(原本计划用Redux,实际用了useReducer)开发,现有代码如下:
import "./styles.css"; import { useRef, useReducer } from "react"; const audioActionType = { TOGGLE_PLAY: "TOGGLE_PLAY", SET_VOLUME: "SET_VOLUME", TOGGLE_ALL_PAUSED: "TOGGLE_ALL_PAUSED", }; const initialAudioState = { audios: [ { playing: false, id: 1, volume: 25, title: "Coffe making", src: "..//audio/espresso-machine.mp3", }, { playing: false, id: 2, volume: 25, title: "Coffe machine", src: "..//audio/espresso-machine.mp3", }, ], allPaused: false, }; function AudioReducer(stateAudios, actionAudios) { switch (actionAudios.type) { case audioActionType.TOGGLE_PLAY: return { ...stateAudios, audios: stateAudios.audios.map((audio) => audio.id === actionAudios.id ? { ...audio, playing: !audio.playing } : audio ), }; case audioActionType.SET_VOLUME: return { ...stateAudios, audios: stateAudios.audios.map((audio) => audio.id === actionAudios.id ? { ...audio, volume: actionAudios.volume } : audio ), }; case audioActionType.TOGGLE_ALL_PAUSED: const anyAudioPlaying = { ...stateAudios.audios }; console.log(anyAudioPlaying); if (stateAudios.allPaused) { return { ...stateAudios, audios: stateAudios.audios.map((audio) => ({ ...audio, playing: false, })), allPaused: false, }; default: return stateAudios; } } export default function App() { const [state, dispatch] = useReducer(AudioReducer, initialAudioState); const audioRefs = useRef( state.audios.reduce((acc, audio) => { acc[audio.id] = new Audio(audio.src); return acc; }, {}) ).current; const togglePlay = (id) => { dispatch({ type: "TOGGLE_PLAY", id }); const audio = audioRefs[id]; state.audios.find((audio) => audio.id === id).playing ? audio.pause() : audio.play(); }; const setVolume = (id, volume) => { dispatch({ type: "SET_VOLUME", id, volume }); audioRefs[id].volume = volume / 100; }; console.log(state); return ( <div id="sounds-section" className="sections"> <input type="checkbox" id="pause" name="pause" checked={state.allPaused} onChange={() => dispatch({ type: "TOGGLE_ALL_PAUSED" })} /> <label htmlFor="pause"> Pause all</label> <div id="sounds"> {state.audios.map((audio) => ( <div className="sound" key={audio.id}> <div> <button onClick={() => togglePlay(audio.id)} dangerouslySetInnerHTML={{ __html: audio.playing ? "⏸" : "▶", }} /> <h4>{audio.title}</h4> </div> {audio.playing && ( <input className="volume" type="range" min="0" max="100" step="1" value={audio.volume} onChange={(e) => { setVolume(audio.id, e.target.value); }} /> )} </div> ))} </div> </div> ); }
目前的难题是无法记录allPaused状态将所有playing属性设为false之前的音频播放状态,无法实现恢复原播放音频的功能。
解决方案
核心问题是没存下暂停前的播放状态,所以要在state里加个字段记录这些状态,然后分两种逻辑处理「暂停所有」和「恢复播放」:
1. 修改初始状态,添加状态存储字段
在initialAudioState里新增previousPlayingState,用来存暂停前每个音频的playing状态:
const initialAudioState = { audios: [ { playing: false, id: 1, volume: 25, title: "Coffe making", src: "../audio/espresso-machine.mp3", }, { playing: false, id: 2, volume: 25, title: "Coffe machine", src: "../audio/espresso-machine.mp3", }, ], allPaused: false, previousPlayingState: {}, // 存储暂停前的播放状态 };
2. 完善Reducer的TOGGLE_ALL_PAUSED逻辑
原来的逻辑完全搞反了,而且没处理恢复的情况,修改后:
case audioActionType.TOGGLE_ALL_PAUSED: // 情况1:当前未暂停,点击后暂停所有 if (!stateAudios.allPaused) { // 先把每个音频的当前playing状态存起来 const previousState = stateAudios.audios.reduce((acc, audio) => { acc[audio.id] = audio.playing; return acc; }, {}); return { ...stateAudios, audios: stateAudios.audios.map(audio => ({ ...audio, playing: false // 全部暂停 })), allPaused: true, previousPlayingState: previousState }; } // 情况2:当前已暂停,点击后恢复之前的播放状态 else { return { ...stateAudios, audios: stateAudios.audios.map(audio => ({ ...audio, playing: stateAudios.previousPlayingState[audio.id] || false })), allPaused: false }; }
3. 用useEffect同步音频播放状态
原来的togglePlay直接操作audio会有状态不同步的问题,改成用useEffect监听state变化,自动控制音频:
// 先导入useEffect import { useRef, useReducer, useEffect } from "react"; // 在App组件里添加这个副作用 useEffect(() => { // 遍历所有音频,根据state里的playing状态控制播放/暂停 Object.entries(audioRefs).forEach(([id, audio]) => { const audioState = state.audios.find(a => a.id === Number(id)); if (audioState) { audioState.playing ? audio.play() : audio.pause(); } }); }, [state.audios]); // 当audios数组变化时触发 // 修改togglePlay,只保留dispatch,去掉直接操作audio的代码 const togglePlay = (id) => { dispatch({ type: audioActionType.TOGGLE_PLAY, id }); };
4. 修复代码里的小问题
- 路径里的
..//改成../,避免路径解析错误 - 原Reducer的TOGGLE_ALL_PAUSED分支缺少闭合的
},上面的代码已经修复
修改后的完整代码
import "./styles.css"; import { useRef, useReducer, useEffect } from "react"; const audioActionType = { TOGGLE_PLAY: "TOGGLE_PLAY", SET_VOLUME: "SET_VOLUME", TOGGLE_ALL_PAUSED: "TOGGLE_ALL_PAUSED", }; const initialAudioState = { audios: [ { playing: false, id: 1, volume: 25, title: "Coffe making", src: "../audio/espresso-machine.mp3", }, { playing: false, id: 2, volume: 25, title: "Coffe machine", src: "../audio/espresso-machine.mp3", }, ], allPaused: false, previousPlayingState: {}, }; function AudioReducer(stateAudios, actionAudios) { switch (actionAudios.type) { case audioActionType.TOGGLE_PLAY: return { ...stateAudios, audios: stateAudios.audios.map((audio) => audio.id === actionAudios.id ? { ...audio, playing: !audio.playing } : audio ), }; case audioActionType.SET_VOLUME: return { ...stateAudios, audios: stateAudios.audios.map((audio) => audio.id === actionAudios.id ? { ...audio, volume: actionAudios.volume } : audio ), }; case audioActionType.TOGGLE_ALL_PAUSED: if (!stateAudios.allPaused) { const previousState = stateAudios.audios.reduce((acc, audio) => { acc[audio.id] = audio.playing; return acc; }, {}); return { ...stateAudios, audios: stateAudios.audios.map((audio) => ({ ...audio, playing: false, })), allPaused: true, previousPlayingState: previousState, }; } else { return { ...stateAudios, audios: stateAudios.audios.map((audio) => ({ ...audio, playing: stateAudios.previousPlayingState[audio.id] || false, })), allPaused: false, }; } default: return stateAudios; } } export default function App() { const [state, dispatch] = useReducer(AudioReducer, initialAudioState); const audioRefs = useRef( state.audios.reduce((acc, audio) => { acc[audio.id] = new Audio(audio.src); return acc; }, {}) ).current; useEffect(() => { Object.entries(audioRefs).forEach(([id, audio]) => { const audioState = state.audios.find((a) => a.id === Number(id)); if (audioState) { audioState.playing ? audio.play() : audio.pause(); } }); }, [state.audios]); const togglePlay = (id) => { dispatch({ type: audioActionType.TOGGLE_PLAY, id }); }; const setVolume = (id, volume) => { dispatch({ type: audioActionType.SET_VOLUME, id, volume }); audioRefs[id].volume = volume / 100; }; return ( <div id="sounds-section" className="sections"> <input type="checkbox" id="pause" name="pause" checked={state.allPaused} onChange={() => dispatch({ type: audioActionType.TOGGLE_ALL_PAUSED })} /> <label htmlFor="pause"> Pause all</label> <div id="sounds"> {state.audios.map((audio) => ( <div className="sound" key={audio.id}> <div> <button onClick={() => togglePlay(audio.id)} dangerouslySetInnerHTML={{ __html: audio.playing ? "⏸" : "▶", }} /> <h4>{audio.title}</h4> </div> {audio.playing && ( <input className="volume" type="range" min="0" max="100" step="1" value={audio.volume} onChange={(e) => { setVolume(audio.id, e.target.value); }} /> )} </div> ))} </div> </div> ); }
内容的提问来源于stack exchange,提问作者amiindless
相关产品推荐
相关产品推荐

