You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 ? "&#x23F8;" : "&#x25B6;",
                }}
              />
              <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 ? "&#x23F8;" : "&#x25B6;",
                }}
              />
              <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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 06:05:56