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

React落地页跳转返回后音画状态异常无法点击停止问题求解

问题原因
  • 页面跳转时LandingPage组件会完全卸载,但你初始化的Audio实例没有被暂停、也没有被销毁,会一直在后台播放。返回LP时组件重新挂载,所有useState存储的播放、动效状态都回到初始false,新组件创建的Audio实例和后台正在播放的旧实例完全独立,所以点击无法停止原有音频,UI动效也和实际播放状态不匹配。
  • 重复定义了8份逻辑完全一致的useAudio钩子和动效状态,冗余代码多,后续维护成本高。
  • 没有做状态持久化,组件卸载重挂载后状态丢失,无法和正在播放的音频同步。
修复步骤

1. 封装通用可复用的useAudio钩子

替换掉原来8个重复的useAudio定义,新钩子同时支持状态持久化、事件监听正确清理:

const useAudio = (src, storageKey) => {
  // 初始化时优先读取本地存储的播放状态,保证返回页面时状态一致
  const [playing, setPlaying] = useState(() => {
    return JSON.parse(sessionStorage.getItem(storageKey) || 'false')
  });
  const [audio] = useState(() => new Audio(src));

  const toggle = () => setPlaying(prev => !prev);

  useEffect(() => {
    audio.loop = true;
    playing ? audio.play() : audio.pause();
    // 状态变化时同步写入本地存储
    sessionStorage.setItem(storageKey, JSON.stringify(playing))
  }, [playing, audio, storageKey]);

  useEffect(() => {
    const handleEnded = () => setPlaying(false);
    audio.addEventListener('ended', handleEnded);
    return () => {
      audio.removeEventListener('ended', handleEnded);
      // 如果需要跳转离开LP时自动暂停音频,取消下一行的注释即可
      // audio.pause();
    };
  }, [audio]);

  return [playing, toggle];
};

2. 统一管理音画配置,消除重复代码

把所有图片、音频的对应关系存在统一配置数组里,不用再写8份重复的state和切换函数:

// 所有音画组合配置,新增/修改项只需要改这个数组
const MEDIA_CONFIG = [
  { id: 'bell', img: Bell, audio: BellAudio, storageKey: 'lp_bell' },
  { id: 'boat', img: Boat, audio: FuneAudio, storageKey: 'lp_boat' },
  { id: 'grass', img: Grass, audio: GrassAudio, storageKey: 'lp_grass' },
  { id: 'pingpong', img: Pingpong, audio: PingpongAudio, storageKey: 'lp_pingpong' },
  { id: 'bus', img: Bus, audio: BusAudio, storageKey: 'lp_bus' },
  { id: 'macba', img: Macba01, audio: MacbaAudio, storageKey: 'lp_macba' },
  { id: 'metro', img: Metro, audio: InsideMetro, storageKey: 'lp_metro' },
  { id: 'morningSun', img: MorningSun, audio: MorningSunAudio, storageKey: 'lp_morning' },
]

3. 重写LandingPage组件逻辑

替换原来写了8组state、toggle的冗余代码,用统一逻辑处理点击、状态同步:

const LandingPage = () => {
  // 初始化动效激活状态,从本地存储读取保证和音频状态一致
  const [activeMap, setActiveMap] = useState(() => {
    const initState = {}
    MEDIA_CONFIG.forEach(item => {
      initState[item.id] = JSON.parse(sessionStorage.getItem(item.storageKey) || 'false')
    })
    return initState
  })

  // 生成所有音频的控制器
  const audioCtrls = MEDIA_CONFIG.map(config => {
    const [playing, toggle] = useAudio(config.audio, config.storageKey)
    return { id: config.id, playing, toggle }
  })

  // 统一点击处理:同时切换音频和动效状态
  const handleClick = (targetId) => {
    const targetCtrl = audioCtrls.find(ctrl => ctrl.id === targetId)
    targetCtrl?.toggle()
    setActiveMap(prev => ({...prev, [targetId]: !prev[targetId]}))
  }

  // 按照原有布局排列元素
  const firstRow = [
    { img: Blank, isBlank: true },
    { ...MEDIA_CONFIG.find(i => i.id === 'bell'), ...audioCtrls.find(c => c.id === 'bell') },
    { img: Blank, isBlank: true },
    { ...MEDIA_CONFIG.find(i => i.id === 'boat'), ...audioCtrls.find(c => c.id === 'boat') },
    { ...MEDIA_CONFIG.find(i => i.id === 'grass'), ...audioCtrls.find(c => c.id === 'grass') },
    { ...MEDIA_CONFIG.find(i => i.id === 'pingpong'), ...audioCtrls.find(c => c.id === 'pingpong') },
  ]
  const secondRow = [
    { ...MEDIA_CONFIG.find(i => i.id === 'bus'), ...audioCtrls.find(c => c.id === 'bus') },
    { img: Blank, isBlank: true },
    { ...MEDIA_CONFIG.find(i => i.id === 'macba'), ...audioCtrls.find(c => c.id === 'macba') },
    { ...MEDIA_CONFIG.find(i => i.id === 'metro'), ...audioCtrls.find(c => c.id === 'metro') },
    { ...MEDIA_CONFIG.find(i => i.id === 'morningSun'), ...audioCtrls.find(c => c.id === 'morningSun') },
    { img: Blank, isBlank: true },
  ]

  return (
    <>
      <h2 id="pleaseClick">Please click on the images!!</h2>
      <Container className="lpPhotos">
        <Row id="item1">
          {firstRow.map((item, idx) => (
            <Col md={2} sm={2} xs={2} key={item.id || `blank-${idx}`}>
              {item.isBlank ? (
                <img src={item.img} id="blank" className="img-fluid" alt="blank" />
              ) : (
                <img
                  src={item.img}
                  id={activeMap[item.id] ? item.id : ""}
                  className="img-fluid"
                  alt={item.id}
                  onClick={() => handleClick(item.id)}
                />
              )}
            </Col>
          ))}
        </Row>
        <Row id="item2">
          {secondRow.map((item, idx) => (
            <Col md={2} sm={2} xs={2} key={item.id || `blank-${idx}`}>
              {item.isBlank ? (
                <img src={item.img} id="blank" className="img-fluid" alt="blank" />
              ) : (
                <img
                  src={item.img}
                  id={activeMap[item.id] ? item.id : ""}
                  className="img-fluid"
                  alt={item.id}
                  onClick={() => handleClick(item.id)}
                />
              )}
            </Col>
          ))}
        </Row>
      </Container>
    </>
  )
}
效果说明
  • 跳转其他页面再返回LP时,会自动从sessionStorage读取之前的播放状态,图片动效和音频播放完全同步,点击可以正常停止/启动音画效果
  • 如果需要跳转离开LP时自动暂停所有音频,只需要把useAudio钩子清理函数里的audio.pause()注释取消即可
  • 修复了原有代码中事件监听无法正确移除的内存泄漏问题,后续新增音画项只需要在MEDIA_CONFIG数组里加配置即可,不需要重复写状态和钩子逻辑

内容的提问来源于stack exchange,提问作者Y.F

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 05:01:01