React封裝UEditor富文本編輯器的實(shí)現(xiàn)步驟
UEditor 作為經(jīng)典富文本編輯器,在后臺(tái)系統(tǒng)中仍有廣泛應(yīng)用,但原生 UEditor 與 React 生態(tài)適配性差。本文剝離業(yè)務(wù)接口和冗余配置,聚焦核心封裝邏輯,拆解從實(shí)例管理到圖片處理的關(guān)鍵實(shí)現(xiàn)思路。
一、基礎(chǔ)架構(gòu):核心設(shè)計(jì)思路
1. 類型與狀態(tài)管理(核心代碼片段)
import React, { useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from 'react';
// 核心狀態(tài)管理(非響應(yīng)式狀態(tài)用useRef避免重渲染)
const UEditor = forwardRef((props, ref) => {
const editorContainer = useRef<HTMLDivElement>(null); // 編輯器容器
const ueInstance = useRef<any>(null); // UEditor實(shí)例
const isPlaceholderActive = useRef(false); // 占位符狀態(tài)
const isUploading = useRef(false); // 上傳狀態(tài)標(biāo)記
// 解構(gòu)核心props(僅保留通用配置)
const {
value = '',
onChange,
style,
placeholder = '請輸入內(nèi)容...',
onBlur
} = props;
// 對外暴露核心方法(關(guān)鍵:讓父組件能操作編輯器)
useImperativeHandle(ref, () => ({
getContent: () => {
const content = ueInstance.current?.getContent() || '';
return isPlaceholderActive.current ? '' : content;
},
setContent: (content: string) => {
// 內(nèi)容設(shè)置邏輯(后文詳解)
},
forceImageFixedSize: () => {
// 圖片尺寸修正邏輯(后文詳解)
}
}));
});關(guān)鍵設(shè)計(jì)點(diǎn):
- 用
forwardRef+useImperativeHandle暴露編輯器核心方法,滿足父組件自定義操作需求; - 用
useRef管理 UEditor 實(shí)例、狀態(tài)標(biāo)記等非響應(yīng)式數(shù)據(jù),避免狀態(tài)變化觸發(fā)組件重渲染; - 僅保留通用 Props,剝離業(yè)務(wù)化配置,保證組件通用性。
二、核心功能:關(guān)鍵痛點(diǎn)解決
1. 實(shí)例生命周期管理(避免內(nèi)存泄漏)
// 動(dòng)態(tài)加載UEditor腳本(核心:按需加載,避免重復(fù)加載)
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);
});
}, []);
// 安全銷毀編輯器(關(guān)鍵:卸載時(shí)徹底清理)
const safeDestroyEditor = useCallback(() => {
if (!ueInstance.current) return;
try {
// 移除所有事件監(jiān)聽器
const listeners = ['contentChange', 'blur', 'afterUpload'];
listeners.forEach(event => {
ueInstance.current.removeAllListeners?.(event);
});
// 從UEditor全局實(shí)例中移除
window.UE?.delEditor(ueInstance.current.key);
ueInstance.current = null;
} catch (error) {
console.warn('UEditor銷毀失敗:', error);
}
}, []);
// 生命周期綁定(核心:組件掛載初始化,卸載銷毀)
useEffect(() => {
// 初始化邏輯(后文詳解)
initEditor();
return () => safeDestroyEditor();
}, [initEditor, safeDestroyEditor]);關(guān)鍵要點(diǎn):
- 動(dòng)態(tài)加載腳本:避免 UEditor 資源全局引入,按需加載且防止重復(fù)加載;
- 銷毀邏輯:不僅銷毀實(shí)例,還要移除所有事件監(jiān)聽器,徹底避免內(nèi)存泄漏;
- 生命周期綁定:通過
useEffect將實(shí)例創(chuàng)建 / 銷毀與組件生命周期嚴(yán)格同步。
2. 圖片樣式控制(解決尺寸混亂問題)
// 圖片尺寸統(tǒng)一修正(核心:清除原生樣式,應(yīng)用響應(yīng)式規(guī)則)
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');
// 應(yīng)用統(tǒng)一的響應(yīng)式樣式
img.style.maxWidth = '100%';
img.style.height = 'auto';
});
} catch (error) {
console.warn('圖片尺寸修正失敗:', error);
}
}, []);
// 監(jiān)聽圖片插入事件(核心:插入/上傳后自動(dòng)修正)
const setupImageHandlers = useCallback(() => {
if (!ueInstance.current) return;
// 圖片插入/上傳后觸發(fā)修正
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 插入圖片會(huì)自動(dòng)添加寬高屬性 / 樣式,需徹底清除;
- 響應(yīng)式樣式:統(tǒng)一設(shè)置
maxWidth: 100%+height: auto,保證圖片適配容器; - 事件監(jiān)聽:圖片插入 / 上傳后自動(dòng)觸發(fā)修正,覆蓋原生方法確保無遺漏。
3. 占位符功能(解決上傳沖突)
// 設(shè)置占位符(核心:自定義樣式,標(biāo)記占位狀態(tài))
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]);
// 上傳前激活編輯器(關(guān)鍵:避免占位符干擾上傳)
const activateEditorForUpload = useCallback(() => {
if (!ueInstance.current || !isPlaceholderActive.current) return;
// 清除占位符,插入零寬空格(有內(nèi)容但不顯示)
ueInstance.current.setContent('<p>​</p>');
isPlaceholderActive.current = false;
isUploading.current = true;
// 超時(shí)重置上傳狀態(tài)(安全措施)
setTimeout(() => {
isUploading.current = false;
}, 3000);
}, [placeholder]);
// 焦點(diǎn)/失焦處理(核心:區(qū)分上傳狀態(tài),避免沖突)
const setupPlaceholderHandlers = useCallback(() => {
if (!ueInstance.current) return;
// 聚焦時(shí)清除占位符(非上傳場景)
ueInstance.current.addListener('focus', () => {
if (!isUploading.current && isPlaceholderActive.current) {
ueInstance.current.setContent('');
isPlaceholderActive.current = false;
}
});
// 失焦時(shí)顯示占位符(非上傳場景)
ueInstance.current.addListener('blur', () => {
if (isUploading.current) return;
const content = ueInstance.current.getContent();
if (!content || content === '<p><br></p>') {
setPlaceholderText();
}
onBlur?.();
});
}, [setPlaceholderText, onBlur]);核心痛點(diǎn)解決:
- 上傳沖突:占位符狀態(tài)下編輯器無實(shí)際內(nèi)容,上傳會(huì)失敗,需在上傳前插入零寬空格;
- 狀態(tài)區(qū)分:標(biāo)記上傳狀態(tài),焦點(diǎn) / 失焦邏輯中跳過占位符處理,避免干擾上傳;
- 體驗(yàn)優(yōu)化:占位符樣式自定義,失焦時(shí)自動(dòng)顯示,聚焦時(shí)自動(dòng)清除。
4. 內(nèi)容同步(React 式狀態(tài)管理)
// 初始化編輯器(核心:內(nèi)容初始化+事件監(jiān)聽)
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');
// 創(chuàng)建UEditor實(shí)例
ueInstance.current = window.UE.getEditor(editorContainer.current.id, {
initialFrameHeight: 240,
autoHeightEnabled: false,
enableAutoSave: false // 禁用自動(dòng)保存,避免與React狀態(tài)沖突
});
// 編輯器就緒后初始化內(nèi)容和事件
ueInstance.current.addListener('ready', () => {
// 初始化內(nèi)容:有值則設(shè)置,無值則顯示占位符
if (value) {
ueInstance.current.setContent(value);
} else {
setPlaceholderText();
}
// 監(jiān)聽內(nèi)容變化,同步到React狀態(tài)
ueInstance.current.addListener('contentChange', () => {
if (isPlaceholderActive.current || isUploading.current) return;
const content = ueInstance.current.getContent();
onChange?.(content);
});
// 初始化圖片處理、占位符、上傳等邏輯
setupImageHandlers();
setupPlaceholderHandlers();
});
}, [loadScript, value, onChange, setPlaceholderText]);
// 監(jiān)聽外部value變化(核心:同步父組件狀態(tài)到編輯器)
useEffect(() => {
if (!ueInstance.current?.isReady) return;
const currentContent = ueInstance.current.getContent();
if (value !== currentContent && !isPlaceholderActive.current) {
ueInstance.current.setContent(value);
// 內(nèi)容變化后修正圖片尺寸
setTimeout(() => safeImageSizeFix(true), 50);
}
}, [value, safeImageSizeFix]);核心同步邏輯:
- 編輯器→React:監(jiān)聽
contentChange事件,過濾占位符 / 上傳狀態(tài),同步內(nèi)容到父組件; - React→編輯器:監(jiān)聽
value變化,實(shí)時(shí)更新編輯器內(nèi)容,同時(shí)修正圖片尺寸; - 初始化:編輯器就緒后根據(jù)
value初始化內(nèi)容,保證初始狀態(tài)一致。
三、封裝總結(jié):核心思路提煉
本次封裝并非重寫 UEditor,而是適配 React 生態(tài)規(guī)則,核心思路可總結(jié)為:
- 生命周期同步:實(shí)例創(chuàng)建 / 銷毀與組件掛載 / 卸載綁定,避免內(nèi)存泄漏;
- 狀態(tài)隔離:非響應(yīng)式狀態(tài)用
useRef管理,響應(yīng)式狀態(tài)通過props/onChange同步; - 痛點(diǎn)針對性解決:
- 圖片樣式:清除原生樣式 + 統(tǒng)一響應(yīng)式規(guī)則 + 事件監(jiān)聽自動(dòng)修正;
- 占位符:上傳前激活編輯器,區(qū)分上傳狀態(tài)避免沖突;
- 內(nèi)容同步:雙向綁定,兼顧 React 狀態(tài)和 UEditor 原生內(nèi)容;
- 擴(kuò)展與通用:暴露核心方法,剝離業(yè)務(wù)配置,保證組件復(fù)用性。
該思路同樣適用于其他原生 JS 編輯器(如 CKEditor、KindEditor)向 React 的適配,核心是遵循 React 的開發(fā)范式,將原生庫能力封裝為符合 React 習(xí)慣的組件。
到此這篇關(guān)于React封裝UEditor富文本編輯器的實(shí)現(xiàn)步驟的文章就介紹到這了,更多相關(guān)React封裝UEditor 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ReactNative頁面跳轉(zhuǎn)Navigator實(shí)現(xiàn)的示例代碼
本篇文章主要介紹了ReactNative頁面跳轉(zhuǎn)Navigator實(shí)現(xiàn)的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
React class和function的區(qū)別小結(jié)
Class組件和Function組件是React中創(chuàng)建組件的兩種主要方式,本文主要介紹了React class和function的區(qū)別小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
淺談react-native熱更新react-native-pushy集成遇到的問題
下面小編就為大家?guī)硪黄獪\談react-native熱更新react-native-pushy集成遇到的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
Rect Intersection判斷兩個(gè)矩形是否相交
這篇文章主要為大家介紹了Rect Intersection判斷兩個(gè)矩形是否相交的算法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
React?+?Typescript領(lǐng)域初學(xué)者的常見問題和技巧(最新)
這篇文章主要介紹了React?+?Typescript領(lǐng)域初學(xué)者的常見問題和技巧,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
使用react+redux實(shí)現(xiàn)彈出框案例
這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
React的createElement和render手寫實(shí)現(xiàn)示例
這篇文章主要為大家介紹了React的createElement和render手寫實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08

