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

