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

Deno+Fresh中仅客户端打包CSR组件的问题求助

问题:Fresh框架中如何将仅支持客户端渲染的Monaco Editor函数与Island组件绑定

我是前端开发新手,编写了调用Monaco Editor API的JS函数:

export function setTheme(themeName) {
    fetch(`./themes/${themeName}.json`)
        .then(response => response.json())
        .then(themeData => {
            console.log(themeName)
            console.log(themeData)
            monaco.editor.defineTheme(themeName, themeData);
            monaco.editor.setTheme(themeName);
        })
}

该函数依赖Deno不支持的API,因此放在静态JS文件中。尝试将其与Fresh的Island组件绑定:

<Dropdown list={themeList} onChange={setTheme} />

但编译失败,服务器始终尝试进行服务端渲染(SSR),即使代码在Island组件内。尝试结合useEffect的方式:

useEffect(() => setTheme(themeName), []);

随后编写了setThm函数:

const setThm = (themeName: string) => {
    useEffect(() => setTheme(themeName), [themeName]);
}

改用普通函数也无效:

function setThm(themeName: string){
    useEffect(() => setTheme(themeName), []);
}

报错信息如下:

error: Uncaught (in promise) ReferenceError: document is not defined
document.addEventListener('DOMContentLoaded', () => {
^
    at file:///path/to/project/static/editor.js:2:1

    info: document global is not available in Deno.
    hint: Use a library like happy-dom, deno_dom, linkedom or JSDom
          and setup the document global according to the library documentation.
Watcher Process failed. Restarting on file change...

补充信息1

static/editor.js中的部分代码,因Monaco依赖window对象,无法在SSR代码中引入:

// The function to initialize the editor after the DOM has been loaded.
document.addEventListener('DOMContentLoaded', () => {
    const require = globalThis.require || {};

    require.config({
        paths: {
            'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs'
        },
    });

    require(['vs/editor/editor.main'], function(monaco) {
        globalThis.monaco = monaco;

        monaco.editor.create(document.getElementById('editor'), {
            value: '#include <iostream>\nusing namespace std;\n\nint main() {\n    std::cout << "Hello, world!" << std::endl;\n    return 0;\n}',
            language: 'cpp'
        });

        setTheme("github-light")

        // Bundle CSR-only functions with components.
        const themeSetter = document.getElementById('theme-setter');
        if (themeSetter){
            themeSetter.onchange = onThemeSetterSelect
            console.log("Theme setter is set")
        }
    });
});

当前临时方案是放弃与Fresh组件绑定,在DOM渲染完成后手动调用静态JS。

补充信息2

尝试用IS_BROWSER判断避免服务端执行:

// Initialize the editor, run CSR-only after the DOM is loaded.

import { useEffect } from "https://esm.sh/v128/preact@10.22.0/hooks/src/index.js";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { init } from "../static/editor.js"

export default function Init() {
    if (!IS_BROWSER) {return <></>}

    useEffect(() => {
        if (IS_BROWSER){
            init()
        }
    })

    return <></>
}

仍报错,因为导入../static/editor.js时,服务端会执行其中的document.addEventListener代码。


解决方案

核心问题是:导入静态JS文件时,服务端会执行文件顶层的代码,哪怕在组件里用IS_BROWSER和useEffect,导入阶段已经触发了document相关代码的执行。需要把CSR代码延迟到客户端真正运行时才加载、执行。

1. 重构静态JS文件,移除顶层DOM操作

把editor.js里的DOM操作和Monaco初始化逻辑封装成函数,不要在顶层直接执行:

// static/editor.js
export function initEditor() {
    return new Promise((resolve) => {
        // 检查DOM是否已加载,避免重复监听
        if (document.readyState === "complete" || document.readyState === "interactive") {
            initMonaco(resolve);
        } else {
            document.addEventListener('DOMContentLoaded', () => initMonaco(resolve));
        }
    });
}

function initMonaco(resolve) {
    const require = globalThis.require || {};

    require.config({
        paths: {
            'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs'
        },
    });

    require(['vs/editor/editor.main'], function(monaco) {
        globalThis.monaco = monaco;

        const editor = monaco.editor.create(document.getElementById('editor'), {
            value: '#include <iostream>\nusing namespace std;\n\nint main() {\n    std::cout << "Hello, world!" << std::endl;\n    return 0;\n}',
            language: 'cpp'
        });

        // 初始化默认主题
        setTheme("github-light");

        resolve({ monaco, editor });
    });
}

export function setTheme(themeName) {
    fetch(`./themes/${themeName}.json`)
        .then(response => response.json())
        .then(themeData => {
            if (globalThis.monaco) {
                monaco.editor.defineTheme(themeName, themeData);
                monaco.editor.setTheme(themeName);
            }
        })
}

2. 在Island组件中使用动态导入+条件执行

在Island组件里,不要直接导入静态JS,而是用动态导入结合IS_BROWSER,确保只有客户端才加载并执行代码:

// islands/EditorIsland.tsx
import { useEffect, useState } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";

export default function EditorIsland() {
    const [monacoApi, setMonacoApi] = useState(null);

    useEffect(() => {
        if (!IS_BROWSER) return;

        // 动态导入CSR代码,服务端不会执行这部分
        import("../static/editor.js").then(({ initEditor, setTheme }) => {
            initEditor().then(api => {
                setMonacoApi({ ...api, setTheme });
            });
        });
    }, []);

    // 主题切换处理函数
    const handleThemeChange = (themeName) => {
        if (monacoApi?.setTheme) {
            monacoApi.setTheme(themeName);
        }
    };

    return (
        <div>
            <div id="editor" style={{ width: "100%", height: "500px" }}></div>
            <select onChange={(e) => handleThemeChange(e.target.value)}>
                <option value="github-light">GitHub Light</option>
                <option value="github-dark">GitHub Dark</option>
                {/* 其他主题选项 */}
            </select>
        </div>
    );
}

3. 关键注意事项

  • 动态导入:import("../static/editor.js")只会在客户端执行,服务端解析组件时不会加载该文件,避免了顶层代码的执行。
  • 封装DOM逻辑:所有依赖document/window的逻辑都放在函数内部,不要在JS文件顶层直接执行,确保导入时不会触发报错。
  • 状态管理:用useState保存Monaco实例和相关函数,确保组件可以正常调用setTheme等方法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 05:29:51