前端富文本编辑器集成完全指南

前言

在现代 Web 应用中,富文本编辑器是一个不可或缺的组件。无论是内容管理系统、博客平台还是在线文档工具,都离不开一个功能完善的编辑器。

本文将详细介绍两款主流富文本编辑器 —— wangEditorTinyMCE 的集成与定制方法,并提供完整的可运行 Demo。

两款编辑器对比速览

特性wangEditorTinyMCE
开源协议MITMIT(核心)/ 商业(高级插件)
包体积轻量(~200KB)较大(~1MB+,按需加载)
上手难度低,配置简洁中,插件体系丰富
插件生态内置基本功能丰富,50+ 官方插件
自定义扩展Boot 注册机制插件 API + setup 回调
适合场景轻量内容编辑、移动端复杂文档编辑、企业级应用
Vue/React 集成官方适配包官方封装组件

第一部分:wangEditor

一、为什么需要自定义编辑器行为?

市面上的富文本编辑器大多开箱即用,但在实际业务场景中,我们常常需要:

  • 自定义元素的渲染方式:如图片、视频、表格等特殊节点的 HTML 输出
  • 处理外部数据的加载:从服务端获取的 HTML 内容需要正确解析并渲染
  • 保持编辑状态的一致性:确保保存和加载的数据格式完全一致

二、自定义元素的 HTML 输出

大多数编辑器内部使用 JSON 节点来描述内容,输出时再将节点转换为 HTML。默认的输出格式往往比较简单,无法满足业务需求。

以视频元素为例

默认情况下,编辑器可能只输出视频的基本标签:

1
2
<!-- 默认输出 -->
<video controls><source src="video.mp4" /></video>

但在实际使用中,我们需要保留更多的视觉信息:

1
2
3
4
<!-- 期望输出 -->
<video width="640" height="360" controls style="width:640px;height:360px">
<source src="video.mp4" />
</video>

实现方案:注册 elem-to-html 转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Boot } from "@wangeditor/editor";

Boot.registerElemToHtml({
type: "video",
elemToHtml: elem => {
const src = elem?.src || "";
const style = elem?.style || "max-width:100%;height:auto";
const width = elem.width ? `width="${elem.width}"` : "";
const height = elem.height ? `height="${elem.height}"` : "";
return `<video ${width} ${height} controls style="${style}">
<source src="${src}">
</video>`;
},
});

关键点

  • 从节点中提取 srcstylewidthheight 等属性
  • 同时输出 HTML 属性和内联样式,确保兼容性
  • 提供合理的默认值(如 max-width:100%;height:auto

三、处理外部数据加载

当编辑器加载外部传入的 HTML 内容时,内置的解析器可能无法正确处理自定义格式。

问题分析

编辑器的内置 HTML 解析器通常只解析标准的 HTML 属性:

1
2
3
4
5
6
7
8
9
10
// 内置解析器的典型实现
function parseVideoHtml(element) {
return {
type: "video",
src: element.getAttribute("src") || "",
width: element.getAttribute("width") || "auto",
height: element.getAttribute("height") || "auto",
// ❌ 不解析 style 属性中的宽高
};
}

如果之前的输出将宽高信息只放在了 style 中,而没有单独的 width/height HTML 属性,加载时这些信息就会丢失。

解决方案一:输出时同时保留属性和样式

1
2
3
4
5
// 在 elemToHtml 中同时输出属性和 style
return `<video width="${wNum}" height="${hNum}"
controls style="${style}">
<source src="${src}">
</video>`;

这样无论内置解析器读属性还是读样式,都能获取到正确的尺寸信息。

解决方案二:加载前预处理内容

对于已经存储的历史内容,可以在加载前做预处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 预处理 HTML:为没有 width/height 属性的 video 标签从 style 中提取并补充
*/
function preprocessContent(html) {
if (!html) return html;
return html.replace(/<video\b([^>]*)>/gi, (_, attrs) => {
let result = attrs;
const w = attrs.match(/style\s*=\s*"[^"]*width\s*:\s*(\d+)/i);
if (w) result += ` width="${w[1]}"`;
const h = attrs.match(/style\s*=\s*"[^"]*height\s*:\s*(\d+)/i);
if (h) result += ` height="${h[1]}"`;
return `<video${result}>`;
});
}

四、完整的数据流闭环

1
2
3
4
编辑器节点 ──(elemToHtml)──> HTML 字符串 ──(保存)──> 服务端
^ │
│ │
└──(解析)── 预处理函数 ◄──(加载)─────────────────────┘
阶段处理方式目的
节点 → HTML自定义 elemToHtml确保输出包含完整信息
HTML 保存直接存储保留原始格式
HTML 加载preprocessContent 预处理统一新老数据格式
HTML → 节点内置解析器从属性中还原节点信息

wangEditor 完整 Demo

以下是一个可直接运行的完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>wangEditor Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header {
padding: 24px 32px;
border-bottom: 1px solid #eee;
}
.header h1 {
font-size: 22px;
color: #1a1a1a;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
color: #999;
}
.toolbar {
border-bottom: 1px solid #eee;
}
#editor-container {
min-height: 400px;
}
.w-e-text-container {
min-height: 400px;
}
.actions {
display: flex;
gap: 12px;
padding: 20px 32px;
border-top: 1px solid #eee;
flex-wrap: wrap;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.btn-primary {
background: #1677ff;
color: #fff;
}
.btn-primary:hover {
background: #4096ff;
}
.btn-outline {
background: #fff;
color: #333;
border: 1px solid #d9d9d9;
}
.btn-outline:hover {
border-color: #1677ff;
color: #1677ff;
}
.preview-section,
.source-section {
border-top: 1px solid #eee;
padding: 32px;
display: none;
}
.preview-section.show,
.source-section.show {
display: block;
}
.preview-section h3,
.source-section h3 {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.preview-content {
line-height: 1.8;
color: #333;
}
.preview-content img,
.preview-content video {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.preview-content blockquote {
border-left: 4px solid #1677ff;
padding: 8px 16px;
margin: 12px 0;
background: #f6f8fa;
color: #666;
}
.preview-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.preview-content th,
.preview-content td {
border: 1px solid #d9d9d9;
padding: 8px 12px;
text-align: left;
}
.preview-content th {
background: #fafafa;
font-weight: 600;
}
.preview-content a {
color: #1677ff;
}
.source-code {
background: #1e1e1e;
color: #d4d4d4;
padding: 16px;
border-radius: 8px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>wangEditor Demo</h1>
<p>轻量级富文本编辑器 — 支持视频、图片、表格、代码块等</p>
</div>
<div class="toolbar" id="toolbar-container"></div>
<div id="editor-container"></div>
<div class="actions">
<button class="btn btn-primary" onclick="getContent()">获取 HTML</button>
<button class="btn btn-outline" onclick="getText()">获取纯文本</button>
<button class="btn btn-outline" onclick="setContent()">加载示例内容</button>
<button class="btn btn-outline" onclick="clearContent()">清空</button>
</div>
<div class="preview-section" id="preview-section">
<h3>HTML 预览</h3>
<div class="preview-content" id="preview-content"></div>
</div>
<div class="source-section" id="source-section">
<h3>HTML 源码</h3>
<pre class="source-code" id="source-code"></pre>
</div>
</div>

<script crossorigin src="https://unpkg.com/@wangeditor/editor@5.1.23/dist/index.min.js"></script>

<script>
const { createEditor, createToolbar, Boot } = window.wangEditor;

// 1. 自定义视频元素输出(保留样式的关键)
try {
Boot.registerElemToHtml({
type: "video",
elemToHtml: elem => {
const src = elem?.src || "";
const toSize = v => {
const num = parseFloat(v);
return v === "auto" || v === "" || v == null || isNaN(num) || num <= 0 ? "" : `${num}px`;
};
const wPx = toSize(elem.width);
const hPx = toSize(elem.height);
let style = elem?.style || "";
if (!style) {
if (wPx) style += `width:${wPx};`;
if (hPx) style += `height:${hPx};`;
}
style = style.replace(/;+$/, "") || "max-width:100%;height:auto";
const wAttr = wPx ? ` width="${parseFloat(wPx)}"` : "";
const hAttr = hPx ? ` height="${parseFloat(hPx)}"` : "";
return `<video${wAttr}${hAttr} controls style="${style}"><source src="${src}"></video>`;
},
});
} catch (e) {}

// 2. 预处理:为仅有 style 的 video 补上 width/height 属性
function preprocessContent(html) {
if (!html || typeof html !== "string") return html;
return html.replace(/<video\b([^>]*)>/gi, (_, attrs) => {
let result = attrs;
const w = attrs.match(/style\s*=\s*"[^"]*width\s*:\s*(\d+)/i);
if (w) {
result = result.replace(/\bwidth\s*=\s*"[^"]*"/i, "").replace(/\bwidth\s*=\s*\d+/i, "");
result += ` width="${w[1]}"`;
}
const h = attrs.match(/style\s*=\s*"[^"]*height\s*:\s*(\d+)/i);
if (h) {
result = result.replace(/\bheight\s*=\s*"[^"]*"/i, "").replace(/\bheight\s*=\s*\d+/i, "");
result += ` height="${h[1]}"`;
}
return `<video${result}>`;
});
}

// 3. 创建编辑器
const editor = createEditor({
selector: "#editor-container",
html: "",
config: {
placeholder: "请输入内容...",
autoFocus: false,
scroll: true,
},
mode: "default",
});

createToolbar({
editor,
selector: "#toolbar-container",
config: { excludeKeys: ["fullScreen"] },
mode: "default",
});

// 4. 操作函数
function getContent() {
const html = editor.getHtml();
document.getElementById("preview-section").classList.add("show");
document.getElementById("preview-content").innerHTML = html;
document.getElementById("source-section").classList.add("show");
document.getElementById("source-code").textContent = html;
}
function getText() {
alert("纯文本:\n\n" + editor.getText());
}
function setContent() {
const html = `<h2>欢迎使用 wangEditor</h2><p>这是一段<strong>示例内容</strong>。</p>
<table><tr><th>功能</th><th>说明</th></tr><tr><td>标题</td><td>支持多级标题</td></tr><tr><td>表格</td><td>支持增删行列</td></tr></table>
<blockquote>这是一个引用块。</blockquote>
<p><a href="https://www.wangeditor.com" target="_blank">wangEditor 官网</a></p>
<p><video width="640" height="360" controls style="width:640px;height:360px"><source src="https://www.w3schools.com/html/mov_bbb.mp4"></video></p>
<pre><code>function hello() { console.log('Hello!') }</code></pre>`;
setTimeout(() => editor.setHtml(preprocessContent(html)), 300);
}
function clearContent() {
editor.clear();
document.getElementById("preview-section").classList.remove("show");
document.getElementById("source-section").classList.remove("show");
}
window.addEventListener("DOMContentLoaded", setContent);
</script>
</body>
</html>

第二部分:TinyMCE

TinyMCE 是全球使用最广泛的富文本编辑器之一,拥有强大的插件生态系统和丰富的 API。

一、基础安装与配置

CDN 引入

TinyMCE 提供免费 CDN,无需注册即可使用:

1
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>

生产环境建议注册 Tiny Cloud 获取免费 API Key,或使用自托管版本。

npm 安装

1
2
3
npm install tinymce
# 或
yarn add tinymce

最简配置

1
2
3
4
5
6
tinymce.init({
selector: "#editor", // 绑定 textarea 或 div
height: 500,
plugins: "lists link image table code",
toolbar: "undo redo | bold italic | alignleft aligncenter | bullist numlist",
});

二、核心配置详解

1. 插件系统(plugins)

TinyMCE 的功能以插件形式组织,按需加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plugins: [
"advlist", // 高级列表(有序/无序列表样式选择)
"autolink", // 自动识别并转换链接
"link", // 插入/编辑链接
"image", // 插入/编辑图片
"table", // 表格编辑
"code", // 源码编辑
"codesample", // 代码高亮
"media", // 插入视频/音频
"fullscreen", // 全屏编辑
"preview", // 预览
"searchreplace", // 查找替换
"wordcount", // 字数统计
].join(" ");

2. 工具栏配置(toolbar)

通过 | 分组,空格分隔按钮:

1
2
3
4
5
toolbar: "undo redo | styles | bold italic underline strikethrough | " +
"alignleft aligncenter alignright alignjustify | " +
"bullist numlist outdent indent | " +
"link image media table | " +
"code preview fullscreen";

3. 图片与视频上传

TinyMCE 通过 images_upload_handler 自定义上传逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
images_upload_handler: (blobInfo, progress) =>
new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("file", blobInfo.blob(), blobInfo.filename());

// 调用你自己的上传接口
fetch("/api/upload", { method: "POST", body: formData })
.then(res => res.json())
.then(data => {
if (data.url) {
resolve(data.url); // 返回图片 URL
} else {
reject("上传失败");
}
})
.catch(reject);
});

三、自定义样式与格式

1. content_style — 编辑器内样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
content_style: `
body { font-family: -apple-system, sans-serif; font-size: 15px; line-height: 1.8; color: #333; }
img { max-width: 100%; height: auto; border-radius: 6px; }
video { max-width: 100%; height: auto; border-radius: 6px; }
blockquote {
border-left: 4px solid #1677ff; padding: 8px 16px;
margin: 12px 0; background: #f6f8fa; color: #666;
}
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #d9d9d9; padding: 8px 12px; }
th { background: #fafafa; font-weight: 600; }
a { color: #1677ff; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow-x: auto; }
`;

2. style_formats — 自定义格式菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
style_formats: [
{ title: "正文", block: "p" },
{ title: "一级标题", block: "h1" },
{ title: "二级标题", block: "h2" },
{ title: "引用块", block: "blockquote" },
{ title: "代码块", block: "pre", classes: "code-block" },
{
title: "红色文字",
inline: "span",
styles: { color: "#ff4d4f", "font-weight": "bold" },
},
{
title: "高亮标记",
inline: "span",
styles: { background: "#fff7e6", padding: "2px 6px", "border-radius": "3px" },
},
];

3. formats — 自定义格式规则

1
2
3
4
5
6
formats: {
// 自定义 red-text 格式
redtext: { inline: 'span', styles: { color: '#ff4d4f', fontWeight: 'bold' } },
// 自定义 highlight 格式
highlight: { inline: 'span', styles: { background: '#fff7e6', padding: '2px 6px', borderRadius: '3px' } },
}

四、setup 回调 — 编辑器生命周期钩子

setup 是 TinyMCE 最强大的扩展点,在编辑器初始化时执行,可以绑定事件、添加自定义按钮等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
setup: editor => {
// 1. 初始化完成
editor.on("init", () => {
console.log("编辑器就绪");
});

// 2. 内容变化
editor.on("change", () => {
console.log("内容变化:", editor.getContent());
});

// 3. 按键事件
editor.on("keydown", e => {
if (e.key === "Tab") {
e.preventDefault();
editor.execCommand("mceInsertContent", false, " ");
}
});

// 4. 粘贴预处理 — 过滤从 Word 复制的内容
editor.on("paste", e => {
// 清理 Word 格式但保留基本格式
// e.content 包含粘贴的 HTML
});

// 5. 添加自定义按钮
editor.ui.registry.addButton("customDate", {
text: "插入日期",
onAction: () => {
const now = new Date().toLocaleDateString("zh-CN");
editor.insertContent(`<span>${now}</span>`);
},
});

// 6. 自定义菜单项
editor.ui.registry.addMenuItem("customMenu", {
text: "自定义操作",
onAction: () => editor.insertContent("<p>自定义内容</p>"),
});
};

五、获取与设置内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取带格式的 HTML
const html = tinymce.activeEditor.getContent();

// 获取纯文本
const text = tinymce.activeEditor.getContent({ format: "text" });

// 设置内容(会替换编辑器全部内容)
tinymce.activeEditor.setContent("<p>新内容</p>");

// 在光标处插入内容
tinymce.activeEditor.insertContent("<span>插入的内容</span>");

// 动态禁用/启用
tinymce.activeEditor.mode.set("readonly"); // 只读
tinymce.activeEditor.mode.set("design"); // 编辑

六、TinyMCE 完整 Demo

以下是一个功能完整的 Demo,展示了 TinyMCE 的常见配置和使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TinyMCE Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 960px;
margin: 0 auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header {
padding: 24px 32px;
border-bottom: 1px solid #eee;
}
.header h1 {
font-size: 22px;
color: #1a1a1a;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
color: #999;
}
.actions {
display: flex;
gap: 12px;
padding: 12px 32px;
border-bottom: 1px solid #eee;
flex-wrap: wrap;
}
.btn {
padding: 8px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.btn-primary {
background: #1677ff;
color: #fff;
}
.btn-primary:hover {
background: #4096ff;
}
.btn-outline {
background: #fff;
color: #333;
border: 1px solid #d9d9d9;
}
.btn-outline:hover {
border-color: #1677ff;
color: #1677ff;
}

/* 双栏布局 */
.main-content {
display: flex;
min-height: 500px;
}
.editor-panel {
flex: 1;
min-width: 0;
}
.output-panel {
width: 380px;
border-left: 1px solid #eee;
display: flex;
flex-direction: column;
max-height: 600px;
}
.output-tabs {
display: flex;
border-bottom: 1px solid #eee;
}
.output-tab {
flex: 1;
padding: 10px 16px;
text-align: center;
font-size: 13px;
cursor: pointer;
background: #fafafa;
border: none;
color: #666;
transition: all 0.2s;
}
.output-tab.active {
background: #fff;
color: #1677ff;
border-bottom: 2px solid #1677ff;
font-weight: 500;
}
.output-body {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.output-body .preview-content {
line-height: 1.8;
color: #333;
}
.output-body .preview-content img,
.output-body .preview-content video {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.output-body .preview-content blockquote {
border-left: 4px solid #1677ff;
padding: 8px 16px;
margin: 12px 0;
background: #f6f8fa;
color: #666;
}
.output-body .preview-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.output-body .preview-content th,
.output-body .preview-content td {
border: 1px solid #d9d9d9;
padding: 8px 12px;
text-align: left;
}
.output-body .preview-content th {
background: #fafafa;
font-weight: 600;
}
.output-body .preview-content a {
color: #1677ff;
}
.source-code {
background: #1e1e1e;
color: #d4d4d4;
padding: 16px;
border-radius: 8px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}

/* 响应式 */
@media (max-width: 800px) {
.main-content {
flex-direction: column;
}
.output-panel {
width: 100%;
border-left: none;
border-top: 1px solid #eee;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>TinyMCE Demo</h1>
<p>企业级富文本编辑器 — 插件体系 + 自定义格式 + 实时预览</p>
</div>
<div class="actions">
<button class="btn btn-primary" id="btn-refresh">刷新预览</button>
<button class="btn btn-outline" id="btn-load-sample">加载示例内容</button>
<button class="btn btn-outline" id="btn-get-text">获取纯文本</button>
<button class="btn btn-outline" id="btn-clear">清空</button>
</div>
<div class="main-content">
<!-- 编辑区 -->
<div class="editor-panel">
<textarea id="tinymce-editor"></textarea>
</div>
<!-- 输出区:预览 + 源码 -->
<div class="output-panel">
<div class="output-tabs">
<button class="output-tab active" data-tab="preview">预览</button>
<button class="output-tab" data-tab="source">源码</button>
</div>
<div class="output-body">
<div class="preview-content" id="output-preview"></div>
<pre class="source-code" id="output-source" style="display:none;"></pre>
</div>
</div>
</div>
</div>

<!-- TinyMCE CDN -->
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>

<script>
// ==================== 示例内容 ====================
const sampleContent = `
<h2 style="text-align: center;">欢迎使用 TinyMCE</h2>
<p>TinyMCE 是全球最流行的<strong>富文本编辑器</strong>之一,提供丰富的插件生态和灵活的 API。</p>

<h3>📊 表格功能</h3>
<table style="border-collapse: collapse; width: 100%;">
<thead>
<tr><th>插件</th><th>功能</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td>advlist</td><td>高级列表</td><td>支持有序/无序列表样式切换</td></tr>
<tr><td>table</td><td>表格编辑</td><td>支持增删行列、单元格合并</td></tr>
<tr><td>codesample</td><td>代码高亮</td><td>基于 Prism.js 的语法高亮</td></tr>
<tr><td>media</td><td>媒体嵌入</td><td>支持视频、音频嵌入</td></tr>
</tbody>
</table>

<h3>🎨 格式与样式</h3>
<p>支持<strong>加粗</strong>、<em>斜体</em>、<span style="text-decoration: underline;">下划线</span>、<span style="text-decoration: line-through;">删除线</span>、
<span style="color: #ff4d4f; font-weight: bold;">红色强调</span>、
<span style="background-color: #fff7e6; padding: 2px 6px; border-radius: 3px;">高亮标记</span>等格式。</p>

<h3>💬 引用块</h3>
<blockquote><p>TinyMCE 的 setup 钩子提供了强大的扩展能力,可以在编辑器初始化、内容变化等生命周期节点执行自定义逻辑。</p></blockquote>

<h3>🔗 超链接</h3>
<p>插入链接:<a href="https://www.tiny.cloud" target="_blank" rel="noopener">TinyMCE 官方网站</a></p>

<h3>📹 视频嵌入</h3>
<div style="max-width: 640px;">
<video width="640" height="360" controls="controls" style="width: 640px; height: 360px;">
<source src="https://www.w3schools.com/html/mov_bbb.mp4">
</video>
</div>

<h3>💻 代码高亮</h3>
<pre class="language-javascript" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px;">
function useRichEditor() {
const editor = tinymce.get('editor')
const html = editor.getContent()
console.log('编辑器内容:', html)
}</pre>

<p style="text-align: center; color: #999; margin-top: 32px;">
—— 你可以自由编辑以上内容 ——
</p>
`;

// ==================== TinyMCE 初始化 ====================
tinymce.init({
selector: "#tinymce-editor",
height: 500,
language: "zh_CN",
menubar: false,
branding: false,
statusbar: true,
resize: true,

// 内容样式
content_style: `
body { font-family: -apple-system, sans-serif; font-size: 15px; line-height: 1.8; color: #333; padding: 16px; }
img { max-width: 100%; height: auto; border-radius: 6px; }
video { max-width: 100%; height: auto; border-radius: 6px; }
blockquote { border-left: 4px solid #1677ff; padding: 8px 16px; margin: 12px 0; background: #f6f8fa; color: #666; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
th, td { border: 1px solid #d9d9d9; padding: 8px 12px; text-align: left; }
th { background: #fafafa; font-weight: 600; }
a { color: #1677ff; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow-x: auto; }
`,

// 插件
plugins: [
"advlist autolink lists link image charmap",
"searchreplace visualblocks code fullscreen",
"media table codesample preview wordcount",
].join(" "),

// 工具栏:split 分组创建多行工具栏
toolbar:
"undo redo | styles | " +
"bold italic underline strikethrough forecolor backcolor | " +
"alignleft aligncenter alignright alignjustify | " +
"bullist numlist outdent indent | " +
"link image media table codesample | " +
"code preview fullscreen",

// 自定义格式
style_formats: [
{ title: "正文", block: "p" },
{ title: "一级标题", block: "h1" },
{ title: "二级标题", block: "h2" },
{ title: "三级标题", block: "h3" },
{ title: "引用块", block: "blockquote" },
{
title: "红色强调",
inline: "span",
styles: { color: "#ff4d4f", "font-weight": "bold" },
},
{
title: "高亮标记",
inline: "span",
styles: { background: "#fff7e6", padding: "2px 6px", "border-radius": "3px" },
},
],

// 表格默认属性
table_default_attributes: { border: "1" },
table_default_styles: { "border-collapse": "collapse", width: "100%" },

// 图片自适应
image_dimensions: false,
image_class_list: [
{ title: "自适应", value: "img-responsive" },
{ title: "圆角", value: "img-rounded" },
],

// 媒体嵌入
media_live_embeds: true,
media_dimensions: false,

// 粘贴配置:保留基本格式
paste_data_images: false,
paste_retain_style_properties: "color font-size font-weight text-decoration",

// 代码高亮
codesample_languages: [
{ text: "JavaScript", value: "javascript" },
{ text: "HTML/XML", value: "markup" },
{ text: "CSS", value: "css" },
{ text: "Python", value: "python" },
{ text: "SQL", value: "sql" },
{ text: "Bash", value: "bash" },
],

// setup 生命周期钩子
setup: editor => {
// 编辑器就绪后加载示例内容
editor.on("init", () => {
editor.setContent(sampleContent);
updatePreview(editor.getContent());
updateSource(editor.getContent());
});

// 内容变化时更新预览
editor.on("change keyup", () => {
const html = editor.getContent();
updatePreview(html);
updateSource(html);
});
},
});

// ==================== 预览与输出 ====================
function updatePreview(html) {
document.getElementById("output-preview").innerHTML = html;
}
function updateSource(html) {
document.getElementById("output-source").textContent = html;
}

// ==================== 按钮事件 ====================
document.getElementById("btn-refresh").addEventListener("click", () => {
const html = tinymce.activeEditor.getContent();
updatePreview(html);
updateSource(html);
});

document.getElementById("btn-load-sample").addEventListener("click", () => {
tinymce.activeEditor.setContent(sampleContent);
});

document.getElementById("btn-get-text").addEventListener("click", () => {
const text = tinymce.activeEditor.getContent({ format: "text" });
alert("纯文本内容:\n\n" + text);
});

document.getElementById("btn-clear").addEventListener("click", () => {
if (confirm("确定要清空编辑器内容吗?")) {
tinymce.activeEditor.setContent("");
updatePreview("");
updateSource("");
}
});

// ==================== Tab 切换 ====================
document.querySelectorAll(".output-tab").forEach(tab => {
tab.addEventListener("click", () => {
document.querySelectorAll(".output-tab").forEach(t => t.classList.remove("active"));
tab.classList.add("active");
const target = tab.dataset.tab;
document.getElementById("output-preview").style.display = target === "preview" ? "" : "none";
document.getElementById("output-source").style.display = target === "source" ? "" : "none";
});
});
</script>
</body>
</html>

TinyMCE Demo 功能说明

功能说明
插件体系按需加载 advlist、table、codesample、media 等插件
自定义格式红色强调、高亮标记等自定义内联样式
双栏实时预览左侧编辑,右侧同步展示预览和 HTML 源码
setup 生命周期初始化时加载示例、内容变化时同步预览
代码高亮codesample 插件支持 JS/HTML/CSS/Python/SQL 等多语言
表格与媒体支持表格编辑、视频嵌入,图片自适应
粘贴配置保留颜色、字号、加粗等基本格式

总结

对比维度wangEditorTinyMCE
适用场景轻量编辑、移动端、简单内容复杂文档、企业后台、多格式需求
扩展方式Boot.registerElemToHtmlplugins + setup 钩子
数据处理节点模型 + 解析器直接操作 HTML
学习曲线低,1天上手中,2-3天熟悉
社区成熟度中文社区活跃全球社区,文档丰富

选型建议

  • 如果追求轻量、快速集成,选 wangEditor
  • 如果需要丰富的插件生态、企业级功能,选 TinyMCE
  • 两者可以共存:管理后台用 TinyMCE 处理复杂内容,移动端用 wangEditor 做简单编辑

希望本文和两个 Demo 能帮助你在富文本编辑器集成的道路上少走弯路!