FineUI 框架设计分析:架构、权衡与现代化改造
AI 摘要基于 FineUI 源码、历史提交与 React、Vue 对比,分析其 JSON 配置驱动、继承体系以及现代化改造的演进脉络。
1 框架定位与核心约束
FineUI 是帆软 FineReport / FineBI 的底层UI框架,服务于报表设计器和BI分析平台。与通用Web框架不同,它的设计由以下业务约束驱动:
| 约束 | 技术影响 |
|---|---|
| 用户通过可视化设计器拖拽构建界面 | UI描述必须可序列化 → JSON配置驱动 |
| 一个仪表板可包含数百个组件,结构完全由用户配置决定 | 需要运行时动态组装 → 字符串类型注册 + createWidget |
| 报表布局精确到像素,且需跨分辨率一致 | 框架必须接管布局 → 内置布局系统 + pixRatio适配 |
| 二次开发用户以Java背景为主 | API风格向Java对齐 → OOP继承 + IoC容器 |
| 企业客户需要定制组件行为但不能fork源码 | 需要无侵入扩展 → BI.config / BI.point (AOP) |
| 需要支持IE9(历史要求,现已放宽) | 不能依赖ES6+ → BI.inherit + IIFE + var |
这些约束解释了FineUI与React/Vue之间几乎所有的设计差异。脱离约束评价技术选型没有意义。
2 架构分层
源码中文件名的数字前缀严格控制加载顺序,体现了清晰的层次依赖:
src/core/
├── 0.foundation.js BI命名空间初始化
├── 1.lodash.js lodash工具库集成
├── 2.base.js BI.inherit / createWidget / 集合工具
├── 3.ob.js OB基类(事件系统,207行)
├── 4.widget.js Widget超类(生命周期 + 响应式,1029行)
├── 5.inject.js IoC容器(shortcut/store/service/provider/config/point)
├── h.js JSX/虚拟DOM辅助
├── plugin.js 插件系统
├── wrapper/
│ ├── layout.js BI.Layout 布局超类
│ └── layout/ HTape/VTape/Center/Float/Grid/Absolute/...
└── ...
src/base/ 基础组件层(Button/Editor/Combo/List/Tree/Pager/...)
src/case/ 业务场景组件
src/component/ 复合组件
src/widget/ 高级组件
typescript/ TypeScript类型声明
继承层次:
BI.OB ← 事件/观察者基类(无DOM)
└── BI.Widget ← UI组件超类(有DOM、生命周期、响应式)
├── BI.Layout ← 布局容器(管理子组件排列)
└── BI.Pane ← 面板基类(loading/empty态)
├── BI.BasicButton
├── BI.Editor
└── ...
3 一条真实的演进主线
如果只看今天的 FineUI,很容易把它理解成一套“偏老派”的企业前端框架:JSON 配置、继承体系、全局 BI 命名空间、内置 IoC 和布局系统都很重。但翻一下真实提交历史,会发现它更像一个被长期业务需求持续塑形的 UI 内核,而不是一次性设计完成的框架。
从当前仓库历史就能看到多条并行版本线,例如 master、release/11.0、final/11.0、persist/11.0、feature/x。这说明 FineUI 的真实演进方式不是“主干一路往前”,而是长期兼容、多版本并行和持续回灌。
3.1 先把 Widget 运行时立住,再谈组件生态
早期历史里能看到一条很清晰的主线:先把 Widget 的生命周期和运行时骨架站稳。
1cc0be8f3 widget 生命周期1835928fd 生命周期严格按照beforemount render mounted的顺序执行e3333e791 生命周期严格按照beforemount render mounted的顺序执行
这类提交说明,FineUI 早期最优先解决的问题不是“组件多不多”,而是“所有组件是否运行在一套稳定的一致生命周期里”。这也解释了为什么今天 BI.Widget 会显得这么重,因为它从一开始就是运行时中心,而不是一个轻量视图基类。
3.2 响应式和 watch 是被后续需求逐步加厚的
响应式能力同样不是一次性完整长出来的。围绕 Widget 的自动 watch 能力,历史里可以看到一串连续提交:
2ee97f029 支持自动watchfe00ac8eb 支持自动watch32685f14c 支持自动watch8906a754a 支持自动watch20438d201 支持自动watch6b9fe9288 无jira任务 同步hook
这说明 FineUI 并不是先有一套成熟响应式系统,再去承载业务;更像是先有组件运行时,再在不断上涨的交互复杂度下,把 watch、hook、依赖追踪能力一点点补厚。今天看到的 __watch + Fix,更像长期演化后的中间结果,而不是最初蓝图。
3.3 TypeScript 不是起点设计,而是后补的现代化层
TypeScript 化的痕迹也非常明确,而且是典型的“后补式现代化”:
a00b6aff0 KERNEL-801 fineui ts环境搭建5be9c7e4c KERNEL-801 feat: 输出BIf2ff83f01 KERNEL-827 feat: widget类d4a00e248 KERNEL-15181 feat: typescript 类型描述文件适配
这些提交说明,FineUI 的核心运行时并不是在类型系统约束下长出来的,而是在稳定的旧架构之上逐步外挂类型声明、再慢慢扩展到 Widget 和基础能力。也正因为如此,今天它的 TS 支持会带着明显的“映射表”和“声明补齐”气质,而不是像 Vue 3 那样天然由源码类型化得到。
3.4 ES6 化、去全局化、monorepo 化都是后期工程主题
如果再往后看工程侧历史,能看到另外一条非常典型的现代化路线:
3e98c47b7 KERNEL-14001 refactor: inject的es6化be9a5e851 refact: KERNEL-14316 去掉 jQuery 以外的全局变量252f69260 REPORT-90850 feat: 使用 monorepo 结合,并验证 fineui 按需要引入1f204c67a Pull request #3591: es6合主线了
这条线说明 FineUI 的现代化优先级也很务实:不是先推翻业务模型,而是先在注入层、模块层、打包层和全局变量治理上逐步松绑。换句话说,FineUI 的“现代化”从来不是一场重写,而是一条长期的结构性减负过程。
只有把这些历史放在一起看,才能理解 FineUI 为什么一方面非常老练,另一方面又显得背着很多时代包袱。因为它不是一套静态框架,而是一套在长期兼容压力下不断加固、不断补强、也不断修旧如旧的企业 UI 内核。
4 核心设计决策逐项分析
4.1 JSON配置驱动
机制:UI通过纯JSON对象描述,BI.createWidget在运行时解析JSON树并递归创建组件实例。
{
type: "bi.border",
items: {
north: { el: { type: "bi.label", text: "标题" }, height: 40 },
center: { el: { type: "bi.chart", chartType: "bar", dataSource: "ds1" } }
}
}
与JSX/Template的核心差异:
| 维度 | FineUI JSON | React JSX | Vue Template |
|---|---|---|---|
| 本质 | 可序列化的数据结构 | 函数调用(createElement) | 字符串模板 → 编译为render函数 |
| 可序列化 | 天然支持 | 不可(函数不可序列化) | 需自行设计协议 |
| 表达能力 | 受限(条件/循环需命令式拼接) | 完整JS能力 | 指令语法(v-if/v-for) |
| IDE补全 | 弱(字符串key,运行时解析) | 强(TS+JSX类型推导) | 强(Volar类型推导) |
| 编译时优化 | 不可能(纯运行时) | 有限(React Compiler实验中) | 深度(Patch Flags / 静态提升) |
| 调试定位 | 差(错误栈指向createWidget内部) | 好(source map精确到JSX行) | 好 |
权衡判断:JSON驱动在设计器/低代码场景中是架构级优势——设计器产出JSON、服务器存储JSON、渲染器消费JSON,全链路一致。JSX/Template要实现同等能力,必须额外构建一层序列化协议。但在纯编码场景中,JSON的表达能力和开发体验明显弱于JSX。
合理的演进方向是双轨制:JSON作为序列化层保留,上层提供编译时DSL(JSX已部分实现,但嫁接式集成,非原生)。
4.2 OOP继承体系
核心实现:BI.inherit——寄生组合式继承:
BI.inherit = function (sp, overrides) {
var sb = function () { return sp.apply(this, arguments); };
var F = function () {};
F.prototype = sp.prototype;
sb.prototype = new F();
sb.superclass = sp.prototype;
_.extend(sb.prototype, overrides, { superclass: sp });
return sb;
};
不调用两次父类构造函数,原型链正确,superclass引用可用,不依赖ES6 class,IE9可运行。
与React/Vue组件模型的对比:
| 维度 | FineUI | React | Vue |
|---|---|---|---|
| 复用机制 | 继承(is-a) | 组合(has-a):Hooks/HOC | 组合(has-a):Composables |
| 扩展"带搜索的下拉树" | 继承链 Combo→SearchCombo→TreeSearchCombo | 组合 <Dropdown><SearchInput/>{children}</Dropdown> | 组合 composable + slot |
| 横切关注点(如"可拖拽") | BI.behavior / BI.point(AOP) | Custom Hook | Composable |
| 基类修改影响范围 | 全局(脆弱基类问题) | 无基类 | 无基类 |
实际问题:
- 脆弱基类:修改
BI.Pane的默认loading行为曾导致数十个下游组件回归。 - 分类困境:组件应继承Widget还是Pane?是Single还是Combo?随着业务增长,分类边界模糊,中间基类膨胀。
- 跨层级复用困难:"可拖拽"功能需要横切Button、Panel、Tree等不相关组件,继承链无法表达,只能靠AOP补救。
React明确宣言"Composition over Inheritance"(官方文档:"we haven't found any use cases where we would recommend creating component inheritance hierarchies")。Vue同样推崇组合式设计。FineUI选择继承的历史原因是2012年的Java社区共识和目标用户画像,但在现代前端语境下,这是最大的架构债务。
4.3 事件系统(BI.OB)
3.ob.js,207行,是整个框架的最底层基类。
on: function (eventName, fn) {
eventName = eventName.toLowerCase(); // 大小写不敏感
// ...
fns.push(fn);
return function () { self.un(eventName, fn); }; // 返回unsubscribe函数
},
fireEvent: function () {
// handler返回false中断后续handler
for (var i = 0; i < fns.length; i++) {
if (fns[i].apply(this, args) === false) return false;
}
return true;
},
destroy: function () {
this.destroyed && this.destroyed();
this._purgeRef();
this.purgeListeners(); // 自动清理所有事件
}
设计亮点:
on()返回取消函数——早于React Hooks的cleanup模式(2019)数年toLowerCase()统一事件名——在无TypeScript的年代消灭了大小写拼写类bugdestroy自动purgeListeners——框架层面防内存泄漏,不依赖开发者手动清理
不足:
- 无事件冒泡(不像DOM事件从子到父传播)
- 无通配符监听
- eventName是纯字符串,TypeScript下无类型约束
对比:React基于合成事件(SyntheticEvent)统一跨浏览器行为 + 事件委托;Vue的$emit有模板层类型推导。FineUI的事件系统更接近Node.js EventEmitter——简单直接,缺乏框架级增强。
4.4 生命周期
4.widget.js编排了完整的组件生命周期(1029行):
_initProps → _initRoot → _constructed(setup) → _init
→ beforeInit(异步) → beforeRender(异步) → beforeCreate
→ render(返回JSON树) → created → _initEffects(响应式绑定)
→ beforeMount → 递归挂载子组件 → mounted
更新:beforeUpdate → update → updated
销毁:beforeDestroy → 递归销毁 → destroyed
12个钩子。对比:
| 特性 | FineUI | React (Hooks) | Vue 3 |
|---|---|---|---|
| 钩子数量 | 12 | 3 (useEffect/useLayoutEffect/useInsertionEffect) | 9 |
| 异步初始化 | beforeInit/beforeRender支持callback+Promise,阻塞渲染 | 无(用Suspense或条件渲染) | 无(用Suspense或条件渲染) |
| 钩子声明位置 | 类定义 + options配置(双通道,都执行) | 函数体内 | setup()内 |
异步beforeInit是FineUI独有的设计。在BI场景中,表格组件必须先从服务器获取列定义才能渲染——如果先渲染再更新,数百组件的仪表板会产生级联布局抖动。beforeInit阻塞渲染,确保组件"准备好了才出现"。
风险:callback不调用则组件永远不渲染,且无报错。这是一个沉默失败模式。我参与加入了超时检测机制,但根本矛盾(阻塞异步 vs 声明式渲染)未解。React/Vue用Suspense优雅地解决了这个问题——声明式的异步边界,不阻塞渲染。
4.5 响应式系统
核心实现:__watch方法,依赖外部Fix库(类Vue响应式内核):
__watch: function (getter, handler, options) {
if (_global.Fix) {
var watcher = new Fix.Watcher(null,
function () { return getter.call(self, self); }, // 依赖收集
function (v) { handler.call(self, self, v); }, // 变更回调
BI.extend({ deep: true }, options)
);
this._watchers.push(function () { watcher.teardown(); });
return watcher.value;
} else {
return getter(); // 降级
}
}
使用约定:options中传入函数的配置项自动视为响应式计算属性。
{
type: "bi.label",
text: function () { return store.getModel().count; }, // 响应式
invisible: function () { return store.getModel().count > 10; } // 响应式
}
与Vue/React响应式的对比:
| 维度 | Vue 3 | React | FineUI |
|---|---|---|---|
| 追踪粒度 | 属性级(Proxy) | 组件级(全量re-render) | 属性级(Fix.Watcher) |
| 更新触发 | 自动(Proxy setter) | 手动(setState) | 自动(Watcher回调) |
| 依赖收集 | 编译时+运行时 | 无(手动声明deps数组) | 纯运行时 |
| 批量更新 | nextTick批处理 | 自动批处理(React 18) | 无内置机制 |
| 编译优化 | Patch Flags / 静态提升 | React Compiler(实验) | 不可能(运行时解析) |
关键差距:
- 无批量更新——多属性同步变更触发多次独立DOM操作
- 无编译时优化——JSON运行时解析,架构上不可能做静态分析
- Fix库封闭——无独立文档/npm包/社区审视,与Widget生命周期深度耦合。对比Vue的
@vue/reactivity已被提取为独立包被多个非Vue项目采用
4.6 IoC容器
5.inject.js实现了完整的依赖注入容器,这是FineUI最被低估的设计:
| API | 语义 | Spring对应 | 单例 |
|---|---|---|---|
BI.shortcut(type, cls) | 注册UI组件 | @Component | 否 |
BI.store(type, cls) | 注册Store | @Service(singleton) | 是 |
BI.service(type, cls) | 注册Service | @Service | 是 |
BI.provider(type, cls) | 注册Provider | @Configuration | 是 |
BI.config(type, fn) | 修改已注册组件默认配置 | BeanPostProcessor | - |
BI.point(type, action, fn) | AOP切面注入 | @Aspect | - |
// 全局修改所有按钮默认高度——不修改任何源码
BI.config("bi.button", function (props) {
props.height = 32;
return props;
});
// 所有按钮点击前植入埋点——零侵入
BI.point("bi.button", "doClick", function () {
analytics.track("button_click", { type: this.options.type });
});
BI.config允许客户在不fork源码的前提下定制任何组件行为,BI.point实现了零侵入式埋点。对于帆软的二次开发生态,这两个能力直接决定了产品的可扩展性和升级安全性。
React的Context和Vue的provide/inject在表达能力上远不及这个IoC容器——它们不支持AOP切面,不支持全局配置覆盖。
不足:type是纯字符串,无命名空间隔离,大型项目存在命名冲突风险;依赖关系是隐式的,静态分析工具无法追踪。
4.7 布局系统
内置10+种布局类型,均继承自BI.Layout:
| 类型 | type标识 | 实现原理 |
|---|---|---|
| 绝对定位 | bi.absolute | position: absolute |
| 垂直 | bi.vertical | flex-direction: column |
| 水平流式 | bi.htape | flex + width计算 |
| 垂直流式 | bi.vtape | flex + height计算 |
| 居中 | bi.center_adapt | flex + justify/align center |
| 左右 | bi.left_right_vertical_adapt | flex + space-between |
| 边框 | bi.border | 上下左右中五区域 |
| 网格 | bi.grid | CSS Grid / table |
| 填充 | bi.fill | flex: 1 |
关键技术细节:
// DocumentFragment批量插入——减少DOM重排
_mountChildren: function () {
var frag = BI.Widget._renderEngine.createFragment();
for (var key in this._children) {
frag.appendChild(child.element[0]);
}
this.appendFragment(frag); // 一次性插入
}
// 间距自动处理:0~1视为百分比,其他除以pixRatio
_optimiseGap: function (gap) {
return (gap > 0 && gap < 1)
? (gap * 100).toFixed(1) + "%"
: gap / BI.pixRatio + BI.pixUnit;
}
与CSS布局方案的对比:
| 维度 | CSS Flexbox/Grid | FineUI Layout |
|---|---|---|
| 可序列化 | 需自定义 | 天然(JSON配置) |
| 分辨率适配 | 手动rem/vw | pixRatio自动转换 |
| 学习曲线 | 需掌握CSS模型 | 查API文档 |
| 灵活性 | 无限 | 受限于预设类型集 |
| 浏览器兼容 | 需polyfill(旧浏览器) | 框架内部兜底 |
5 我参与的现代化改造
5.1 TypeScript类型体系
问题:BI.createWidget({ type: "bi.button", ... })中type是字符串,IDE无法推导该组件支持哪些属性。typescript/目录有部分声明但大量any。
方案:利用TypeScript条件类型构建字符串字面量到接口的映射:
interface WidgetTypeMap {
"bi.button": ButtonOptions;
"bi.label": LabelOptions;
"bi.vertical": VerticalLayoutOptions;
// ...
}
function createWidget<T extends keyof WidgetTypeMap>(
config: { type: T } & WidgetTypeMap[T]
): WidgetInstanceMap[T];
局限:映射表手动维护,新增组件易遗漏。根本原因是字符串注册机制对静态类型天然不友好——这是JSON配置驱动架构的结构性代价。Vue 3选择用TypeScript完全重写,类型安全是自然获得的;我们是在运行时系统上嫁接编译时约束。
5.2 Composition API(setup)推广
setup()在我加入前已添加到4.widget.js,但使用率低。我的工作是在新代码中推广并总结实践。
源码实现:
_constructed: function () {
if (this.setup) {
pushTarget(this);
var delegate = this.setup(this.options);
if (BI.isPlainObject(delegate)) {
BI.extend(this, delegate); // 返回对象 → 混入实例
} else {
this.render = delegate; // 返回函数 → 作为render
}
popTarget();
}
}
改造前后对比:
// Options API(旧):命令式更新,var self = this,ref回调存引用
BI.shortcut("my.counter", BI.inherit(BI.Widget, {
props: { count: 0 },
render: function () {
var self = this;
return {
type: "bi.vertical",
items: [
{ type: "bi.label", ref: function (r) { self._label = r; },
text: this.options.count },
{ type: "bi.button", text: "+1",
handler: function () {
self.options.count++;
self._label.setText(self.options.count);
}}
]
};
}
}));
// Composition API(新):响应式引用,自动更新,逻辑内聚
BI.shortcut("my.counter", BI.inherit(BI.Widget, {
setup: function () {
var count = Fix.ref(0);
return function () {
return {
type: "bi.vertical",
items: [
{ type: "bi.label",
text: function () { return count.value; } },
{ type: "bi.button", text: "+1",
handler: function () { count.value++; } }
]
};
};
}
}));
结构性矛盾:BI.inherit(BI.Widget, { setup: ... }) —— 组合式逻辑嫁接在继承体系上。无法像Vue那样将Fix.ref / Fix.computed提取为独立composable在任意上下文复用,因为Fix与Widget生命周期耦合。下一步应将Fix的响应式原语独立化(参考@vue/reactivity的解耦实践)。
5.3 布局系统Flexbox迁移
问题:早期布局长期混用旧式容器与直接 DOM 编排,Flexbox 迁移之后,三种渲染思路的差异大致如下:
| 策略 | React | Vue 3 | FineUI |
|---|---|---|---|
| 渲染机制 | Virtual DOM Diff | VDOM + 编译时Patch Flags | 直接DOM操作 |
| 状态变更时 | 重执行render → diff → 最小DOM操作 | 精确追踪变更依赖 → 跳过静态子树 | 开发者/watcher直接调用DOM API |
| 首次渲染 | createElement开销 + VDOM构建 | 模板编译 + VDOM构建 | JSON解析 + 直接DOM创建 |
| 静态内容优化 | 有限 | 深度(静态提升/树打平) | 无(运行时解析) |
| 高频更新 | 优(声明式,框架算最小diff) | 最优(精确到属性级追踪 + 编译优化) | 差(需手动编排更新) |
| 低频/静态场景 | 有VDOM diff冗余开销 | 有VDOM构建开销(编译优化缓解) | 最优(无中间层,直接操作) |
BI仪表板的典型场景是"创建一次,展示很久"——大量组件创建后不再变化。FineUI的直接DOM操作在此场景下跳过了VDOM构建和diff的中间开销。但当交互增多(筛选联动、实时数据),缺乏自动化更新机制就成为短板。
6 当前技术债与改进方向
| 问题 | 现状 | 目标 | 阻碍 |
|---|---|---|---|
| 全局命名空间 | BI.*承载所有功能,无法Tree-shaking | ES Module化,按需导入 | 存量代码中BI.引用数以万计 |
| 类型安全 | 嫁接式TS映射表,手动维护 | 源码级TypeScript | 需重写核心模块 |
| 开发者工具 | 无(仅console.log + DOM Inspector) | DevTools插件(组件树 + options检查) | 资源投入 |
| 批量更新 | __watch逐个触发,多属性变更多次DOM操作 | 微任务批处理(参考Vue nextTick) | 需改造Fix.Watcher调度层 |
| Fix库封闭 | 无独立文档/包,与Widget耦合 | 独立npm包 + 文档 | 需解耦生命周期绑定 |
| 布局类型膨胀 | 10+种布局type,命名混乱 | 统一Flex模型 + 参数控制 | 存量迁移 |
| 文档 | API参考为主,缺少设计理念解释 | 增加"为什么"层面的文档 | 内容投入 |
7 设计哲学全景对比
FineUI React Vue
────── ───── ───
UI描述 JSON配置对象 JSX HTML模板
可序列化 天然 不可 不可
编程范式 OOP继承 函数式组合 多范式组合
数据流 事件驱动+响应式 单向(setState) 响应式(Proxy)
渲染策略 直接DOM Virtual DOM Diff VDOM+编译优化
编译优化 不可能 有限 深度
DI/AOP 内置IoC容器 Context(弱) provide/inject(弱)
企业定制 极强(config/point) 需自建 需自建
类型安全 弱→改善中 强 强
Tree-shaking 不可能(全局BI) 原生支持 原生支持
目标用户 Java背景开发者 JS/TS工程师 全栈开发者
核心权衡总结:
- 配置化 vs 代码化:JSON配置赢在机器可读性(序列化/设计器),输在人可读性(表达能力/IDE体验)。
- 开箱即用 vs 生态自组装:FineUI内置布局/组件/IoC/AOP,二次开发零配置;React只提供视图层,灵活但需自行选型。
- 继承 vs 组合:继承在Java背景用户中直觉性强、变体组件复用高效,但脆弱基类和分类困境是长期债务。
- 直接DOM vs Virtual DOM:低频静态场景FineUI更快,高频交互场景React/Vue更优。
- 稳定性 vs 先进性:FineUI以向后兼容为第一优先级(服务数万企业客户),架构创新节奏慢于React/Vue。
没有绝对优劣。框架的价值取决于它在特定约束下解决问题的效率。 FineUI的JSON驱动、IoC容器、AOP切面在企业BI场景中是不可替代的架构优势;React/Vue在通用Web开发中的生态、工具链、社区是FineUI不具备的。