可视化项目架构分析
常规的前端开发,大家借助 Vue/React 等框架,可以极大的提升开发效率和质量,这些框架本身就融入了前端的架构设计在里面,也就是定义了一套程序设计的原则和约束,因此才能保证非功能需求的质量。
可视化开发在非该方向的人员看来很难,是因为缺乏类似 Vue/React 这样的框架。解决这个问题的办法其实也是和前端开发类似的,需要我们掌握一些常用的架构设计方法,将其转变为类似常规前端开发的模式,这样才能提升效率和质量。
因此我对之前的可视化项目进行了梳理,将其架构设计分为几个类别,便于后续需求参考。
分层架构
一般都会结合其他架构一起用,很少就单独的仅用这个架构,除非特别简单的项目。
需求特性
需要清晰分离数据处理、业务逻辑和展示逻辑
多人协作开发需要明确的职责划分,不同开发人员可以专注于不同层次
架构特性
- 关注点分离 (Separation of Concerns)
- 每层有明确的职责边界
- 层间通过接口进行通信
- 可维护性高
- 修改一层不会影响其他层
- 便于单元测试和调试
- 可扩展性好
- 可以独立扩展某一层
- 新功能可以按层添加
- 代码复用性强
- 业务逻辑层可被多个表现层复用
- 数据访问层可被多个业务模块共享
代表项目
分层 + 组件化架构
适合3D可视化项目
模板
不过这个已经OUT了,需要重新完善下。
需求特性
- 3D可视化项目更注重渲染性能和组件复用
- 分层+组件化更适合图形应用
- 事件驱动的交互模式天然匹配3D场景
- 组件化便于复用和维护(3D场景中有很多通用组件,比如相机控制、光照、材质等)
架构特性
- 分层结构:将系统划分为多个层次,每一层负责特定的功能
- 组件化设计:将系统划分为多个独立的组件,每个组件负责特定的功能
- 事件驱动:通过事件机制实现组件间的通信
- 高内聚低耦合:每个组件职责单一,组件间通过事件通信,降低耦合度
- 可扩展性强:可以方便地添加新的组件或修改现有组件
代表项目
以上述2个项目为例,分析此类架构的设计:
层次结构
- Core层:Stage、Controller、Element、Dataset 等核心抽象
- Controller层:具体业务控制器(如 BarController)
- Element层:可视化元素(轴、网格、线条、节点等)
- Utility层:工具函数和辅助类
和MVC的区别
- 职责划分不同
传统MVC:
Model ← → Controller ← → View
本项目:
1 | |
没有明确的Model层
- Dataset 更像是数据容器,缺乏业务逻辑
- 业务逻辑分散在 Controller 和 Element 中
View层过于复杂
- Element 既有状态管理又有渲染逻辑
- 违反了MVC中View应该”被动”展示的原则
通信方式不同
- MVC:Controller直接调用Model和View
- 本项目:通过事件系统和依赖注入进行通信
MVC架构
适合2D可视化业务开发(需求变更频繁、定制性高、复用性低)、单组件开发。
一般MVC也会结合分层架构一起用:
- Controller层:处理应用流程和事件管理
- Business Logic层:包含数据验证和业务规则
- Presentation层:负责渲染和用户交互
- Data Access层:管理数据存储和映射
各层之间有严格的依赖流向(自上而下),没有循环依赖,具有良好的封装性和可扩展性,开发门槛也低。
模板
business-visualization-template
需求特性
- 单组件、业务特定可视化
- 强交互性:元素之间、整个场景有很多交互和联动,涉及布局计算
架构特性
- 职责分离
- 高内聚低耦合
- 开发效率高
代表项目
各种POC项目:PacificVis2025-女性投资者分析、大模型天梯图、AInvest矩形树图
常见问题
- 职责边界划分不清晰:View中操作数据,或者Model中执行渲染动作等。
- MVC的依赖强耦合:比如在Controller内部硬编码new各种对象,这在我们之前的3D项目中很常见,其实很不好,应该改为依赖注入(DI)的方式,或者直接用DI库,比如inversify.js
- 万能Controller:逻辑都往Controller里面塞,后期Controller的内容越积越多。
微内核架构
适合组件库的开发。
模板
(TODO)待建设。
需求特性
- 组件库
- 功能的扩展性需求强烈:需要支持第三方或其他团队开发自定义图表
- 高定制化:不同业务场景和业务方,其图表差异较大
- 技术异构性:不同图表采用不同的技术栈(纯DOM、D3.js、ZRender等等)
- 有跨团队协作需求:我们的人不够,有时候需要拉其他团队的人一起开发他们的业务图表
- 动态加载与发布:不同业务场景,只会用到一部分图表,不适合全量加载,否则资源太大了
- 复杂性与维护性:图表类型是增量发展的,需要控制复杂度,否则后期修改牵一发而动全身,需要将核心机制与业务图表解耦,编写单个图表时,代码量不大,复杂度可控,降低维护难度
架构特性
- 核心内核:提供基础服务的最小核心系统
- 插件机制:可插拔的外部组件,用于扩展功能
- 消息传递:内核与插件通过消息/事件进行通信
- 隔离性:插件与核心内核隔离运行
- 动态扩展:支持在运行时添加/移除插件
代表项目
- VISALL、StandardChart、HIVIS
PS: VISALL、StandardChart这2个项目目前都存在架构问题,微内核实现得不是很好,需要优化。
大数据渲染架构
https://uclab.fh-potsdam.de/coins/
| 优化技术 | 实现方式 | 性能提升 | 适用场景 |
|---|---|---|---|
| WebGL批量渲染 | 实例化绘制 | 3-5倍 | 大规模对象 |
| 视锥裁剪 | 空间索引 + 边界检测 | 50-80%减少 | 2D/3D场景 |
| 对象池化 | 预分配 + 复用 | 减少GC压力 | 频繁创建销毁 |
| 纹理图集 | 紧凑打包 | 减少Draw Call | 图像密集型 |
| 时间分片 | 帧分割 | 避免阻塞UI | 复杂计算 |
| LRU缓存 | 智能淘汰 | 90%内存优化 | 资源管理 |
其他项目
架构分析说明
以下是对其他项目的架构分析,分析标准如下:
1 | |
AInvest矩形树图
架构类型:微内核
说明:因为这个项目是在iFind现有代码上二次开发的,因此架构设计上做了一些折中,并未完整实现微内核的特性,其实这个需求也并不需要微内核架构。
为什么不是MVC
- 没有明确的Controller层
- MVC中Controller负责处理用户输入和业务流程
- 本项目中用户交互直接由 InteractionHandler.js:55 处理,通过事件系统分发
- 缺少统一的业务流程控制器
- Model层被分散管理
- MVC要求Model层统一管理数据和业务逻辑
- 本项目中数据相关功能分散在多个管理器:
- DataManager.js 负责数据处理
- StateManager.js 负责状态管理
- ResourceManager.js 负责资源管理
- 没有统一的数据模型抽象
- View层高度复杂化
- MVC的View层主要负责展示
- 本项目的渲染层(RenderEngine.js)包含了复杂的:
- 视口计算 (_isNodeInViewport())
- 批量渲染 (_batchRender())
- 性能优化 (_filterVisibleNodes())
- 超出了传统View层的职责范围
- 通信机制不同
- MVC:Controller直接调用Model和View
- 本项目:采用事件驱动的微内核通信 (_setupManagerCommunication())
dynamic-chart
项目链接:dynamic-chart
架构类型:微内核
状态:但是属于半成品,存在很多问题,并且插件机制、动态注册和卸载等,也没实现:
- 新旧架构混用严重
- 问题:同时存在新旧两套架构体系,如 DynamicChart.js(已弃用)和新的 core/ 架构
- 证据:src/chart/DynamicChart.js:8 标记为 @deprecated 已弃用,但仍存在
- 影响:增加维护成本,开发者困惑,架构混乱
- 组件直接依赖具体实现
- 问题:12个组件直接导入 Chart 类而非使用 IChartContext 接口
- 证据:src/component/axis/Axis.ts:1 等12个文件都存在 import Chart from ‘../../core/Chart’
- 违反原则:违背了微内核的接口隔离和依赖倒置原则
- 依赖注入不彻底
- 问题:虽然有 DependencyContainer,但组件仍大量使用相对路径导入
- 证据:66个文件使用 ../ 相对路径导入,说明依赖注入未完全落实
- 影响:组件间耦合度高,难以独立测试和替换
- 核心职责边界不清
- 问题:LineChart.ts:1-45 导入过多具体实现,应该通过依赖注入获取
- 证据:LineChart 同时导入 D3、具体组件、工具类等,违反单一职责原则
- 影响:核心变得臃肿,不再是”最小化核心”
- 全局状态和DOM操作污染
- 问题:DynamicChart.js:52-67 直接操作 window._thsclog 和 console.warn
- 证据:核心组件中存在全局变量依赖
- 违反原则:微内核应该避免环境依赖,保持纯净性
- 重复功能未抽象
- 问题:util/common.ts 和 core/TypeGuards.ts 存在重复功能
- 证据:util/common.ts 中标记 @deprecated Use TypeGuards from core instead
- 影响:代码重复,维护困难
chart-kit
项目链接:chart-kit
架构类型:微内核
状态:但是问题非常多,做到一小半就停掉了。
主要问题:
- 核心职责过重:Chart.ts 不仅承担组件管理,还直接处理布局计算、事件分发、渲染管理;核心类包含过多具体实现逻辑,违反了微内核”最小化核心”原则
- 插件间耦合度过高:缺乏统一的通信机制,比如Element插件直接访问Chart实例的内部属性
- 缺乏标准化的插件注册机制
- 生命周期管理不完善
- 配置管理分散
- 错误处理和容错机制不足
d3-charts
项目链接:d3-charts
架构类型:模块化插件架构 (Modular Plugin Architecture),也称为组件化架构 (Component-Based Architecture)
特性
组件化设计
- 每个图表类型(如 line、bar、pie)都是独立的组件
- 组件包含 Model(数据层)和 View(视图层)
- 组件间通过 GlobalModel 统一管理
插件式扩展
- 支持动态注册新的图表类型
- 通过 registerComponent() 可以添加新的组件
- 组件发现机制基于类型映射
分层架构
- 核心层:Charts 类(src/d3charts.js:38)作为核心控制器
- 模型层:ComponentModel、SeriesModel 处理数据和配置
- 视图层:ChartView、ComponentView 处理渲染
- 工具层:util、shape 等提供通用功能
事件驱动
- 基于 zrender 的事件系统
- 支持动作队列机制(src/d3charts.js:73)
- 组件间通过事件通信
为什么不是微内核
微内核架构的核心特征缺失:
没有真正的内核机制
- 微内核应该有最小的核心功能集,其他功能通过外部服务提供
- 但这个项目的 Charts 类(src/d3charts.js:38)包含了大量核心逻辑:渲染管理、事件处理、生命周期管理等
- 不是”最小化内核”,而是”重型控制器”
缺乏进程/服务隔离
- 微内核架构中插件应该在独立进程中运行
- 这里所有组件都在同一个 JavaScript 运行环境中,没有隔离机制
没有 IPC 通信机制
- 微内核需要进程间通信
- 这里是直接的函数调用和事件监听
证据对比:
- 微内核:内核只提供最基本的服务(内存管理、进程调度)
- 当前架构:Charts 类直接管理渲染、事件、组件生命周期等
为什么不是MVC
Controller 职责分散
- MVC 应该有明确的 Controller 层处理用户输入
- 这里 Charts 类同时承担了 Controller 和部分 Model 职责,职责不清晰
- 事件处理分散在多个层级
Model 层过度复杂
- MVC 的 Model 应该主要是数据模型
- 这里有 ComponentModel、GlobalModel、SeriesModel,还包含大量业务逻辑
- Model 承担了过多管理职责
View 层不够独立
- MVC 的 View 应该独立于业务逻辑
- 这里的 View 依赖特定的 Model 结构,耦合度较高
- View 类(src/view/Chart.js)直接操作底层图形库
缺乏路由和状态管理
- 典型的 MVC 应用需要路由机制
- 这里没有明确的 URL 路由或应用状态管理
financial-chart
项目链接:financial-chart
架构类型:MVC
状态:但是这个项目是在之前一位实习的同事的流水账代码基础上累加需求的,项目代码写得非常烂,一言难尽:
主要问题:
控制器职责过重
- EquityChart.ts 承担了过多职责,既作为MVC的Controller,又包含了大量业务逻辑
- 文件过长(近2000行),违反了单一职责原则
- 包含了数据请求、事件处理、业务逻辑等多种功能
Model层设计不完善
- Model类更像数据处理器,缺乏真正的数据状态管理
- 没有数据验证、数据同步机制
- 缺乏观察者模式来通知View数据变化
View层耦合度高
- View直接持有Controller引用(View.ts:30),违反了MVC的依赖原则
- View内部包含大量业务逻辑判断(EquityChart.ts:196中的type判断)
- 渲染逻辑和业务逻辑混合在一起
缺乏抽象层
- 没有接口或抽象类定义各层职责
- 组件间直接实例化,缺乏依赖注入
- 扩展性受限,添加新功能需要修改核心类
数据流混乱
- 存在双向数据绑定(这个问题,新同事经常犯),Controller直接操作View
- View直接调用Controller的方法,违反了单向数据流原则
- 缺乏统一的状态管理机制
模块化不足
- Action类几乎为空(Action.ts:1-3),设计不完整
- 工具函数分散在util目录,缺乏分类
- 缺乏清晰的包结构设计
关联文章
可视化业务开发框架解析