设计)
Slate v2 编辑器 read/update 运行时架构与事务目标新鲜度Target Freshness设计【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文讲解当前仓库中 Slate v2 编辑器架构的一次关键设计演进以editor.read(...)/editor.update(...)为公共生命周期的 read/update 运行时架构以及围绕“目标新鲜度target freshness”建立的事务级隐式目标解析机制。该方案用于解决富文本编辑器中最典型的一类陈旧选择stale selection问题——例如浏览器光标位于第二段用户点击“标题”工具栏按钮结果第一段被误改。读完本文你将掌握 read/update 生命周期契约、事务transaction目标解析的内部流程、primitive 方法与扩展方法的分层方式、React 19.2 运行时契约以及这套架构对应的硬性裁剪清单hard cuts与性能门槛。本文以 docs/plans/2026-04-23-slate-v2-selection-fresh-editor-methods-architecture-plan.md 为主干并结合仓库内packages/slate与packages/slate-react的真实实现交叉印证。一、设计背景从“陈旧选择”到语义方法爆炸1.1 真实缺陷paragraph-2 标题误改 paragraph-1方案文档用一段可复现的追踪线索tracer概括了 Slate v2 当时要解决的回归类别浏览器DOM选区位于段落 2用户点击“标题一”工具栏按钮结果段落 1 被切换成了标题。根因是工具栏/插件命令读取的是模型侧的陈旧 selection而不是浏览器当前真实的选区。也就是说应用层与插件层在发起 mutation 时绕过了“运行时选择目标新鲜度”这一不变式invariant。1.2 为什么“加一堆语义方法”不是正解早期的修复方向是“目标新鲜方法”target-fresh method——为每种节点都提供一个 toggle 方法例如toggleMarktoggleBlocktoggleListtoggleAlignmenttoggleTodotoggleCallouttoggleWhateverCustomNode方案明确指出这是一种语义 API 膨胀semantic API bloat不是最终 DX。Slate 的持久优势在于基于任意 JSON 类文档模型的灵活 primitive transforms。完美的 v2 应该让这些 primitive 在单一生命周期下变得安全而不是用一个无穷无尽的语义方法目录取代灵活性。1.3 最终方向Authoritative Direction文档在“Authoritative Direction”一节给出了最终裁决不转向另一种编辑器架构而是加大力度融合各家之长Slate model operations Lexical-style read/update lifecycle ProseMirror-style transaction and DOM-selection discipline Tiptap-style extension ergonomics React 19.2 optimized rendering/runtime APIs核心理念是>editor.read(() { const selection editor.getSelection(); const active editor.hasNodes({ match: isHeading }); }); editor.update(() { editor.unwrapNodes({ match: isList }); editor.setNodes({ type: list-item }); editor.wrapNodes({ type: bulleted-list, children: [] }); });内部运行时契约则是一条清晰的流水线editor.update - transaction - resolve implicit target once - primitive editor methods use the transaction target when at is omitted - operations - EditorCommit - history / collaboration / React runtime / DOM repairtx.resolveTarget()是正确的但它是内部引擎室 APIengine-room API——插件作者不应学习 DOM 新鲜度策略。二、read/update 公共生命周期契约2.1 读取边界editor.read(fn)方案把editor.read定义为唯一的主一致性读取边界primary coherent read boundaryeditor.read(() { editor.getSelection(); editor.getChildren(); editor.getMarks(); editor.getOperations(); editor.getLastCommit(); });规则editor.read是同步的它从不导入 DOM selection它看到的是一致的模型/运行时状态如实现需要它允许冲刷flush挂起的更新editor.read之外的读取只允许存在于明确文档化为安全的稳定 live API。2.2 写入边界editor.update(fn, options?)editor.update(() { editor.setNodes({ type: heading-one }); });规则editor.update创建或复用一条事务嵌套的 update 折叠为一条事务/一次 commit隐式目标每条事务最多解析一次带显式at的 primitive 方法从不导入 DOM selection不带显式at的 primitive 方法使用事务目标update 内发出的所有 operations 汇总为一个EditorCommithistory、collaboration、React runtime、DOM repair 共同消费这个 commit。方案为editor.update预留了 update tags标签用于高级 history/collab/render 行为history-pushhistory-mergepastecollabskip-dom-selectionskip-scroll2.3 公共头条原则All writes happen in editor.update. All coherent reads happen in editor.read. Primitive editor methods are safe inside updates. Operations remain collaboration truth. Commits are local runtime truth.三、Primitive 方法与 Convenience 方法的分层3.1 保留灵活 primitive 作为主 API方案明确保留以下灵活的 primitive 方法作为主力的“高阶用户/插件 API”并要求它们在editor.update内是安全的editor.setNodes(props, options?)editor.unsetNodes(key, options?)editor.wrapNodes(element, options?)editor.unwrapNodes(options?)editor.insertNodes(nodes, options?)editor.removeNodes(options?)editor.mergeNodes(options?)editor.splitNodes(options?)editor.moveNodes(options?)editor.insertText(text, options?)editor.delete(options?)editor.insertFragment(fragment, options?)editor.select(selection)这些方法与当前仓库packages/slate/src/create-editor.ts中绑定的实现一一对应——createEditor通过bindFirst把setNodes、insertText、insertFragment、wrapNodes、unwrapNodes、mergeNodes、splitNodes、moveNodes、removeNodes、unsetNodes、select等方法绑定到 editor 实例上见 packages/slate/src/create-editor.ts。3.2 Convenience 方法允许但不构成架构editor.toggleMark(bold); editor.toggleBlock(heading-one);方案强调不要为每个应用专属节点族扩张核心 convenience 方法。自定义节点族应该在editor.update内部用 primitive 组合实现。这一设计在仓库中已有落地。create-editor.ts绑定并导出了toggleBlock与toggleMarkpackages/slate/src/create-editor.ts并以editor.tf.toggleBlock(...)/editor.tf.toggleMark(...)的形式暴露在 transforms 命名空间packages/slate/src/interfaces/editor/editor-transforms.ts。其核心实现位于 packages/slate/src/internal/transforms-extension/toggleBlock.tsexport const toggleBlock (editor, type, { defaultType, someOptions, wrap, ...options } {}) { const at options.at ?? editor.selection; if (!at) return; const isActive editor.api.some({ at, ...someOptions, match: { type } }); // ...wrap 分支使用 unwrapNodes / wrapNodes // ...默认分支使用 setNodes在 isActive 与 defaultType 相等时提前返回 };注意其中const at options.at ?? editor.selection;这一行正是“显式at优先、缺省时回退到隐式目标”策略在真实源码中的体现。对 mark 的同类实现见 packages/slate/src/internal/transforms-extension/toggleMark.ts其测试覆盖了editor.tf.toggleMark(italic)、editor.tf.toggleMark(subscript, { remove: superscript })等组合场景toggleMark.spec.tsx。3.3 Optional Chain API可选糖方案允许未来以语法糖形式提供链式调用但明确约束editor .chain() .unwrapNodes({ match: isList }) .setNodes({ type: list-item }) .wrapNodes({ type: bulleted-list, children: [] }) .run();规则chain().run()只是editor.update之上的糖不得引入第二套事务引擎普通工具栏命令不得要求focus()仪式。四、扩展模型确定性扩展 API 而非 monkeypatching4.1 公共扩展形态方案给出了两类扩展写法。内联扩展editor.extend({ name: todo, methods: { toggleTodo() { this.update(() { this.setNodes({ type: todo, checked: true }); }); }, }, });命名包形态const TodoExtension defineEditorExtension({ name: todo, methods: { toggleTodo() { this.update(() { this.setNodes({ type: todo, checked: true }); }); }, }, normalizers: [], commands: [], });4.2 规则与反模式扩展方法通过editor.update组合任意方法 monkeypatching 不是扩展模型command registry 只允许用于内部/事件路由插件作者不应选择命令策略扩展依赖/冲突由registry 拥有而非临时拼凑的 React context。反模式清单Bad// Bad: 实例方法 monkeypatching editor.toggleTodo (options) { this.setBlock({ type: todo, checked: options.checked }); }; // Bad: 自由 transform 作为扩展机制 Transforms.setNodes(editor, props); // Bad: 直接读公共可变字段 editor.selection; // Bad: 命令策略对象 editor.registerCommand(x, { policy: formatting });文档中还有一组“半好”的反例说明即使用事务也要走扩展 API直接在实例上赋值editor.myTransform ...并调用Editor.withTransaction(editor, (tx) ...)依然是反模式正确做法是把事务放进注册方法内部editor.extend({ methods: { myTransform(options) { return Editor.withTransaction(this, (tx) { const selection tx.getSelection(); // selection is fresh if needed }); }, }, });4.3 为什么方法优先优于命令策略方案用一段尖锐的论证收尾Why This Is Better Than Command Policies如果插件作者必须在formatting、selection-transform、ui-only、import-dom-before-command之间选择架构就已经失败了。因为方法自己知道是否需要隐式选区事务自己知道是否请求了隐式目标解析React 运行时自己知道DOM 选区是否具备权威性。所以最安全的 API 就是editor.setNodes(props); editor.setNodes(props, { at });不需要额外的策略。五、目标新鲜度契约Target Freshness Contract5.1 内部流水线editor.update - active transaction - tx.resolveTarget() - target runtime asks slate-react only when an implicit current-selection mutation needs browser target freshness5.2 触发边界触发primitive 写方法且未提供显式at隐式以当前选区为目标的事务解析。不触发editor.readeditor.getSelection()带显式at的方法从不解析隐式目标的事务不修改编辑器状态的应用/UI 代码。三个典型例子editor.setNodes({ type: heading-one }); // 隐式目标若 DOM 拥有选区tx.resolveTarget 导入当前 DOM 选区 editor.setNodes({ type: heading-one }, { at: [1] }); // 显式目标无需 DOM 导入 editor.insertText(x); // 输入管线已由模型拥有目标新鲜度 no-op5.3 框架无关的 Target Runtime Hook核心层保持 React-free拥有一个框架无关的 hookeditor.targetRuntime { resolveImplicitTarget(editor, request) { return Editor.getLiveSelection(editor); }, };slate-react安装浏览器实现resolveImplicitTarget(editor, request) { if (domSelectionBelongsToEditor(editor) domSelectionIsAuthoritativeForTarget(editor)) { return importDOMSelectionAsTarget(editor); } return Editor.getLiveSelection(editor); }请求对象可以携带内部诊断信息隐式目标解析、事务 id、当前选区来源、事件/运行时拥有者但普通插件作者永远不需要手动传入。5.4 运行时权威状态Runtime Authority States这些状态指导targetRuntime.resolveImplicitTarget决策是内部运行时状态不是公共命令策略旋钮DOM current selectionDOM 当前选区model-owned selection模型拥有选区app/internal control应用/内部控制shell-backed selectionshell 支撑选区composition输入法组合clipboard/drag/drop剪贴板/拖拽5.5 写边界事务是唯一 mutation 边界editor method - Editor.withTransaction - tx.apply / tx helpers不允许公共 API 在事务之外应用 operations 或变更运行时状态。反例与正例// Bad editor.apply(op); editor.operations.push(op); editor.selection range; // Good editor.applyOperation(op); editor.withTransaction((tx) tx.apply(op)); editor.select(range);editor.applyOperation(...)只有在“创建/复用事务”且“发出与其他写入相同的 commit 元数据”时才是允许的。六、React 19.2 运行时契约6.1 React 接收运行时事实而非编辑真相React 获得live text/node reads实时文本/节点读取last commit最近一次提交dirty paths脏路径dirty runtime ids脏运行时 iddirty top-level ranges脏顶层范围source-scoped projection invalidation按来源作用域的投影失效semantic islands语义孤岛direct DOM text sync capability result直接 DOM 文本同步能力结果explicit fallback signal when DOM sync declines an operationDOM 同步拒绝操作时的显式回退信号React 不得依赖Editor.getSnapshot()处理紧急渲染路径urgent render paths。6.2 直接 DOM 同步的退出条件opt out以下场景中直接 DOM 同步必须退出转为 React 回退自定义渲染器projections/decorationscomposition输入法组合placeholders/zero-width占位符/零宽字符多个字符串节点影响可访问性的标记应用自有的输入处理器被跳过的文本操作skipped text ops强制 React 回退这一条在仓库中有对应的问题解决方案文档记录docs/solutions/ui-bugs/2026-05-07-slate-react-chrome-composition-fallback-must-clean-unmanaged-projection-dom-text.md以及方案执行台账中引用的 docs/solutions/ui-bugs/2026-04-23-slate-react-unsynced-dom-text-ops-must-force-react-fallback.md直接 DOM 文本同步未真正处理文本操作时必须强制 React 回退。七、Hard Cuts从公共 API/文档/示例中移除什么从主公共 API、文档与示例中移除Transforms.*可变的editor.selection可变的editor.children可变的editor.marks可变的editor.operations公共editor.apply公共editor.onChange命令策略对象command policy objectsReactEditor.runCommandchild-count chunking任意方法 monkeypatching作为主 overlay API 的 legacydecorate内部允许保留children/selection/marks/operations 的私有存储用于事件/内核路由的内部 command registry内部tx.resolveTarget仅在被积极删除期间存在的内部兼容包装公共字段被硬裁之后读写只能走方法editor.getSelection(); editor.getChildren(); editor.getMarks(); editor.getOperations(); editor.select(range); editor.setNodes(props); editor.insertText(text); editor.withTransaction(...);方案明确editor.selection是**默认陈旧stale-by-default**的不得出现在应用/插件 UI 命令代码中。八、从状态之艺State-Of-The-Art借鉴什么、不借鉴什么8.1 借鉴 Lexical借鉴editor.update(fn)公共写入生命周期、editor.read(fn)公共一致性读取生命周期、update tags、脏叶子/元素纪律适配 Slate 路径/运行时 id、transform/update 生命周期纪律无 update-listener 瀑布、扩展依赖图概念。不借鉴$function命名、class 化节点模型、以完整 Lexical DOM reconciler 作为主线。Slate v2 适配保留 JSON 节点与 operations采用 read/update 生命周期紧急路径用 commit 脏区域而非全量快照读取直接 DOM 文本同步作为能力而非全局 reconciler。8.2 借鉴 ProseMirror借鉴事务权威transaction authority、位置/选区映射纪律、面向 history/collab 安全的选区书签、selectionFromDOM/selectionToDOM显式桥接方向、DOM 观察者纪律浏览器 DOM 变化经由唯一 owner 进入、decorations 作为映射数据而非渲染回调。不借鉴整数位置文档模型、僵化的 schema-first 模型、以 ProseMirror 插件复杂度作为公共扩展 API、迫使 React 站在编辑器之外的 view 层。Slate v2 适配路径/运行时 id 取代整数位置事务与操作映射维持选区/脏真相slate-react通过单一运行时 owner 拥有 DOM import/export/repair。8.3 借鉴 Tiptap借鉴扩展可用性extension ergonomics、可发现的 commands/methods、后续可选的链式 API、可组合的 React UI helpers 与基于 selector 的 UI 状态。不借鉴强制editor.chain().focus().toggleX().run()仪式、以 ProseMirror 泄漏作为常规逃生口、“React 应尽量与编辑器隔离”的建议。Slate v2 适配editor.update消除了目标新鲜度所需的focus()仪式可选的editor.chain()未来可作为editor.update之上的糖React 集成应该是一等公民且快速而不是被回避。8.4 借鉴 Edix借鉴方法在“当前选区”上操作 激进的 operation/selection 测试纪律——命令 选区行为必须通过生成式浏览器场景证明而不是一次性示例。九、实施计划七个阶段与浏览器关卡方案正文中的实施计划共 7 个阶段第 8 节之后是执行台账记录了前几个阶段的实际落地结果阶段主题关键产物Phase 1Read/Update 生命周期editor.read(fn)、editor.update(fn, options?)、update tags、嵌套 update 折叠、read 在 dev/test 下拒绝写、update 产出单一EditorCommitPhase 2Primitive 方法运行时契约每个 primitive 写方法运行在editor.update内或复用活动 update隐式选区 primitive 在缺省at时使用事务目标显式at绕过 DOM 新鲜度Phase 3扩展运行时editor.extend(...)、defineEditorExtension(...)、冲突检测、确定性方法组合Phase 4React 运行时对齐slate-react安装目标运行时、拥有 DOM import/export/repair、直接 DOM 同步返回能力结果、被跳过的文本操作强制 React 回退Phase 5公共 API 硬裁移除公共可变字段、公共Transforms.*、apply/onChange扩展点Phase 6浏览器关卡Browser Gauntletsupdate/read 写边界、工具栏/应用命令、marks、简单块、从 primitive 构建的自定义块变换、列表/对齐变换、inline/void 边界、删除类操作、粘贴、undo/redo、IME、shadow DOM、移动端语义回退、大文档 shell 激活Phase 7性能门槛见下方命令每一行面向用户的浏览器场景都必须断言模型树/文本、模型选区、可见 DOM、可观测处的 DOM 选区/光标、commit 元数据、无非法内核迁移、后续输入。9.1 性能门槛命令bun run bench:react:rerender-breadth:local REACT_HUGE_COMPARE_BLOCKS5000 REACT_HUGE_COMPARE_ITERATIONS5 REACT_HUGE_COMPARE_TYPE_OPS10 bun run bench:react:huge-document:legacy-compare:local bun run bench:core:observation:compare:local bun run bench:core:huge-document:compare:local性能目标React 大文档在稳定输入与 ready/mount 上比 legacy chunk-off 与 chunk-on 更快只允许显式接受的首激活first-activation权衡紧急渲染路径不调用全量快照被跳过的直接 DOM 同步不引发广泛 React 重渲染。十、执行台账从红测试到窄目标运行时10.1 Phase 0 审计执行前的审计命令rg -n editor\.(selection|children|marks|operations|onChange|apply)\b|Transforms\. packages/slate packages/slate-react site/examples/ts -g *.ts -g *.tsx审计表揭示了几个高信号结论NodeTransforms与SelectionTransforms多数已委托给 editor 方法TextTransforms.removeText读取Editor.getSnapshot(editor).selection?.anchor默认陈旧Editor.deleteBackward/deleteForward/deleteFragment直接读editor.selection并调用Transforms.delete被标记为P0 陈旧选择风险richtext.tsx工具栏的toggleBlock直接调用Transforms.unwrapNodes/setNodes/wrapNodes被标记为 paragraph-2 标题 bug 的直接来源。10.2 Phase 1红测试追踪在richtext.test.ts中加入 Chromium 浏览器追踪器bunx playwright test ./playwright/integration/examples/richtext.test.ts --projectchromium --grep toolbar heading --workers1 --retries0初始失败段落 1 变成标题、段落 2 保持段落引入懒目标新鲜度后段落 2 正确变成标题但随后出现新的红后续原生输入落在段落 2 而模型/DOM 选区停在 offset 0 未前进且既有 richtext 行出现导航/输入回归。被拒绝的策略从每个getCurrentSelection读取全局运行 DOM 目标新鲜度——修复了工具栏却回归了既有浏览器选择/变异路径逐个补丁输入路径会打地鼠并违背方案目标。10.3 Phase 2窄目标运行时将新鲜度从通用的getCurrentSelection收窄到显式事务目标解析器移除getCurrentSelection中的不安全广度新鲜度策略增加框架无关类型TargetFreshnessRequest、EditorTargetRuntime增加核心助手setTargetRuntime(editor, runtime)、resolveImplicitTarget(editor, fallback)增加事务 APItx.getModelSelection()、tx.resolveTarget({ at? })将setNodes隐式目标解析迁移到tx.resolveTarget在Editable安装slate-react目标运行时将 DOM 导入限定在隐式目标解析保持语义手柄选区与原生浏览器移动与 App 命令目标解析隔离。关键测试证据bun test ./packages/slate/test/transaction-target-runtime-contract.ts --bail 1 bunx playwright test ./playwright/integration/examples/richtext.test.ts --projectchromium --grep generated navigation and typing|toolbar heading --workers1 --retries0 bun test ./packages/slate/test/transaction-contract.ts --bail 1 bun test ./packages/slate/test/snapshot-contract.ts --bail 1结果事务目标运行时契约3 passed生成式导航 工具栏标题2 passedChromium richtext 选区/光标/粘贴子集18 passed事务契约23 passed快照契约190 passed重渲染广度无广泛文本/祖先渲染。10.4 Mark 方法与组合回退切片后续切片进一步把Editor.addMark/Editor.removeMark的选区决策移入Editor.withTransaction与tx.resolveTarget()新增editor.toggleMark(key, value?)并将事务内隐式目标解析缓存为“每条事务至多一次”。同时修复了大文档组合回退syncTextOperationsToDOM上报文本操作同步计数Slate仅在文本操作未被直接 DOM 同步时强制 React selector 更新组合中选区修复在 DOM 选区无 range 时创建末端光标而非不安全地调用collapseToEnd()。10.5 块方法跟进切片新增核心方法editor.setBlock(props, options?)与editor.toggleBlock(type, options?)全部经由Editor.withTransaction与tx.resolveTarget({ at })实现并把 richtext 标题/引用工具栏命令迁移到editor.toggleBlock(...)。方案特别强调列表与对齐需要各自更丰富的命令契约不要假装泛化的toggleBlock(type)能解决列表包裹或对齐语义。十一、完成标准与 Non-Goals11.1 完成标准Completion Criteriaeditor.read与editor.update成为公共生命周期契约primitive 方法在editor.update下安全隐式目标解析是内部且事务作用域的插件作者无需目标仪式即可用 primitive 构建自定义节点行为公共可变字段从文档/示例/类型面硬裁Transforms.*从主文档/示例移除slate-react紧急路径消费 live reads/dirty commits 而非全量快照生成式浏览器关卡覆盖已知命令/输入族跨浏览器集成全绿或每个剩余平台限制被显式接受5000 块 React 性能门槛保持绿色。11.2 Non-Goals明确不做不做 React-first 核心不做 Lexical 节点类模型不做 ProseMirror 整数位置模型不做强制的 Tiptap 式focus().chain().run()仪式不暴露公共命令策略对象不提供slate/compat不为每种自定义节点类型爆发语义方法不用完整自定义 DOM reconciler 取代 React 作为主渲染器。十二、最终裁决与仓库现状对照12.1 最终裁决method-first public API transaction-owned lazy target resolution React-installed selection runtime hard-cut public mutable fields deterministic extension methods transaction-only write boundary generated app-command browser gauntlets一句话判据如果插件作者需要理解 DOM 选区导入策略架构就失败了。12.2 与当前仓库实现的对照当前仓库packages/slate已经落地了方案中的大量内容createEditor集中绑定全部 editor 方法与 transformspackages/slate/src/create-editor.tstoggleBlock/toggleMark作为 transforms-extension 实现packages/slate/src/internal/transforms-extension/并配有覆盖多种组合语义的单元测试如 toggleBlock.spec.tsxsetNodes等 primitive 通过getAt(editor, options.at) ?? editor.selection解析目标packages/slate/src/internal/transforms/setNodes.tsgetAt助手位于 packages/slate/src/utils/getAt.tsslate-history子包提供with-history/ history APIpackages/slate/src/slate-history/对应 commit 消费端一批新增编辑器方法deleteBackward、deleteForward、deleteFragment、insertBreak等位于 packages/slate/src/internal/editor/。需要说明的是本方案文档自身标记为status: superseded其正文中给出的.tmp/slate-v2/packages/...路径是执行期的临时工作区路径最终公共 API 方向收敛为editor.read(...)/editor.update(...)见“Authoritative Direction”一节并且方案文档声明“不要转向不同的编辑器架构”而是在既有 Slate 模型 operations 之上继续加强 read/update 生命周期。因此阅读本文时应把.tmp/slate-v2路径理解为该计划当时的工作目录仓库内的实际实现以packages/slate、packages/slate-react为准。12.3 关联材料主执行计划docs/plans/2026-04-23-slate-v2-perfect-architecture-master-plan.md前置批次计划docs/plans/2026-04-23-slate-v2-remaining-perfect-architecture-batches-plan.md数据模型优先决策docs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.md装饰多叶文本累计偏移映射docs/solutions/logic-errors/2026-04-04-decorated-multi-leaf-text-needs-cumulative-offset-mapping.md瞬态 DOM 点缺口上的焦点恢复失败闭合docs/solutions/logic-errors/2026-04-09-slate-react-focus-restore-must-fail-closed-on-transient-dom-point-gaps.md未同步 DOM 文本操作必须强制 React 回退docs/solutions/ui-bugs/2026-04-23-slate-react-unsynced-dom-text-ops-must-force-react-fallback.md【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考