draft-js 和 slate 都是基于 React 构建的富文本编辑器框架,旨在帮助开发者在 Web 应用中实现复杂的文本编辑功能。draft-js 由 Facebook 开源,曾广泛使用,采用不可变数据(Immutable.js)管理状态,但目前处于维护模式,不再推荐用于新项目。slate 则是一个完全可定制的框架,没有内置 UI,依赖纯 JSON 数据模型,允许开发者构建任意类型的编辑器(如 Notion 风格的块级编辑器),目前社区活跃且持续更新。
在 React 生态中,draft-js 和 slate 都曾是实现富文本编辑的热门选择。然而,随着技术演进,两者的命运截然不同。本文将从架构设计、数据模型、扩展性及维护状态四个维度,为资深开发者提供决策依据。
draft-js 目前处于维护模式。Meta 团队已明确表示不再添加新功能,仅修复严重 Bug。这意味着长期项目面临技术债务风险。
// draft-js: 官方已不再推荐用于新项目
// npm install draft-js
// 警告:依赖 Immutable.js,与现代 React 并发特性兼容性差
slate 保持活跃更新。社区驱动,版本迭代频繁,积极适配 React 新特性(如 React 18)。
// slate: 持续维护中
// npm install slate slate-react
// 支持最新的 React 渲染机制
数据结构的差异直接影响性能优化和序列化难度。
draft-js 强制依赖 Immutable.js。这导致状态对象无法直接序列化,必须转换,且与原生 JS 对象交互繁琐。
// draft-js: 必须转换才能存储到数据库
import { convertToRaw } from 'draft-js';
const rawContent = convertToRaw(editorState.getCurrentContent());
const jsonString = JSON.stringify(rawContent); // 需要额外步骤
slate 使用纯 JSON 对象。数据即状态,可直接存储、传输,无需转换层,调试更直观。
// slate: 数据本身就是 JSON
const value = [
{
type: 'paragraph',
children: [{ text: 'A plain JSON node' }]
}
];
// 可直接 JSON.stringify(value) 存入数据库
这是两者最核心的架构分歧。
draft-js 是电池included 的黑盒。它提供了默认的块类型、装饰器系统,但修改底层行为(如粘贴处理、光标逻辑)非常困难,常需 Hack 内部 API。
// draft-js: 自定义块类型较繁琐,需注册组件映射
const blockRendererFn = (block) => {
if (block.getType() === 'atomic') {
return {
component: AtomicBlock,
editable: false,
};
}
};
// 难以拦截底层键盘事件或修改选择逻辑
slate 是完全可插拔 的白盒。它只提供核心引擎,UI 和逻辑全由你定义。通过 withReact、withHistory 等插件模式增强功能。
// slate: 通过 Higher-Order Functions 增强编辑器
import { withReact } from 'slate-react';
import { withHistory } from 'slate-history';
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
// 可轻松自定义快捷键、粘贴规则或渲染逻辑
看代码是最直接的体验差异。
draft-js 需要管理 EditorState 和 onChange 的闭环,且必须包裹在 Draft.Editor 组件中。
// draft-js: 状态管理较重
import { EditorState, RichUtils } from 'draft-js';
function MyEditor() {
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const handleKeyCommand = (command, editorState) => {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
setEditorState(newState);
return 'handled';
}
return 'not-handled';
};
return (
<Editor
editorState={editorState}
onChange={setEditorState}
handleKeyCommand={handleKeyCommand}
/>
);
}
slate 将编辑器实例与 React 状态分离,使用 Editable 组件渲染,逻辑更清晰。
// slate: 逻辑与视图分离更彻底
import { Slate, Editable, withReact } from 'slate-react';
import { createEditor } from 'slate';
function MyEditor() {
const editor = useMemo(() => withReact(createEditor()), []);
const [value, setValue] = useState([{ type: 'paragraph', children: [{ text: '' }] }]);
return (
<Slate editor={editor} initialValue={value} onChange={newValue => setValue(newValue)}>
<Editable placeholder="Enter some rich text..." />
</Slate>
);
}
draft-js 使用内置的 RichUtils 处理内联样式。
// draft-js: 使用预设样式
const toggleBold = () => {
setEditorState(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
};
// 样式名 'BOLD' 是硬编码的字符串
slate 需要手动操作节点属性,更灵活但代码更多。
// slate: 手动操作节点
const toggleBold = () => {
const marks = Editor.marks(editor);
const isBold = marks && marks.bold;
Editor.addMark(editor, 'bold', isBold ? null : true);
};
// 可以自定义任意属性名,如 'isBold', 'weight': 700 等
| 特性 | draft-js | slate |
|---|---|---|
| 维护状态 | ⚠️ 维护模式 (不推荐新项目) | ✅ 活跃更新 |
| 数据格式 | 🔒 Immutable.js (需转换) | 📄 纯 JSON (原生支持) |
| 定制能力 | 📦 有限 (黑盒) | 🔧 极高 (白盒/插件化) |
| 学习曲线 | 📉 中等 (文档旧) | 📈 陡峭 (概念多) |
| 包体积 | 🐘 较大 (含 Immutable) | 🐦 较小 (按需引入) |
| 适用场景 | 🕰️ 旧项目维护 | 🚀 复杂定制/新产品 |
draft-js 就像一台老式打印机 🖨️——功能固定,坏了难修,备件(文档/社区支持)越来越少。除非你被绑定在旧系统中,否则不要在新架构中引入它。
slate 就像一套精密的机械零件 🧩——你需要自己组装,但能造出任何你想要的机器。适合需要深度控制编辑体验、数据结构复杂的场景(如在线文档、CMS 编辑器)。
最终建议:对于 2024 年及以后的新项目,首选 slate。如果 slate 的学习成本过高,可考虑基于 slate 封装的上层库(如 plate),或评估其他现代替代品(如 TipTap),但应避免使用 draft-js。
当需要高度自定义编辑器行为、数据结构或构建类似 Notion 的块级编辑体验时,选择 slate。它适合对编辑器逻辑有完全控制权的团队,但需要接受较陡峭的学习曲线和更多的初始代码编写工作。
仅当维护现有旧项目或需求极其简单且不需要复杂定制时选择 draft-js。由于官方已停止功能更新且依赖过时的 Immutable.js,新项目应避免使用,否则未来迁移成本极高。
This package contains the core logic of Slate. Feel free to poke around to learn more!
Note: A number of source files contain extracted types for Interfaces or Transforms. This is done currently to enable custom type extensions as found in packages/src/interfaces/custom-types.ts.