在企业级React项目中,富文本编辑器往往是内容管理系统的核心组件。我经历过多次编辑器选型,从最初的UEditor到Draft.js,最终锁定TinyMCE-React作为长期技术方案。这个选择基于三个关键因素:首先是商业化产品的稳定性,TinyMCE作为付费产品有专业团队持续维护;其次是React深度集成,官方提供的@tinymce/tinymce-react组件真正实现了React思维;最重要的是企业级功能支持,包括但不限于:
实测在千万级PV的CMS系统中,经过优化的TinyMCE-React组件能保持秒级初始化速度。相比某些开源编辑器,它在处理超长文档(超过5万字)时仍能保持流畅编辑体验。
很多教程会直接让你通过CDN引入TinyMCE,但在企业项目中我强烈推荐使用npm包管理。先安装核心依赖:
bash复制yarn add tinymce @tinymce/tinymce-react
这里有个隐藏坑点:tinymce和@tinymce/tinymce-react的版本必须严格匹配。我在项目中曾因版本冲突导致工具栏不显示,最终锁定以下组合:
json复制"dependencies": {
"tinymce": "^6.3.0",
"@tinymce/tinymce-react": "^4.2.0"
}
创建一个基础的EditorComponent.tsx:
typescript复制import { Editor } from '@tinymce/tinymce-react';
import { useRef } from 'react';
export default function RichTextEditor() {
const editorRef = useRef<any>(null);
const initConfig = {
height: 500,
menubar: false,
plugins: 'lists link image table code',
toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright | bullist numlist outdent indent | code'
};
return (
<Editor
tinymceScriptSrc="/path/to/tinymce.min.js"
onInit={(evt, editor) => editorRef.current = editor}
initialValue="<p>初始内容</p>"
init={initConfig}
/>
);
}
注意几个关键配置项:
tinymceScriptSrc 建议指向自托管文件onInit 回调中获取editor实例init 中的工具栏配置采用管道符(|)分组原始方案直接在前端处理base64上传,这在大文件场景下会导致内存溢出。我们的优化方案分三步走:
javascript复制images_upload_handler: async (blobInfo, success, failure) => {
const file = blobInfo.blob();
if (file.size > 2 * 1024 * 1024) {
const compressed = await compressImage(file); // 自定义压缩函数
await uploadToOSS(compressed);
} else {
await uploadToOSS(file);
}
}
OSS直传方案:
通过后端签发临时STS凭证,前端直接上传到OSS,避免服务端带宽瓶颈。
CDN缓存策略:
为上传图片设置自动压缩的CDN规则,比如对?x-oss-process=image/resize,w_800这类参数进行缓存。
企业项目往往需要支持中英文切换,TinyMCE的语言包加载有三种模式:
javascript复制import 'tinymce/langs/zh_CN';
// ...
language: 'zh_CN'
typescript复制const [lang, setLang] = useState('en');
useEffect(() => {
import(`tinymce/langs/${lang}`).then(() => {
editorRef.current?.editorCommands?.execCommand('mceLoadLanguage', false, lang);
});
}, [lang]);
javascript复制tinymce.init({
language_url: `//cdn.domain.com/tinymce/langs/${userLang}.js`,
language: userLang
});
TinyMCE默认会加载所有插件,通过webpack的魔法注释可以实现真正按需加载:
javascript复制const plugins = [
import(/* webpackMode: "eager" */ 'tinymce/plugins/table'),
import(/* webpackChunkName: "editor-adv" */ 'tinymce/plugins/advlist')
];
Promise.all(plugins).then(() => {
tinymce.init({
plugins: 'table advlist'
});
});
结合React的Suspense实现编辑器延迟加载:
typescript复制const Editor = React.lazy(() => import('@tinymce/tinymce-react'));
function RichTextWrapper() {
return (
<Suspense fallback={<div className="editor-skeleton" />}>
<RichTextEditor />
</Suspense>
);
}
配合Intersection Observer可以在用户滚动到编辑器区域时再触发加载:
typescript复制const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
import('./EditorComponent');
observer.disconnect();
}
});
observer.observe(document.querySelector('#editor-placeholder'));
将编辑器配置抽离为独立模块:
typescript复制// configs/editor.ts
export const BASE_CONFIG = {
skin: 'oxide',
icons: 'default',
content_css: 'default'
};
export const getConfig = (options: Record<string, any>) => ({
...BASE_CONFIG,
...options
});
以开发字数统计增强插件为例:
plugins/wordcount-plus/plugin.js:javascript复制tinymce.PluginManager.add('wordcount-plus', (editor) => {
editor.ui.registry.addMenuItem('wordcount', {
text: '字数统计',
onAction: () => {
const count = editor.plugins.wordcount.body.getWordCount();
editor.notificationManager.open({
text: `当前字数:${count}`,
type: 'info'
});
}
});
});
typescript复制import wordcountPlus from '../../plugins/wordcount-plus/plugin';
const init = {
plugins: 'wordcount wordcount-plus',
setup: (editor) => {
editor.on('init', () => {
tinymce.PluginManager.requireLangPack('wordcount-plus', 'zh_CN');
});
}
}
企业编辑器必须防范XSS攻击,TinyMCE提供多层防护:
javascript复制tinymce.init({
invalid_elements: 'script,iframe',
valid_elements: 'p[style],strong,em,a[href|target]'
});
typescript复制import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(editorContent, {
FORBID_TAGS: ['style'],
FORBID_ATTR: ['onclick']
});
code复制Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' cdn.tiny.cloud; style-src 'self' 'unsafe-inline'
使用Cypress进行编辑器交互测试:
javascript复制describe('RichText Editor', () => {
it('should format text', () => {
cy.get('.tox-editor').click();
cy.get('button[title="Bold"]').click();
cy.get('p').should('have.html', '<strong>Text</strong>');
});
});
调试时可以使用内置的tinymce.dom工具:
javascript复制tinymce.activeEditor.dom.debug = true;
以Ant Design为例,需要处理样式隔离问题:
less复制// 重写编辑器样式作用域
.tox-tinymce {
.ant-card & {
border-radius: 8px;
}
.tox-toolbar__primary {
background: @ant-primary-color;
}
}
结合Y.js实现实时协作:
typescript复制import { WebrtcProvider } from 'y-webrtc';
const provider = new WebrtcProvider('tinymce-demo', editorRef.current.editor.dom.doc);
editorRef.current.editor.on('input', () => {
const content = editorRef.current.getContent();
provider.doc.getXmlFragment('content').insert(0, content);
});
在项目迭代过程中,我们发现将编辑器版本锁定在特定小版本(如6.3.1而非^6.3.0)能避免很多兼容性问题。对于高频使用的表格功能,最终通过自定义插件扩展了表格样式选择器,这比每次手动调整CSS效率提升显著。