别只会预设模式!Univer 插件架构一图看懂,电商/金融项目选型照着抄
【免费下载链接】univerThe Office Harness for AI Agents — Spreadsheets, Docs, Slides, Canvas, Relational Tables, and PDF in one runtime.项目地址: https://gitcode.com/GitHub_Trending/un/univer
翻开 Univer 的文档,绝大多数教程会带你走同一条"最快路径":引入UniverSheetsCorePreset,传一个容器节点,createUniver一把梭,表格就渲染出来了。这条路径确实能帮你 5 分钟跑通 Demo,但如果你以为"预设模式就是 Univer 的全部",那么在真正进入电商、金融这类对权限、性能、交付体积和定制深度都有硬要求的项目时,很容易踩进"demo 能跑、上线受阻"的坑。
原因很简单:Univer 里并没有所谓的"预设模式"这个运行时模式,预设(Preset)本质上是官方替你打包好的一组插件清单。理解这层关系,你就拿到了选型的钥匙:什么时候照抄预设,什么时候该拆开插件自己组装。本文结合仓库源码,把"预设 vs 插件"这条分界线、插件注册与命令通信机制讲透,并给出电商与金融场景可直接照抄的选型对照表。
预设不是一种模式,是一摞插件的"组合套餐"
先看仓库里预设的物理形态。presets/src/preset.ts 中,IPreset的定义极其简单:
export interface IPreset { plugins: IPresetPlugin[]; locales?: IUniverConfig['locales']; }也就是说,一个预设 = 一个插件数组 + 可选的本地化语言包。而createUniver所做的工作,就是把预设里的插件逐一解包、去重后注册到同一个Univer实例上(presets/src/preset.ts):
export function createUniver(options: CreateUniverOptions) { const univer = new Univer({ /* ... */ }); presets?.forEach((preset) => { const { plugins } = Array.isArray(preset) ? preset[0] : preset; plugins.forEach((p) => { // 以 pluginName 为 key 做去重,后注册者覆盖先注册者 pluginsMap.set(realPlugin.pluginName, { plugin: realPlugin, options: pluginConfig }); }); }); pluginsMap.forEach(({ plugin, options }) => { univer.registerPlugin(plugin, options); }); const univerAPI = FUniver.newAPI(univer); return { univer, univerAPI }; }注意两个细节:其一,插件以pluginName为唯一键存入 Map,同名插件"后进覆盖先进";其二,如果你在presets之外又显式注册了一个已被预设包含的同名插件,createUniver会直接抛错(Plugin xxx already registered by presets or other ways!),防止重复注册导致状态混乱。
"组合套餐"的典型代表是UniverSheetsCorePreset(presets/packages/preset-sheets-core/src/preset.ts),它一口气打包了 12 个插件:网络层、文档内核、渲染引擎、UI 外壳、公式引擎、电子表格内核、数字格式、公式 UI……每个插件还支持通过 config 传入精细配置:
return { plugins: [ UniverNetworkPlugin, [UniverDocsPlugin, { hasScroll: docs?.hasScroll }], UniverRenderEnginePlugin, [UniverUIPlugin, { container, toolbar, ribbonType, menu, contextMenu }], UniverDocsUIPlugin, useWorker ? [UniverRPCMainThreadPlugin, { workerURL: workerSrc }] : null, [UniverSheetsPlugin, { isRowStylePrecedeColumnStyle: sheets?.isRowStylePrecedeColumnStyle }], // ... ].filter((v) => !!v) as IPreset['plugins'], };更值得学习的是它的可选 Worker 分支:当传入workerURL时,主线程会挂上UniverRPCMainThreadPlugin,公式引擎切换为"不执行、只注册"的notExecuteFormula: true状态,计算任务全部交给配套的 preset-sheets-core/src/worker.ts 中UniverSheetsCoreWorkerPreset所定义的公式 Worker。这正是 Univer 处理大数据表格性能的关键设计:界面渲染与公式计算分离线程。
在官方示例里,多个预设像积木一样堆叠(examples/src/sheets/mount.ts):
const presets = [ UniverSheetsCorePreset({ container: host, ribbonType }), UniverSheetsDrawingPreset(), UniverSheetsConditionalFormattingPreset(), UniverSheetsFilterPreset(), UniverSheetsHyperLinkPreset(), UniverSheetsDataValidationPreset(), UniverSheetsFindReplacePreset(), UniverSheetsNotePreset(), UniverSheetsSortPreset(), UniverSheetsTablePreset(), UniverSheetsThreadCommentPreset(), ]; const { univer, univerAPI } = createUniver({ presets });所以,"预设模式 vs 插件模式"的真实分界线不是两种运行时,而是你站在包工头还是总装车间的视角:预设模式 = 站在包工头视角,按功能域(表格/筛选/批注)成批进货;插件模式 = 站在总装车间视角,按pluginName逐个精确装配,装什么、装多少、以什么配置装,全部自己说了算。包体大小、启动耗时、依赖可控性,全在这条分界线的哪一侧。
插件注册与通信机制实战
插件的"户口本"与生命周期
在 packages/core/src/services/plugin/plugin.service.ts 中,所有插件必须继承Plugin基类并声明一组静态元数据——pluginName(全局唯一 ID)、packageName(版本对齐用)、type(绑定到 Sheet/Doc/Slide 等单元类型):
export abstract class Plugin extends Disposable { static pluginName: string; static packageName = pkg.name; static version = pkg.version; static type: UniverInstanceType = UniverInstanceType.UNIVER_UNKNOWN; onStarting(): void {} onReady(): void {} onRendered(): void {} onSteady(): void {} }四个生命周期钩子对应 Univer 实例的四个阶段:Starting(依赖注入与装配)、Ready(注册渲染模块、初始化控制器)、Rendered(UI 已挂载)、Steady(一切就绪)。把逻辑放进正确阶段,是避免"插件间互相抢跑"的核心纪律。
插件之间还允许显式声明依赖。@DependentOn装饰器会把依赖写入构造器上的DependentOnSymbol(packages/core/src/services/plugin/plugin.service.ts):
@DependentOn( UniverDocsPlugin, UniverFormulaEnginePlugin, UniverRenderEnginePlugin, UniverSheetsPlugin, UniverDocsUIPlugin, UniverSheetsHyperLinkPlugin, UniverSheetsUIPlugin ) export class UniverSheetsHyperLinkUIPlugin extends Plugin { /* ... */ }而PluginService._loadFromPlugins会基于依赖关系做拓扑排序(DFS):被依赖的插件先实例化,未被注册的依赖会被"自动补齐注册"。这也解释了预设内部插件顺序为何是安全的——顺序由依赖图决定,而非数组字面量顺序。一个真实的、完整的插件生命周期样板可以在 packages/sheets-hyper-link-ui/src/plugin.ts 里看到:onStarting注入服务与控制器,onReady把渲染控制器注册进IRenderManagerService并实例化交互控制器,onRendered才拉起弹窗、权限等重度依赖模块。
插件间的通信:命令总线
插件之间不直接互相调用方法,而是通过ICommandService这条命令总线通信。在 packages/core/src/services/command/command.service.ts 中,命令被分为三类:
COMMAND:业务编排命令,会进一步触发 MUTATION;MUTATION:真正写入快照(snapshot)的数据变更,是协同编辑冲突解决的最小单元;OPERATION:不落快照的 UI 态变更(滚动位置、侧边栏开关等)。
命令 ID 采用命名空间.类型.命令名的约定,例如sheet.command.set-range-values。业务代码无需关心命令由哪个插件注册,只需面向 ID 编程。Facade API 把这层包装得更薄(packages/core/src/facade/f-univer.ts):
univerAPI.executeCommand('sheet.command.set-range-values', { value: { v: "Hello, Univer!" }, range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 } });命令执行前后分别有beforeCommandExecuted与onCommandExecuted监听点。这是插件扩展能力的核心钩子:拦截、审计、埋点、权限校验都可以挂在这里而不侵入业务代码——对金融类强合规场景尤其重要。
架构层面,把插件、命令总线、公式引擎、渲染层之间的关系画成一页图,docs/img/sheet-architecture.png 给出了官方视角的完整分层:
电商、金融场景的选型对照表
下面这张对照表可以直接抄进你的技术方案评审文档。核心判断维度:交付速度、包体与性能、定制深度、协作能力、维护成本。
| 维度 | 预设模式(Preset 组合) | 插件模式(手动装配) |
|---|---|---|
| 集成速度 | 极高,一个createUniver跑通 | 低,需理解依赖图与生命周期 |
| 包体/首屏 | 偏大,需配合 Tree Shaking | 精确可控,按需装插件 |
| 功能覆盖 | 官方按功能域打包,开箱即用 | 完全自定义,可只装内核 |
| 定制深度 | 通过 preset 的 config 透传 | 可写自定义插件、覆盖依赖 |
| 协作能力 | preset 已含 RPC/Worker 骨架 | 需自行组合协同相关插件 |
| 维护成本 | 升级跟随官方 preset 节奏 | 版本对齐与依赖管理需自查 |
电商:预算有限、功能要全、上线要快
电商后台表格的典型诉求是:SKU 批量导入导出、条件格式标异常价、筛选/查找、下拉数据校验、多语言、移动端要能看。这类项目直接照抄预设组合是最优解——UniverSheetsCorePreset打底,按需追加UniverSheetsFilterPreset、UniverSheetsDataValidationPreset、UniverSheetsFindReplacePreset、UniverSheetsConditionalFormattingPreset(参照 examples/src/sheets/mount.ts 的拼装方式)。
一个官方示例里非常实用的技巧是移动端插件替换(examples/src/mount-example.ts):预设包只认pluginName,所以可以建立一个"桌面插件名 → 移动端插件"的映射表,用replacePresetPlugins在保持 preset 清单不变的前提下整体换血,实现一套配置、双端渲染:
export function replacePresetPlugins(presets: IPreset[], replacements: Map<string, PluginCtor<Plugin>>): IPreset[] { return presets.map((preset) => ({ ...preset, plugins: preset.plugins.map((entry) => { const [plugin, config] = Array.isArray(entry) ? entry : [plugin]; return [replacements.get(plugin.pluginName) ?? plugin, config]; }), })); }电商报表多为"读多写少",公式计算量可控,可以暂时不配 Worker;一旦涉及几千行公式联动,把workerURL配上、切换成UniverSheetsCoreWorkerPreset的线程分离模式,改造成本也只是一行配置。
金融:性能是底线,权限与合规是红线
金融场景(交易台账、风控模型参数表、对账底稿)的诉求完全不同:数据量大、公式重计算频繁、操作必须可审计可回滚、多实例要隔离。
- 服务端合规报表:金融里很多表格能力要在 Node.js 服务端无头运行(生成对账报表、批量重算公式)。此时不要引入 UI 插件,直接面向
preset-sheets-node-core(presets/packages/preset-sheets-node-core/src/preset.ts)装配纯内核插件,配合 Worker 公式引擎在服务端完成计算。 - 操作审计与回滚:所有数据变更必须经过
CommandType.MUTATION,天然进入命令栈;配合onCommandExecuted监听点把每次变更写入审计日志,撤销/重做由IUndoRedoService兜底。在 presets/src/preset.ts 中可以看到,开启collaboration: true时createUniver会把本地IUndoRedoService等覆盖为null——即"协同模式 = 用远端变更集取代本地回滚栈",这是金融场景做多人协作时最容易忽略的语义切换。 - 多实例隔离:银行往往需要在同一页面嵌入多个隔离的表格(多账户、多币种视图)。Univer 的每个实例持有独立
Injector(依赖注入容器),packages/core/src/univer.ts 中createUniverInjector支持传入parentInjector构建注入器层级,插件与服务在实例间天然隔离,一个页面挂 10 个实例互不污染。官方多实例能力可参考 docs/img/multi-instances.png:
迁移路径:预设起步,插件收尾
对大多数团队,正确的工程策略不是二选一,而是两段式演进:
- MVP 阶段:直接用官方预设快速验证业务价值,把
UniverSheetsCorePreset+ 3~4 个功能预设跑通,重点确认交互模型和数据结构(Workbook/Sheet/Formula)符合业务; - 规模化阶段:当出现"预设装不进业务"的诉求(自定义单元格渲染、私有权限协议、专属命令审计链、包体瘦身),沿着依赖图把 preset 拆开,逐个替换为自研插件,并通过
@DependentOn声明与官方插件的关系。
只要理解"预设 = 插件集合 + 注册顺序",拆与装都是同一套语言:registerPlugin、PluginService的拓扑排序、ICommandService的命令总线。Univer 的插件化设计把"能力边界"划得异常清晰——数据在命令里流动,UI 在渲染控制器里生长,公式在独立引擎里计算,协作在变更集里同步。看懂这条边界,无论是电商的"快"还是金融的"稳",选型都只是在同一个架构上做加减法。
【免费下载链接】univerThe Office Harness for AI Agents — Spreadsheets, Docs, Slides, Canvas, Relational Tables, and PDF in one runtime.项目地址: https://gitcode.com/GitHub_Trending/un/univer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考