2025-12-18-前端画布设计Vol.2 实现富文本编辑

文章发布时间:

最后更新时间:

页面浏览: 加载中...

画布项目中富文本编辑器的实现浅析

0x00 概述

该画布项目采用 wangEditor(v5 版本)的 React 封装组件 @wangeditor/editor-for-react 实现富文本编辑功能。主要通过两个组件协作完成:

  • RichTextEditor.tsx:核心富文本编辑器封装,负责工具栏和编辑区的渲染与配置。
  • BottomTextEditor.tsx:底部面板式编辑器,仅在选中单个文本元素时显示,将富文本编辑器集成到画布操作流程中,支持实时更新元素内容并记录撤销/重做操作。

主要形式是一个底部面板,单击文本元素会出现

0x01 RichTextEditor 组件实现

RichTextEditor 是对 wangEditor 的二次封装,提供可复用的富文本编辑器

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
import "@wangeditor/editor/dist/css/style.css";
import { useState, useEffect } from "react";
import { Editor, Toolbar } from "@wangeditor/editor-for-react";

const toolbarConfig: Partial<IToolbarConfig> = {
toolbarKeys: [
"bold",
"italic",
"underline",
"through",
"|",
"fontSize",
"fontFamily",
"color",
"bgColor",
"|",
"justifyLeft",
"justifyCenter",
"justifyRight",
"|",
"undo",
"redo",
],
};

const editorConfig: Partial<IEditorConfig> = {
placeholder: "请输入文本...",
autoFocus: true,
};

useEffect(() => {
return () => {
if (editor == null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);

return (
<div className="...">
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="simple" />
<Editor
defaultConfig={editorConfig}
value={value}
onCreated={setEditor}
onChange={(editor) => onChange(editor.getHtml())}
mode="simple"
style={{ height: "200px", overflowY: "auto" }}
/>
</div>
);
  1. 工具栏配置(toolbarConfig) 通过 toolbarKeys 指定显示的菜单键(加粗、斜体、下划线、删除线、字体大小/家族、颜色、背景色、对齐方式)以及撤销/重做。
  2. 编辑器配置(editorConfig) 设置占位符和自动聚焦。wangEditor 支持更丰富的配置(如最大长度、自定义菜单等),此处保持最小化。
  3. 生命周期管理 使用 useEffect 在组件卸载或 editor 实例变化时调用 editor.destroy(),防止内存泄漏。
  4. 内容同步value props 控制初始 HTML,onChange 回调通过 editor.getHtml() 获取最新 HTML 内容并向上通知。

0x02 BottomTextEditor 组件实现

BottomTextEditor 将富文本编辑器集成到画布状态管理中,仅针对选中单个文本元素时激活。

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
const { selectedIds, elements, updateElement } = useStore();

const selectedId = selectedIds.length === 1 ? selectedIds[0] : null;
const element = selectedId ? elements[selectedId] : null;

const [localHtml, setLocalHtml] = useState("");

useEffect(() => {
if (element && element.type === "text") {
setLocalHtml(element.text || "");
}
}, [element?.id, element?.text]);

const handleChange = (html: string) => {
setLocalHtml(html);
const initialText = element.text || "";
updateElement(element.id, { text: html });

const updateCommand = new UpdateElementPropertyCommand(
{ id: element.id, property: "text", oldValue: initialText, newValue: html },
"修改文本内容"
);
undoRedoManager.executeCommand(updateCommand);
};

return (
<div className="fixed bottom-8 ... animate-slide-up">
<RichTextEditor value={localHtml} onChange={handleChange} />
</div>
);
  1. 选中元素判断 从 Zustand store(canvasStore)获取选中 ID 和元素集合,仅当选中单个文本类型元素时渲染编辑器。
  2. 本地状态(localHtml) 使用 useState 维护本地 HTML 副本,并在选中元素变化时通过 useEffect 同步 store 中的 element.text。 此设计主要解决中文输入法(IME)组成阶段的问题:在拼音输入过程中,wangEditor 的 onChange 会频繁触发,若直接更新全局 store,可能导致输入延迟、光标跳动或内容混乱。通过本地状态缓冲实时变化,避免不必要的 store 更新。
  3. 内容变更处理(handleChange)
    • 更新本地状态。
    • 实时调用 updateElement 更新画布 store,驱动 canvas 重新渲染文本元素。
    • 创建 UpdateElementPropertyCommand 命令并执行,支持撤销/重做(undo/redo)。命令记录旧值和新值,处理历史操作

0x03 关键问题解决与设计

  1. 输入法兼容性 中文输入过程中,组成事件(composition)会多次触发编辑器变更。若直接在 onChange 中更新全局状态,可能导致性能问题或输入体验不佳。本实现通过本地状态缓冲 setLocalHtml(html) 缓解该问题
  2. 实时渲染与历史管理 实时更新 store 确保画布文本即时反映变更;同时通过命令模式记录操作,实现完整的 undo/redo 支持。

0x04 当前实现的风险与不足分析

当前实现虽已满足基础富文本编辑需求,但是还是有一些问题

  1. 输入法体验优化不彻底 虽通过本地状态缓冲缓解了 IME 组成阶段的频繁更新问题,但未监听 compositionstart/compositionend 事件。在某些极端输入场景(如快速切换输入法或长句输入)下,仍可能出现光标偏移或临时内容丢失的现象。
  2. 内容净化与 XSS 防护不足 完全依赖 wangEditor 内置的有限转义机制,未引入 DOMPurify 等专用净化库。在用户插入外部链接、图片或自定义 HTML 时,存在潜在的存储型或 DOM 型 XSS 风险,尤其在内容后续导出或分享场景中。
  3. 样式与对齐精度问题 wangEditor 生成的 HTML 结构(如多层 span/div 嵌套)与画布自定义文本渲染逻辑可能不完全匹配,导致编辑器中预览效果与画布最终渲染存在细微差异(如行高、字间距、对齐方式)。
  4. 性能与内存管理 频繁的实时更新(onChange 触发 store 更新与命令记录)在长文本场景下可能导致轻微卡顿;此外,未对编辑器实例进行复用,当快速切换不同文本元素时会反复创建/销毁实例,增加内存开销。

面对这些问题后续都可以做一些改进:

  1. 引入 DOMPurify 进行内容净化来处理 XSS 问题 在内容存储前(提交到 store 或后端)及渲染时统一调用 DOMPurify.sanitize,对 HTML 进行严格过滤。自定义白名单以保留 wangEditor 支持的必要标签与属性,同时移除所有事件处理器及危险协议。
  2. 完善输入法兼容性 在 RichTextEditor 中监听 composition 事件,在组成阶段暂不触发 handleChange,仅在 compositionend 后统一更新 store 与命令记录,进一步消除输入延迟与光标问题。
  3. 性能优化 实现编辑器实例复用(单一全局实例,根据选中元素动态切换内容),并在长文本时增加防抖处理,减少不必要的 store 更新与命令执行。

0x05 总结

主要就是 wangEditor 的 React 封装,结合 Zustand 状态管理和命令模式,来适配画布类项目的文本编辑需求,实现了基础的文本编辑功能