背景
因为 AIGC 的兴起,大量的人拥入了这个赛道,但是从本质上来讲,成功的模式都是一个套路,那就是把 AIGC 的能力加持到自己已有的产品上。我们来看看,如:
- Notion 号称最强个人知识管理笔记,因为AI的加持据说又冲了一波量。
- 微软office 全家桶,这个就属于赢麻了的那种,据说订阅用户上涨很明显。
- new bing,刚出来的时候,排队,拍过队的人应该体会到当时排到的那种爽快,其实用的也很爽的说。
所以,我们看到了,要想在AIGC上做点什么,立足点,还是需要本身有一个比较好的产品。
因为本人的圈子,其实接触到一些对编辑器增加AIGC有需求的人,因此,我还是比较像涉足一下如何打造一个比较好的所见即所得编辑器,因此,我想,是时候去研究下所见即所得编辑器的实现原理了。
起初,我看到了一个叫做 Tiptap 的所见即所得编辑器,生态圈子不错,深入看了下,他是站在了 ProseMirror 的肩膀上,所以,索性,直接从最底层去了解下,所以就有了这篇文章,不准备写得比较深,重点在于了解其原理。
什么叫做所见即所得文本编辑器
其实就是是一种让用户在编辑过程中直观地看到最终输出效果的编辑器。用户可以在编辑器中对文本进行排版、调整样式、插入图片等操作。而我们要讲的ProseMirror 就是这样一种编辑器,带这个这个期待,来了解一下它的实现原理。
ProseMirror 实际上主要分为:文档模型、视图、交互、插件、系列化反序列化这几大块,其整体的系统架构图如下
其源码都放在这里:https://github.com/ProseMirror
文档模型(Document Model):ProseMirror 使用一个称为“文档模型”的数据结构来表示编辑器中的内容。文档模型是一个树状结构,由节点(Node)和标记(Mark)组成。节点表示文档中的各种元素,如段落、标题、列表等;标记表示文本的样式,如加粗、斜体等。这种数据结构使得 ProseMirror 能够灵活地处理富文本内容,并为实现撤销、重做等功能提供了基础,其中Node的的类的描述可以参考下图。
视图(View):ProseMirror 将文档模型与 DOM(文档对象模型)相互映射,使得用户在编辑器中看到的内容与文档模型保持一致。当用户在编辑器中进行操作时,ProseMirror 会更新文档模型,并将变更同步到 DOM,从而实现所见即所得的效果,其主要暴露的类EditorView一撇如下,变更同步到DOM的操作实际上也清晰,他们放在了DocViewDesc的update方法里面,我都将其一撇放在了下面,有兴趣的可以打开源码在详细研究下。
代码语言:javascript复制export class EditorView {
private _props: DirectEditorProps
private directPlugins: readonly Plugin[]
private _root: Document | ShadowRoot | null = null
/// @internal
focused = false
/// Kludge used to work around a Chrome bug @internal
trackWrites: DOMNode | null = null
private mounted = false
/// @internal
markCursor: readonly Mark[] | null = null
/// @internal
cursorWrapper: {dom: DOMNode, deco: Decoration} | null = null
/// @internal
nodeViews: NodeViewSet
/// @internal
lastSelectedViewDesc: ViewDesc | undefined = undefined
/// @internal
docView: NodeViewDesc
/// @internal
input = new InputState
private prevDirectPlugins: readonly Plugin[] = []
private pluginViews: PluginView[] = []
/// @internal
domObserver!: DOMObserver
/// Holds `true` when a hack node is needed in Firefox to prevent the
/// [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651)
/// @internal
requiresGeckoHackNode: boolean = false
/// The view's current [state](#state.EditorState).
public state: EditorState
/// Create a view. `place` may be a DOM node that the editor should
/// be appended to, a function that will place it into the document,
/// or an object whose `mount` property holds the node to use as the
/// document container. If it is `null`, the editor will not be
/// added to the document.
constructor(place: null | DOMNode | ((editor: HTMLElement) => void) | {mount: HTMLElement}, props: DirectEditorProps) {
this._props = props
this.state = props.state
this.directPlugins = props.plugins || []
this.directPlugins.forEach(checkStateComponent)
this.dispatch = this.dispatch.bind(this)
this.dom = (place && (place as {mount: HTMLElement}).mount) || document.createElement("div")
if (place) {
if ((place as DOMNode).appendChild) (place as DOMNode).appendChild(this.dom)
else if (typeof place == "function") place(this.dom)
else if ((place as {mount: HTMLElement}).mount) this.mounted = true
}
this.editable = getEditable(this)
updateCursorWrapper(this)
this.nodeViews = buildNodeViews(this)
this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this)
this.domObserver = new DOMObserver(this, (from, to, typeOver, added) => readDOMChange(this, from, to, typeOver, added))
this.domObserver.start()
initInput(this)
this.updatePluginViews()
}
交互(Interaction):ProseMirror 通过监听用户的键盘和鼠标事件,实现对文档模型的编辑。例如,当用户按下退格键时,ProseMirror 会删除文档模型中相应的字符;当用户点击工具栏按钮时,ProseMirror 会在文档模型中添加或修改相应的样式。此外,ProseMirror 还支持通过拖放、粘贴等方式导入外部内容。这里面可以看到一些默认的快捷键配置。
插件(Plugins):ProseMirror 采用插件机制来扩展编辑器的功能。开发者可以编写插件实现自定义的功能,如协同编辑、自动保存等。插件可以订阅和处理文档模型的变更事件,以实现与编辑器的交互。插件系统的相关源码是可以参考这里的。实际上这个状态库是维护了整个 Editor 的状态,你可以把它理解为 Redux,或者 Vuex之类的东西。一个插件的描述大概需要包括下面几个部分。
代码语言:javascript复制export interface PluginSpec<PluginState> {
/// The [view props](#view.EditorProps) added by this plugin. Props
/// that are functions will be bound to have the plugin instance as
/// their `this` binding.
props?: EditorProps<Plugin<PluginState>>
/// Allows a plugin to define a [state field](#state.StateField), an
/// extra slot in the state object in which it can keep its own data.
state?: StateField<PluginState>
/// Can be used to make this a keyed plugin. You can have only one
/// plugin with a given key in a given state, but it is possible to
/// access the plugin's configuration and state through the key,
/// without having access to the plugin instance object.
key?: PluginKey
/// When the plugin needs to interact with the editor view, or
/// set something up in the DOM, use this field. The function
/// will be called when the plugin's state is associated with an
/// editor view.
view?: (view: EditorView) => PluginView
/// When present, this will be called before a transaction is
/// applied by the state, allowing the plugin to cancel it (by
/// returning false).
filterTransaction?: (tr: Transaction, state: EditorState) => boolean
/// Allows the plugin to append another transaction to be applied
/// after the given array of transactions. When another plugin
/// appends a transaction after this was called, it is called again
/// with the new state and new transactions—but only the new
/// transactions, i.e. it won't be passed transactions that it
/// already saw.
appendTransaction?: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => Transaction | null | undefined
/// Additional properties are allowed on plugin specs, which can be
/// read via [`Plugin.spec`](#state.Plugin.spec).
[key: string]: any
}
序列化与解析(Serialization & Parsing):ProseMirror 提供了将文档模型转换为其他格式(如 HTML、Markdown 等)的功能,以便于在不同平台和系统之间共享和存储富文本内容。同时,ProseMirror 也支持将这些格式的内容解析为文档模型,从而在编辑器中显示和编辑。关于这些个的具体的了解,比如文档转markdown,或者互转,就可以参考这里 https://github.com/ProseMirror/prosemirror-markdown/tree/master/src
通过以上几个方面的实现,ProseMirror 能够提供一种所见即所得的富文本编辑体验,让用户在编辑过程中直观地看到最终输出效果。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!