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

如何在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>
    );
}

问题分析

  1. DOM就绪与脚本加载顺序冲突:useEffect执行时,React虽已渲染#dosbox元素,但Mendix组件生命周期中可能存在DOM未完全挂载的情况,导致Dosbox初始化找不到目标容器。
  2. 全局实例管理混乱:直接将Dosbox实例挂载到window,在Mendix多组件环境易引发冲突,且组件卸载时未清理实例,造成内存泄漏。
  3. 脚本加载可靠性不足:async模式下脚本加载顺序不可控,嵌套加载jQuery和Dosbox脚本可能因网络或Mendix资源策略导致加载失败。
  4. 组件清理不完整:仅移除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,避免多组件冲突。

额外注意事项

  1. Mendix资源路径配置:将DOOM-@evilution.zip上传为Mendix静态资源,使用正确的相对路径或资源URL。
  2. CORS设置:若游戏包存于外部服务器,需确保服务器允许CORS访问。
  3. 容器样式:给dosbox-container设置固定宽高,否则Dosbox无法正常渲染。

内容的提问来源于stack exchange,提问作者user25418053

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 21:52:07