关于二次开发开源项目的理解(redash)

Posted by 杨青波 on 2021-04-19

关于二次开发的一些思考

很多同学因为想要对某些技术深耕,又或是因为面试会问去了解技术的底层原理之类的,但是很难坚持或是看不懂之类的。其实阅读源码,了解底层原理这东西最好还是结合实际情况来进行。比如你是一个Java Developer 但是你项目中并没有怎么深度使用elasticsearch但是你因为想要学的更多一点去自己阅读elasticsearch的源码,大概率你会坚持不下来,这主要是你压根没有使用场景,没有在做中体会这些源码的精妙设计和原理。如果非要阅读的话,我建议也是从实际角度出发,指定一个目标,比如成为elasticsearch的贡献者之类的明确目标,再细一点的目标也是可以的,比如修复某个issue,从实际角度出发你增加你的兴趣,而且也能体现出你阅读源码的作用,不然即使看完源码发现除了可能学到了一些开源框架的设计模式啥没学到,对技术的兴趣会打折的。
所以从实际角度出发,你要解决什么问题你就去了解什么东西,往往解决一个问题才是你学习的开始。

为什么我们要二次开发redash?

在程序员的职业生涯中总会碰到因为某些原因导致我们需要对开源框架进行二次开发,这时阅读源码并理解架构是我们能快速完成任务的前提。最近我参与了redash这款开源框架的二次开发,主要是一些前端的定制化开发相关的内容。
在我们的整个大团队中基本上每个团队都会有一些spark job 的数据需要展示,而我们对redash本身提供的一些数据展示模型并不满意,也不能覆盖我们的所有使用场景,所以我们对redash进行了一些二次开发。

redash是什么?

redash是一款可以连接到任何数据源,轻松实现可视化仪表板和共享数据的框架。它非常适合用来做数据驱动决策的数据分析和展示,并且使用起来比较方便,支持浏览器编辑各种SQL,NoSQL的脚本。

关于redash的定制化widget的简述

在我们的使用场景中我们将redash页面通过iframe方式嵌入到我们的项目中,而嵌入的页面是个dashboard,每个dashboard中有多个widget,我们通过url传输一些参数,postMessage传输一些异步数据。
所以我以开发一种新的widget为目标来看,我要完成任务我就要了解以dashboard和widget组件相关的内容。然后阅读源码发现,这里其实就是一种工厂模式,在redash中定义了多种widget,我们只要选用不同类型的widget就能生成不同的报表。
这是个配置类,配置了所有已开发的widget,基本类似于使用一个map将所有widget给保存起来,key是widget名称,value则是整个widget的组件,我们使用widget只需要在UI上选中即可生成对应的报表。
这种设计UI组件的方式特别适合多场景多类型的组件开发,可以灵活的根据所选参数进行UI的组件的变更。参考这种设计方式,我们在项目中也多处运用到了这个模式,比如我们创建一些实验的时候要根据实验渠道展现不同的UI组件,以及我们不同渠道的实验需要展示不同的readout,都运用上了这种设计。

以下是widget 的注册类,我选取了其中关键部分代码来简单分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { find, flatten, each } from "lodash";
import PropTypes from "prop-types";

// 将所有的widget目录都引进来,主要就是两个个组件Renderer(用来展示查询出来的数据的) 和 Editor(用来配置展示的数据模型)
import boxPlotVisualization from "./box-plot";
import chartVisualization from "./chart";
import choroplethVisualization from "./choropleth";
import cohortVisualization from "./cohort";
import counterVisualization from "./counter";
import detailsVisualization from "./details";
import funnelVisualization from "./funnel";
import mapVisualization from "./map";
import pivotVisualization from "./pivot";
import sankeyVisualization from "./sankey";
import sunburstVisualization from "./sunburst";
import tableVisualization from "./table";
import wordCloudVisualization from "./word-cloud";

type VisualizationConfig = {
type: string;
name: string;
getOptions: (...args: any[]) => any;
isDefault?: boolean;
isDeprecated?: boolean;
Renderer: (...args: any[]) => any;
Editor?: (...args: any[]) => any;
autoHeight?: boolean;
defaultRows?: number;
defaultColumns?: number;
minRows?: number;
maxRows?: number;
minColumns?: number;
maxColumns?: number;
};

// @ts-expect-error ts-migrate(2322) FIXME: Type 'Requireable<InferProps<{ type: Validator<str... Remove this comment to see the full error message
const VisualizationConfig: PropTypes.Requireable<VisualizationConfig> = PropTypes.shape({
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
getOptions: PropTypes.func.isRequired,
isDefault: PropTypes.bool,
isDeprecated: PropTypes.bool,
Renderer: PropTypes.func.isRequired,
Editor: PropTypes.func,
// other config options
autoHeight: PropTypes.bool,
defaultRows: PropTypes.number,
defaultColumns: PropTypes.number,
minRows: PropTypes.number,
maxRows: PropTypes.number,
minColumns: PropTypes.number,
maxColumns: PropTypes.number,
});

const registeredVisualizations = {};

function validateVisualizationConfig(config: any) {
const typeSpecs = { config: VisualizationConfig };
const values = { config };
PropTypes.checkPropTypes(typeSpecs, values, "prop", "registerVisualization");
}

function registerVisualization(config: any) {
validateVisualizationConfig(config);
config = {
Editor: () => null,
...config,
isDefault: config.isDefault && !config.isDeprecated,
};

// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
if (registeredVisualizations[config.type]) {
throw new Error(`Visualization ${config.type} already registered.`);
}

// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
registeredVisualizations[config.type] = config;
}

// 这里将所有已经开发了的报表类型注册进去,后面就可以通过选取不同类型的widget来展示对应的widget的UI。
each(
flatten([
boxPlotVisualization,
chartVisualization,
choroplethVisualization,
cohortVisualization,
counterVisualization,
detailsVisualization,
funnelVisualization,
mapVisualization,
pivotVisualization,
sankeyVisualization,
sunburstVisualization,
tableVisualization,
wordCloudVisualization,
]),
registerVisualization
);

export default registeredVisualizations;

widget的代码就不贴了,就是两个组件Renderer和Editor,目录结构基本如下:

关于我们生产实践中的redash使用

由于数据原因,我们会出现一些数据查询结果为空,如果单纯的使用SQL来生成报表,那么我们有时候回出现部分dashboard没有数据,这会让用户疑惑,所以我们使用python脚本来生成查询的SQL,并且可以提供一个post_query来处理查询出的结果,如果一些查询结果不符合我们的期望,我们可以在post_query中来进行处理(没数据,或者数据为0之类的,我们要将它转换成用户友好的结果)。
redash支持使用Python脚本,以及URL请求等获取数据。
这里从官网撸了一张gif演示redash的强大数据分析能力,这个演示是最基本的SQL查询用法:

结语

一般来说,业务团队对开源框架的开发基本止于二次开发,基本上不会有多少新功能或者大量issue的修复。因为在做技术选型时会考虑到这个框架给我们带来的利弊权衡,如果有太多新feature和issue需要处理,大概率不会选取这个框架,而基于框架设计本身的二次开发扩展就没有那么耗时耗力了。但是不管是二次开发还是写一些新feature和修一些issue都是要对这个框架架构有个整体上的认知。
而对于中间件和基础架构团队来说,如果要对一个开源框架进行大量的feature开发和修复issue,那么基本上会涉及到多个团队以及一些法务和行政等部门的商议。需要考虑是不是要接着提交到开源社区,这个过程比较麻烦。
我们团队给clickhouse提的一个feature到现在还在交涉中。
往往解决一个问题才是学习的开始



支付宝打赏 微信打赏

赞赏一下