Qumulo仪表盘前端开发技术:React + D3.js

前言:Qumulo是一家总部位于美国西雅图的企业数据存储公司,专注于开发简单灵活、可扩展和高效的企业数据智能存储系统。
原文:http://qumulo.com/blog/front-end-engineering-at-qumulo-react-d3-js/
作者:Eric Wright
渣翻译勿喷

Qumulo仪表盘前端开发技术:React + D3.js_第1张图片

开发文化

Qumulo给用户提供可交互的分析结果,帮助用户实时掌握他们的服务器的存储容量和性能表现。我们一直在改进分析,及时听取用户需求,而且一直在进行UI迭代,追求更完美的数据组织方式和交互体验。

每两周升级一次web UI意味着我们不能允许工具成为阻碍和桎梏,如果新的技术能够带来更高的开发效率,我们就会欣然接受。2012年,项目伊始,我们使用Backbone、jquery和underscore,三年后的今天,我们几乎完全迁移到了React、D3.js、ES2015、和Babel,经历了从Backbone的模型到Flux的单向数据流的转变。代码库逐步更新迭代对我们来说至关重要,我们不可能停下来用最新的技术重写我们的web app。

吊炸天的技术: React + D3.js

这两个都是非常棒的Javascript库,然而让它俩能融为一体高效运转着实是一个不小的挑战,在过去的两年里我们尝试了很多种方法,终于找到了一种能充分利用两个库优势的途径。

React和D3搭配使用的难点在哪里呢?在于找到一种既能让这两个库能协同合作渲染元素还要充分发挥它们各自优势的方法。

我们先来看一个用来渲染折线图的React组件的例子。这个图表显示了集群存储空间使用量随时间的变化。

Qumulo仪表盘前端开发技术:React + D3.js_第2张图片

这是一个简单的例子,本例中有时间轴x轴、使用量y轴以及一个折线图像,我们将此分为四个React组件:三个子组件以及管理它们数据的父图像组件。

let CapacityHistoryGraph = React.createClass({
    render() {
        return (
            
                <TimeAxis data={this.props.graphData} />
                <LineChart data={this.props.graphData} />
                <CapacityAxis data={this.props.graphData} />
            
        );
    }
});

当父组件的图像数据 prop改变时,React会调用CapacityHistoryGraphrender()函数,发送新props的到子组件。以折线图为例,下面是一个结合D3的React组件的常见方式

let LineChart = React.createClass({
     render() {
         return "lineChart" />;
     }

     componendDidMount() {
         // React rendered the  element, let D3 render the rest
         this.line = d3.svg.line()
             .x((d) => { return d.x; })
             .y((d) => { return d.y; });
         this.renderLineChart(this.props.data);
     }

     shouldComponentUpdate(nextProps) {
         // Let D3 update the chart, but prevent React from re-rendering
         this.renderLineChart(nextProps.data);
         return false;
     }

     renderLineChart(data) {
         d3.select(this.refs.lineChart)
             .removeAll()
             .append("path")
             .attr("d", this.line(data));
     }
 });

从技术角度讲,这个方法可行,折线图会随着props改变而更新。但是这种方法存在不足,因为在第一渲染后,接下来的渲染被停止了,我们在对抗React的默认生命周期(见标注的return false声明)。更糟糕的是,当你创建了更多的React+D3组件时,如果其中任何一个需要对子组件进行渲染,你却不能发送prop变动。因为它们不再会预渲染。组件的可组合性是React的关键原则,这个原则是我们不应该放弃的。

### 一个更好的方法

经过多次尝试,我们找到了一种最优的方案:既能充分发挥每个框架的优势又不会丧失其特性。我们先来罗列下功能目标:

  • React组件必需有可组性,以保证能最大程度的重用
  • React组件必需能渲染并且发送props到子组件
  • 渲染过程中D3负责计算(比如基于缩放和视口计算出需要在哪里绘制点)
  • 包括D3计算的DOM元素在内的React组件都要尽可能成为渲染的一部分

    所以我们是怎么修改之前的折线图来满足这四点的呢?我们没有让D3创建并分配点,而是让React渲染,在渲染过程中由D3来填充点。

    let LineChart = React.createClass({
     componentWillMount() {
         this.line = d3.svg.line()
             .x((d) => { return d.x; })
             .y((d) => { return d.y; });
     }
    
     render() {
         return (
             
                 this.line(this.props.data)} />
             
         );
     )
    });
    

    现在折线图组件的代码非常简洁易读了。但是对时间轴组件来说事情有点复杂,因为我们想让D3计算时间刻度、刻度线数量以及位置。d3.svg.axis能完成这些工作但是需要一个存在的DOM元素 。因此我们需要用React的componentDidMount/componentDidUpdate生命周期钩子来向React渲染后的DOM中增加元素。

    let TimeAxis = React.createClass({
     componentWillMount() {
         this.xScale = d3.time.scale();
         this.xAxis = d3.svg.axis()
             .scale(this.xScale)
             // ... and initialize ticks, tickFormat, orientation
     },
    
     componentDidMount() {
         this.renderAxis();
     },
    
     componentDidUpdate() {
         this.renderAxis();
     },
    
     render() {
         return "timeAxis" />;
     },
    
     renderAxis() {
         let minDate = new Date(_.first(this.props.data).timestamp);
         let maxDate = new Date(_.last(this.props.data).timestamp);
    
         this.xScale
             .domain([minDate, maxDate])
             .range([0, this.props.width]);
    
         d3.select(this.refs.timeAxis).call(this.xAxis);
     }
    });
    

目前为止我们对这个方法很满意,一个简单的概括:

  • 永远让React组件进行渲染
  • 在渲染中由我们自己创建SVG元素
  • 利用D3进行计算和在需要的时候生成SVG元素

如此,我们基于D3的React组件有了很好的重用性,写新组建也变得非常容易。

你可能感兴趣的:(前端数据可视化)