ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Plate Slate v2 装饰源脏标记:Wave 10「源码级失效声明」的设计落地与验证复盘

Plate Slate v2 装饰源脏标记:Wave 10「源码级失效声明」的设计落地与验证复盘 Plate Slate v2 装饰源脏标记Wave 10「源码级失效声明」的设计落地与验证复盘【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文围绕 docs/plans/2026-04-15-slate-v2-decoration-wave-10-execution.md 这份执行文档系统讲解 Plate Slate v2 覆盖层decoration / annotation / widget中「源码级失效声明source dirtiness declarations」这一 Wave 10 架构波次的完整落地过程从目标、约束、API 设计、调用方审计到实现、验证、架构评审与回归修复。读完本文你将理解七类装饰源的脏标记语义、dirtiness标量/数组/函数三种声明形态、外部源显式刷新的正确用法以及如何用测试与命令栈证明「按源重算」而非「全量重算」。一、背景覆盖层从「全量重算」走向「按源失效」在 Plate 项目的 Slate v2 装饰架构中旧的decorate回调把语法高亮、搜索结果、评论、远程光标、诊断、评审建议等混在一个不稳定回调里既无法表达瞬态覆盖与持久锚点的区别也难以支撑大文档性能。为此decoration-roadmap.md 锁定了三条一等公民车道three first-class lanesDecorations瞬态覆盖可重叠、由快照状态或显式外部状态派生Annotations持久注解带 id、以 Bookmark 为公开锚点、随事务重基Widgets锚定 UI气泡、标签、按钮、诊断弹层等。这三条车道可以共享投影projection管道但不能共享所有权语义。Wave 08 已经完成覆盖层架构锁定、锚点子层、投影运行时、装饰源 API、注解/Widget 层、桥接加固、大文档与 React 调度证明、迁移与 RC 对账Wave 9 在 core 侧补上了SnapshotChange变更元数据Wave 10 则是让 decoration / annotation / widget 各源声明「什么会弄脏自己」而不是把每次编辑器提交都当作同等相关。正如路线图在 Sequencing 一节所写没有 Wave 9React 存储无法低成本知道什么变了没有 Wave 10所有源看起来一样脏没有 Wave 11局部订阅会掩盖大范围投影重算成本。二、Wave 10 的目标、范围与硬性约束执行文档在 Goal / Scope / Constraints 三个小节把 Wave 10 界定得非常明确。2.1 目标与范围目标执行装饰路线图 Wave 10——源码级失效声明source dirtiness declarations让各源显式声明自己的失效条件。范围Scope为装饰源增加增量式additive脏标记声明对 projection、annotation、widget 三类 store 做编辑器变更过滤editor-change filtering显式刷新继续对外部源或应用自有源生效subscribe(...)、refreshSource(...)、refreshAll()仅在本次波次被强制触达的范围内补充针对性测试与文档。2.2 约束Constraints不做范围缩减现有简单调用方必须保持可用source-compatible缺失脏标记声明时必须回退到全量刷新绝不静默产出过期覆盖层不允许把 Wave 11 的内容伪装成 Wave 10 混进来收尾必须包含全新验证、架构评审architect review、deslop 去冗余、以及 deslop 后的复验。2.3 阶段流水线Phases执行文档把 Wave 10 拆成六个阶段并全部勾选完成建立上下文与波次范围Ground context and wave scope选择增量式脏标记 API 并审计现有调用方实现 Wave 10 源码级失效source-level invalidation用针对性测试与包级证据验证架构评审deslop deslop 后复验。三、锁定的七类源source classes路线图 Wave 10 章节 锁定了装饰源的七种脏标记类别执行文档在 Findings 中逐条确认。这七类构成dirtiness语义的权威枚举源类别语义典型用途always最安全的回退永远视为脏无法归类或追求简单的源selection选区派生选区派生覆盖层、选区 Widgettext文本路径派生搜索高亮、语法高亮node节点/元素元数据节点级元数据覆盖层annotationBookmark 持久锚点评论锚点、注解external应用自有源通过subscribe(...)、refreshSource(...)、refreshAll()显式刷新的源custom对变更记录做谓词判定高级调用方的自定义失效逻辑配套的 API 姿态API posture同样来自路线图脏标记声明起初保持可选缺少声明 全量刷新对应非协商条款第 22 条未知脏标记必须回退全量刷新而不是产出过期覆盖层声明必须足够稳错误的声明不能在常见场景下静默弄脏可见覆盖层外部源仍然自己拥有显式刷新。四、Additive API 设计从小枚举到三种形态执行文档的 Findings 明确记录了一个重要事实最小可用的 Wave 10 API 最终长过了「单个枚举成员」原因有三mark必须被纳入——因为 Wave 9 的变更元数据已经把mark作为操作类别发布出来Wave 9 的操作类别为 text / selection / mark / structural / replace若 Wave 10 不承认mark变更过滤就会漏掉标记类操作声明需要数组形态——像[text, node]这种混合源一次提交可能同时包含文本与节点结构变化无法用单个标量表达函数形态是高级自定义谓词面——对应custom类谓词直接接收变更记录做判定。因此最终dirtiness支持三种形态标量类selection、类数组[text, node]、谓词函数接收 change record 返回布尔。另一个关键 API 决策createSlateProjectionStore(...)保持源码兼容——通过「追加可选 store options」而非「修改 source 回调签名」的方式接入脏标记因此现有简单调用方一行都不用改。这与 Wave 10 约束「current simple callers must keep working」直接呼应。五、调用方审计与触点清单执行文档 Progress 部分记录了落地前的两次审计源/store 触点审计五个文件decoration-sources.tsannotation-store.tswidget-store.tsprojection-store.tshooks/use-slate-decoration-sources.tsx直接调用点影响审计五个公开面createSlateProjectionStorecreateSlateDecorationSourceStoreuseSlateDecorationSourcescreateSlateAnnotationStorecreateSlateWidgetStore审计的结论是所有调用点都只做增量扩展不破坏既有签名。此外执行文档把两条本地经验local learnings列为设计前提core 的 Range 语义与 React 覆盖层缓存必须分离。这条来自 docs/solutions/logic-errors/2026-04-03-slate-react-v2-projection-proof-must-split-range-semantics-from-react-overlay-store.mdcore 拥有逻辑 Range 含义Editor.projectRangeReact 拥有覆盖层缓存与订阅广度createSlateProjectionStore、useSlateProjectionsDOM 拥有几何。如果一套装饰/注解设计说不清这三行就不算完成。annotation / widget / projection store 的输入身份必须稳定。这条来自 docs/solutions/logic-errors/2026-04-15-annotation-store-inputs-must-keep-stable-data-references.mdstore 合约按引用比较输入身份本身就是 API 的一部分。执行文档还顺带记录了两处本仓库缺失的引用文档docs/shared/agent-tiers.md与docs/solutions/patterns/critical-patterns.md属于审计过程中发现的文档缺口不影响实现落地。六、三种声明形态的实战用法6.1 标量形态按源类别声明来自 source-scoped-overlay-invalidation 方案文档 的基准行在同一编辑器上建立三个不同脏类别的投影 storeconst selectionStore createSlateProjectionStore(editor, deriveSelectionRanges, { dirtiness: selection, sourceId: selection-source, }) const textStore createSlateProjectionStore(editor, deriveTextTailRanges, { dirtiness: text, sourceId: text-source, }) const externalStore createSlateProjectionStore( editor, () deriveExternalRanges(externalActiveRef.current), { dirtiness: external, sourceId: external-source, } )基准结果证明了脏类别是选择性的selective选区变化 → selection 重算1text0external0文本编辑 → selection0text1external0外部刷新 → selection0text0external1。同一行基准也诚实暴露了仍未关闭的部分一旦某个 store 快照变化该 store 上两个 runtime-id 订阅者仍会一起重渲染left1、right1。也就是说「重算选择性」为真而「store 内订阅者局部性」尚未为真——这正好把 Wave 10重算选择与 Wave 11订阅/索引局部性的边界划清楚。6.2 数组形态混合源[text, external]搜索高亮是「文本变化 外部搜索词变化」双重来源的典型场景。修复记录上文 6.x 引用的 annotation 方案文档中的 Projection Store Update 一节给出了正确的做法——store 保持稳定外部控制状态放 ref再用显式refresh({ reason: external })驱动const searchRef useRef() const projectionStore useMemo( () createSlateProjectionStore( editor, (snapshot) collectSearchProjections(snapshot.children, searchRef.current), { dirtiness: [text, external], sourceId: search-highlighting } ), [editor] ) const handleSearchChange useCallback( (event: ChangeEventHTMLInputElement) { searchRef.current event.currentTarget.value projectionStore.refresh({ reason: external }) }, [projectionStore] )反例Bad是直接让 store 依赖 React 搜索 state输入变化 → state 重建 → store 重建 → 编辑器 remount 路径把焦点夺回编辑器。这印证了执行文档的本地经验「stable input identity matters for annotation/widget/projection stores」。6.3 Annotation store 的输入身份纪律同样的教训适用于useSlateAnnotationStore(...)。Bad 写法每次渲染都重建data对象const annotationStore useSlateAnnotationStore( editor, comments.map((comment) ({ id: comment.id, bookmark: comment.bookmark, data: { body: comment.body, label: comment.label, tone: comment.tone, }, })) )createSlateAnnotationStore(...)只有在 bookmark、resolved range 与data对象保持稳定引用时才认为注解快照未变。每次渲染新建data会导致每次渲染都刷新 store进而级联成编辑器重渲染甚至Maximum update depth exceeded。正确写法是 memoize 条目、直接复用comment本体const annotations useMemo( () comments.map((comment) ({ id: comment.id, bookmark: comment.bookmark, data: comment, })), [comments] ) const annotationStore useSlateAnnotationStore(editor, annotations)可复用规则给 store 喂数组时memoize 数组、对未变化条目保持data引用稳定除非你确实想触发刷新否则不要在渲染内联重建派生载荷对象。七、编辑器变更过滤与外部源显式刷新Wave 10 的核心机制是Wave 9 之后Editor.subscribe(...)的订阅者可以拿到SnapshotChange变更记录含 operations、dirty paths、touched runtime ids、replace epoch、操作类别 text/selection/mark/structural/replaceWave 10 让各 store 用dirtiness声明做变更过滤——只有命中声明类别的提交才触发本源重算其余提交直接忽略。对外部/应用自有源显式刷新仍是唯一入口三条通道保持原样subscribe(...)订阅外部数据源外部变化时触发refreshSource(id)只刷新一个装饰源refreshAll()全量刷新。执行文档特别强调Wave 10 之后refreshSource(id)是「真正只刷新目标装饰源」而不再像旧实现那样「嘴上说刷一个、实际把整个源集合都刷一遍」。这意味着外部源仍然自己拥有显式刷新语义路线图第 4 条非协商条款失效是显式的——节点弄脏、源刷新、注解变更、可选全量刷新。八、验证矩阵与执行证据8.1 路线图要求的测试Required testsWave 10 章节明确要求五条测试证据search-highlight 源忽略纯选区变化选区 Widget 忽略无关文本变化annotation store 在 Bookmark 范围重基时更新外部源在其订阅触发时更新无需编辑器变更错误或缺失的脏标记声明回退到全量刷新。8.2 执行记录的证明行proof rows执行文档 Progress 记录了三处新增的 Wave 10 证明行packages/slate-react/test/projections-and-selection-contract.tsx投影与选区契约packages/slate-react/test/annotation-store-contract.tsx注解 store 契约packages/slate-react/test/widget-layer-contract.tsxWidget 层契约以及一条针对最终 bug 的回归行「refreshSource only recomputes the targeted decoration source」refreshSource只重算目标装饰源。8.3 命令栈证据执行文档给出的完整证据栈对slate-react包pnpm install pnpm turbo build --filter./packages/slate-react pnpm turbo typecheck --filter./packages/slate-react pnpm lint:fix pnpm --filter slate-react test # → 95 passed # 针对性覆盖层契约切片 → 9 passed # lsp_diagnostics 对受影响文件 → 0 errors首次全量验证即通过修复导出缺口后重跑全套仍为95 passeddeslop 后复验再次全绿。需要说明的是这些命令与packages/slate-react/src/*实现文件属于执行文档所述的外部 slate-v2 代码库本文所述仓库内以路线图与执行/方案文档形式保存了完整决策与证据记录。九、三个典型错误从构建失败到两轮架构评审执行文档 Errors 一节如实记录了过程中踩过的三个坑这是理解 Wave 10 API 形态的最佳反面教材联合类型收窄漏洞新dirtiness联合类型在projection-store.ts里先出现类型收窄空洞导致构建失败。修复方式是在标量类分支之前加显式数组类型守卫array type guard——这正对应「数组形态」必须与「标量形态」在类型层面被严格区分。公开面导出缺口第一轮架构评审驳回新脏标记 context/helper 已实现于projection-store.ts却没有从包入口index.ts再导出导致包级公开面不完整。修复后重跑全套证据栈。refreshSource(id)泄漏第二轮架构评审驳回实现初期refreshSource(id)仍会刷新所有已声明源因为reason: refresh让所有脏类别看起来都是脏的——即刷新原因被误当成了「全类脏」。修复定位在decoration-sources.ts的源码级刷新路径并补上「只重算目标装饰源」的回归测试行。两轮架构评审最终结论为APPROVEDdeslop 复查结论为所辖文件范围内无残留死代码、重复逻辑或值得改动的清理项deslop 后复验全部通过。十、与 Wave 9 / Wave 11 的衔接为什么顺序不可打乱Wave 10 不是孤立波次它与前后波次是严格串行关系路线图 Sequencing 与 Inter-wave stop gatesWave 9core change metadata and touched runtime-id publication让快照监听器可选收到轻量变更记录operations、dirty paths、touched runtime ids、replace epoch、操作类别。没有 Wave 9React store 无法低成本知道什么变了——这是 Wave 10 变更过滤的输入前提。执行文档也确认mark类别正是因为 Wave 9 已发布它而被纳入 Wave 10 API。Wave 10source dirtiness declarations各源声明什么能弄脏自己。没有 Wave 10所有源看起来一样脏Wave 9 的元数据优势无法兑现为「按源重算」。Wave 11indexed / child-scoped projection recompute用 path/range 索引让局部投影避免遍历每个文本条目并把「source ids 重算数」「runtime ids 触及数」「slice 身份变化数」计入基准产物。没有 Wave 11局部订阅会掩盖大范围投影重算成本——正如第八节基准所示Wave 10 只解决了「重算选择性」订阅者扇出fan-out局部性要留给 Wave 11。三者的关系可以用一句话概括Wave 9 提供「什么变了」的元数据Wave 10 让每个源声明「我关心哪些变化」Wave 11 让一次重算真正只覆盖被命中的局部区域。十一、实践要点总结对想要在 Slate v2 覆盖层架构上开发搜索高亮、评论锚点、选区 Widget 的开发者Wave 10 的经验可以收敛为四条可执行准则简单调用方无需声明不传dirtiness就回退全量刷新行为安全、绝不会静默过期dirtiness是可选优化不是强制义务。声明要匹配真实来源搜索高亮用[text, external]选区 Widget 用selection注解锚点用annotation拿不准就用always保底。外部状态走 ref 显式刷新store 保持稳定身份外部控制状态放useRef变化时调projectionStore.refresh({ reason: external })给 annotation/widget/projection store 喂数组时 memoize 并保持data引用稳定。区分两种「局部性」重算选择性只重算命中源与订阅者扇出store 内订阅者是否一起重渲染是两回事验证时分开度量避免把「fake green」当成关闭。最终Wave 10 让装饰路线图达成这样的状态源级失效显式化且有测试背书公共文档可以教授源脏标记而不必强迫简单示例使用它示例保持简单除非它专门演示高级失效。这正是 decoration-roadmap.md 对 Wave 10 退出条件Exit的定义也是本轮执行文档「APPROVED 95 passed deslop 干净」完整收尾后交付给下一波次Wave 11 索引化投影重算的接力棒。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表