例如,初始化一个自定义快捷键功能的富文本编辑器,使用非 React 编辑器,可能需要这么写:
或者更加原始的写法:
试着运行一下上面的例子,就会发现页面上呈现的是一块可编辑的区域,而不像传统的富文本编辑器(比如 TinyMCE),渲染出一个带有工具栏的输入框。如果我们给 Editor 传入 readOnly 属性,Editor 就会变成一个纯粹的富文本渲染组件,可以用来渲染一篇文章。只要传入 EditorState 类型对象作为输入,Editor 组件就能渲染其中的富文本内容 。Editor 组件同时也包含一系列响应用户操作的接口如 onChange,以及用于操作 EditorState 对象的工具函数/类。真正是富文本编辑器的应该是我们封装后的 MyEditor 组件。
很容易猜测出其中一些属性的含义,比如 undoStack/redoStack 是「撤销/重做」栈,selection 标识当前的选区,lastChangeType 记录最后一次变更操作的类型。EditorState 提供一系列实例方法来获取和操作这些属性。
这里的核心是 currentContent 属性,currentContent 是 ContentState 类型的对象,ContentState规定了如何存储具体的富文本内容,包括文字、块级元素、行内样式、元数据等。
在浏览器里打印下图所示的内容经过 convertToRaw 转化的结果:
可以看到的是输出的对象有一个名为 blocks 的属性,blocks 是一个数组,每一项代表当前内容中的一个块级元素。
blocks 的第一项 type 是 'unstyled',代表一个普通的段落,text 属性存储文字内容,inlineStyleRanges 也是一个数组,它的第一项表明该块级元素第 7 个位置被添加了 'BOLD' 样式,样式长度为 5,因此,这一行文本的第 8 到第 12 个字符被添加了加粗的行内样式。
富文本内容的结构化存储一个显而易见的好处是表现力更强
以用 Python 判断富文本中有没有图片为例。用传统的 HTML 方式存储富文本:
富文本内容的结构化存储的另一个好处是内容的存储和渲染逻辑分离
分离能够带来更高的灵活性
存储和渲染的逻辑分离更容易保证渲染结果的确定性
以一段既加粗又倾斜的文本为例,对于一般的基于 HTML 存储的富文本编辑器,如果先倾斜后加粗,很可能得到这个结果:
如果先加粗后倾斜,则是:
内容的存储和渲染逻辑分离带来的另一个可能的好处是多端复用
比如在 app 端做原生渲染,结构化数据比 HTML 更利于解析。
通过 blockRendererFn 自定义渲染当前 block 的方式,例如指定调用 Media 组件去渲染 type 为 atomic 的 block,当前 block 会被注入到组件的 props 中:
keyBindingFn 和 handleKeyCommand 用于定义键盘事件的处理方式,下面是一个快捷键切换到 readOnly 模式的例子:
entity 具有 type 和 data,值得注意的是 entity 还有一个取值为 'Immutable'、'Mutable' 或 'Segmented' 的 mutability 属性,这个属性规定着对应着 entity 的文本将如何被修改/删除。典型的场景是 mention,@xxx 中一旦有一个字符被修改或删除,mention 应该整体被移除或替换,否则就会出现 @ 的名字和实际 @ 的用户不一致的情形,因此,mention 这种类型的 entity 应该被声明为 'Immutable'。
类似又不同于 blockRendererFn 自定义 block 的渲染,decorator 支持定义 block 内符合某种条件的文本的渲染,strategy 函数负责描述找到这段文本的方式,在这里是找到所有对应类型为 mention 的 entity 的文字,然后用 Mention 组件进行渲染。
以及没有提到的:
通过 Entity、Decorator、插件机制的配合,我们可以比较简单地实现一个小的功能插件,比如把粘贴进编辑器的链接自动替换为该链接对应网页的标题,我把它命名为 LinkTitlePlugin:
较保守的方案:draft2HTML,存 HTML
Pros:
Cons:
较激进的方案:HTML2draft,存 draft
Pros:
Cons:
一次尝试
考虑到转换老数据的风险和协同各端适配新数据格式的成本,决定先不做数据存储层面的改动。恰巧的是提问功能只涉及到数据的增而不涉及到数据的修改,偏向保守的第一个方案可以满足知乎新版 Web 个人页的需求,同时把改版的风险和成本降到最低。
决定方案以后我们做了以下三件事来完成提问功能:
下一步
当然,在未来我们不可避免地会涉及到数据(比如提问、回答)的修改。因此在上一步的基础之上,我们去实现 HTML2draft 函数,支持新老数据在新编辑器中的修改。同样出于成本和风险的考虑,我们打算继续不改变数据存储的方式。HTML 字符串从数据库出来,转换为 ContentState 对象传入编辑器,编辑完毕后重新转换回 HTML 存入数据库,两种格式的相互转换在浏览器端进行。