跳到正文

模块与扩展 ​

在已熟悉 核心概念、组件基础 与 浏览器模块 的前提下,本篇说明如何把 BrowserModule、协作包、自定义 providers 等 拼进同一个 Textbus 实例:Module / imports / Plugin 的职责、启动与销毁顺序,以及 Registry 同名注册、providers 覆盖 的常见结论与排错。

若当前需求只是增加块类型、格式或属性,请优先查阅 入门 与 组件高级;本篇侧重 配置合并与扩展点时机。

TextbusConfig 与 Module ​

TextbusConfig(new Textbus({ ... }) 的参数类型)继承 Module:根配置本身就可以带 components / formatters / attributes / providers / plugins 以及 Module 的生命周期钩子。另有 imports?: Module[],用于把 BrowserModule、CollaborateModule 等预打包模块与根配置 合并进同一编辑器实例。

Module 是 普通对象(类实例或字面量均可),描述「一批注册项 + 可选钩子」;不是必须继承某个基类。

常用配置字段 ​

字段作用
components文档中会出现的 组件类(componentName 唯一)。
formatters / attributes编辑器内可用的 格式 / 属性;可写 实例,或 (textbus) => 实例 延迟创建。
imports多个 Module 依次合并进当前编辑器。
providers@viewfly/core 的 Provider[],向 IoC 容器注册或 覆盖 实现(如替换 Adapter 需明确 provide 符号)。
plugins在主视图 render 完成之后 执行 setup(见下文 Plugin)。
readonly / historyStackSize / zenCoding / additionalAdapters只读、历史栈、语法糖、附加 Adapter 等全局项。

浏览器环境必须提供可用的 Adapter 与 NativeSelectionBridge(一般由 BrowserModule 注册);否则 render 会失败。

imports 与列表合并 ​

效果上可记住(组件 / 格式 / 属性 与 providers / plugins 规则不同):

  • components / formatters / attributes:根配置里的项 先于 各 import 模块 参与合并;同名时,根配置里的实现优先;若只在 imports 之间 有冲突,imports 数组里靠前的模块优先。
  • providers:根与各模块的 providers 按 根在前、随后按 imports 顺序 参与合并;同一 token 多次提供时,通常后出现的覆盖先前的(若与预期不符,用最小配置验证或调整 imports 顺序)。
  • plugins:根与各模块的 plugins 按顺序 排成一条,在 主视图就绪之后 依次执行 setup。

formatters / attributes 使用 (textbus) => 实例 时,工厂会在 绑定到当前编辑器实例 后再得到最终实例列表。

Module 生命周期(顺序要点) ​

beforeEach ​

在 创建编辑器实例 时调用:先按 imports 顺序 调用各模块的 beforeEach,再调用 根 config.beforeEach(若有)。适合在 正式注册前 做轻量准备。

setup ​

在 render 流程中 await:先按 imports 顺序 调用各模块的 setup,再调用 根 config.setup。可返回 销毁回调(或返回 Promise,解析值为回调);destroy() 时会执行这些回调以释放你在 setup 里挂接的资源。

多个 setup 以 Promise.all 等待,彼此之间没有固定先后顺序(仅保证全部完成后再继续后续启动步骤)。

onAfterStartup ​

在 编辑器完成初始化且主视图已就绪 之后,先按 imports 顺序、再调用根 onAfterStartup。适合 依赖 DOM 已存在或编辑循环已运转 的逻辑(例如自动聚焦、埋点)。

onDestroy ​

在 textbus.destroy() 中,会先走 根配置与插件的 onDestroy,再通知各 import 模块,随后执行 setup 返回的清理函数,最后 结束文档视图与内核相关服务。编写 onDestroy 时 不要假设插件仍可用,按 由外到内 释放自定义资源。

页面卸载时务必调用 destroy(),避免输入层与订阅泄漏。

Plugin 与 Module.plugins ​

Plugin 只有 setup(textbus) 与可选 onDestroy(),没有 components / providers 等字段。

执行时机:Plugin.setup 在 主 Adapter 渲染完成之后 才执行,晚于所有 Module.setup。适合 只依赖已就绪 DOM / 已挂载视图 的扩展(例如挂载工具栏、调试浮层)。与 Module 的分工:Module 负责「注册模型与平台依赖、搭好容器」;Plugin 负责「在视图就绪后挂接 UI 或副作用」。

Registry 与同名解析 ​

textbus.get(Registry) 根据 componentName、格式名、属性名 解析字面量并创建组件或插槽。同名项 最终生效的是哪一份,由上一节 「components / formatters / attributes 合并顺序」 决定:需要 覆盖 某内置块或格式时,把 自己的类或实例 写在 new Textbus 根配置 上,或把 Module 放在 imports 更前(在 仅调整 imports 顺序 时)。

providers 自定义与覆盖 ​

providers 与 @textbus/core 使用的 @viewfly/core Provider 形态一致(provide / useClass / useFactory / useValue / deps 等)。用于:

  • 由 BrowserModule 提供 Adapter、NativeSelectionBridge 等浏览器默认实现;
  • 由协作等模块 替换 History 等 token;
  • 由业务 注册 MessageBus、CustomUndoManagerConfig 等(见 协作编辑)。

覆盖某能力时,provide 必须与目标 token 完全一致;不确定时以 类型定义 或 协作编辑 等专题文档中的 provide 写法 为准。

示例:自定义 Module 与 Plugin ​

ts
import type { Module, Textbus } from '@textbus/core'

export const featureModule: Module = {
  setup(textbus: Textbus) {
    const sub = textbus.onReady.subscribe(() => {
      // 就绪后再访问 DOM / Commander 等
    })
    return () => sub.unsubscribe()
  },
}
ts
import type { Plugin, Textbus } from '@textbus/core'

export const toolbarPlugin: Plugin = {
  setup(textbus: Textbus) {
    // 主视图已挂载,可安全 querySelector、挂外部 UI
    void textbus
  },
  onDestroy() {},
}
ts
import { Textbus } from '@textbus/core'
import { featureModule } from './feature-module'
import { toolbarPlugin } from './plugin-toolbar'

const editor = new Textbus({
  imports: [featureModule],
  plugins: [toolbarPlugin],
})

实际工程须再并入 BrowserModule(或自行提供 Adapter 与 NativeSelectionBridge),见 浏览器模块。

排错摘要 ​

  • 未配 BrowserModule(或等价 Adapter + NativeSelectionBridge):render 报错,提示缺少 NativeSelectionBridge / Adapter。
  • 同名组件不生效:检查 components 合并顺序(根与 imports 先后),确认 componentName 与数据字面量一致。
  • providers 未覆盖预期:检查 同名 provide 是否被更晚的 Module 覆盖;必要时调整 imports 顺序 或把绑定挪到 最后加载的模块。
  • 未 destroy():监听与 setup 返回的清理函数 不会执行,易造成泄漏或二次挂载异常。

接下来 ​

基于 MIT 许可发布