这就是 univer
零. 开篇
壹. 对代码架构的理解
外表的美只能取悦于人的眼睛,而内在的美却能感染人的灵魂。 ——伏尔泰
Univer 中的模块拆分和依赖关系
无依赖环原则
依赖反转
class SheetPlugin { private _commandService = new CommandService(); }
class SheetPlugin { constructor( // ... @ICommandService private readonly _commandService: ICommandService, // ... ) otherMethod(){ this._commandService.registerCommand(SomeCommand); } }
浅谈 Univer 中的 MVC 架构模式
controller
、 view
和 model
后缀命名的文件,大致也能看出其采用了传统的 MVC 架构模式。Univer 中 MVC 架构更类似于 MVC with Rails, 因为视图层不直接读取 Model 层数据,也不订阅模型层的改变(下面会提到,是订阅了 Mutations),而是做了一层数据缓存(类似 ViewModel), SheetSkeleton 类 - Univer 是如何组织和管理模型层的?
- Univer 中控制器有哪些职责,如何保证控制器代码架构清晰?
- Univer 的视图层怎么组织和管理的?
模型层(Model)
row-manager
、 column-manager
、相关的类和方法来管理每个 sheet 模型数据,拿 row-manager
来说,我们可以获取表格行的一些信息和数据: getRowData(): ObjectArray<IRowData>; getRowHeight(rowPos: number): number; getRowOrCreate(rowPos: number): IRowData; // ...
控制器(Controller)的职责
Controllers 职责
- 初始化一些渲染逻辑和事件的监听,如在 SheetRenderController 类中,在应用
Rendered
生命周期执行,会去初始化页面的数据刷新(_initialRenderRefresh),会去监听 Commands 的执行,涉及到 Mutation 修改模型层,还会触发页面渲染逻辑 - 和视图层交互,拿到视图层的一些数据信息。如在 AutoHeightController 类中,会根据 Commands 所需,通过视图层计算 sheet 自动行高
- 绑定 UI 事件,如在 HeaderResizeController 类中,会在应用
Rendered
生命周期执行,在初始化中,为spreadsheetRowHeader、spreadsheetColumnHeader 绑定 hover 事件,显示和隐藏 resize header(用于调节行列高度和宽度),也为 resize header 绑定 pointer down/move/up 等事件,这样 resize header 就会响应拖拽移动,处理相关用户操作,最终也会反应到模型层的修改和视图层的更新
Commands 职责
Commands
有三种类型: COMMAND
、 MUTATION
、 OPERATION
- COMMAND 就是用户的一次交互操作,有用户行为触发,可以派生出另外一个
COMMAND
,比如用户点击菜单中 text wrap 菜单项,会触发SetTextWrapCommand
,SetTextWrapCommand
会派生出SetStyleCommand
统一处理所有样式的更改。一个COMMAND
可以派生另外一个COMMAND
,但是不能分叉,因为我们需要在COMMAND
中处理 undo/redo 相关操作(后面 undo/redo 可能会移到数据层)。但是一个COMMAND
可以派生出多个MUTATION
和OPERATION
- MUTATION 可以理解为对模型层数据的原子操作,比如
SetRangeValuesMutation
修改选区范围内的单元格样式和值,SetWorksheetRowHeightMutation
修改选区范围内行的高度。MUTATION 的执行,不仅会修改模型数据,同时也会触发视图的重新渲染。MUTATION 中修改的数据需要处理协同,和解决协同中的冲突 - OPERATION 是对应用状态的变更,是应用的某个临时状态,如页面滚动位置、用户光标位置、当前的选区等,不涉及到协同和解决冲突的问题,主要用于之后 live share (类似于飞书的 magic share)等功能
Services 职责
- 管理应用生命周期,如 LifecycleService 类,保存应用生命周期的状态值,并提供
subscribeWithPrevious
方法供其他模块订阅应用生命周期状态值的变更,并做响应任务执行,如依赖的初始化等 - 处理应用的 History 操作和存储历史操作,这样用户可以 undo/redo 之前的操作。在 LocalUndoRedoService 类中,通过
pushUndoRedo
方法将 undo/redo 信息推入栈中,通过updateStatus
方法触发 undo/redo 操作 - 处理网络 IO 和 websocket 链接
- 负责整个应用的生命周期管理
- 绑定和响应 UI 事件,如双击、光标移动等
- 控制视图的渲染和触发渲染的逻辑
- 和视图层通信,如拿计算后页面布局信息
- 通过 Command/Mutation 改变模型层,触发界面渲染
- 处理 undo/redo 相关工作
- 负责协作、和网络 IO
视图层(View)
base-render
文件夹中,如 sheet 渲染相关的: Spreadsheet、SpreadsheetRowHeader、SpreadsheetColumnHeader 等。同时在 Canvas 组件上定义了一套事件响应机制,保证了各个组件能够独立响应事件,但是并不会在视图层处理这些事件。这些事件都需要在 Controllers 中处理 Base-ui/Components
文件夹中代码负责菜单基础组件的渲染和用户事件的发布,base-ui 模块也负责整个应用的框架渲染。如在 DesktopUIController 类中,bootstrapWorkbench 启动了整个应用框架渲染,以及 Canvas 元素的挂载等 贰. Univer sheet 数据结构
叁. 应用启动到渲染的过程
应用的生命周期
export const enum LifecycleStages { /** * Register plugins to Univer. */ Starting, /** * Univer business instances (UniverDoc / UniverSheet / UniverSlide) are created and services or controllers provided by * plugins get initialized. The application is ready to do the first-time rendering. */ Ready, /** * First-time rendering is completed. */ Rendered, /** * All lazy tasks are completed. The application is fully ready to provide features to users. */ Steady, }
Starting
、 Ready
、 Rendered
和 Steady
。如在 Starting 阶段去注册各个插件到 univer 上面,在 Ready 阶段实例化 UniverSheet,并且执行各个插件的初始化函数,Rendered 阶段完成首次渲染,Steady 阶段,应用完成启动,用户可以使用完整功能 _tryStart
方法中, LifecycleService 类实例化,应用进入 Staring 阶段, 在这个阶段也会去执行插件的 onStarting 钩子函数 _tryProgressToReady
方法中,设置 LifecycleService stage 值为 Ready, 在这个阶段也会执行各个插件的 onReady 钩子函数 @OnLifecycle(LifecycleStages.Rendered, SheetRenderController) export class SheetRenderController extends Disposable { //... }
启动到渲染的整个过程
- base-docs:用于单元格和公式的编辑
- base-render:Canvas 渲染引擎,也包含 sheet、doc、slide 所需的基础组件,负责 Canvas 渲染整个过程
- base-sheets:管理 sheet canvas 相关的渲染,如 row header、column header、单元格等,同时也处理大量sheet相关业务逻辑
- base-ui:管理 React DOM 渲染的基础组件,如菜单相关的组件。同时也负责整个 univer sheet 页面框架的渲染,以及和用户交互的操作都会放在这个插件中,如快捷键注册、复制、剪切黏贴等
- ui-plugin-sheets:负责一些基础 UI 的渲染和业务逻辑,如右键菜单、单元格富文本编辑相关的任务
/** * Create a univer sheet instance with internal dependency injection. */ createUniverSheet(config: Partial<IWorkbookConfig>): Workbook { let workbook: Workbook; const addSheet = () => { workbook = this._univerSheet!.createSheet(config); this._currentUniverService.addSheet(workbook); }; if (!this._univerSheet) { this._univerSheet = this._rootInjector.createInstance(UniverSheet); this._univerPluginRegistry .getRegisterPlugins(PluginType.Sheet) .forEach((p) => this._univerSheet!.addPlugin(p.plugin as unknown as PluginCtor<any>, p.options)); this._tryStart(); this._univerSheet.init(); addSheet(); this._tryProgressToReady(); } else { addSheet(); } return workbook!; }
// base-ui-plugin.ts override onStarting(_injector: Injector): void { this._initDependencies(_injector); } override onReady(): void { his._initUI(); }
// ui-desktop.controller.tsx bootstrapWorkbench(options: IWorkbenchOptions): void { this.disposeWithMe( bootStrap(this._injector, options, (canvasElement, containerElement) => { this._initializeEngine(canvasElement); this._lifecycleService.stage = LifecycleStages.Rendered; this._focusService.setContainerElement(containerElement); setTimeout(() => (this._lifecycleService.stage = LifecycleStages.Steady), STEADY_TIMEOUT); }) ); } // ... function bootStrap( injector: Injector, options: IWorkbenchOptions, callback: (canvasEl: HTMLElement, containerElement: HTMLElement) => void ): IDisposable { let mountContainer: HTMLElement; // ... const root = createRoot(mountContainer); const ConnectedApp = connectInjector(App, injector); const desktopUIController = injector.get(IUIController) as IDesktopUIController; const onRendered = (canvasElement: HTMLElement) => callback(canvasElement, mountContainer); function render() { const headerComponents = desktopUIController.getHeaderComponents(); const contentComponents = desktopUIController.getContentComponents(); const footerComponents = desktopUIController.getFooterComponents(); const sidebarComponents = desktopUIController.getSidebarComponents(); root.render( <ConnectedApp {...options} headerComponents={headerComponents} contentComponents={contentComponents} onRendered={onRendered} footerComponents={footerComponents} sidebarComponents={sidebarComponents} /> ); } // ... render(); // ... }
// sheet-canvas-view.ts @OnLifecycle(LifecycleStages.Ready, SheetCanvasView) export class SheetCanvasView { // ... constructor( // ... ) { this._currentUniverService.currentSheet$.subscribe((workbook) => { // ... const unitId = workbook.getUnitId(); if (!this._loadedMap.has(unitId)) { this._currentWorkbook = workbook; this._addNewRender(); this._loadedMap.add(unitId); } }); } private _addNewRender() { // ... if (currentRender != null) { this._addComponent(currentRender); } const should = workbook.getShouldRenderLoopImmediately(); if (should && !isAddedToExistedScene) { engine.runRenderLoop(() => { scene.render(); }); } // ... } private _addComponent(currentRender: IRender) { // ... currentRender.mainComponent = spreadsheet; currentRender.components.set(SHEET_VIEW_KEY.MAIN, spreadsheet); currentRender.components.set(SHEET_VIEW_KEY.ROW, spreadsheetRowHeader); currentRender.components.set(SHEET_VIEW_KEY.COLUMN, spreadsheetColumnHeader); currentRender.components.set(SHEET_VIEW_KEY.LEFT_TOP, SpreadsheetLeftTopPlaceholder); // ... this._sheetSkeletonManagerService.setCurrent({ sheetId, unitId }); } private _addViewport(worksheet: Worksheet) { // ... scene .addViewport( viewMain, viewColumnLeft, viewColumnRight, viewRowTop, viewRowBottom, viewLeftTop, viewMainLeftTop, viewMainLeft, viewMainTop ) .attachControl(); } }
currentSheet$
,如果该 sheet 没有被render 过,那么就会调用 _addNewRender
方法,添加 sheet 所需的 canvas 渲染组件,添加 viewport,然后将 scene 的渲染添加到渲染引擎的渲染循环中(runRenderLoop) // sheet-render.controller.ts @OnLifecycle(LifecycleStages.Rendered, SheetRenderController) export class SheetRenderController extends Disposable {}
// sheet-render.controller.ts private _commandExecutedListener() { this.disposeWithMe( his._commandService.onCommandExecuted((command: ICommandInfo) => { // ... if (COMMAND_LISTENER_SKELETON_CHANGE.includes(command.id)) { // ... if (command.id !== SetWorksheetActivateMutation.id) { this._sheetSkeletonManagerService.makeDirty( { unitId, sheetId, commandId: command.id, , true ); } this._sheetSkeletonManagerService.setCurrent({ unitId, sheetId, commandId: command.id, }); } this._renderManagerService.getRenderById(unitId)?.mainComponent?.makeDirty(); // refresh spreadsheet }) ); }
COMMAND_LISTENER_SKELETON_CHANGE
列表内,标记当前 skeleton 为 dirty,mainComponent 为 dirty,这样 Canvas 渲染引擎就会在下个渲染循环中重新渲染页面了 // initialize-editor.controller.ts private _initialize() { this._currentUniverService.createDoc({ id: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, documentStyle: {}, }); // create univer doc formula bar editor instance this._currentUniverService.createDoc({ id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, documentStyle: {}, }); }
肆. 界面如何响应用户操作?
// menu.ts export function WrapTextMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<WrapStrategy> { // ... return { id: SetTextWrapCommand.id, // ... }; } // ToolbarItem.tsx <Select // ... onClick={(value) => { let commandId = id; // ... commandService.executeCommand(commandId, value); }} // ... />
export const SetTextWrapCommand: ICommand<ISetTextWrapCommandParams> = { type: CommandType.COMMAND, id: 'sheet.command.set-text-wrap', handler: async (accessor, params) => { // ... const commandService = accessor.get(ICommandService); const setStyleParams: ISetStyleParams<WrapStrategy> = { style: { type: 'tb', value: params.value, }, }; return commandService.executeCommand(SetStyleCommand.id, setStyleParams); }, };
// set-style.command.ts const { undos, redos } = accessor.get(SheetInterceptorService).onCommandExecute({ id: SetStyleCommand.id, params, });
// auto-height.controller.ts // for intercept set style command. sheetInterceptorService.interceptCommand({ getMutations: (command: { id: string; params: ISetStyleParams<number> }) => { if (command.id !== SetStyleCommand.id) { return { redos: [], undos: [], }; } // ... const selections = selectionManagerService.getSelectionRanges(); return this._getUndoRedoParamsOfAutoHeight(selections); }, });
calculateAutoHeightInRange
方法最终计算出行的自动行高 // auto-height.controller.ts private _getUndoRedoParamsOfAutoHeight(ranges: IRange[]) { // ... const { skeleton } = sheetSkeletonService.getCurrent()!; const rowsAutoHeightInfo = skeleton.calculateAutoHeightInRange(ranges); // ... }
// sheet-render.controller.ts private _commandExecutedListener() { this.disposeWithMe( this._commandService.onCommandExecuted((command: ICommandInfo) => { // ... if (COMMAND_LISTENER_SKELETON_CHANGE.includes(command.id)) { // ... if (command.id !== SetWorksheetActivateMutation.id) { this._sheetSkeletonManagerService.makeDirty( { unitId, sheetId, commandId: command.id, }, true ); } // ... } this._renderManagerService.getRenderById(unitId)?.mainComponent?.makeDirty(); // refresh spreadsheet }) ); }
伍. 更多阅读

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
以 Kubernetes 原生方式实现多集群告警
作者:向军涛、雷万钧 来源:2023 上海 KubeCon 分享 可观测性来源 在 Kubernetes 集群上,各个维度的可观测性数据,可以让我们及时了解集群上应用的状态,以及集群本身的状态。 Metrics 指标:监控对象状态的量化信息,通常会以时序数据的形式采集和存储。 Events:这里特指的是 Kubernetes 集群上所报告的各种事件,他们是以 Kubernetes 资源对象的形式存在。 Auditing:审计,是与用户 API 和安全相关的一些事件。 Logs:日志,是应用和系统对它们内部所发生各种事件的详细记录。 Traces:链路,主要记录了请求在系统中调用时的链路信息。 告警规则 接下来介绍一下几个可观测性维度上,我们是如何实现告警的。 metrics 在云原生监控领域,Prometheus 是被广泛使用的,它可以说是一个事实上的标准。 对于一个单独的集群来说,或者说是集群自己管理指标存储的场景,我们直接部署一个 Prometheus,就可以提供指标采集、存储、查询和告警的功能。当然也可以额外部署一个 Ruler 组件,来专门进行规则的评估和告警,这样可以减轻 P...
- 下一篇
KubeSphere 社区双周报 | KubeSphere 3.4.1 发布 | 2023.10.27-11.09
KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书、新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列社区动态。 本次双周报涵盖时间为:2023.10.27-2023.11.09。 贡献者名单 新晋 KubeSphere Contributor 两周内共有 8 位新晋 KubeSphere Contributor,感谢各位对 KubeSphere 社区的贡献! GitHub ID 证书 Ganbingkun 下载证书 MisterMX 下载证书 Shimada666 下载证书 donniean 下载证书 guerzon 下载证书 liuxu623 下载证书 nyuxiao 下载证书 samt42 下载证书 新晋 KubeSphere Talented Speaker 在上周六(11.4)KubeSphere 社区联合 SOFAStack 社区及 KubeBlocks 社区共同组织了成都站 Meetup,在本次 Meetup 中共诞生了五位新的 KubeSphere Talented Spe...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- Linux系统CentOS6、CentOS7手动修改IP地址
- 2048小游戏-低调大师作品
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8