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

 更新时间:2025年12月29日 08:27:42   作者:萌新梦  
本文主要介绍了React封装UEditor富文本编辑器,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

UEditor 作为经典富文本编辑器,在后台系统中仍有广泛应用,但原生 UEditor 与 React 生态适配性差。本文剥离业务接口和冗余配置,聚焦核心封装逻辑,拆解从实例管理到图片处理的关键实现思路。

一、基础架构:核心设计思路

1. 类型与状态管理(核心代码片段)

import React, { useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from 'react';
 
// 核心状态管理(非响应式状态用useRef避免重渲染)
const UEditor = forwardRef((props, ref) => {
    const editorContainer = useRef<HTMLDivElement>(null); // 编辑器容器
    const ueInstance = useRef<any>(null); // UEditor实例
    const isPlaceholderActive = useRef(false); // 占位符状态
    const isUploading = useRef(false); // 上传状态标记
    
    // 解构核心props(仅保留通用配置)
    const {
        value = '',
        onChange,
        style,
        placeholder = '请输入内容...',
        onBlur
    } = props;
 
    // 对外暴露核心方法(关键:让父组件能操作编辑器)
    useImperativeHandle(ref, () => ({
        getContent: () => {
            const content = ueInstance.current?.getContent() || '';
            return isPlaceholderActive.current ? '' : content;
        },
        setContent: (content: string) => {
            // 内容设置逻辑(后文详解)
        },
        forceImageFixedSize: () => {
            // 图片尺寸修正逻辑(后文详解)
        }
    }));
});

关键设计点

  • forwardRef+useImperativeHandle暴露编辑器核心方法,满足父组件自定义操作需求;
  • useRef管理 UEditor 实例、状态标记等非响应式数据,避免状态变化触发组件重渲染;
  • 仅保留通用 Props,剥离业务化配置,保证组件通用性。

二、核心功能:关键痛点解决

1. 实例生命周期管理(避免内存泄漏)

// 动态加载UEditor脚本(核心:按需加载,避免重复加载)
const loadScript = useCallback((src: string) => {
    return new Promise((resolve, reject) => {
        if (document.querySelector(`script[src="${src}"]`)) return resolve(true);
        
        const script = document.createElement('script');
        script.src = src;
        script.onload = () => resolve(true);
        script.onerror = () => reject(new Error(`加载失败: ${src}`));
        document.body.appendChild(script);
    });
}, []);
 
// 安全销毁编辑器(关键:卸载时彻底清理)
const safeDestroyEditor = useCallback(() => {
    if (!ueInstance.current) return;
    
    try {
        // 移除所有事件监听器
        const listeners = ['contentChange', 'blur', 'afterUpload'];
        listeners.forEach(event => {
            ueInstance.current.removeAllListeners?.(event);
        });
        // 从UEditor全局实例中移除
        window.UE?.delEditor(ueInstance.current.key);
        ueInstance.current = null;
    } catch (error) {
        console.warn('UEditor销毁失败:', error);
    }
}, []);
 
// 生命周期绑定(核心:组件挂载初始化,卸载销毁)
useEffect(() => {
    // 初始化逻辑(后文详解)
    initEditor();
    return () => safeDestroyEditor();
}, [initEditor, safeDestroyEditor]);

关键要点

  • 动态加载脚本:避免 UEditor 资源全局引入,按需加载且防止重复加载;
  • 销毁逻辑:不仅销毁实例,还要移除所有事件监听器,彻底避免内存泄漏;
  • 生命周期绑定:通过useEffect将实例创建 / 销毁与组件生命周期严格同步。

2. 图片样式控制(解决尺寸混乱问题)

// 图片尺寸统一修正(核心:清除原生样式,应用响应式规则)
const safeImageSizeFix = useCallback((force = false) => {
    if (!ueInstance.current) return;
    
    try {
        const doc = ueInstance.current.document || document;
        const imgElements = doc.body.querySelectorAll('img');
        
        imgElements.forEach((img: HTMLImageElement) => {
            // 清除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(() => {
    if (!ueInstance.current) return;
    
    // 图片插入/上传后触发修正
    ueInstance.current.addListener('afterInsertImage', () => {
        setTimeout(() => safeImageSizeFix(true), 100);
    });
    
    // 覆盖原生插入方法,确保样式生效
    const originalInsertImage = ueInstance.current.insertImage;
    ueInstance.current.insertImage = function (...args) {
        const result = originalInsertImage.call(this, ...args);
        setTimeout(() => safeImageSizeFix(true), 50);
        return result;
    };
}, [safeImageSizeFix]);

核心解决思路

  • 清除原生样式:UEditor 插入图片会自动添加宽高属性 / 样式,需彻底清除;
  • 响应式样式:统一设置maxWidth: 100%+height: auto,保证图片适配容器;
  • 事件监听:图片插入 / 上传后自动触发修正,覆盖原生方法确保无遗漏。

3. 占位符功能(解决上传冲突)

// 设置占位符(核心:自定义样式,标记占位状态)
const setPlaceholderText = useCallback(() => {
    if (!ueInstance.current) return;
    const placeholderHtml = `<p style="color: #999; font-style: italic;">${placeholder}</p>`;
    ueInstance.current.setContent(placeholderHtml);
    isPlaceholderActive.current = true;
    onChange?.('');
}, [placeholder, onChange]);
 
// 上传前激活编辑器(关键:避免占位符干扰上传)
const activateEditorForUpload = useCallback(() => {
    if (!ueInstance.current || !isPlaceholderActive.current) return;
    
    // 清除占位符,插入零宽空格(有内容但不显示)
    ueInstance.current.setContent('<p>&#8203;</p>');
    isPlaceholderActive.current = false;
    isUploading.current = true;
    
    // 超时重置上传状态(安全措施)
    setTimeout(() => {
        isUploading.current = false;
    }, 3000);
}, [placeholder]);
 
// 焦点/失焦处理(核心:区分上传状态,避免冲突)
const setupPlaceholderHandlers = useCallback(() => {
    if (!ueInstance.current) return;
    
    // 聚焦时清除占位符(非上传场景)
    ueInstance.current.addListener('focus', () => {
        if (!isUploading.current && isPlaceholderActive.current) {
            ueInstance.current.setContent('');
            isPlaceholderActive.current = false;
        }
    });
    
    // 失焦时显示占位符(非上传场景)
    ueInstance.current.addListener('blur', () => {
        if (isUploading.current) return;
        
        const content = ueInstance.current.getContent();
        if (!content || content === '<p><br></p>') {
            setPlaceholderText();
        }
        onBlur?.();
    });
}, [setPlaceholderText, onBlur]);

核心痛点解决

  • 上传冲突:占位符状态下编辑器无实际内容,上传会失败,需在上传前插入零宽空格;
  • 状态区分:标记上传状态,焦点 / 失焦逻辑中跳过占位符处理,避免干扰上传;
  • 体验优化:占位符样式自定义,失焦时自动显示,聚焦时自动清除。

4. 内容同步(React 式状态管理)

// 初始化编辑器(核心:内容初始化+事件监听)
const initEditor = useCallback(async () => {
    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', () => {
        // 初始化内容:有值则设置,无值则显示占位符
        if (value) {
            ueInstance.current.setContent(value);
        } else {
            setPlaceholderText();
        }
        
        // 监听内容变化,同步到React状态
        ueInstance.current.addListener('contentChange', () => {
            if (isPlaceholderActive.current || isUploading.current) return;
            
            const content = ueInstance.current.getContent();
            onChange?.(content);
        });
        
        // 初始化图片处理、占位符、上传等逻辑
        setupImageHandlers();
        setupPlaceholderHandlers();
    });
}, [loadScript, value, onChange, setPlaceholderText]);
 
// 监听外部value变化(核心:同步父组件状态到编辑器)
useEffect(() => {
    if (!ueInstance.current?.isReady) return;
    
    const currentContent = ueInstance.current.getContent();
    if (value !== currentContent && !isPlaceholderActive.current) {
        ueInstance.current.setContent(value);
        // 内容变化后修正图片尺寸
        setTimeout(() => safeImageSizeFix(true), 50);
    }
}, [value, safeImageSizeFix]);

核心同步逻辑

  • 编辑器→React:监听contentChange事件,过滤占位符 / 上传状态,同步内容到父组件;
  • React→编辑器:监听value变化,实时更新编辑器内容,同时修正图片尺寸;
  • 初始化:编辑器就绪后根据value初始化内容,保证初始状态一致。

三、封装总结:核心思路提炼

本次封装并非重写 UEditor,而是适配 React 生态规则,核心思路可总结为:

  1. 生命周期同步:实例创建 / 销毁与组件挂载 / 卸载绑定,避免内存泄漏;
  2. 状态隔离:非响应式状态用useRef管理,响应式状态通过props/onChange同步;
  3. 痛点针对性解决
    • 图片样式:清除原生样式 + 统一响应式规则 + 事件监听自动修正;
    • 占位符:上传前激活编辑器,区分上传状态避免冲突;
    • 内容同步:双向绑定,兼顾 React 状态和 UEditor 原生内容;
  4. 扩展与通用:暴露核心方法,剥离业务配置,保证组件复用性。

该思路同样适用于其他原生 JS 编辑器(如 CKEditor、KindEditor)向 React 的适配,核心是遵循 React 的开发范式,将原生库能力封装为符合 React 习惯的组件。

到此这篇关于React封装UEditor富文本编辑器的实现步骤的文章就介绍到这了,更多相关React封装UEditor 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • ReactNative页面跳转Navigator实现的示例代码

    ReactNative页面跳转Navigator实现的示例代码

    本篇文章主要介绍了ReactNative页面跳转Navigator实现的示例代码,具有一定的参考价值,有兴趣的可以了解一下
    2017-08-08
  • React Native中Mobx的使用方法详解

    React Native中Mobx的使用方法详解

    这篇文章主要给大家介绍了关于React Native中Mobx的使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-12-12
  • React class和function的区别小结

    React class和function的区别小结

    Class组件和Function组件是React中创建组件的两种主要方式,本文主要介绍了React class和function的区别小结,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • 浅谈react-native热更新react-native-pushy集成遇到的问题

    浅谈react-native热更新react-native-pushy集成遇到的问题

    下面小编就为大家带来一篇浅谈react-native热更新react-native-pushy集成遇到的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Rect Intersection判断两个矩形是否相交

    Rect Intersection判断两个矩形是否相交

    这篇文章主要为大家介绍了Rect Intersection判断两个矩形是否相交的算法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • React + Typescript领域初学者的常见问题和技巧(最新)

    React + Typescript领域初学者的常见问题和技巧(最新)

    这篇文章主要介绍了React + Typescript领域初学者的常见问题和技巧,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • 使用react+redux实现弹出框案例

    使用react+redux实现弹出框案例

    这篇文章主要为大家详细介绍了使用react+redux实现弹出框案例,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • React的createElement和render手写实现示例

    React的createElement和render手写实现示例

    这篇文章主要为大家介绍了React的createElement和render手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • React-hooks中的useEffect使用步骤

    React-hooks中的useEffect使用步骤

    这篇文章主要介绍了React-hooks中的useEffect,对于React组件来说,主作用是根据数据(state/props)渲染UI,除此之外都是副作用(比如手动修改DOM、发送ajax请求),本文通过实例代码给大家介绍的非常详细,需要的朋友参考下吧
    2022-05-05
  • react父组件调用子组件的方式汇总

    react父组件调用子组件的方式汇总

    在react中常用props实现子组件数据到父组件的传递,但是父组件调用子组件的功能却不常用,下面这篇文章主要给大家介绍了关于react父组件调用子组件的相关资料,需要的朋友可以参考下
    2022-08-08

最新评论