ProseMirror 架构总结


ProseMirror 架构总结

ai生成

1. 一句话概括

ProseMirror 是一个偏底层、模块化、可扩展的富文本编辑器框架。它不直接提供完整产品级编辑器 UI,而是提供文档模型、状态管理、事务变换、插件系统、DOM 视图、选区、历史、快捷键、输入规则、表格等基础能力,让业务方可以在它之上搭建自己的编辑器内核。

如果用一句面试话术概括:

ProseMirror 的核心价值是把富文本编辑从“操作 DOM”抽象成“操作受 schema 约束的不可变文档树”,所有修改通过 transaction 和 step 表达,再由 view 层把新的 editor state 同步到 DOM。

在 Hawk 项目里,ProseMirror 不是直接从 node_modules 使用,而是 vendored 到 packages/prosemirror,当前对应 2025.06.03 左右的版本,主要模块版本大致包括:

模块 版本
prosemirror-model ^1.25.1
prosemirror-state ^1.4.3
prosemirror-view ^1.40.0
prosemirror-transform ^1.10.4
prosemirror-commands ^1.7.1
prosemirror-schema-basic ^1.2.4

2. ProseMirror 的核心架构

2.1 总体分层

1
2
3
4
5
6
业务编辑器 / SDK
-> commands / plugins / NodeView / clipboard / table / collaboration
-> prosemirror-view: EditorView、DOM 事件、NodeView、Decoration
-> prosemirror-state: EditorState、Selection、Transaction、Plugin
-> prosemirror-transform: Step、StepMap、Mapping、Transform
-> prosemirror-model: Schema、Node、Mark、Fragment、Slice、DOMParser、DOMSerializer

ProseMirror 的几个关键模块职责很清晰:

模块 职责
model 定义文档树、schema、node、mark、fragment、slice、DOM 解析和序列化
state 定义 editor state、selection、transaction、plugin 和 plugin state
transform 定义 step、step map、mapping 和 transform,用于描述文档变更
view 把 editor state 渲染到 DOM,处理输入事件、DOM selection、NodeView、Decoration
commands 提供常见编辑命令,如 split block、join、delete selection、toggle mark 等
history 基于 transaction/step 实现 undo/redo
keymap 把键盘事件映射为 command
input-rules 根据输入文本触发自动转换,如 # 转 heading
tables 表格 schema、cell selection、table map、列宽、复制粘贴等能力

2.2 文档模型:受 Schema 约束的树

ProseMirror 文档不是 HTML 字符串,也不是浏览器 DOM,而是一棵结构化的不可变文档树:

1
2
3
4
5
6
7
doc
paragraph
text("Hello", marks=[bold])
bullet_list
list_item
paragraph
text("World")

核心概念:

概念 说明
Schema 定义允许出现哪些 node/mark,以及它们之间的嵌套规则
Node 文档树中的结构节点,例如 doc、paragraph、heading、table、image
Mark 行内格式或语义,例如 bold、italic、link、comment
Fragment 一组子节点的不可变集合
Slice 剪切/粘贴/替换时使用的文档片段,带 open depth 信息
ResolvedPos 对整数 position 的结构化解释,可获取父节点、深度、offset 等

这套模型的优点是编辑行为可以严格受 schema 约束。例如 paragraph 里能放 inline,table_cell 里能放 block,image 是 inline atom 还是 block node,都可以由 schema 明确描述。

2.3 坐标系统:整数 position + ResolvedPos

ProseMirror 用整数 position 表示文档位置。这个坐标不是 DOM offset,也不是纯文本下标,而是树结构里的位置索引。

典型操作:

1
2
3
4
5
6
7
8
9
10
11
const { selection } = state;
const { $from, $to } = selection;

$from.pos;
$from.depth;
$from.parent;
$from.parentOffset;
$from.before(depth);
$from.after(depth);
$from.start(depth);
$from.end(depth);

这套 position 系统是 ProseMirror 难学的地方之一,但也是它强大的基础。因为 transform、selection、collaboration、history、decoration 都可以基于同一套 position mapping 机制工作。

2.4 状态模型:不可变 EditorState

EditorState 包含重建当前编辑器所需的核心状态:

  • 当前文档 doc
  • 当前选区 selection
  • 当前 stored marks
  • 插件状态 plugin state
  • schema 和 plugins 配置

ProseMirror 不鼓励直接改 DOM,也不鼓励直接 mutate state。标准流程是:

1
2
3
4
5
6
7
用户输入 / 命令 / 插件事件
-> 创建 transaction
-> transaction 记录 step、selection、meta
-> dispatch(transaction)
-> oldState.apply(transaction) 得到 newState
-> EditorView.updateState(newState)
-> DOM 视图更新

这个流程让所有编辑动作都可追踪、可测试、可回放。

2.5 Transaction 和 Step:编辑行为的最小表达

Transaction 是一次编辑意图的载体。它继承自 Transform,内部可以包含多个 Step

常见 step 包括:

Step 说明
ReplaceStep 替换一段内容
ReplaceAroundStep 包裹、提升、复杂结构替换
AddMarkStep 给范围添加 mark
RemoveMarkStep 移除范围内的 mark
AttrStep 修改节点属性
DocAttrStep 修改 doc 属性

Step 的关键价值不只是“修改文档”,还包括:

  • 可以 apply 到旧文档生成新文档
  • 可以 invert,用于 undo
  • 可以生成 StepMap,把旧 position 映射到新 position
  • 可以序列化,方便协同、回放和持久化

在协同编辑、评论锚点、远程光标、历史记录、装饰层更新里,StepMapMapping 非常关键。

2.6 Selection:不只文本光标

ProseMirror 的 selection 是状态的一部分,并且支持扩展。

内置常见类型:

Selection 说明
TextSelection 文本光标或文本范围
NodeSelection 选中一个完整节点,如图片、附件
AllSelection 选中整个文档
CellSelection 表格插件提供的单元格范围选区

这比 window.getSelection() 更适合复杂编辑器,因为它可以跟随 transaction 映射,也可以参与 undo/redo 和插件状态计算。

2.7 Plugin:扩展编辑器行为的核心机制

ProseMirror 的插件系统是它最重要的扩展点之一。一个 plugin 可以提供:

  • state:自己的插件状态
  • props:拦截 DOM 事件、处理 paste/drop、提供 decorations、nodeViews 等
  • appendTransaction:在 transaction 之后追加修正 transaction
  • filterTransaction:阻止某些 transaction
  • view:创建和销毁 plugin view
  • key:通过 PluginKey 定位插件实例和状态

典型插件能力:

  • 快捷键
  • 历史 undo/redo
  • 复制粘贴
  • 表格选择
  • placeholder
  • 评论锚点
  • 协同光标
  • 目录
  • slash menu
  • format painter
  • find/replace

面试时可以强调:ProseMirror 的插件更接近“编辑器中间件 + 状态切片 + 事件扩展点”的组合,而不是简单的 UI plugin。

2.8 View:DOM 只是 EditorState 的投影

EditorView 负责把 EditorState 展示到 DOM,并处理用户输入事件。

重要能力:

能力 说明
DOM diff/update 根据 state 更新 DOM
DOM event handling 处理键盘、鼠标、输入法、粘贴、拖拽
DOM selection sync 同步浏览器 selection 和 ProseMirror selection
NodeView 自定义复杂节点渲染和交互
MarkView 自定义 mark 渲染
Decoration 不改文档内容的临时渲染,如搜索高亮、远程光标

NodeView 是做复杂富文档节点的关键,例如图片、附件、表格、公式、mention、网页卡片等。它允许业务方自己控制节点 DOM、事件、更新逻辑和销毁逻辑。

3. ProseMirror 的优点

3.1 数据模型严谨,适合复杂文档

ProseMirror 使用 schema 约束文档结构,天然适合复杂文档编辑器。列表、表格、嵌套块、块级卡片、行内卡片、评论、协同锚点等能力可以通过 node/mark/attrs 建模,而不是混在 HTML 里靠约定解析。

3.2 Transaction/Step 机制适合协同、历史和可测试性

每次变更都能表示为 transaction 和 step,使编辑行为具备可追踪、可回放、可映射、可撤销的基础。这对协同编辑、undo/redo、评论位置跟随、远程光标、操作日志分析都很重要。

3.3 插件机制强,可做产品级扩展

插件可以介入状态、事件、transaction 生命周期和视图渲染,适合构建大型编辑器。业务能力可以拆到不同 plugin,避免把所有逻辑堆在一个 editor class 里。

3.4 视图层相对解耦

ProseMirror 自己管理 DOM,但不绑定 React/Vue。业务可以在 NodeView 中接入任意 UI 框架,也可以把核心编辑能力做成 SDK,被不同产品壳复用。

3.5 对结构化文档更友好

相比以 Delta 或 HTML 为中心的方案,ProseMirror 的树形文档模型更自然地表达:

  • 多级嵌套结构
  • 表格
  • task list
  • block quote
  • code block
  • block card
  • heading hierarchy
  • rich document object

3.6 生态成熟

ProseMirror 的底层能力稳定,Tiptap、Milkdown、Remirror 等框架都基于 ProseMirror 做了更高层封装。对于需要自研编辑器内核的团队,ProseMirror 是比较可靠的底座。

4. ProseMirror 的缺点

4.1 学习曲线陡

ProseMirror 的核心概念多,而且抽象偏底层。新人需要理解 schema、node、mark、position、ResolvedPos、transaction、step、mapping、selection、plugin、NodeView 等一整套体系。

尤其是 position 坐标系统和 step mapping,对没有编辑器经验的人不直观。

4.2 API 偏底层,产品能力需要自建

ProseMirror 不提供开箱即用的完整编辑器产品。工具栏、菜单、图片上传、评论、协同 UI、slash menu、目录、权限、移动端适配等都需要业务自己实现。

这对中大型团队是灵活性,对小团队则是成本。

4.3 插件之间容易产生复杂交互

多个 plugin 同时处理 paste、keydown、appendTransaction、decorations、selection 时,顺序和边界很重要。大型编辑器如果缺少规范,容易出现:

  • transaction 被多个 plugin 反复修正
  • selection mapping 错误
  • decoration 位置漂移
  • NodeView update 与 DOMObserver 冲突
  • paste 规则互相覆盖

4.4 DOM 和状态同步问题难排查

ProseMirror 自己维护 DOM view tree。如果 NodeView 写得不规范,或者业务直接操作 ProseMirror 管理的 DOM,可能导致 selection、composition、mutation observer、view update 出现复杂问题。

输入法、移动端 Safari、复制粘贴、表格拖拽尤其容易踩坑。

4.5 Schema 设计前期压力大

schema 一旦用于线上文档数据,就会变成协议。node/mark/attrs 的设计如果前期不稳定,后续迁移成本很高。复杂编辑器必须提前考虑兼容、降级、未知节点、序列化和跨端解析。

5. 与 Quill、Slate、Lexical、Tiptap 的对比

5.1 总览对比

维度 ProseMirror Quill Slate Lexical Tiptap
定位 底层编辑器框架 开箱即用富文本编辑器 高度可定制的 React 编辑器框架 Meta 出品的现代编辑器框架 ProseMirror 的上层封装
数据模型 Schema 约束的树 Delta 线性操作模型 + Parchment Blot JSON 树,操作通过 operations EditorState + node tree 本质仍是 ProseMirror schema/doc
UI 绑定 不绑定框架 自带编辑器实例和主题 强依赖 React 使用习惯 支持 React 但核心独立 常与 Vue/React 使用
扩展方式 Plugin、NodeView、Schema Module、Blot、Clipboard matcher Plugin pattern、normalization、renderElement Node、Command、Transform、Plugin-like extension Extension
复杂结构 中等,复杂嵌套成本高 强,但需要自己处理很多约束
协同基础 Step/Mapping 适合协同 Delta 天然适合 OT 表达 需自行设计或接入生态 有现代协作生态,但选型需评估 依赖 ProseMirror/Yjs 等
学习成本 中低 中高 中高
开箱程度 中低 高于 ProseMirror
适合场景 自研复杂文档、长期演进 SDK 标准富文本、轻量编辑 React 深度定制编辑器 现代 Web 编辑器、性能敏感场景 快速基于 ProseMirror 做产品

5.2 ProseMirror vs Quill

Quill 的核心数据格式是 Delta。Delta 本质上是一个线性的操作序列,常见形式是 insert/retain/delete 加 attributes。

Quill 的优势:

  • API 更简单,上手更快
  • 默认主题、toolbar、clipboard 等能力更开箱
  • Delta 模型适合表达文本编辑操作和 OT
  • 对常规富文本场景足够高效

Quill 的劣势:

  • 面对复杂树形结构时表达力弱于 ProseMirror
  • 深度嵌套、复杂表格、块级卡片、复杂 schema 约束会变得困难
  • 很多能力依赖 Parchment/Blot 扩展,复杂后调试成本上升
  • 数据模型更接近线性文档,对结构化富文档不如树模型自然

ProseMirror 相比 Quill 的核心优势:

  • 文档结构表达能力更强
  • schema 可以严格约束文档合法性
  • step map 对 selection、decoration、协同锚点跟随很有价值
  • plugin 生命周期更适合大型编辑器内核拆分

面试回答可以这样说:

如果做一个中等复杂度的富文本输入框,Quill 更省事;如果做类似在线文档、表格、评论、协同、复杂块结构、长期协议兼容的编辑器,ProseMirror 的架构更稳。

5.3 ProseMirror vs Slate

Slate 也是树形文档模型,并且非常强调可定制。它和 React 结合紧密,开发体验对 React 团队比较友好。

Slate 的优势:

  • JSON 数据结构直观
  • React renderElement/renderLeaf 模式容易理解
  • 自定义节点和渲染很灵活
  • 适合做深度定制的编辑体验

Slate 的劣势:

  • 很多约束需要应用层自己维护 normalization
  • 历史、协同、复杂 schema、复杂表格等能力需要更多自建或依赖生态
  • 对大型文档和复杂插件体系,工程纪律要求高

ProseMirror 相比 Slate 更强的点:

  • schema 约束更系统
  • transform/step/mapping 更成熟
  • 插件和 transaction 生命周期更完整
  • 对复杂编辑器底层行为的可预测性更强

Slate 相比 ProseMirror 更舒服的点:

  • React 心智更自然
  • 文档 JSON 更容易直接读写
  • 简单定制时心智负担小一些

5.4 ProseMirror vs Lexical

Lexical 是 Meta 推出的现代编辑器框架,强调性能、可扩展、命令机制和不可变 editor state。

Lexical 的优势:

  • 架构现代,更新批处理和状态模型清晰
  • 性能表现好
  • 命令、节点、transform 等机制清楚
  • React 集成体验好
  • 对现代前端团队比较友好

Lexical 的潜在劣势:

  • 生态和历史沉淀相比 ProseMirror 仍需要结合具体场景评估
  • 如果已有 ProseMirror 生态、插件、数据协议和协同链路,迁移成本高
  • 一些复杂文档能力需要团队自己验证成熟度

ProseMirror 的优势在于成熟和底层能力完整,Lexical 的优势在于现代化和性能体验。新项目如果没有历史包袱,可以评估 Lexical;但如果目标是复杂在线文档内核,ProseMirror 仍然是非常稳的选择。

5.5 ProseMirror vs Tiptap

Tiptap 不是 ProseMirror 的竞争对手,而是 ProseMirror 的上层封装。它把 schema、command、plugin、NodeView 等能力包装成 extension 体系,并提供更现代的 API。

Tiptap 的优势:

  • 开箱体验比 ProseMirror 好
  • extension 写法更统一
  • 文档和社区更贴近产品开发
  • React/Vue 集成更方便

Tiptap 的劣势:

  • 底层问题仍然要理解 ProseMirror
  • 高度定制时可能需要绕过封装
  • 对大型自研 SDK,封装层可能限制底层控制力

如果团队目标是快速做业务编辑器,可以选 Tiptap;如果目标是掌控底层协议、事务、插件、协同、渲染和跨产品 SDK,直接使用 ProseMirror 更可控。

6. 为什么 Hawk 这类项目适合 ProseMirror

结合当前仓库,Hawk 是一个新一代文档编辑器 SDK,而不是简单页面组件。它需要处理:

  • richdoc 协议适配
  • schema 长期演进
  • 表格、图片、附件、公式、mention、未知节点
  • 复制粘贴和跨应用兼容
  • 命令 API
  • NodeView/MarkView 渲染
  • 协同位置映射
  • 插件化编辑行为
  • Jest/Playwright/Storybook 测试体系

这些目标更接近“编辑器内核工程”,而不是“富文本输入框”。因此 ProseMirror 的强 schema、transaction、step mapping、plugin、NodeView 机制更适合长期演进。

可以把 Hawk 的分层理解为:

1
2
3
4
5
6
Cougar / 业务宿主
-> Hawk core: schema、commands、editor API
-> Hawk plugins: clipboard、table、toc、slash、format painter 等
-> Hawk views: NodeView / MarkView
-> richdoc-adaptor: Richdoc Delta <-> ProseMirror doc/transaction
-> vendored ProseMirror: model/state/transform/view

7. 面试官可能会问的问题与答案

Q1:ProseMirror 的核心架构是什么?

答:ProseMirror 可以分为 model、state、transform、view 四层。model 定义 schema、node、mark 和文档树;state 保存不可变的 editor state、selection 和 plugin state;transform 用 step/transaction 表达文档修改,并通过 step map 做位置映射;view 负责把 state 渲染到 DOM 并处理用户输入。业务能力主要通过 plugin、command、NodeView、Decoration 扩展。

Q2:为什么说 ProseMirror 不是直接操作 DOM?

答:因为 ProseMirror 的真实数据源是 EditorState.doc,DOM 只是 view 层的投影。编辑时应该创建 transaction 修改文档树,再 dispatch 生成新 state,最后由 view 更新 DOM。如果绕过 ProseMirror 直接改 DOM,容易破坏 selection、mutation observer 和 view desc 的一致性。

Q3:Schema 在 ProseMirror 里解决什么问题?

答:Schema 定义文档允许有哪些 node 和 mark,以及它们的嵌套关系和属性。它保证文档结构合法,也决定 DOM parse/serialize 规则。对在线文档来说,schema 也是数据协议的一部分,设计不稳定会带来兼容和迁移成本。

Q4:Transaction 和 Step 有什么区别?

答:Step 是最小的文档修改单元,例如 replace、add mark、remove mark、改属性。Transaction 是一次编辑动作的容器,可以包含多个 step,同时还可以携带 selection、stored marks 和 meta。可以理解为 step 描述“怎么改文档”,transaction 描述“一次编辑行为及其上下文”。

Q5:StepMap / Mapping 为什么重要?

答:文档变化后,旧 position 可能失效。StepMap 描述一次 step 如何把旧位置映射到新位置,Mapping 则串联多个 StepMap。它用于更新 selection、decoration、评论锚点、远程光标、协同位置和 undo/redo。复杂编辑器里位置映射是底层关键能力。

Q6:ProseMirror 插件能做什么?

答:插件可以维护自己的状态、处理 DOM 事件、提供 decorations、注册 NodeView、拦截或追加 transaction、实现 plugin view。快捷键、历史、输入规则、复制粘贴、表格选择、评论、协同光标、目录、搜索高亮都可以用 plugin 实现。

Q7:appendTransaction 和 filterTransaction 有什么区别?

答:filterTransaction 在 transaction 应用前执行,用来判断是否允许这次 transaction;appendTransaction 在一批 transaction 应用后执行,可以根据新旧 state 追加一个新的 transaction,常用于结构修正、补齐属性、清理非法状态等。

Q8:NodeView 适合解决什么问题?

答:NodeView 用于自定义节点的 DOM 渲染和交互,适合图片、附件、公式、mention、网页卡片、复杂表格单元等不适合用普通 DOMSerializer 表达的节点。NodeView 可以控制 update、ignoreMutation、stopEvent、destroy 等生命周期。

Q9:Decoration 和 Mark 有什么区别?

答:Mark 是文档内容的一部分,会进入 doc 数据,例如 bold、link、comment mark。Decoration 是 view 层临时渲染,不改变文档数据,例如搜索高亮、选区提示、远程光标、拼写错误波浪线。需要持久化的语义用 mark,不需要进入文档的数据用 decoration。

Q10:ProseMirror 和 Quill 最大区别是什么?

答:Quill 以 Delta 线性操作模型为核心,上手更快、开箱程度更高,适合常规富文本。ProseMirror 以 schema 约束的树形文档模型为核心,transaction/step/mapping 更强,适合复杂结构化文档、表格、块级节点、协同锚点和长期协议演进。

Q11:为什么复杂在线文档更倾向 ProseMirror?

答:复杂在线文档需要严格结构、嵌套块、表格、卡片节点、评论、协同、历史、复制粘贴和协议兼容。ProseMirror 的 schema、step、mapping、plugin、NodeView 能把这些能力拆成可组合的底层机制,而不是堆在 DOM 或字符串处理里。

Q12:ProseMirror 的主要缺点是什么?

答:学习曲线高,API 偏底层;产品能力不够开箱,需要业务自建;插件之间可能产生复杂交互;NodeView 和 DOM 同步问题难排查;schema 设计前期压力大,一旦线上数据依赖后迁移成本高。

Q13:ProseMirror 如何支持协同编辑?

答:ProseMirror 的 step 可以序列化、应用、反转,并且每个 step 有 StepMap,可以做位置映射。这为协同编辑提供了底层基础。但 ProseMirror 本身不是完整协同系统,仍需要 OT/CRDT 服务、冲突处理、权限、网络同步和业务协议适配。

Q14:为什么复制粘贴在 ProseMirror 编辑器里很复杂?

答:复制粘贴涉及 Slice、open depth、schema 合法性、DOMParser、DOMSerializer、clipboard text/html/plain、自定义内部格式、表格结构、外部 HTML 清洗、图片和文件、跨浏览器差异。复杂编辑器还要保证内部复制保真、外部粘贴安全和结构合法。

Q15:ProseMirror 里如何设计一个新节点?

答:通常要先确定节点语义和层级,是 block、inline、atom 还是 selectable;再设计 attrs 和兼容策略;然后写 NodeSpec,包括 content、group、marks、parseDOM、toDOM;如果交互复杂再实现 NodeView;最后补 command、paste 规则、序列化、测试和旧数据兼容。

Q16:什么时候用 Mark,什么时候用 Node?

答:Mark 适合描述一段 inline 内容上的附加语义或样式,例如 bold、link、comment。Node 适合描述文档结构或独立对象,例如 paragraph、heading、image、table、mention。判断标准是它是否改变文档结构、是否需要独立选中、是否有复杂属性和生命周期。

Q17:怎么避免 ProseMirror 插件变得混乱?

答:要明确插件边界:一个插件只负责一类状态和行为;避免多个插件同时改同一类 transaction;把 command、state、view、utils 分层;对 appendTransaction 保持克制;关键行为补 transaction 级单测;复杂 UI 通过 plugin state 和 NodeView/外层 React 状态建立清晰同步关系。

Q18:如果面试官问“为什么不用 Tiptap”怎么答?

答:Tiptap 是 ProseMirror 的上层封装,适合快速做产品编辑器。但如果项目需要深度控制 schema、transaction、step、mapping、插件生命周期、协议适配、协同和底层补丁,直接基于 ProseMirror 更可控。Hawk 这类 SDK 项目更关注内核能力和长期维护,因此直接掌控 ProseMirror 底层更合理。

Q19:ProseMirror 和 Slate 怎么选?

答:如果团队强 React、需求是高度自定义但结构复杂度可控,Slate 开发心智更自然。如果需求是复杂文档内核、强 schema、协同位置映射、表格和长期协议稳定,ProseMirror 更成熟。Slate 灵活,ProseMirror 更系统。

Q20:如何向非编辑器背景的人解释 ProseMirror?

答:可以类比 Redux。EditorState 类似 store state,transaction 类似 action 加 reducer 上下文,plugin 类似中间件和状态切片,view 类似根据 state 渲染 UI。区别是 ProseMirror 的 state 是文档树和选区,transaction 内部还有 step 和位置映射,专门解决富文本编辑问题。

8. 面试表达建议

回答 ProseMirror 相关问题时,不要只背 API 名词,最好按下面结构回答:

  1. 先讲核心抽象:schema 约束的文档树。
  2. 再讲编辑流程:command/input -> transaction -> step -> new state -> view update。
  3. 再讲扩展机制:plugin、NodeView、Decoration。
  4. 最后结合业务:复杂文档、表格、复制粘贴、协同、协议兼容为什么需要这些机制。

一个比较完整的回答模板:

我理解 ProseMirror 的核心是 model/state/transform/view 四层。model 用 schema 定义合法文档树,state 保存不可变编辑器状态,transform 用 transaction 和 step 表达每次修改,view 负责 DOM 渲染和输入事件。它的难点在 position、mapping、plugin 交互和 NodeView,但这些能力也让它适合复杂在线文档。相比 Quill,ProseMirror 更底层、更难上手,但结构表达、事务追踪、协同位置映射和大型插件化能力更强。


文章作者: 赤蓝紫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 赤蓝紫 !
评论
  目录