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
相关产品推荐
相关产品推荐

