如何在React useEffect执行前运行代码?DOOM游戏组件加载故障
解决Mendix React组件中DOOM JS API启动失败的问题
问题背景
我找到一款用于DOOM的JavaScript API,在HTML环境中可正常运行,但将其作为Mendix组件在React中实现时遇到问题:useEffect总是优先加载脚本,导致游戏无法正常启动。尝试ChatGPT提供的解决方案后,立即出现客户端脚本错误,现寻求可行解决办法。
现有尝试代码
第一段代码
import React, { useEffect } from 'react'; export function DoomGame({ sampleText }) { useEffect(() => { const script = document.createElement('script'); script.src = "https://js-dos.com/cdn/js-dos-api.js"; script.async = true; script.onload = () => { const dosbox = new window.Dosbox({ id: "dosbox", onload: function (dosbox) { dosbox.run("upload/DOOM-@evilution.zip", "./doom"); }, onrun: function (dosbox, app) { console.log("App '" + app + "' is run"); } }); }; document.body.appendChild(script); return () => { document.body.removeChild(script); }; }, []); const handleFullscreen = () => { if (window.dosbox) { window.dosbox.requestFullScreen(); } }; return ( <> <div id="dosbox"></div> <br/> <button onClick={handleFullscreen}>Make fullscreen</button> </> ); }
第二段代码(ChatGPT提供方案)
import React, { useEffect, useRef } from 'react'; export function DoomGame() { const dosboxRef = useRef(null); useEffect(() => { // Load jQuery const jqueryScript = document.createElement('script'); jqueryScript.src = "https://code.jquery.com/jquery-3.6.0.min.js"; jqueryScript.async = true; jqueryScript.onload = () => { // Load js-dos-api.js after jQuery is loaded const dosboxScript = document.createElement('script'); dosboxScript.src = "https://js-dos.com/cdn/js-dos-api.js"; dosboxScript.async = true; dosboxScript.onload = () => { const dosbox = new window.Dosbox({ id: "dosbox", onload: function (dosbox) { dosbox.run("upload/DOOM-@evilution.zip", "./doom"); }, onrun: function (dosbox, app) { console.log("App '" + app + "' is run"); } }); window.dosboxInstance = dosbox; // Store dosbox instance globally to use in handleFullscreen }; document.body.appendChild(dosboxScript); }; document.body.appendChild(jqueryScript); return () => { document.body.removeChild(jqueryScript); }; }, []); const handleFullscreen = () => { const dosboxInstance = window.dosboxInstance; if (dosboxInstance) { dosboxInstance.requestFullScreen(); } }; return ( <div> <div id="dosbox" ref={dosboxRef} className="dosbox-container"></div> <br /> <button onClick={handleFullscreen}>Make fullscreen</button> </div> ); }
问题分析
- DOM就绪与脚本加载顺序冲突:useEffect执行时,React虽已渲染
#dosbox元素,但Mendix组件生命周期中可能存在DOM未完全挂载的情况,导致Dosbox初始化找不到目标容器。 - 全局实例管理混乱:直接将Dosbox实例挂载到
window,在Mendix多组件环境易引发冲突,且组件卸载时未清理实例,造成内存泄漏。 - 脚本加载可靠性不足:
async模式下脚本加载顺序不可控,嵌套加载jQuery和Dosbox脚本可能因网络或Mendix资源策略导致加载失败。 - 组件清理不完整:仅移除jQuery脚本,未清理Dosbox实例和关联DOM元素,导致重复初始化错误。
可行解决方案
方案1:确保DOM就绪后初始化Dosbox
import React, { useEffect, useRef } from 'react'; export function DoomGame() { const dosboxContainerRef = useRef(null); let dosboxInstance = null; useEffect(() => { if (!dosboxContainerRef.current) return; // 避免重复加载脚本 if (window.Dosbox) { initDosbox(); return; } const script = document.createElement('script'); script.src = "https://js-dos.com/cdn/js-dos-api.js"; script.onload = initDosbox; script.onerror = () => console.error("Dosbox API脚本加载失败"); // 将脚本添加到组件容器内,避免全局冲突 dosboxContainerRef.current.appendChild(script); return () => { // 销毁Dosbox实例 if (dosboxInstance) { dosboxInstance.destroy(); dosboxInstance = null; } // 移除脚本 if (script.parentNode) { script.parentNode.removeChild(script); } }; }, []); const initDosbox = () => { dosboxInstance = new window.Dosbox({ element: dosboxContainerRef.current, // 直接传入DOM元素,避免ID查找 onload: function (dosbox) { dosbox.run("upload/DOOM-@evilution.zip", "./doom") .catch(err => console.error("DOOM启动失败:", err)); }, onrun: function (dosbox, app) { console.log(`应用'${app}'已启动`); } }); }; const handleFullscreen = () => { if (dosboxInstance) { try { dosboxInstance.requestFullScreen(); } catch (err) { console.error("全屏切换失败:", err); } } }; return ( <div> <div ref={dosboxContainerRef} className="dosbox-container" style={{width: '800px', height: '600px'}}></div> <br /> <button onClick={handleFullscreen}>全屏模式</button> </div> ); }
方案2:使用动态导入替代手动创建脚本
import React, { useEffect, useRef, useState } from 'react'; export function DoomGame() { const dosboxContainerRef = useRef(null); const [isLoading, setIsLoading] = useState(true); let dosboxInstance = null; useEffect(() => { if (!dosboxContainerRef.current) return; const loadDosbox = async () => { try { // 动态加载Dosbox API await import("https://js-dos.com/cdn/js-dos-api.js"); dosboxInstance = new window.Dosbox({ element: dosboxContainerRef.current, onload: (dosbox) => { dosbox.run("upload/DOOM-@evilution.zip", "./doom") .catch(err => console.error("DOOM启动失败:", err)); }, onrun: (dosbox, app) => { console.log(`应用'${app}'已启动`); setIsLoading(false); } }); } catch (err) { console.error("Dosbox API加载失败:", err); setIsLoading(false); } }; loadDosbox(); return () => { if (dosboxInstance) { dosboxInstance.destroy(); } }; }, []); const handleFullscreen = () => { if (dosboxInstance) { try { dosboxInstance.requestFullScreen(); } catch (err) { console.error("全屏切换失败:", err); } } }; return ( <div> {isLoading && <div>加载中...</div>} <div ref={dosboxContainerRef} className="dosbox-container" style={{width: '800px', height: '600px'}}></div> <br /> {!isLoading && <button onClick={handleFullscreen}>全屏模式</button>} </div> ); }
关键优化点
- 直接传入DOM元素:避免ID查找,解决Mendix中DOM未就绪或ID冲突问题。
- 完善错误捕获:覆盖脚本加载、游戏启动、全屏切换等环节的错误,便于排查。
- 实例生命周期管理:组件卸载时销毁Dosbox实例,防止内存泄漏和重复初始化。
- 避免重复加载:检查
window.Dosbox是否存在,防止多次加载脚本。 - 组件内实例存储:用组件内变量替代全局
window,避免多组件冲突。
额外注意事项
- Mendix资源路径配置:将
DOOM-@evilution.zip上传为Mendix静态资源,使用正确的相对路径或资源URL。 - CORS设置:若游戏包存于外部服务器,需确保服务器允许CORS访问。
- 容器样式:给
dosbox-container设置固定宽高,否则Dosbox无法正常渲染。
内容的提问来源于stack exchange,提问作者user25418053
相关产品推荐
相关产品推荐

