模块与扩展
在已熟悉 核心概念、组件基础 与 浏览器模块 的前提下,本篇说明如何把 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
import type { Module, Textbus } from '@textbus/core'
export const featureModule: Module = {
setup(textbus: Textbus) {
const sub = textbus.onReady.subscribe(() => {
// 就绪后再访问 DOM / Commander 等
})
return () => sub.unsubscribe()
},
}import type { Plugin, Textbus } from '@textbus/core'
export const toolbarPlugin: Plugin = {
setup(textbus: Textbus) {
// 主视图已挂载,可安全 querySelector、挂外部 UI
void textbus
},
onDestroy() {},
}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返回的清理函数 不会执行,易造成泄漏或二次挂载异常。
