
简介飞鱼FlyFish是一套开源的数据可视化编码平台源码面向需要快速构建数据报表与应用的前端开发者、数据分析师及二次开发人员解决从数据建模、拖拽式图表设计到程序化集成等全流程问题。该zip压缩包共包含12252个文件主要为JavaScript、TypeScript、JSON、Markdown等类型涵盖前端交互、后端服务、配置文件与说明文档也可见Java、C/C等扩展模块能较完整地反映平台项目结构和内部机制。压缩包大小273.81MB目录层次清晰便于按模块阅读和定制。资源已有339人浏览学习。通过研读源码读者可以掌握飞鱼数据建模、自定义配置、报表生成、前后端API的调用方式并了解如何将其可视化组件嵌入既有系统适合希望在数据可视化领域快速上手或对平台二次扩展的开发者。1. 从 fly-fish-master 源码包认识 FlyFish不止是报表工具在拿到fly-fish-master这个源码包时第一眼会以为它只是一个报表工具但打开目录结构后你会发现它更像一个可二次开发的数据可视化编码平台。它把数据建模、拖拽画布、组件渲染、权限协作打包到了一套独立工程里而且默认依赖里带了 sqlite3 静态库和 argon2 加密库意味着它开箱即用不需要先搭一套后端服务就能跑起来。比起 PowerBI 这类重量级产品FlyFish 的优势在于它可以用代码定义组件、用 API 嵌入现有系统适合需要快速交付企业级数据可视化大屏、又想保留灵活性的团队。下面我会结合源码结构从数据层、组件层、集成层几个角度拆开聊。2. 数据建模与数据源接入FlyFish 的数据层设计2.1 数据源抽象从 SQLite 到外部数据库打开fly-fish-master的依赖目录你会看到sqlite3.1、sqlite3.a这样的文件旁边还有argon2.a。前者是嵌入式数据库的静态库用来保存 FlyFish 的元数据仪表板配置、用户信息、数据集定义都落在 SQLite 里后者是密码散列库负责对用户密码和数据源密钥做加密存储。这个组合决定了 FlyFish 初始化成本很低——不需要 DBA 先建库服务启动后自动创建本地存储。但团队一旦进入生产环境SQLite 扛不住多实例并发通常会把元数据库切到 MySQL让 SQLite 只保留本地开发时的缓存。FlyFish 把外部数据源抽象成一个个dataSource对象新建数据源时填的连接信息本质上就是下面这段 JSON 的变体。源码里配置管理模块会把这段 JSON 序列化后加密落库而不是明文存放{ name: production_mysql, type: mysql, options: { host: rm-bp1xxxxx.mysql.rds.aliyuncs.com, port: 3306, database: analytics, username: flyfish_app, password: encrypted_by_argon2, params: { useSSL: true, serverTimezone: Asia/Shanghai } } }这段配置里type决定了 FlyFish 加载哪种数据库驱动options会原样传给底层的连接池。需要特别留意的是password字段FlyFish 存储配置时先经过 argon2 散列再写进元数据库所以你在任何一张配置表里都看不到明文密码。这带来一个实际影响——如果想改数据源密码必须在平台上重新保存一次数据源不能直接去数据库里UPDATE否则 FlyFish 读取到的是散列值底层驱动拿到后根本无法建立连接。2.2 数据模型的字段映射与清洗数据源接好之后FlyFish 的数据建模步骤是把一条 SQL 变成数据集。这个过程的本质是定义一条可参数化的查询再把结果集字段分别标注为维度或度量。我实际建模时通常会先写一段临时 SQL在数据库客户端里确认结果集字段名和类型再粘到 FlyFish 的数据集编辑器里。以一个订单分析场景为例SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, region, COUNT(DISTINCT user_id) AS uv, SUM(order_amount) AS gmv, AVG(order_amount) AS avg_amount FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY day, region ORDER BY day这条 SQL 返回五个字段。FlyFish 后端会读取查询结果的元信息自动推断字段类型并在数据模型里生成一张字段映射表。下面是我在项目里常用的字段属性配置平台会按照这张表来决定图表组件能接收哪些字段字段名类型角色聚合方式是否可筛选daystring维度none是regionstring维度none是uvint度量count_distinct否gmvdecimal度量sum否avg_amountdecimal度量avg否这张映射表不只是给前端画布看的它同时决定了拖拽组件的字段通道维度字段可以拖进 X 轴、颜色、筛选器度量字段才能拖进 Y 轴和数值标签。很多新手在 FlyFish 里碰到“字段拖不进去”的问题十有八九是角色被自动识别成了度量回到数据模型里把角色改成维度就能解决。另外如果你在 SQL 里用了COUNT(DISTINCT ...)记得把聚合方式设为count_distinct如果误设为sum平台会先把子查询结果展开再对每条记录求和最终数字会翻好几倍。2.3 数据接口的实时刷新机制数据集定义好之后前端图表并不会直接连数据库查询而是统一走 FlyFish 的query接口。这个接口接收数据集 ID 和过滤条件后端把预定义的 SQL 和参数拼装后交给数据源执行再把结果转成统一 JSON 返回。这样设计的好处是权限拦截、缓存、审计都集中在后端前端永远拿到的是一份标准结构数据。企业级数据可视化大屏最常见的需求是实时刷新。FlyFish 支持两种模式轮询和 WebSocket。秒级以上的刷新用轮询实现最简单常见做法是在后端暴露一个只接受 POST 的查询接口前端图表组件根据refreshInterval定时发起请求。下面是一段简化后的后端实现基于 Node.jsapp.post(/api/dataset/query, async (req, res) { const { datasetId, filters, pageSize } req.body; const dataset await datasetService.load(datasetId); const sql buildSQL(dataset.baseSql, filters, pageSize); const rows await datasource.query(sql); res.json({ code: 0, data: rows, timestamp: Date.now() }); });这段代码里buildSQL的作用是把前端下发的filters安全地拼接到数据集 SQL 里而不是直接把字符串拼进查询语句避免 SQL 注入。pageSize是可选项但大屏场景我建议固定返回最近 500 条因为 ECharts 渲染上万个点会明显掉帧。如果刷新间隔少于 1 秒轮询会对数据源产生较大压力这时应该改走 WebSocket 通道。不过 WebSocket 也有它的坑每个浏览器标签页都会占用一个连接几十个大屏页面同时打开时连接数会迅速冲到上限所以我一般会在 WebSocket 服务前面加一层统一推送网关让多个客户端共享一份订阅。3. 拖拽式可视化编码组件渲染与配置下发3.1 组件库的组织方式FlyFish 和传统报表工具最大的差异在于它把所有图表能力封装成了一个个独立的组件包。源码里components目录下每个图表都是一个子工程内部至少包含三个文件schema.js描述组件有哪些可配置属性view.js负责渲染config.vue负责生成右侧属性面板。这种组织方式和 ECharts 生态里“配置项驱动”的思路一脉相承只不过 FlyFish 把配置项收敛成了声明式结构。下面是一份简化后的schema.js它定义了一个基础柱状图组件。FlyFish 的编码平台会读取这个文件自动生成属性面板里的字段下拉框、颜色选择器和开关控件export default { name: BarChart, label: 基础柱状图, version: 1.0.0, props: { data: { type: dataset, required: true }, xField: { type: dimension, title: X轴 }, yField: { type: measure, title: Y轴 }, color: { type: color, title: 配色, default: #5470c6 }, showLabel: { type: switch, title: 显示标签, default: true } } };props里的type字段决定了属性面板渲染成什么控件下表是 FlyFish 内置类型和控件的对应关系props.type属性面板控件保存值示例dimension字段下拉框维度regionmeasure字段下拉框带聚合方式gmvcolor颜色选择器#5470c6switch开关true理解这个映射关系后你就能明白为什么说 FlyFish 是“编码平台”而不是普通的拖拽设计器。组件是代码写的属性面板由代码生成最终画布上的布局也是代码序列化出来的 JSON。如果你要新增一个图表类型只需要往components目录里添加一个符合规范的组件包然后在索引文件里注册平台就能在拖拽面板里看到它。3.2 画布布局的数据结构拖拽界面的底层布局信息在 FlyFish 里是一棵 JSON 树。每个节点都包含组件名、坐标、尺寸和数据绑定关系。这个结构既是仪表板的保存格式也是预览时恢复现场的依据。我打开一个真实项目的大屏配置看到的核心结构长这样{ id: dash_001, version: 3, layout: { type: grid, cols: 12, rows: 6, children: [ { component: BarChart, x: 0, y: 0, w: 4, h: 3, props: { dataSetId: ds_sales_30d, xField: day, yField: gmv, refreshInterval: 15 } } ] } }cols和rows定义了画布网格总量子节点用x/y/w/h定位。前端拖拽时只更新这棵树并把props实时发给对应组件实例。version字段是结构版本号源码包里有一段迁移逻辑旧版本布局文件会被逐步升级而不是直接拒绝加载。二次开发的时候要特别注意如果你往布局树里新增了一个业务字段必须同步修改迁移函数否则老仪表板打开时会因为缺少字段而白屏。这段 JSON 还展示了 FlyFish 组件的数据绑定方式组件自己不关心 SQL它只从dataSetId对应的数据集里取数。这样的解耦让同一个图表组件可以复用到多个数据集上也让大屏模板的批量迁移变得很容易——只需替换dataSetId整张页面就指向了另一套数据。3.3 交互配置筛选器、下钻与联动拖拽平台不能只做静态展示还要支持点击图表、筛选数据这类探索性操作。FlyFish 把交互行为也编码在仪表板配置里而不是让用户去写事件回调。例如给一个柱状图配置“点击柱子下钻到省份”平台会在view.js里预先埋好事件触发点你只需要在配置面板里声明联动关系。典型的联动配置存放在仪表板的interactions字段中{ trigger: BarChart:click, action: FilterPanel:setFilter, params: { field: region, valueFrom: event.payload.region } }这里的valueFrom用点路径从点击事件对象里取值然后写入筛选面板的region字段最终触发整张页面重新查询。如果你写的联动不生效优先去检查事件负载里的字段名是否匹配很多问题不是联动逻辑错误而是组件view.js返回的事件字段叫area你在配置里写的是region对不上自然没反应。我会在自定义组件里统一约定事件负载字段名避免散落在各组件里大小写不一致。4. 从数据到报表企业级数据可视化大屏的完整搭建4.1 用飞鱼搭建一个实时大屏操作路径我实际部署 FlyFish 时推荐的路径是先接数据源再建数据集然后从空白画布拖入标题、指标卡、趋势图、排名表最后配置刷新和联动。直接从模板市场选一个免费数据可视化大屏模板也可以但模板里的数据集和字段命名不一定符合你的业务所以从空白画布开始更容易把数据模型和展示对齐。下面是一个指标卡组件的配置示例选自 FlyFish 内置IndicatorCard组件的实际使用方式{ component: IndicatorCard, props: { dataSetId: ds_realtime_order, metric: { field: order_count, aggregation: sum, precision: 0 }, comparison: { enabled: true, type: weekOverWeek, showArrow: true }, refreshInterval: 10 } }metric.field指定统计字段aggregation必须和数据集映射表里定义的聚合方式一致comparison.type设置为周环比时后端会生成两条时间范围的 SQL在同一个请求里返回当前值和对比值。如果你发现指标卡数字不更新先去检查refreshInterval有没有填然后看浏览器 Network 面板里查询请求是否返回 304。碰到缓存导致的假刷新我给查询接口加了一个cache: false参数或者在请求头里带一个随机时间戳。4.2 布局与主题定制从免费模板到企业级样式FlyFish 模板市场里的免费数据可视化大屏模板配色大多偏炫酷适合展示但未必适合内部运营系统。生产环境通常需要按企业主视觉做定制这个能力在 FlyFish 的theme模块里。主题本质上是一组覆盖 ECharts 的颜色、字体和边框变量保存主题后平台会把变量编译成 scss 变量注入到每个组件样式中。下面是我常用的主题覆盖项主题键默认值说明colorPalette#5470c6等图表系列色板第一项是主色fontFamilyPingFang SC全局字体数字建议用DIN AlternatebackgroundColor#0c1126大屏底色深色背景减少视觉干扰borderRadius4卡片圆角改成 8 更现代glowtrue数值阴影营销大屏常开内部系统建议关主题更新后FlyFish 会自动重绘当前画布上所有组件。深色大屏最容易被忽略的问题是主题色和标题色对比度不够比如backgroundColor设成接近黑色但图表的标题色还是深灰截图后内容几乎看不清。我的做法是每次修改背景色时同时把titleColor和descriptionColor调成亮色并且在大屏发布前用真实数据做一整轮视觉走查。4.3 权限与协作多人编辑的实现思路多人同时编辑同一个仪表板最大的问题是并发冲突。FlyFish 源码里采用的策略是“版本号 乐观锁”打开项目时拉取最新版本保存时检查版本号是否一致不一致就提示用户手动处理。这个方案比 OT 算法简单也不容易出错。后端保存接口的乐观锁逻辑可以抽象成下面这段代码async function saveDashboard(req, res, next) { const { id, version, content } req.body; const current await dashboardRepo.get(id); if (current.version ! version) { return res.status(409).json({ code: 409, message: version conflict }); } const newVersion version 1; await dashboardRepo.update(id, { content, version: newVersion, updatedBy: req.user.id }); res.json({ code: 0, version: newVersion }); }这里的关键在于version每次保存递增客户端提交时必须携带自己看到的版本号服务端比对不一致就返回 409客户端收到后弹出冲突提示让用户选择「强制覆盖」或「刷新后合并」。如果要自己扩展协作能力我建议把updatedBy也记录到历史表并且单独维护一份操作日志。这样即便冲突升级也能回溯到具体是哪个人在哪个版本上做的修改而不是靠聊天记录来找责任方。5. 自定义组件与 API 集成把 FlyFish 嵌入你的应用5.1 自定义组件的开发约定FlyFish 的自定义组件只需要满足三个约定导出name、props和render方法。一个最简单的散点图组件可以写成这样// my-scatter.js export default { name: MyScatter, props: { data: { type: dataset, required: true }, xField: { type: dimension }, yField: { type: measure } }, render(container, props, context) { const chart echarts.init(container); chart.setOption({ xAxis: { type: value, data: props.data.map(d d[props.xField]) }, yAxis: { type: value }, series: [{ type: scatter, data: props.data.map(d [d.x, d.y]) }] }); context.on(resize, () chart.resize()); return chart; } };把它放到components/custom目录并注册索引后拖拽面板就会出现这个组件。需要留意的是render里必须用context.on(resize)监听画布尺寸变化否则大屏从小屏切到大屏时图表不会自动撑满容器经常出现左边空一块的情况。5.2 通过 API 嵌入到现有系统如果不想把 FlyFish 的整个前端嵌套进来可以直接用它的查询 API 获取标准 JSON 数据再渲染到自己的 React 或 Vue 页面里。这样 FlyFish 就变成了一个数据可视化中间层你既能复用它的数据建模能力又不被它的界面绑定。一个典型的查询请求是curl -X POST https://your-host/api/dataset/query \ -H Authorization: Bearer $FLYFISH_TOKEN \ -H Content-Type: application/json \ -d {datasetId:ds_sales_30d,filters:{region:华东},pageSize:500}返回结构是{ code: 0, data: rows, timestamp }rows里的字段名和你数据建模时定义的一致。这个接口有一个坑filters里的字段必须存在于数据集映射表中传一个不存在的字段会被后端直接拒绝而不会自动忽略。所以在嵌入前我会先调用数据集的元信息接口拉一份字段清单出来再动态生成筛选表单。5.3 性能优化与常见坑最后分享三个我在 FlyFish 生产环境里踩过的问题。第一大屏上组件多时把 ECharts 的动画关掉性能提升非常明显在配置里设animation: false即可。第二图表刷新应该只在页面可见时进行用浏览器的IntersectionObserver监听大屏容器如果页面被切到后台就暂停所有刷新请求避免大量无效查询。第三不要指望平台帮你扛住全量数据数据集 SQL 里的LIMIT和后端查询接口的pageSize要同时设置否则在千万级数据面前任何前端可视化方案都会卡死。本文还有配套的精品资源点击获取