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 内核,而不是一次性设计完成的框架。

从当前仓库历史就能看到多条并行版本线,例如 masterrelease/11.0final/11.0persist/11.0feature/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 支持自动watch
  • fe00ac8eb 支持自动watch
  • 32685f14c 支持自动watch
  • 8906a754a 支持自动watch
  • 20438d201 支持自动watch
  • 6b9fe9288 无jira任务 同步hook

这说明 FineUI 并不是先有一套成熟响应式系统,再去承载业务;更像是先有组件运行时,再在不断上涨的交互复杂度下,把 watch、hook、依赖追踪能力一点点补厚。今天看到的 __watch + Fix,更像长期演化后的中间结果,而不是最初蓝图。

3.3 TypeScript 不是起点设计,而是后补的现代化层

TypeScript 化的痕迹也非常明确,而且是典型的“后补式现代化”:

  • a00b6aff0 KERNEL-801 fineui ts环境搭建
  • 5be9c7e4c KERNEL-801 feat: 输出BI
  • f2ff83f01 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 JSONReact JSXVue 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组件模型的对比

维度FineUIReactVue
复用机制继承(is-a)组合(has-a):Hooks/HOC组合(has-a):Composables
扩展"带搜索的下拉树"继承链 Combo→SearchCombo→TreeSearchCombo组合 <Dropdown><SearchInput/>{children}</Dropdown>组合 composable + slot
横切关注点(如"可拖拽")BI.behavior / BI.point(AOP)Custom HookComposable
基类修改影响范围全局(脆弱基类问题)无基类无基类

实际问题

  1. 脆弱基类:修改BI.Pane的默认loading行为曾导致数十个下游组件回归。
  2. 分类困境:组件应继承Widget还是Pane?是Single还是Combo?随着业务增长,分类边界模糊,中间基类膨胀。
  3. 跨层级复用困难:"可拖拽"功能需要横切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的年代消灭了大小写拼写类bug
  • destroy自动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个钩子。对比:

特性FineUIReact (Hooks)Vue 3
钩子数量123 (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 3ReactFineUI
追踪粒度属性级(Proxy)组件级(全量re-render)属性级(Fix.Watcher)
更新触发自动(Proxy setter)手动(setState)自动(Watcher回调)
依赖收集编译时+运行时无(手动声明deps数组)纯运行时
批量更新nextTick批处理自动批处理(React 18)无内置机制
编译优化Patch Flags / 静态提升React Compiler(实验)不可能(运行时解析)

关键差距

  1. 无批量更新——多属性同步变更触发多次独立DOM操作
  2. 无编译时优化——JSON运行时解析,架构上不可能做静态分析
  3. 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.absoluteposition: absolute
垂直bi.verticalflex-direction: column
水平流式bi.htapeflex + width计算
垂直流式bi.vtapeflex + height计算
居中bi.center_adaptflex + justify/align center
左右bi.left_right_vertical_adaptflex + space-between
边框bi.border上下左右中五区域
网格bi.gridCSS Grid / table
填充bi.fillflex: 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/GridFineUI Layout
可序列化需自定义天然(JSON配置)
分辨率适配手动rem/vwpixRatio自动转换
学习曲线需掌握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 迁移之后,三种渲染思路的差异大致如下:

策略ReactVue 3FineUI
渲染机制Virtual DOM DiffVDOM + 编译时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-shakingES 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工程师           全栈开发者

核心权衡总结

  1. 配置化 vs 代码化:JSON配置赢在机器可读性(序列化/设计器),输在人可读性(表达能力/IDE体验)。
  2. 开箱即用 vs 生态自组装:FineUI内置布局/组件/IoC/AOP,二次开发零配置;React只提供视图层,灵活但需自行选型。
  3. 继承 vs 组合:继承在Java背景用户中直觉性强、变体组件复用高效,但脆弱基类和分类困境是长期债务。
  4. 直接DOM vs Virtual DOM:低频静态场景FineUI更快,高频交互场景React/Vue更优。
  5. 稳定性 vs 先进性:FineUI以向后兼容为第一优先级(服务数万企业客户),架构创新节奏慢于React/Vue。

没有绝对优劣。框架的价值取决于它在特定约束下解决问题的效率。 FineUI的JSON驱动、IoC容器、AOP切面在企业BI场景中是不可替代的架构优势;React/Vue在通用Web开发中的生态、工具链、社区是FineUI不具备的。