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

createPortal新窗口无法渲染CustomQuillEditor的修复方案

修复Quill编辑器在新窗口Portal中空白的问题

问题背景

实现了NewWindowPortal组件用于在新窗口渲染内容,普通组件能正常显示,但传入CustomQuillEditor时,新窗口打开后仅显示空白文档,无内容渲染。

原NewWindowPortal组件代码

export const NewWindowPortal = ({ children }: { children: (props) => ReactNode }) => {
    const ref = useRef(null);
    const containerEl = useMemo(() => document.createElement("div"), []);

    useEffect(() => {
        const externalWindow = window.open("", "", "width=600,height=400,left=200,top=200");
        externalWindow?.document?.body?.appendChild(containerEl);
    }, []);

    return (
        <>
            <BodyShort>This is a new window</BodyShort>
            {createPortal(children({ ref }), containerEl)}
        </>
    );
};

组件调用代码

{openInNewWindow && (
    <NewWindowPortal>
        {(props) => (
            <CustomQuillEditor
                ref={props.ref}
                resize={resize}
                readOnly={lesemodus || readOnly}
                defaultValue={reformatText(value)}
                onTextChange={onTextChange}
            />
        )}
    </NewWindowPortal>
)}

原CustomQuillEditor组件代码

export const CustomQuillEditor = ({ readOnly, defaultValue, onTextChange, ref, resize }: EditorProps) => {
    const containerRef = useRef(null);

    useEffect(() => {
        const container = containerRef.current;
        const editorContainer = container.appendChild(container.ownerDocument.createElement("div"));
        const quill = new Quill(editorContainer, {
            theme: "snow",
            readOnly,
            modules: {
                history: {},

                toolbar: readOnly
                    ? false
                    : {
                          container: [
                              ["bold", "italic", "underline", { header: 3 }],
                              // [{ 'color': "red" }, { 'background': "yellow" }]
                          ],
                      },
                clipboard: {
                    allowed: {
                        tags: ["strong", "h3", "h4", "em", "p", "br", "span", "u"],
                        // attributes: ['href', 'rel', 'target', 'class', "style"]
                        attributes: [],
                    },
                    customButtons: [],
                    keepSelection: true,
                    substituteBlockElements: true,
                    magicPasteLinks: false,
                    removeConsecutiveSubstitutionTags: false,
                },
            },
        });

        ref.current = quill;

        if (defaultValue) {
            const delta = quill.clipboard.convert({ html: defaultValue });
            quill.setContents(delta, "silent");
            quill.history.clear();
        }

        quill.on(Quill.events.TEXT_CHANGE, () => {
            if (quill.getLength() <= 1) {
                onTextChange("");
            } else {
                onTextChange(quill.getSemanticHTML().replaceAll("<p></p>", "<p><br/></p>"));
            }
        });

        return () => {
            ref.current = null;
            container.innerHTML = "";
        };
    }, [ref]);

    return (
        <div
            spellCheck={false}
            className={`ql-top-container ${readOnly ? "readonly" : ""} ${resize ? "resizable" : ""}`}
            ref={containerRef}
        ></div>
    );
};

问题原因

  1. 文档上下文不匹配:组件通过Portal挂载到新窗口后,容器的文档上下文切换为新窗口的document,但原useEffect依赖仅为ref,不会重新初始化编辑器,导致Quill实例绑定到原页面document,无法在新窗口渲染。
  2. 依赖项缺失:useEffect依赖数组只包含ref,readOnly、defaultValue等关键props变化时不会触发编辑器重新初始化,更新逻辑异常。
  3. 事件监听未清理:未移除Quill的TEXT_CHANGE事件监听,可能引发内存泄漏。

修复方案

修复后的CustomQuillEditor组件

export const CustomQuillEditor = ({ readOnly, defaultValue, onTextChange, ref, resize }: EditorProps) => {
    const containerRef = useRef(null);

    useEffect(() => {
        const container = containerRef.current;
        if (!container) return;

        // 使用容器当前所在的文档创建编辑器容器,适配新窗口的document上下文
        const editorContainer = container.ownerDocument.createElement("div");
        container.appendChild(editorContainer);

        const quill = new Quill(editorContainer, {
            theme: "snow",
            readOnly,
            modules: {
                history: {},
                toolbar: readOnly
                    ? false
                    : {
                          container: [
                              ["bold", "italic", "underline", { header: 3 }],
                          ],
                      },
                clipboard: {
                    allowed: {
                        tags: ["strong", "h3", "h4", "em", "p", "br", "span", "u"],
                        attributes: [],
                    },
                    customButtons: [],
                    keepSelection: true,
                    substituteBlockElements: true,
                    magicPasteLinks: false,
                    removeConsecutiveSubstitutionTags: false,
                },
            },
        });

        ref.current = quill;

        if (defaultValue) {
            const delta = quill.clipboard.convert({ html: defaultValue });
            quill.setContents(delta, "silent");
            quill.history.clear();
        }

        // 提取事件处理函数,方便后续清理
        const handleTextChange = () => {
            if (quill.getLength() <= 1) {
                onTextChange("");
            } else {
                onTextChange(quill.getSemanticHTML().replaceAll("<p></p>", "<p><br/></p>"));
            }
        };

        quill.on(Quill.events.TEXT_CHANGE, handleTextChange);

        return () => {
            ref.current = null;
            // 移除事件监听,避免内存泄漏
            quill.off(Quill.events.TEXT_CHANGE, handleTextChange);
            container.innerHTML = "";
        };
    }, [ref, readOnly, defaultValue, onTextChange]); // 添加所有相关依赖,确保props变化时重新初始化编辑器

    return (
        <div
            spellCheck={false}
            className={`ql-top-container ${readOnly ? "readonly" : ""} ${resize ? "resizable" : ""}`}
            ref={containerRef}
        ></div>
    );
};

额外优化:给新窗口注入Quill样式

新窗口的document未加载原页面的Quill CSS,可能导致编辑器样式丢失,可在NewWindowPortal中添加样式注入逻辑:

export const NewWindowPortal = ({ children }: { children: (props) => ReactNode }) => {
    const ref = useRef(null);
    const containerEl = useMemo(() => document.createElement("div"), []);

    useEffect(() => {
        const externalWindow = window.open("", "", "width=600,height=400,left=200,top=200");
        if (!externalWindow) return;

        // 注入Quill Snow主题样式,替换为你项目中实际的样式路径
        const styleLink = externalWindow.document.createElement('link');
        styleLink.rel = 'stylesheet';
        styleLink.href = 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
        externalWindow.document.head.appendChild(styleLink);

        externalWindow.document.body.appendChild(containerEl);

        // 窗口关闭时清理资源
        const handleUnload = () => {
            containerEl.remove();
        };
        externalWindow.addEventListener('unload', handleUnload);

        return () => {
            externalWindow.removeEventListener('unload', handleUnload);
            externalWindow.close();
        };
    }, []);

    return (
        <>
            <BodyShort>This is a new window</BodyShort>
            {createPortal(children({ ref }), containerEl)}
        </>
    );
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 16:10:55