梅山藏宝 發表於 2025-12-29 08:27:42

React封装UEditor富文本编辑器的实现步骤

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、基础架构:核心设计思路</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">1. 类型与状态管理(核心代码片段)</a></li></ul><li><a href="#_label1">二、核心功能:关键痛点解决</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_1">1. 实例生命周期管理(避免内存泄漏)</a></li><li><a href="#_lab2_1_2">2. 图片样式控制(解决尺寸混乱问题)</a></li><li><a href="#_lab2_1_3">3. 占位符功能(解决上传冲突)</a></li><li><a href="#_lab2_1_4">4. 内容同步(React 式状态管理)</a></li></ul><li><a href="#_label2">三、封装总结:核心思路提炼</a></li><ul class="second_class_ul"></ul></ul></div><p>UEditor 作为经典富文本编辑器,在后台系统中仍有广泛应用,但原生 UEditor 与 React 生态适配性差。本文剥离业务接口和冗余配置,聚焦核心封装逻辑,拆解从实例管理到图片处理的关键实现思路。</p>
<p class="maodian"><a name="_label0"></a></p><h2>一、基础架构:核心设计思路</h2>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>1. 类型与状态管理(核心代码片段)</h3>
<div class="jb51code"><pre class="brush:js;">import React, { useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from 'react';

// 核心状态管理(非响应式状态用useRef避免重渲染)
const UEditor = forwardRef((props, ref) =&gt; {
    const editorContainer = useRef&lt;HTMLDivElement&gt;(null); // 编辑器容器
    const ueInstance = useRef&lt;any&gt;(null); // UEditor实例
    const isPlaceholderActive = useRef(false); // 占位符状态
    const isUploading = useRef(false); // 上传状态标记
   
    // 解构核心props(仅保留通用配置)
    const {
      value = '',
      onChange,
      style,
      placeholder = '请输入内容...',
      onBlur
    } = props;

    // 对外暴露核心方法(关键:让父组件能操作编辑器)
    useImperativeHandle(ref, () =&gt; ({
      getContent: () =&gt; {
            const content = ueInstance.current?.getContent() || '';
            return isPlaceholderActive.current ? '' : content;
      },
      setContent: (content: string) =&gt; {
            // 内容设置逻辑(后文详解)
      },
      forceImageFixedSize: () =&gt; {
            // 图片尺寸修正逻辑(后文详解)
      }
    }));
});</pre></div>
<p><strong>关键设计点</strong>:</p>
<ul><li>用<code>forwardRef</code>+<code>useImperativeHandle</code>暴露编辑器核心方法,满足父组件自定义操作需求;</li><li>用<code>useRef</code>管理 UEditor 实例、状态标记等非响应式数据,避免状态变化触发组件重渲染;</li><li>仅保留通用 Props,剥离业务化配置,保证组件通用性。</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>二、核心功能:关键痛点解决</h2>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>1. 实例生命周期管理(避免内存泄漏)</h3>
<div class="jb51code"><pre class="brush:js;">// 动态加载UEditor脚本(核心:按需加载,避免重复加载)
const loadScript = useCallback((src: string) =&gt; {
    return new Promise((resolve, reject) =&gt; {
      if (document.querySelector(`script`)) return resolve(true);
      
      const script = document.createElement('script');
      script.src = src;
      script.onload = () =&gt; resolve(true);
      script.onerror = () =&gt; reject(new Error(`加载失败: ${src}`));
      document.body.appendChild(script);
    });
}, []);

// 安全销毁编辑器(关键:卸载时彻底清理)
const safeDestroyEditor = useCallback(() =&gt; {
    if (!ueInstance.current) return;
   
    try {
      // 移除所有事件监听器
      const listeners = ['contentChange', 'blur', 'afterUpload'];
      listeners.forEach(event =&gt; {
            ueInstance.current.removeAllListeners?.(event);
      });
      // 从UEditor全局实例中移除
      window.UE?.delEditor(ueInstance.current.key);
      ueInstance.current = null;
    } catch (error) {
      console.warn('UEditor销毁失败:', error);
    }
}, []);

// 生命周期绑定(核心:组件挂载初始化,卸载销毁)
useEffect(() =&gt; {
    // 初始化逻辑(后文详解)
    initEditor();
    return () =&gt; safeDestroyEditor();
}, );</pre></div>
<p><strong>关键要点</strong>:</p>
<ul><li>动态加载脚本:避免 UEditor 资源全局引入,按需加载且防止重复加载;</li><li>销毁逻辑:不仅销毁实例,还要移除所有事件监听器,彻底避免内存泄漏;</li><li>生命周期绑定:通过<code>useEffect</code>将实例创建 / 销毁与组件生命周期严格同步。</li></ul>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2. 图片样式控制(解决尺寸混乱问题)</h3>
<div class="jb51code"><pre class="brush:js;">// 图片尺寸统一修正(核心:清除原生样式,应用响应式规则)
const safeImageSizeFix = useCallback((force = false) =&gt; {
    if (!ueInstance.current) return;
   
    try {
      const doc = ueInstance.current.document || document;
      const imgElements = doc.body.querySelectorAll('img');
      
      imgElements.forEach((img: HTMLImageElement) =&gt; {
            // 清除UEditor原生添加的宽高样式/属性
            img.style.width = '';
            img.style.height = '';
            img.removeAttribute('width');
            img.removeAttribute('height');
            
            // 应用统一的响应式样式
            img.style.maxWidth = '100%';
            img.style.height = 'auto';
      });
    } catch (error) {
      console.warn('图片尺寸修正失败:', error);
    }
}, []);

// 监听图片插入事件(核心:插入/上传后自动修正)
const setupImageHandlers = useCallback(() =&gt; {
    if (!ueInstance.current) return;
   
    // 图片插入/上传后触发修正
    ueInstance.current.addListener('afterInsertImage', () =&gt; {
      setTimeout(() =&gt; safeImageSizeFix(true), 100);
    });
   
    // 覆盖原生插入方法,确保样式生效
    const originalInsertImage = ueInstance.current.insertImage;
    ueInstance.current.insertImage = function (...args) {
      const result = originalInsertImage.call(this, ...args);
      setTimeout(() =&gt; safeImageSizeFix(true), 50);
      return result;
    };
}, );</pre></div>
<p><strong>核心解决思路</strong>:</p>
<ul><li>清除原生样式:UEditor 插入图片会自动添加宽高属性 / 样式,需彻底清除;</li><li>响应式样式:统一设置<code>maxWidth: 100%</code>+<code>height: auto</code>,保证图片适配容器;</li><li>事件监听:图片插入 / 上传后自动触发修正,覆盖原生方法确保无遗漏。</li></ul>
<p class="maodian"><a name="_lab2_1_3"></a></p><h3>3. 占位符功能(解决上传冲突)</h3>
<div class="jb51code"><pre class="brush:js;">// 设置占位符(核心:自定义样式,标记占位状态)
const setPlaceholderText = useCallback(() =&gt; {
    if (!ueInstance.current) return;
    const placeholderHtml = `&lt;p style="color: #999; font-style: italic;"&gt;${placeholder}&lt;/p&gt;`;
    ueInstance.current.setContent(placeholderHtml);
    isPlaceholderActive.current = true;
    onChange?.('');
}, );

// 上传前激活编辑器(关键:避免占位符干扰上传)
const activateEditorForUpload = useCallback(() =&gt; {
    if (!ueInstance.current || !isPlaceholderActive.current) return;
   
    // 清除占位符,插入零宽空格(有内容但不显示)
    ueInstance.current.setContent('&lt;p&gt;&amp;#8203;&lt;/p&gt;');
    isPlaceholderActive.current = false;
    isUploading.current = true;
   
    // 超时重置上传状态(安全措施)
    setTimeout(() =&gt; {
      isUploading.current = false;
    }, 3000);
}, );

// 焦点/失焦处理(核心:区分上传状态,避免冲突)
const setupPlaceholderHandlers = useCallback(() =&gt; {
    if (!ueInstance.current) return;
   
    // 聚焦时清除占位符(非上传场景)
    ueInstance.current.addListener('focus', () =&gt; {
      if (!isUploading.current &amp;&amp; isPlaceholderActive.current) {
            ueInstance.current.setContent('');
            isPlaceholderActive.current = false;
      }
    });
   
    // 失焦时显示占位符(非上传场景)
    ueInstance.current.addListener('blur', () =&gt; {
      if (isUploading.current) return;
      
      const content = ueInstance.current.getContent();
      if (!content || content === '&lt;p&gt;&lt;br&gt;&lt;/p&gt;') {
            setPlaceholderText();
      }
      onBlur?.();
    });
}, );</pre></div>
<p><strong>核心痛点解决</strong>:</p>
<ul><li>上传冲突:占位符状态下编辑器无实际内容,上传会失败,需在上传前插入零宽空格;</li><li>状态区分:标记上传状态,焦点 / 失焦逻辑中跳过占位符处理,避免干扰上传;</li><li>体验优化:占位符样式自定义,失焦时自动显示,聚焦时自动清除。</li></ul>
<p class="maodian"><a name="_lab2_1_4"></a></p><h3>4. 内容同步(React 式状态管理)</h3>
<div class="jb51code"><pre class="brush:js;">// 初始化编辑器(核心:内容初始化+事件监听)
const initEditor = useCallback(async () =&gt; {
    if (!editorContainer.current) return;
   
    // 加载UEditor核心脚本(ueditor.config.js/ueditor.all.min.js等)
    await loadScript('/UEditor/ueditor.config.js');
    await loadScript('/UEditor/ueditor.all.min.js');
   
    // 创建UEditor实例
    ueInstance.current = window.UE.getEditor(editorContainer.current.id, {
      initialFrameHeight: 240,
      autoHeightEnabled: false,
      enableAutoSave: false // 禁用自动保存,避免与React状态冲突
    });
   
    // 编辑器就绪后初始化内容和事件
    ueInstance.current.addListener('ready', () =&gt; {
      // 初始化内容:有值则设置,无值则显示占位符
      if (value) {
            ueInstance.current.setContent(value);
      } else {
            setPlaceholderText();
      }
      
      // 监听内容变化,同步到React状态
      ueInstance.current.addListener('contentChange', () =&gt; {
            if (isPlaceholderActive.current || isUploading.current) return;
            
            const content = ueInstance.current.getContent();
            onChange?.(content);
      });
      
      // 初始化图片处理、占位符、上传等逻辑
      setupImageHandlers();
      setupPlaceholderHandlers();
    });
}, );

// 监听外部value变化(核心:同步父组件状态到编辑器)
useEffect(() =&gt; {
    if (!ueInstance.current?.isReady) return;
   
    const currentContent = ueInstance.current.getContent();
    if (value !== currentContent &amp;&amp; !isPlaceholderActive.current) {
      ueInstance.current.setContent(value);
      // 内容变化后修正图片尺寸
      setTimeout(() =&gt; safeImageSizeFix(true), 50);
    }
}, );</pre></div>
<p><strong>核心同步逻辑</strong>:</p>
<ul><li>编辑器&rarr;React:监听<code>contentChange</code>事件,过滤占位符 / 上传状态,同步内容到父组件;</li><li>React&rarr;编辑器:监听<code>value</code>变化,实时更新编辑器内容,同时修正图片尺寸;</li><li>初始化:编辑器就绪后根据<code>value</code>初始化内容,保证初始状态一致。</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>三、封装总结:核心思路提炼</h2>
<p>本次封装并非重写 UEditor,而是<strong>适配 React 生态规则</strong>,核心思路可总结为:</p>
<ol><li><strong>生命周期同步</strong>:实例创建 / 销毁与组件挂载 / 卸载绑定,避免内存泄漏;</li><li><strong>状态隔离</strong>:非响应式状态用<code>useRef</code>管理,响应式状态通过<code>props</code>/<code>onChange</code>同步;</li><li><strong>痛点针对性解决</strong>:<ul><li>图片样式:清除原生样式 + 统一响应式规则 + 事件监听自动修正;</li><li>占位符:上传前激活编辑器,区分上传状态避免冲突;</li><li>内容同步:双向绑定,兼顾 React 状态和 UEditor 原生内容;</li></ul></li><li><strong>扩展与通用</strong>:暴露核心方法,剥离业务配置,保证组件复用性。</li></ol>
<p>该思路同样适用于其他原生 JS 编辑器(如 CKEditor、KindEditor)向 React 的适配,核心是遵循 React 的开发范式,将原生库能力封装为符合 React 习惯的组件。</p>
頁: [1]
查看完整版本: React封装UEditor富文本编辑器的实现步骤