milkdown 斜杠插件 @milkdown/plugin-slash 完全指南:从 SlashProvider 到 slashFactory 的源码级解析

milkdown 斜杠插件 @milkdown/plugin-slash 完全指南:从 SlashProvider 到 slashFactory 的源码级解析 milkdown 斜杠插件 milkdown/plugin-slash 完全指南从 SlashProvider 到 slashFactory 的源码级解析【免费下载链接】milkdown Plugin driven WYSIWYG markdown editor framework.项目地址: https://gitcode.com/GitHub_Trending/mi/milkdownmilkdown/plugin-slash是 milkdown 插件体系中负责输入字符触发候选列表的通用方案官方文档以斜杠命令/为典型场景但其设计初衷并不仅限于 slash提及、:表情、#标签等一切输入若干字符后弹出建议列表的交互都可以用它来实现。读完本文你将掌握如何基于 slash-provider.ts 与 slash-plugin.ts 从零搭建自定义触发型菜单视图、绑定进编辑器配置、按需定制触发键与显隐逻辑并理解其在 Crepe 等真实模块中的落地用法。文档定位与核心设计思想官方文档 docs/api/plugin-slash.md 开篇就强调了一个容易被忽略的设计哲学虽然这个插件叫slash但它并不局限于斜杠命令。它同样可以用于其他命令例如提及或:表情。它被设计用来解决一个问题输入一些字符并得到一个建议列表。这一句话定义了整个插件的抽象边界——它不关心列表里装了什么只负责何时弹出、弹在哪、如何更新与销毁。菜单内容、过滤逻辑、命令执行全部由使用者自行实现插件本身只提供一套完整的定位 显隐 生命周期基础设施。在架构上milkdown/plugin-slash由两个部分组成组成导出职责工厂函数slashFactory(id)创建一对$ctxPluginSpec 配置与$proseProseMirror Plugin并暴露key/pluginKey供编辑器配置时注入定位提供器SlashProvider封装基于floating-ui/dom的浮层定位负责内容挂载、debounce 更新、显隐判定与销毁入口文件 index.ts 只有两行export *所有能力都收敛在这两个核心文件中同时milkdown/kit通过 packages/kit/src/plugin/slash.ts 一行export * from milkdown/plugin-slash重新导出因此新项目建议统一从milkdown/kit/plugin/slash引入旧路径milkdown/plugin-slash依然可用。快速上手两步接入文档给出的用法非常精简分为创建视图与绑定视图两步但其中每一步都埋着关键实现细节下面结合源码逐一展开。第一步创建 Slash View所有自定义触发菜单的第一步是实现 ProseMirror 的Plugin.view因为 milkdown 插件体系本身不限制视图实现方式任何能拿到EditorView的地方都可以接入。import { SlashProvider } from milkdown/kit/plugin/slash function slashPluginView(view) { const content document.createElement(div) const provider new SlashProvider({ content, }) return { update: (updatedView, prevState) { provider.update(updatedView, prevState) }, destroy: () { provider.destroy() content.remove() }, } }从源码看这段代码背后的行为是slash-provider.ts构造函数把传入的content直接作为浮层根元素保存到this.element并把它挂到root ?? view.dom.parentElement ?? document.body挂载发生在首次update时见#onUpdate中的#initialized分支slash-provider.ts内部用 lodash 的debounce包装了#onUpdate默认防抖 200ms因此连续敲击字符时浮层不会高频重算位置update方法只是把当前EditorView与上一个prevState转发给防抖后的更新器slash-provider.tsdestroy会cancel掉待执行的防抖更新slash-provider.ts所以视图销毁时务必先调provider.destroy()再移除contentDOM避免内存泄漏。也就是说update阶段会自动完成四件事首次挂载内容节点 → 跳过合成输入与无变化状态 → 判断 shouldShow 决定显隐 → 用computePosition计算并写入left/top样式slash-provider.ts。第二步在 editor.config 中绑定有了视图工厂还需要把它注入到编辑器插件中。slashFactory生成的插件对象持有一个上下文切片key通过ctx.set(slash.key, {...})即可注入PluginSpecimport { Editor } from milkdown/core import { slashFactory } from milkdown/plugin-slash const slash slashFactory(my-slash) Editor.make() .config((ctx) { ctx.set(slash.key, { view: slashPluginView, }) }) .use(slash) .create()注意这里第一行引入的是milkdown/core的Editor而slashFactory来自插件包若使用milkdown/kit则Editor与slashFactory均可统一从 kit 引入。绑定完成后$prose会读取注入的PluginSpec并构造一个 ProseMirrorPlugin其PluginKey为${id}_SLASHslash-plugin.ts。slashFactory 源码解析一对一的 Ctx Prose 组合slashFactory是文档 API 部分第一个被引用的符号slashFactory其完整实现只有 20 余行slash-plugin.ts核心逻辑如下export function slashFactoryId extends string, State any(id: Id) { const slashSpec $ctxPluginSpecState, SlashPluginSpecIdId( {}, ${id}_SLASH_SPEC ) const slashPlugin $prose((ctx) { const spec ctx.get(slashSpec.key) return new Plugin({ key: new PluginKey(${id}_SLASH), ...spec, }) }) const result [slashSpec, slashPlugin] as SlashPluginId result.key slashSpec.key result.pluginKey slashPlugin.key // ... return result }几个值得注意的设计点唯一 id 约定id会拼进两个标识——上下文切片名${id}_SLASH_SPEC与 ProseMirror 的PluginKey${id}_SLASH。因此同一个编辑器里可以并行创建多个 slash 插件如/菜单与提及只要 id 不重复即可互不干扰返回的是数组 附加属性结构返回值类型SlashPluginId, State是[$CtxPluginSpec, $Prose] { key, pluginKey }的交叉类型数组同时携带key供ctx.set注入 spec和pluginKey供$prose暴露需要拿到实际 ProseMirror Plugin 时可用State 泛型State默认any允许注入自定义类型的插件状态元信息slashSpec.meta与slashPlugin.meta分别标记了包名与 displayNameCtxslashSpec|${id}/Proseslash|${id}便于调试与$ctx可视化。也就是说文档中绑定视图那一小段配置最终会被折叠成完整的三步链路ctx.set(slash.key, spec)→$prose读取 spec → 构造Plugin含你传入的view。所有 ProseMirror 官方PluginSpec字段props、view、state、appendTransaction等都会被展开透传并不局限于文档示例中的view一个键。SlashProviderOptions完整的可配置项文档 API 部分引用了SlashProviderOptions其完整定义在 slash-provider.ts。构造函数slash-provider.ts中每个选项都有默认值兜底汇总如下选项类型默认值作用contentHTMLElement必填浮层内容根节点由你自行创建并渲染菜单 UIdebouncenumber200更新防抖毫秒数减少输入过程中的频繁定位计算shouldShow(view, prevState?) boolean内置的#_shouldShow自定义浮层显隐判定返回false时调用hide()triggerstring \| string[]/触发字符内置判定逻辑只比较当前文本块最后一个字符offsetOffsetOptions0传给floating-ui/dom的偏移量默认 0middlewareMiddleware[][]附加的 floating-ui 中间件追加在内部flip()/offset()之后floatingUIOptionsPartialComputePositionConfig{}覆盖定位配置若传入middleware或placement会整体替换内部默认设置rootHTMLElement无浮层挂载的根节点缺省时取view.dom.parentElement ?? document.body其中两个选项的行为在源码中有明确体现trigger支持多字符数组slash-provider.ts内置#_shouldShow取getContent返回文本的最后一个字符target若trigger是数组则用includes判断否则严格相等。这正是一个 provider 同时响应/与的实现基础middleware的拼装顺序slash-provider.ts默认placement: bottom-start中间件顺序为[flip(), offset(this.#offset), ...this.#middleware]你的自定义中间件始终排在最后一旦在floatingUIOptions里显式传入middleware或placement整个内部配置都会被覆盖。shouldShow 的默认判定与 getContent 的精细控制文档没有展开说明默认显隐逻辑但这是决定菜单何时出现的关键源码中的默认链路如下slash-provider.tsgetContent(view)取当前光标所在文本块的内容取不到则false取内容最后一个字符与trigger比对命中则浮层显示并定位。而getContent内部有一整套静默失败条件任何一条不满足都会返回undefined光标选区非空selection.empty为假时不显示——菜单只在光标处触发选区不是TextSelection时不显示编辑器未聚焦且当前活动元素不在浮层内部时不显示编辑器只读!view.editable时不显示光标不在匹配节点内时不显示——默认只匹配paragraph节点可通过第二参数自定义getContent(view, (node) node.type.name paragraph)此外返回文本时它通过textBetween(..., \uFFFC)用对象替换符\uFFFC代替内联叶子节点如图片、inline code并只回溯光标前最多 500 个字符避免长段落全量读取带来的性能开销slash-provider.ts。显隐的最终落地通过show()/hide()完成它们分别把element.dataset.show置为true/false并依次触发可覆写的onShow/onHide回调slash-provider.ts。这为用 CSS 控制菜单透明度/动画和通知外部框架同步状态提供了统一入口。真实落地Crepe 的块级菜单如何复用 SlashProviderslashFactorySlashProvider并不是文档里的孤例milkdown 的 WYSIWYG 编辑器 Crepe 的块级编辑菜单就是基于这套机制实现的实现在 packages/crepe/src/feature/block-edit/menu/index.ts用slashFactory(CREPE_MENU)创建唯一的菜单插件并通过ctx.set(menu.key, { view: (view) new MenuView(ctx, view, config) })注入视图menu/index.tsMenuView内部把debounce调小到20ms让菜单跟随输入更跟手重写shouldShow并调用this.getContent(view, matchNode)把匹配节点从默认的paragraph扩展为[paragraph, heading]同时额外排除代码块与列表isInCodeBlock/isInList、要求光标位于节点末尾并把currentText中/之后的部分作为菜单过滤关键字menu/index.ts。这是SlashProviderOptions所有高级用法自定义shouldShow、自定义getContent匹配函数、offset集中出现的真实范例——它证明了一个通用的 SlashProvider 通过注入业务判定函数就能从斜杠命令菜单演化为任意上下文菜单。Crepe 的可配置项BlockEditFeatureConfig也直接复用SlashProviderOptions的子集debounce、shouldShow、offset、middleware等见 packages/crepe/src/feature/block-edit/index.ts也就是说 Crepe 使用者可以把这些参数一路透传给底层的 SlashProvider。框架集成React 与 Vue对于 React / Vue 项目官方示例分别基于react-slash与vue-slash的完整工程文档中以 StackBlitz 在线示例提供。落到实现层面二者遵循同一套模式用slashFactory创建插件在view中实例化SlashProvider菜单内容不再用原生 DOM 手写而是把content元素作为 React/Vue 组件的挂载点通过onShow/onHide或dataset.show与框架状态同步菜单项渲染、关键字过滤、命令执行view.dispatch插入 Markdown 或执行命令全部留在框架组件内完成SlashProvider 只负责定位与显隐。由于SlashProvider不依赖任何框架 API只操作传入的HTMLElement与EditorView理论上任何声明式框架都能以同样方式接入这正是它作为纯 ProseMirror 插件保持框架无关的架构红利。生命周期与性能要点小结把文档用法与源码实现对应起来接入milkdown/plugin-slash时值得牢记的工程要点正确实现Plugin.view的update/destroyupdate转发给provider.updatedestroy里先provider.destroy()取消防抖再移除 DOM二者缺一不可多个 slash 实例互不冲突slashFactory的 id 同时决定 Ctx key 与 PluginKey为/、、:分别建实例是官方推荐的扩展方式默认只响应paragraph与光标末尾要在标题、代码块、列表等节点触发必须自定义shouldShow并在其中调用getContent(view, matchNode)浮层定位完全委托floating-ui/domflip/offset已内置复杂场景再通过middleware或floatingUIOptions扩展状态同步依赖dataset.show与onShow/onHide无论用 CSS 还是框架响应式状态都以此作为菜单显隐的唯一事实来源。本文涉及的完整源码路径插件实现 slash-provider.ts 与 slash-plugin.ts、入口 index.ts、依赖声明 package.jsonfloating-ui/dom、lodash-es、kit 导出 packages/kit/src/plugin/slash.ts以及真实使用范例 packages/crepe/src/feature/block-edit/menu/index.ts。【免费下载链接】milkdown Plugin driven WYSIWYG markdown editor framework.项目地址: https://gitcode.com/GitHub_Trending/mi/milkdown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考