整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

手把手教你D3.js 实现数据可视化极速上手到Vue应用



D3近年来一直是JavaScript最重要的数据可视化库之一,在创建者Mike Bostock的维护下,前景依然无量,至少现在没有能打的:

  • D3与众多其他库的区别在于无限定制的能力(直接操作SVG)。
  • 它的底层API提供对原生SVG元素的直接控制,但它也带来了高学习曲线的成本。
  • 我们将把D3和Vue结合在一起 - 使用Vue的动态数据绑定,清晰的语法和模块化结构,可以充分发挥D3的最佳性能。 根据广泛定义,D3可拆分为以下几种分库:



  1. 绝大部分的D3课程或书籍,都会着重讲解在其DOM操作功能上,但这明显与近几年来的web框架理念相违背。
  2. 用于数据可视化的D3,其核心在于使用绘图指令装饰数据,从源数据创建新的可绘制数据,生成SVG路径以及从数据和方法在DOM中创建数据可视化元素(如轴)的功能。
  3. 有许多用于管理DOM的工具,所有这些工具都可以在D3中集成数据可视化功能。这也是D3能与Vue无缝结合的原因之一。

于此,我们不需要从D3 DOM操作功能开始学起,直接通过实例来入门D3。

1. D3.js 渐进入门

以下实例的模版均为以下形式:

<html>
    <head>
        <link rel="stylesheet" href="index.css">
        <title>Learn D3.js</title>
    </head>
    <body>
        <!--或其它标签-->
        <h1>First heading</h1>
        
        <script src="https://d3js.org/d3.v4.min.js"></script>
        <script src="index.js"></script>
    </body>
</html>
复制代码

1. 选择和操作



你需要学习的第一件事是如何使用D3.js选择和操作DOM元素。该库在操作DOM方面实际上非常强大,因此理论上可以将其用作jQuery的替代品。以下代码请逐行添加运行。

// index.js
d3.select();
d3.selectAll();

d3.select('h1').style('color', 'red')
.attr('class', 'heading')
.text('Updated h1 tag');

d3.select('body').append('p').text('First Paragraph');
d3.select('body').append('p').text('Second Paragraph');
d3.select('body').append('p').text('Third Paragraph');

d3.selectAll('p').style('')
复制代码

2.数据加载和绑定


当你要创建可视化时,了解如何加载数据以及将其绑定到DOM非常重要。所以在这个实例中,你将学到这两点。


let dataset = [1, 2, 3, 4, 5];

d3.select('body')
    .selectAll('p')
    .data(dataset)
    .enter()
    .append('p') // appends paragraph for each data element
    .text('D3 is awesome!!');
    //.text(function(d) { return d; });
复制代码

3.创建一个简单的柱状图



首先需要添加一个svg标签

<h1>Bar Chart using D3.js</h1>

<svg class="bar-chart"></svg>
复制代码

然后在index.js中添加(已添加关键注释):

// 数据集
let dataset = [80, 100, 56, 120, 180, 30, 40, 120, 160];
// 定义svg图形宽高,以及柱状图间距
let svgWidth = 500, svgHeight = 300, barPadding = 5;
// 通过图形计算每个柱状宽度
let barWidth = (svgWidth / dataset.length);

// 绘制图形
let svg = d3.select('svg')
    .attr("width", svgWidth)
    .attr("height", svgHeight);

// rect,长方形
// 文档:http://www.w3school.com.cn/svg/svg_rect.asp

let barChart = svg.selectAll("rect")
    .data(dataset) //绑定数组
    .enter() // 指定选择集的enter部分
    .append("rect") // 添加足够数量的矩形
    .attr("y", d => svgHeight - d ) // d为数据集每一项的值, 取y坐标
    .attr("height", d => d) // 设定高度
    .attr("width", barWidth - barPadding) // 设定宽度
    .attr("transform", (d, i) =>  {
        let translate = [barWidth * i, 0]; 
        return "translate("+ translate +")";
    }); // 实际是计算每一项值的x坐标
复制代码

4. 在图形上方显示数值


这时就需要在上述代码中创建svg的 text文本


let text = svg.selectAll("text")
    .data(dataset)
    .enter()
    .append("text")
    .text(d => d)
    .attr("y", (d, i) => svgHeight - d - 2)
    .attr("x", (d, i) =>  barWidth * i)
    .attr("fill", "#A64C38");
复制代码

过程比较简单,就是返回文本,计算x/y坐标,并填充颜色。

5. scales: 比例尺函数

D3中有个重要的概念就是比例尺。比例尺就是把一组输入域映射到输出域的函数。映射就是两个数据集之间元素相互对应的关系。比如输入是1,输出是100,输入是5,输出是10000,那么这其中的映射关系就是你所定义的比例尺。

D3中有各种比例尺函数,有连续性的,有非连续性的,在本例子中,你将学到d3.scaleLinear() ,线性比例尺

5.1 d3.scaleLinear(),线性比例尺

使用d3.scaleLinear()创造一个线性比例尺,其中:

  • domain()是输入域
  • range()是输出域
  • 相当于将domain中的数据集映射到range的数据集中。
let scale = d3.scaleLinear().domain([1,5]).range([0,100])
复制代码

映射关系:



值得注意的是,上述代码只是定义了一个映射规则,映射的输入值并不局限于domain()中的输入域。

scale(1) // 输出:0
scale(4) // 输出:75
scale(5) // 输出:100
scale(-1) // 输出:-50
scale(10) // 输出:225
复制代码

于是我们来改造3~4的例子:

let dataset = [1,2,3,4,5];

let svgWidth = 500, svgHeight = 300, barPadding = 5;
let barWidth = (svgWidth / dataset.length);


let svg = d3.select('svg')
    .attr("width", svgWidth)
    .attr("height", svgHeight);
    
let yScale = d3.scaleLinear()
    .domain([0, d3.max(dataset)])
    .range([0, svgHeight]);
        
let barChart = svg.selectAll("rect")
    .data(dataset)
    .enter()
    .append("rect")
    .attr("y", d => svgHeight - yScale(d))
    .attr("height", d => yScale(d))
    .attr("width", barWidth - barPadding)
    .attr("transform", (d, i) => {
        let translate = [barWidth * i, 0]; 
        return "translate("+ translate +")";
    });
复制代码

然后就会得到以下图形:



6. Axes:轴



轴是任何图表的组成部分,本例子中将会用到上面讲到的比例尺函数。

let data= [80, 100, 56, 120, 180, 30, 40, 120, 160];

let svgWidth = 500, svgHeight = 300;

let svg = d3.select('svg')
    .attr("width", svgWidth)
    .attr("height", svgHeight);

// 首先是拿最大值构建x轴坐标
let xScale = d3.scaleLinear()
    .domain([0, d3.max(data)])
    .range([0, svgWidth]);
         
// 接下来是反转值,用作y轴坐标。
let yScale = d3.scaleLinear()
    .domain([0, d3.max(data)])
    .range([svgHeight, 0]);

// 横轴的API使用
let x_axis = d3.axisBottom()
    .scale(xScale);
    
// 纵轴的API使用
let y_axis = d3.axisLeft()
    .scale(yScale);
    
// 在svg中提供了如g元素这样的将多个元素组织在一起的元素。
// 由g元素编组在一起的可以设置相同的颜色,可以进行坐标变换等,类似于Vue中的 <template>

svg.append("g")
    .attr("transform", "translate(50, 10)")
    .call(y_axis);
         
let xAxisTranslate = svgHeight - 20;
         
svg.append("g")
    .attr("transform", "translate(50, " + xAxisTranslate  +")")
    .call(x_axis);
复制代码

7. 创建简易的SVG元素


在这里面,你会创建<rect>,<circle>和<line>元素


let svgWidth = 600, svgHeight = 500;
let svg = d3.select("svg")
    .attr("width", svgWidth)
    .attr("height", svgHeight)
    .attr("class", "svg-container")
    
let line = svg.append("line")
    .attr("x1", 100)
    .attr("x2", 500)
    .attr("y1", 50)
    .attr("y2", 50)
    .attr("stroke", "red");
    
let rect = svg.append("rect")
    .attr("x", 100)
    .attr("y", 100)
    .attr("width", 200)
    .attr("height", 100)
    .attr("fill", "#9B95FF");
    
let circle = svg.append("circle")
    .attr("cx", 200)
    .attr("cy", 300)
    .attr("r", 80)
    .attr("fill", "#7CE8D5");
复制代码

8. 创建饼图



let data = [
    {"platform": "Android", "percentage": 40.11}, 
    {"platform": "Windows", "percentage": 36.69},
    {"platform": "iOS", "percentage": 13.06}
];

let svgWidth = 500, svgHeight = 300, radius =  Math.min(svgWidth, svgHeight) / 2;
let svg = d3.select('svg')
    .attr("width", svgWidth)
    .attr("height", svgHeight);

//Create group element to hold pie chart    
let g = svg.append("g")
    .attr("transform", "translate(" + radius + "," + radius + ")") ;

// d3.scaleOrdinal() 序数比例尺
// schemeCategory10, 颜色比例尺
// D3提供了一些颜色比例尺,10就是10种颜色,20就是20种:
let color = d3.scaleOrdinal(d3.schemeCategory10);

let pie = d3.pie().value(d => d.percentage);

let path = d3.arc()
    .outerRadius(radius)
    .innerRadius(0);
 
let arc = g.selectAll("arc")
    .data(pie(data))
    .enter()
    .append("g");

arc.append("path")
    .attr("d", path)
    .attr("fill", d => color(d.data.percentage));
        
let label = d3.arc()
    .outerRadius(radius)
    .innerRadius(0);
            
arc.append("text")
    .attr("transform",  d => `translate(${label.centroid(d)})`)
    .attr("text-anchor", "middle")
    .text(d => `${d.data.platform}:${d.data.percentage}%`);
复制代码

9. 创建折线图



最后,你将学习如何创建折线图以显示近四个月的比特币价格。要获取数据,你将使用外部API。这个项目还将你在整个课程中学到的很多概念结合在一起,所以这是一个很好的可视化课程结束。

// 外部API,注意日期记得补零
const api = 'https://api.coindesk.com/v1/bpi/historical/close.json?start=2019-03-31&end=2019-07-01';

/**
 * dom内容加载完毕时,从API中加载数据
 */
document.addEventListener("DOMContentLoaded", function(event) {
fetch(api)
    .then(response => response.json())
    .then(data => {
        let parsedData = parseData(data);
        drawChart(parsedData);
    })
    .catch(err =>  console.log(err))
});

/**
 * 将数据解析为键值对
 */
parseData = data =>{
    let arr = [];
    for (let i in data.bpi) {
        arr.push({
            date: new Date(i), //date
            value: +data.bpi[i] //convert string to number
        });
    }
    return arr;
}

/**
 * 创建图表
 */
drawChart  = data => {
let svgWidth = 600, svgHeight = 400;
let margin = { top: 20, right: 20, bottom: 30, left: 50 };
let width = svgWidth - margin.left - margin.right;
let height = svgHeight - margin.top - margin.bottom;

let svg = d3.select('svg')
    .attr("width", svgWidth)
    .attr("height", svgHeight);
    
let g = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

let x = d3.scaleTime()
    .rangeRound([0, width]);

let y = d3.scaleLinear()
    .rangeRound([height, 0]);

let line = d3.line()
    .x(d=> x(d.date))
    .y(d=> y(d.value))
    x.domain(d3.extent(data, function(d) { return d.date }));
    y.domain(d3.extent(data, function(d) { return d.value }));

g.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x))
    .select(".domain")
    .remove();

g.append("g")
    .call(d3.axisLeft(y))
    .append("text")
    .attr("fill", "#000")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", "0.71em")
    .attr("text-anchor", "end")
    .text("Price ($)");

g.append("path")
    .datum(data)
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-linejoin", "round")
    .attr("stroke-linecap", "round")
    .attr("stroke-width", 1.5)
    .attr("d", line);
}

复制代码

以上原实例均来自:Learn D3 for free。

源码地址:https://scrimba.com/g/gd3js

scrimba是一个非常神奇的网站。它是使用交互式编码截屏工具构建的。


所有的操作都是:


暂停截屏视频 → 编辑代码 → 运行它! → 查看更改

非常值得安利一波。接下来进入第二部分:Vue中使用D3.js的正确姿势

2. Vue中使用D3.js的正确姿势

我们将使用D3和Vue构建一个基本的柱状图组件。网上有一堆例子,但我们将专注于写Vue,而不是滥用D3。

1. 安装依赖

首先,我们需要为项目安装依赖项。我们可以简单地安装和使用D3整库:

npm i d3
复制代码

但我在前面讲到,实际上D3是几个分库的集合,考虑到项目的优化,我们只安装所需的模块。



使用Vue Cli 初始化项目即可。

2. 创建柱状图



3. 柱状图模块导入



4. 创建svg元素



因Vue数据响应的特性,我们不需要用到D3操作DOM的那套链式创建。

5. 数据与窗口大小响应


在mounted钩子中,我们将为窗口调整大小事件添加一个监听器,它将触发绘制动画,并将<svg>大小设置为新窗口的比例。我们不会立即渲染,而是等待300毫秒,以确保完全调整窗口大小。


以下是完整的BarChart.vue,请配合注释食用:

<template>
  <div id="container" class="svg-container" align="center">
    <h1>{{ title }}</h1>
    <svg v-if="redrawToggle === true" :width="svgWidth" :height="svgHeight">
      <g>
        <rect
          v-for="item in data"
          class="bar-positive"
          :key="item[xKey]"
          :x="xScale(item[xKey])"
          :y="yScale(0)"
          :width="xScale.bandwidth()"
          :height="0"
        ></rect>
      </g>
    </svg>
  </div>
</template>

<script>
import { scaleLinear, scaleBand } from "d3-scale";
import { max, min } from "d3-array";
import { selectAll } from "d3-selection";
import { transition } from "d3-transition";

export default {
  name: "BarChart",
  props: {
    title: String,
    xKey: String,
    yKey: String,
    data: Array
  },
  mounted() {
    this.svgWidth = document.getElementById("container").offsetWidth * 0.75;
    this.AddResizeListener();
    this.AnimateLoad();
  },
  data: () => ({
    svgWidth: 0,
    redrawToggle: true
  }),
  methods: {
    // 绘制柱形
    AnimateLoad() {
      selectAll("rect")
        .data(this.data)
        .transition()
        .delay((d, i) => {
          return i * 150;
        })
        .duration(1000)
        .attr("y", d => {
          return this.yScale(d[this.yKey]);
        })
        .attr("height", d => {
          return this.svgHeight - this.yScale(d[this.yKey]);
        });
    },
    // 调整窗口大小后300毫秒重新绘制图表
    // 即响应式绘制
    AddResizeListener() {
      window.addEventListener("resize", () => {
        this.$data.redrawToggle = false;
        setTimeout(() => {
          this.$data.redrawToggle = true;
          this.$data.svgWidth =
            document.getElementById("container").offsetWidth * 0.75;
          this.AnimateLoad();
        }, 300);
      });
    }
  },
  computed: {
    dataMax() {
      return max(this.data, d => {
        return d[this.yKey];
      });
    },
    dataMin() {
      return min(this.data, d => {
        return d[this.yKey];
      });
    },
    xScale() {
      return scaleBand()
        .rangeRound([0, this.svgWidth])
        .padding(0.1)
        .domain(
          this.data.map(d => {
            return d[this.xKey];
          })
        );
    },
    // 通过线性比例尺自动生成
    yScale() {
      return scaleLinear()
        .rangeRound([this.svgHeight, 0])
        .domain([this.dataMin > 0 ? 0 : this.dataMin, this.dataMax]);
    },
    svgHeight() {
      return this.svgWidth / 1.61803398875; // 黄金比例
    }
  }
};
</script>

<style scoped>
.bar-positive {
  fill: steelblue;
  transition: r 0.2s ease-in-out;
}

.bar-positive:hover {
  fill: brown;
}

.svg-container {
  display: inline-block;
  position: relative;
  width: 100%;
  padding-bottom: 1%;
  vertical-align: top;
  overflow: hidden;
}
</style>
复制代码

我们将从父组件App.vue获取数据:

<template>
  <div id="app">
    <BarChart title="Bar Chart" xKey="name" yKey="amount" :data="barChartData"/>
  </div>
</template>

<script>
import BarChart from "./components/BarChart.vue";

export default {
  name: "App",
  components: {
    BarChart
  },
  data: () => ({
    barChartData: [
      {
        name: "张三",
        amount: 25
      },
      {
        name: "李四",
        amount: 40
      },
      {
        name: "老王",
        amount: 15
      },
      {
        name: "老赖",
        amount: 9
      }
    ]
  })
};
</script>

<style>
#app {
  font-family: "Open Sans", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #282f36;
  margin-top: 30px;
}
</style>
复制代码

这时候yarn run serve后将会看到:



好像还缺点显示数值,考虑到该图高度是根据比例尺生成,我们调整下y坐标:

yScale() {
  return scaleLinear()
    .rangeRound([this.svgHeight, 0])
    .domain([this.dataMin > 0 ? 0 : this.dataMin + 2, this.dataMax + 2]);
},
复制代码

在AnimateLoad()末尾添加:

selectAll("text")
  .data(this.data)
  .enter()
复制代码

最后在<g>元素中添加:

<text
  v-for="item in data"
  :key="item[xKey].amount"
  :x="xScale(item[xKey]) + 30"
  :y="yScale(item[yKey]) - 2"
  fill="red"
>{{ item[xKey]}} {{ item[yKey]}}
</text>
复制代码



3. 参考文章



  • The Hitchhiker’s Guide to d3.js
  • D3 is not a Data Visualization Library
  • D3中常用的比例尺
  • D3 vs G2 vs Echarts
  • Dynamic Data Visualizations With Vue.js and D3

4. 总结

该库几乎凭 Mike Bostock 一人之力完成,且在学术界、专业团队中享有极大声誉。



  • D3更接近底层,与 g2、echarts 不同,d3 能直接操作 svg,所以拥有极大的自由度,几乎可以实现任何 2d 的设计需求。
  • 正如其名 Data Driven Documents,其本质是将数据与 DOM 绑定,并将数据映射至 DOM 属性上。
  • D3 长于可视化,而不止于可视化,还提供了数据处理、数据分析、DOM 操作等诸多功能。
  • 如果有想深耕数据可视化方面的前端,D3不得不学。



掌握 D3 后,限制作品水平的只会是想象力而不再是技术。

源码地址:https://github.com/roger-hiro/d3-bar-chart-vuejs

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 原地址:https://juejin.im/post/5d1e074af265da1bca51f8ec

animate.css是一堆很酷,有趣且跨浏览器的动画,供你在项目中使用。非常适合强调,主页,滑块和一般的加水效果。



animate.css v4正在进行许多改进和重大更改,包括CSS自定义属性支持(又称CSS变量)和类前缀,以确保安全使用。感兴趣的小伙伴可以上github关注进展以及提供反馈!

Github

animate.css的受欢迎程度毋庸置疑,在Github上star数高达接近63k,这是一个非常可观的数据,我相信其实大多数人或多或少都用过它

https://daneden.github.io/animate.css/



安装使用

  • 使用npm安装
$ npm install animate.css --save

或者 yarn:

$ yarn add animate.css

要在你网站中使用animate.css,只需将样式表放入文档的<head>中,然后将动画类(animated)与任何动画名称一起添加到元素中,那么一个简单的动画效果就实现了,一下就是一个最简单的例子:

<head>
 <link rel="stylesheet" href="animate.min.css">
</head>

<h1 class="animated infinite bounce delay-2s">Example</h1>

以下是你可以使用的所用动画效果class



可以更改动画的持续时间,添加延迟或更改动画播放的次数:

.yourElement {
 animation-duration: 3s;
 animation-delay: 2s;
 animation-iteration-count: infinite;
}


  • JavaScript的用法:

将animate.css与Javascript结合使用时,可以使用animate.css进行大量其他工作。一个简单的例子:

const element = document.querySelector('.my-element')
element.classList.add('animated', 'bounceOutLeft')

还可以检测动画何时结束:

const element = document.querySelector('.my-element')
element.classList.add('animated', 'bounceOutLeft')
element.addEventListener('animationend', function() { doSomething() })

可以使用以下简单功能来添加和删除动画:

function animateCSS(element, animationName, callback) {
 const node = document.querySelector(element)
 node.classList.add('animated', animationName)
 function handleAnimationEnd() {
 node.classList.remove('animated', animationName)
 node.removeEventListener('animationend', handleAnimationEnd)
 if (typeof callback === 'function') callback()
 }
 node.addEventListener('animationend', handleAnimationEnd)
}

并像这样使用它:

animateCSS('.my-element', 'bounce')

// or
animateCSS('.my-element', 'bounce', function() {
 // Do something after animation
})

注意,这些示例使用的是ES6的const声明,不再支持IE10和某些古老的浏览器。



  • 设定延迟和速度:

可以直接在元素的class属性上添加延迟,如下所示:

<div class="animated bounce delay-2s">Example</div>


  • 快慢class

通过添加这些类,可以控制动画的速度,如下所示:

<div class="animated bounce faster">Example</div>


  • 自定义构建

Animate.css由gulp.js提供支持,这意味着你可以轻松创建自定义版本。

总结

有些时候你看到别人的网站,感觉速度也不是很快,但是很自然,那么很有可能是使用了动画,使用动画不会加快网站的访问速度,但是可以让网页浏览器来更加的平滑、更加的自然,使用起来会感觉很舒适,不会给人卡顿的感觉!

文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注!

前端框架发展到今天,已经出现了很多被广泛认同的理念,也可以叫作它们的特征。在所有的特征中,最具代表性的特征之一即是数据驱动。

用户交互的对象——界面

不论 web 应用的内部逻辑如何组织,最终与用户产生交互的仍然是界面(User Interface,简称 UI)。对 web 应用来说,界面则主要由 DOM 元素来呈现。因此,归根到底,为了让用户通过 web 应用完成操作,DOM 元素需要根据实际需要来不断变化。

例如,用户通过我们的页面来查询当天苹果的价格,页面上有一个“查询”按钮,还有一个显示苹果价格的区域,此外,在查询过程中,还有一个加载中的提示。

<button>查询</button>
<div class="price">今日苹果价格为 10.00/千克</div>
<div class="loading">加载中</div>

在查询并显示结果的过程中,DOM 实际上会经历这样几个状态:

  1. 鼠标移动到按钮上时,按钮的样式改变(通过 CSS:hover或者通过 JS)。
  2. 按钮按下时,按钮的样式改变(通过 CSS:active或者通过 JS)。
  3. 清空价格区域的显示内容。
  4. 将加载中的提示显示出来。
  5. 待查询到数据后,将数据拼装成文字“今日苹果价格为 10.00/千克”,放入价格区域中。
  6. 将加载中的提示隐藏。

命令式编程实现界面变化

如果将 1 和 2 使用 CSS 来处理,那么早期我们的 JavaScript 代码大概是这样:

// 3. 清空价格区域的显示内容
price.innerHTML = '';
// 4. 将加载中的提示显示出来
loading.style.display = 'block';
// 查询价格
queryPrice(function(data){
    // 5. 待查询到数据后,将数据拼装成文字“今日苹果价格为 10.00/千克”,放入价格区域中
    price.innerHTML = '今日苹果价格为 ' + data.price + '/千克';
    // 6. 将加载中的提示隐藏
    loading.style.display = 'none';
});

即使是引入 jQuery 之类的库,整个过程仍然不会有实质性的变化,主要靠每一行 JavaScript 代码命令式地修改 DOM 元素来达到修改界面的目的。

数据驱动界面改动

既然需要不断修改界面,而命令式修改界面又如此繁琐,是否有别的方法来修改界面呢?数据驱动的概念应运而生。它的基本思想是:使用数据来描述应用的状态,将界面的修改与数据的修改绑定起来,从而实现数据的任何修改都直接反映到界面的修改。

如果用一个公式来表示的话,可以写成UI=F(state),UI 即指用户界面,state指应用的状态,而F则是应用状态与用户界面的映射关系定义。它最直观的理解是“应用的任何状态,都可以通过一种确定的映射关系,反映到界面的某种状态上”,因此只要状态发生变化,界面也会发生变化。

上述例子使用 Vue 来编写:

<template>
    <button @click="query">查询</button>
    <div class="price">今日苹果价格为 {{price}}/千克</div>
    <div class="loading" v-show="isLoading">加载中</div>
</template>
<script>
    export default {
        data(){
            return {
                price: 0,
                isLoading: false
            }
        },
        methods: {
            query(){
                this.isLoading = true;
                queryPrice((data) => {
                    this.price = data.price;
                    this.isLoading = false;
                });
            }
        }
    }
</script>

在上述代码中,应用的状态就是data,包括两个值priceisLoading。我们的逻辑代码只需要对这两个值进行操作,即可以完成整个应用功能的界面变化。

而状态和界面的映射则由<template>来定义,例如今日苹果价格为 {{price}}/千克表示状态中的price变量需要显示在此处,v-show="isLoading"则表示是否显示加载中完全由变量值isloading决定。

事实上当前前端主流的几大框架,在界面的渲染上都不约而同选择了数据驱动的方式,深入理解这一模式有助于我们更好地理解前端框架。

数据驱动带来的好处

数据驱动界面变更能迅速被广泛接受,也可以从侧面说明它确实为开发者带来了一些好处。

首先,因为开发者仅需要管理数据,使得关于界面细节控制的代码不再需要开发者编写。同时,由于状态被抽象出来,同一个变量值在界面上的多处变化全部由映射关系来决定,而不需要开发者手工修改每一处变化。这两者结合起来使得开发者的心智负担大大减少,需要关注的代码量也大大减少,从而使得开发效率得以大幅提升,出现 bug 的概率也大大减少。

其次,专门将应用状态抽象出来,使得开发者必须认真思考代码的组织方式,而因为界面相关细节的消失,大部分的代码都变成了逻辑代码,使得传统编程中的模式都可以被应用到前端代码中,从而使得前端代码能够支持更大规模的应用,也能更好地组织前端代码本身,使得代码更容易阅读和维护。

状态的抽象也使得开发者可以精准地保存和还原任意一个界面状态。因为界面的每一时刻的界面表现都是由这一时刻的应用状态决定,因此只要能够将此时的应用状态进行保存,就能在另一个时间、空间中重现应用此时的界面表现。

这个特性在某一些场景下非常好用,例如线上 bug 的排查。如果我们有办法取到用户的当前状态,就有办法完全还原用户的界面表现,从而快速复现应用碰到的 bug,而不用再苦苦和用户沟通详细的操作步骤,一点点地确认应用可能是哪里出了问题。除此之外,这个特性还可以用于实现“时间旅行”效果,即应用界面的回放。我们只需要将状态的变更都记录下来,就能看到应用从初始化一直到最终状态中间发生的完整事情。它本身可以作为一个效果来使用,也可以用来支持一些功能(例如撤销/重做)。

因为应用界面完全由应用状态决定,而状态映射到界面的操作一般由框架来帮忙我们完成,因此在测试的时候,就有机会将重点放在状态的测试上。即在很多情况下,我们只需要测试逻辑和数据,确保应用状态是正确的,即可大概认为界面是正确的。

因为界面测试的成本要远高于逻辑和数据测试,如果我们能在不做界面测试的情况下也保证应用逻辑和状态是正确的,将大大提升测试效率。

小结

时至今日,数据驱动已经成为绝大部分前端开发者的共识,Vue 也是这一理论的实践者,后面我们将看到 Vue 是如何实现这一重要特征的。

欢迎关注「慕课网」,发现更多IT圈优质内容,分享干货知识,帮助你成为更好的程序员!