ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Draft.js ContentBlock 完全指南:区块数据模型、API 方法与样式/实体范围遍历实战

2026/9/20 6:54:56 拓冰建站 浏览量
Draft.js ContentBlock 完全指南:区块数据模型、API 方法与样式/实体范围遍历实战 前端UI组件【免费下载链接】draft-jsA React framework for building text editors.项目地址https://gitcode.com/gh_mirrors/dr/draft-js点击查看免费下载ContentBlock是 Draft.js 内容模型中代表单个文本块的不可变数据结构一个段落、一个列表项、一段引用、一个代码块在编辑器中都是一个ContentBlock。本文以官方 API 文档 docs/APIReference-ContentBlock.md 为骨架结合仓库源码逐字段、逐方法地剖析它的 Record 默认值、类型系统、样式/实体编码方式并通过真实测试快照与序列化调用链演示findStyleRanges/findEntityRanges的实战用法。读完本文你将能够直接构造、读取、遍历ContentBlock并理解 Draft.js 文档数据模型ContentState→OrderedMap→ContentBlock→CharacterMetadata的完整链路。一、ContentBlock 在 Draft.js 数据模型中的位置ContentBlock是基于 Immutable.jsRecord的类它表示单个区块block的完整状态包含三类信息区块的纯文本内容plain text区块类型例如段落paragraph、标题header-one、列表项unordered-list-item等实体entity、内联样式inline style以及深度depth信息。一个ContentState对象内部保存着一个OrderedMap将每个区块的 key 映射到对应的ContentBlock对象这些ContentBlock组合在一起才构成编辑器内容的全部。也就是说ContentState是整篇文档ContentBlock是文档里的每一段。从源码看ContentState.js 的getBlockMap()返回的就是这个OrderedMapBlockMap并提供getBlockForKey(key)、getBlockBefore/getBlockAfter等定位接口而真正承载内容的类有两个ContentBlock.js 是扁平块v0.10 前的默认实现ContentBlockNode.js 是实验性的树形嵌套块实现对应draft_tree_data_support实验开关。两者都实现了 BlockNode.js 中定义的BlockNode接口——这 11 个方法的签名正是本文后面要逐一讲解的 API 全集。区块类型DraftBlockTypeContentBlock的类型字段语义上基本等同于块级 HTML 元素内置的合法类型定义在 DraftBlockType.js 的CoreDraftBlockType联合类型中类型含义unstyled无样式段落默认类型paragraph段落header-one~header-six一级到六级标题unordered-list-item无序列表项ordered-list-item有序列表项blockquote引用块code-block代码块atomic原子块媒体、图片、视频等不可编辑内容section/article语义化分节类型同时该文件允许CustomBlockType string即自定义类型可以是任意合法字符串——这也是实现自定义区块如mention、card等的基础。注意官方文档正文只列举了前 13 种而当前仓库源码额外包含section与article二者属于核心类型的扩展写代码时应以 DraftBlockType.js 为准。二、构造 ContentBlockRecord 字段与默认值ContentBlock可以直接通过构造函数创建其Record的默认值定义在 ContentBlock.jsconst defaultRecord: BlockNodeConfig { key: , type: unstyled, text: , characterList: List(), depth: 0, data: Map(), };六个 Record 字段的含义如下字段类型默认值说明keystring区块唯一标识建议用generateRandomKey生成typeDraftBlockTypeunstyled区块类型textstring区块纯文本characterListListCharacterMetadataList()每个字符的样式与实体元数据depthnumber0缩进深度目前仅用于列表项dataMapany, anyMap()块级自定义元数据构造示例来自测试文件 ContentBlock-test.js 的写法import {ContentBlock, CharacterMetadata} from draft-js; import {List} from immutable; import {BOLD} from SampleDraftInlineStyle; const block new ContentBlock({ key: a, type: unstyled, text: Alpha, characterList: List.of( CharacterMetadata.create({style: BOLD, entity: x}), CharacterMetadata.EMPTY, CharacterMetadata.EMPTY, CharacterMetadata.create({style: BOLD}), CharacterMetadata.create({entity: x}), ), });关键行为characterList的自动填充。当只传入text而不传characterList时构造函数会调用decorateCharacterListContentBlock.js为每一个字符生成一个空样式、空实体的CharacterMetadataif (text !characterList) { config.characterList List(Repeat(CharacterMetadata.EMPTY, text.length)); }这正是官方文档所述缺省characterList时会默认按提供的文本生成空CharacterMetadata列表的实现依据。测试must have appropriate default values验证了这一点仅传入text: Alpha时getCharacterList().count()为5且每个字符的style为空数组、entity为null见快照 ContentBlock-test.js.snap。关于 key 的生成文档明确建议使用generateRandomKey。查看其实现 generateRandomKey.js它通过Math.floor(Math.random() * Math.pow(2, 24)).toString(32)生成一个 32 进制的字母数字字符串并维护seenKeys表避免重复、用isNaN(key)排除纯数字 key防止与行号/偏移量语义混淆。为什么 block 是不可变的ContentBlock继承自 ImmutableRecord所有字段一经创建不可变更任何编辑操作都要通过block.set(text, newText)之类的形式返回新对象旧对象保持不变。配合不可变的List与CharacterMetadata编辑操作可以大量复用未变化的数据结构结构共享structural sharing这正是官方文档强调的通过大量使用不可变性与数据持久化编辑内容对编辑器内存占用的影响很小的根本原因。编辑函数也因此在单个List上可以放心地执行slice、concat等操作而不会破坏其他引用。三、characterList样式与实体的统一编码characterList是块内每个字符对应的CharacterMetadata组成的不可变List——这正是 Draft.js 编码内联样式和实体的方式。每个CharacterMetadata只保存两个字段见 CharacterMetadata.jsconst defaultRecord: CharacterMetadataConfig { style: EMPTY_SET, // OrderedSetstring如 OrderedSet.of(BOLD, ITALIC) entity: null, // ?string指向 DraftEntity 实例的 key };换句话说一行characterList同时携带了每个字符属于哪些内联样式和每个字符挂在哪个实体上两份信息。官方文档指出把内联样式和实体这样编码在一起后对ContentBlock做编辑的函数只需要在单个List上做切片、拼接等操作即可同步维护样式与实体边界无需维护两套并行结构。CharacterMetadata还做了一层非常重要的对象池化CharacterMetadata.create(config)CharacterMetadata.js会先查 pool命中则直接复用已有实例因为实际内容中样式/实体的组合种类通常很少复用极大减少了对象数量与内存占用。CharacterMetadata.EMPTY是全局唯一共享的空元数据实例这正是decorateCharacterList可以放心Repeat(CharacterMetadata.EMPTY, n)的原因。四、方法详解API 全集以下方法签名与官方文档一一对应同时给出 ContentBlock.js 中的实现细节与测试验证。getKey(): string返回该块的字符串 key。key 是字母数字字符串建议通过generateRandomKey生成详见上文。getKey(): stringgetType(): DraftBlockType返回块类型取值即第一节列出的CoreDraftBlockType或任意自定义字符串。getType(): DraftBlockTypegetText(): string返回块的完整纯文本。该值不含任何样式、装饰或 HTML 信息——装饰器decorator渲染出的富文本外观并不在此列。getText(): stringgetCharacterList(): ListCharacterMetadata返回每个字符对应的CharacterMetadata不可变List包含块内全部样式与实体信息。具体到每个CharacterMetadata的能力可参考官方文档 docs/APIReference-CharacterMetadata.md。getCharacterList(): ListCharacterMetadatagetLength(): number返回纯文本长度实现即this.getText().lengthContentBlock.js。注意 Unicode 陷阱它直接使用 JavaScript 字符串的length属性不具备 Unicode 感知能力——代理对surrogate pair如 emoji、部分生僻字会被计为 2 个字符。若文本包含这类字符遍历偏移量时要自行处理。getLength(): numbergetDepth(): number返回块的深度值目前仅用于列表项嵌套层级。配合列表类型的type字段与blockRenderMap可渲染出多级嵌套列表。getDepth(): numbergetInlineStyleAt(offset: number): DraftInlineStyle返回指定偏移量处的DraftInlineStyle一个OrderedSetstring。源码实现ContentBlock.jsgetInlineStyleAt(offset: number): DraftInlineStyle { const character this.getCharacterList().get(offset); return character ? character.getStyle() : EMPTY_SET; }注意偏移量越界时返回空OrderedSet而非抛错越界时character为undefined落入EMPTY_SET分支调用方无需额外判空。测试验证ContentBlock-test.js 的must properly retrieve style at offset快照见 ContentBlock-test.js.snap对上述示例块偏移 0 与 3 处为[BOLD]偏移 1、2、4 处为空数组。getEntityAt(offset: number): ?string返回指定偏移量处的实体 key若该处无实体返回null。实现与getInlineStyleAt对称ContentBlock.jsgetEntityAt(offset: number): ?string { const character this.getCharacterList().get(offset); return character ? character.getEntity() : null; }越界同样返回null。测试快照显示示例块偏移 0 处为x、偏移 4 处为x、偏移 1~3 处为null。getData(): Mapany, any返回块级元数据如自定义 block 渲染所需的textAlignment、textDirection等任意key → value的不可变Map。getData(): Mapany, anyfindStyleRanges(filterFn, callback): void对块内每段连续同一样式的字符区间执行回调。签名findStyleRanges( filterFn: (value: CharacterMetadata) boolean, callback: (start: number, end: number) void ): void语义先按样式相等切分出连续区间再对满足filterFn的区间以(start, end)半开区间形式调用callback。底层算法由 findRangesImmutable.js 实现用reduce扫描characterList每当相邻两个字符样式不相等时结束上一个区间若其满足过滤条件则回调foundFn(cursor, nextIndex)最后再单独处理尾区间。ContentBlock传入的相等判定函数haveEqualStyle是比较charA.getStyle() charB.getStyle()ContentBlock.js。findEntityRanges(filterFn, callback): void与findStyleRanges完全对称只是按实体相等切分区间haveEqualEntity比较getEntity()ContentBlock.jsfindEntityRanges( filterFn: (value: CharacterMetadata) boolean, callback: (start: number, end: number) void ): void对上面那个 5 字符示例块第 0 与第 4 个字符带实体x测试快照给出了精确的区间输出findStyleRanges(() true, cb)→[0,1], [1,3], [3,4], [4,5]每次样式切换都产生新区间findEntityRanges(() true, cb)→[0,1], [1,4], [4,5]实体只在 0 和 4 处出现中间 1~4 为无实体区段。这两对方法可看作按样式/实体维度对块做区间切分的高层 API是渲染层与序列化层最常用的遍历手段。五、属性Properties速查所有属性均通过 Immutable Map/Record API 设置与读取官方文档的 Note 提醒构造ContentBlock或修改属性请使用 Immutable 的 Map API 语义即new ContentBlock({...})/block.set(...)而非直接赋值。属性与方法的对应关系如下属性类型对应方法keystringgetKey()typeDraftBlockTypegetType()textstringgetText()characterListListCharacterMetadatagetCharacterList()depthnumbergetDepth()dataMapany, anygetData()六个字段的默认值见第二节表格。测试must provide default valuesContentBlock-test.js验证了空构造new ContentBlock({})的结果type unstyled、text 、getCharacterList()与空List()相等。六、实战用 findEntityRanges 做序列化与实体收集findEntityRanges不只是理论 APIDraft.js 自身的序列化管线就在使用它。以 raw 序列化为例convertFromDraftStateToRaw.js 在遍历contentState.getBlockMap()时对每个 block 调用findEntityRanges收集块内出现的所有实体 keycontentState.getBlockMap().forEach(block { block.findEntityRanges( character character.getEntity() ! null, start { const entityKey block.getEntityAt(start); // 以 start 作为区间起点配合 getEntityAt 拿到该区间统一的实体 key const stringifiedEntityKey DraftStringKey.stringify(entityKey); if (entityCacheRef[stringifiedEntityKey]) { return; // 已收集过跳过 } entityCacheRef[stringifiedEntityKey] entityKey; entityMap[stringifiedEntityKey] ${entityStorageKey}; entityStorageKey; }, ); insertRawBlock(block, entityMap, rawBlocks, blockCacheRef); });这段代码体现了findEntityRanges的经典用法过滤filterFn只保留有实体的字符区间character.getEntity() ! null回调只给起点回调参数是(start, end)此处只取start再用block.getEntityAt(start)取得该区间统一的实体 key——因为区间内的实体必然相同组合应用回调内还可以继续调用getEntityAt/getInlineStyleAt等读取方法实现任意区间感知的逻辑。同样的模式也出现在encodeEntityRanges、getRangesForDraftEntity等模块中可见掌握findStyleRanges/findEntityRanges是理解 Draft.js 渲染与序列化两条主链路的关键。七、周边 API 协同从 ContentState 取块到装饰器渲染最后把ContentBlock放回完整链路中看它的协作对象取块通过 ContentState.js 的getBlockMap()拿到OrderedMap用getBlockForKey(key)、getKeyBefore/getKeyAfter、getBlockBefore/getBlockAfter在块间游走块的 key 正是SelectionState中anchorKey/focusKey的值。样式/实体读取块上的getInlineStyleAt/getEntityAt把字符偏移量翻译成样式集合 / 实体 key是渲染层逐字符取样式如DraftEditorLeaf渲染 BOLD/ITALIC和取实体装饰的入口。区间遍历findStyleRanges/findEntityRanges供渲染层如DraftEditorDecoratedLeaves按实体区间包一层装饰组件与序列化层如上文convertFromDraftStateToRaw使用将连续的字符区间一次性映射为样式节点或实体包裹。装饰器协作getText()与getLength()是装饰器CompositeDraftDecorator计算匹配范围的基础块级自定义数据getData()则服务于自定义 block 渲染blockRenderMap。八、常见注意事项小结key 必须稳定唯一块的 key 是内容身份标识建议始终使用generateRandomKey生成不要用数组下标否则删除/插入会破坏 React 协调与选区定位。characterList 与 text 长度需一致虽然构造时会自动补齐空元数据但手动构造characterList时应保证其长度与text.length一致否则findStyleRanges等区间逻辑的边界会与实际字符对不上。getLength 非 Unicode 感知代理对字符计为 2涉及 emoji 的选区/偏移计算需要格外小心。越界偏移量是安全的getInlineStyleAt越界返回空OrderedSetgetEntityAt越界返回null无需额外防御但要留意这两种空值语义不同。修改须通过不可变 API任何字段变更都返回新ContentBlock直接赋值无效编辑器内容变更请优先走Modifier/RichUtils/EditorState层它们内部以ContentBlock为单位做结构共享式更新。围绕ContentBlock的方法与字段还可以继续阅读 docs/APIReference-ContentState.md块的容器、docs/APIReference-CharacterMetadata.md字符级样式/实体以及 docs/APIReference-EditorState.md块与选区的组合状态即可拼出 Draft.js 数据层的完整地图。赞分享前端UI组件【免费下载链接】draft-jsA React framework for building text editors.项目地址https://gitcode.com/gh_mirrors/dr/draft-js点击查看免费下载相关推荐Open-Assistant 数据模块 oasst_data 完全指南JSONL 导出格式解析与消息树遍历实战Open Assistant 数据模块 oasst_data 完全指南JSONL 导出格式解析与消息树遍历实战 本文以 Open Assistant 仓库中的人工智能大模型强化学习微调数据标注后端前端如何快速掌握AMD Ryzen调试工具SMUDebugTool的完整使用指南如何快速掌握AMD Ryzen调试工具SMUDebugTool的完整使用指南 你是否曾经好奇过为什么别人的AMD Ryzen处理器性能总是比你强或者为什么前端UI组件NocoBase 表格区块Table Block完全指南列配置、数据范围、树表与操作按钮实战NocoBase 表格区块Table Block完全指南列配置、数据范围、树表与操作按钮实战 表格区块是 NocoBase 内置的核心数据区块之一用于以低代码后端前端人工智能AI 应用工作流自动化上一篇gitui性能优化秘诀Rust零成本抽象与内存管理下一篇开源项目 BilibiliDown 的扩展与二次开发潜力创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考