93

图表库源码剖析 - 轻巧的 SVG.js

 6 years ago
source link: https://zhuanlan.zhihu.com/p/31225775?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

图表库源码剖析 - 轻巧的 SVG.js

有一晚上打死 20 多只蚊子的体验吗~ ?

平时我们写业务可视化组件的时候 会经常使用到 SVG, 也会使用 SVG 做一些动画。SVG.js 是一个开源项目,能够让你更优雅、便捷的方法编写 SVG。 我们也可以通过对 SVG.js 的分析来一起看下可以如何去写一个可视化的工具库。

动图封面

SVG.js 目标:让 svg 写起来更简单, 下面来分析下它的源码和背后的设计思路,目标是如何做到的;

SVG.js 简介

SVG.js 是用于操作 SVG 和执行 SVG 动画的轻量级库。一个好的类库,可扩展性也是必不可少的, SVG.js 扩展性做的也不错;
SVG.js 没有依赖关系,并且小(比较同类常用类库 Snap.svg、 Raphael),同时提供接近完整的SVG规范。

SVG.js 用法

// 原生 svg 写法
<div id="drawing">
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="300" height="300">
    <rect width="100" height="100" fill="#f06"></rect>
  </svg>
</div> 
// SVG.js 写法 
var draw = SVG('drawing').size(300, 300)
var rect = draw.rect(100, 100).attr({ fill: '#f06' })

SVG.js 把 SVG 所有的图形帮我们封装了一下, 可以让我们更好的使用 js 用链式写法,面向对象的写法 更好的画自己业务组件;让一切回归简单;
SVG.js 将基础的 shape 进行了包装,主要包括:circle, ellipse, line, polygon, polyline, rect 等。

除了面向对象包装和链式写法, 它还有更好玩的语法糖, 一起来看看

// 传统写法
[ 'M', 0, 0, 'L', 100, 100, 'z' ]

// SVG.js 写法
[
  ['M', 0, 0]
, ['L', 100, 100]
, ['z']
]

这样的写法阅读起来更方便

从Demo 中我们可以发现 SVG.js 定义的API 还是简单易用的,

SVG.js 的设计思路也挺简单, 从官网的 Overview 上也可以看出来,把整体按照功能划分成几大类, 这几个大类定义好后, 所有和其相关的具体的组件或者类就可以依照具体的功能场景和用途放到哪儿,就可以知道该怎么去整理实体关系结构, 然后就可以针对每个组件去开发实现其功能了。

  • 【Parents】svg 容器相关。 所有的容器都继承与 SVG.parent, 提供了很多基础的方法
  • 【Elements】svg 展现元素相关。 所有渲染的元素的基本原型就SVG.Element , 提供了很多基础的方法
  • 【Referencing】svg 获取元素相关。 是一堆的获取容器的方法
  • 【Manipulating】svg 操作相关。 包括 属性、位置、大小、样式、数据 等;
  • 【Boxes】Bounding Box, svg边界盒子相关。
  • 【Animating】svg 动画相关。 包括 几个动画组件和工具类 所有的动画都是从这里实现的和扩展出去的;
  • 【Events】svg 事件相关。 包括 几个动画组件和工具类 所有的动画都是从这里实现的和扩展出去的;
  • 【Classes】和svg 本身无关的一些工具类。
  • 【Extending】和svg 扩展相关的。 可以重写或者扩展组件,或者扩展插件;

来个整体概览:

设计模式在每个设计中多多少少 或者不经意间都会用到, SVG.js 中用到的设计计模式我们一起来看下:

工厂模式,create

无论创造哪种元素 都会经过SVG.js 模块中的 create 方法; 统一进行创建;

策略模式, fx

SVG.js 的动画有很多种类, 这些不同的种类可以单独封装成一个策略,供上层统一调用;

extends

在这里你可以使用 SVG.extend 对某一个元素类的方法进行包装增强, 也可以增加方法扩展等.

API设计

SVG.js 整体的API 极简且完备、语义清晰简单、符合直觉、易于记忆, 名字和原生的SVG 和规范保持一致。

抽象方法篇

整体的抽象思路(ER 实体关系图)

总体的ER模型(E-R图 实体关系图 Entity Relationship Diagram )设计的还算不错, 但缺乏了一点面向对象抽象的设计;
比如下列代码中 poly这里使用 SVG.js extends 的扩展方法, 让同类的一系列类拥有相同的方法; 这个地方可以抽象到 poly 类中, 让每个子类 包含或者集成父类, 这样在每个子类可以方便的看到自己拥有哪些方法,继承了哪些类。

// poly.js 
SVG.extend(SVG.Polyline, SVG.Polygon, {
  // Get array
  array: function() {
      ...
  }
  // Plot new path
, plot: function(p) {
   ...
  }
  ...
})

SVG.js 这样的写法, 阅读一个类都有哪些集成比较费劲, 如查看Element 都哪些方法, 在代码上怎么看(当然可以通过运行时的原型链获查看到), 我们得搜, 然后才可以知道 都进行了哪些的扩展, 才知道增加了哪些方法,这是设计上的值得挑剔的地方。

SVG.invent 实现解析

SVG.invent 方法是用来创建新元素的方法, SVG.js 中绝大部分类的创建都是通过这个基础方法实现的。

// 设计发明 新元素
SVG.invent = function(config) {
  // 创建元素初始值设定项
  var initializer = typeof config.create == 'function' ?
    config.create :
    function() {
      this.constructor.call(this, SVG.create(config.create))
    }

  // 设置继承原型链
  if (config.inherit)
    initializer.prototype = new config.inherit

  // 扩展方法(新元素的方法增加都放在这里)
  if (config.extend)
    SVG.extend(initializer, config.extend)

  // 将构造方法里的方法附加到父容器上 (注意 Container, 因为没有设置 parent, 所以大部分的construct方法都会去增强Container)
  if (config.construct)
    SVG.extend(config.parent || SVG.Container, config.construct)

  return initializer
}

SVG.invent 是新增加发明新元素的一个基类方法,新增加的元素会主要包几个配置项:

create

是创建元素初始值的设定项 。可以是一个字符串, 会以该字符串的名称来创建Dom元素, 在实例化的时候会作为参数传入改Dom元素;也可以是一个function, 如果新元素是用来渲染用的 那么在function内部也会去实现 this.constructor.call 的方法 去传入element, 且增加其他处理, 如果新元素不是渲染用的元素 如 SVG.Set、SVG.Array、SVG.Transformation 等, 则不必去调用 this.constructor.call

inherit

设置原型链方法, 继承方法使用; 可以设置新元素类继承哪个父类

extend

扩展方法, 新元素的新方法都在这里

construct

每个新元素的 construct 里会有一些方法, 这些方法会extend 到父容器中, 如果没有设置 parent 父容器, 则会extend 到 SVG.Container 中 , 这就是为什么 new Doc 后获取到的元素会有所有你想要的方法; 其中 SVG.Doc (继承关系如上ER图) 对应的原型链是 Doc → Container → Parent -> Element 。

一起来看下SVG.js 一个简单的 polygon 多边形 底层是怎么执行的;

var draw = SVG('drawing').size(300, 130)
var polygon = draw.polygon([
    [50, 0],
    [60, 40],
    [100, 50],
    [60, 60],
    [50, 100],
    [40, 60],
    [0, 50],
    [40, 40]
  ])

我们一起来分析下上边小Demo 2 个主要的方法: SVG(‘drawing’) 、draw.polygon(…) ;

Step1: SVG(‘drawing’) 流程

SVG(‘drawing’) 主要是在id 为drawing的 Dom元素上生成一个 SVG Dom 元素(Element), 并返回一个 SVG.Doc 对象, 这个返回的对象(Element)可以用来生成一切你想要的SVG 元素 如 Polygon 、 Rect 等。前边说过 SVG.Doc (继承关系如上ER图) 对应的原型链是 Doc → Container → Parent -> Element ,所以SVG.Doc 对象 拥有了所有SVG.js 所想想要提供的可以操作的方法。

Step2: draw.polygon(…) 流程

draw.polygon(…) 方法如上图流程, 2.1、2.2 会创建 Polygon 元素, 并放到父容器里边; 2.3 会去绘制新的路径点; 2.4、2.5 会通过重新 attr 方法 重新绘制 路径; 这样一个流程就可以画出来对应 Polygon 图形;

最上边的NEFE->海豚的 渐变(Morph)图片 是使用SVG.js 的插件 pathmorph 做的效果, 很简单

// create path
 var path = draw.path(nefe)
 // animate path
 setTimeout(function() {
   path.animate(1000).plot(dolphin);
 }, 2000); 

但这个插件不是太强大, 我们有时候 需要去设置 起始 svg 的某些点,对应结束 svg 的某些点; 有这些,整个渐变的过程才看起来更协调; 不然渐变渐变的有点生硬了。
可以暴漏出来 哪些元素进行相互渐变的接口, 如:

// api 建议 可以设计成
animate.to("#E", "#tail", time) //字母E变体成 海豚尾巴

   // demo
 path.animate({
    time:1000,
    [{from:'#E',to:'#tail'}]
 }).plot(dolphin);

如果提供这样的API , 就可以满足大部分的动画的效果,可以自由的设置复杂的动效;

SVG.js 把纯粹是 XML 的 SVG 文件的写法包装成 JS , 让由写 HTML 或者 XML 的体验转化为了脚本, 这样使整个开发体验更流畅; SVG.js 提供的API 这种面向对象的链式写法会让开发者不用考虑原生 SVG 如何渲染到 Dom 上、如何写一堆的代码设置去达到想要的效果, 使用 SVG.js会更简洁;SVG.js 的动画也让很多特效的开发变得更有可能 , 可以有助于加大开发人员对于动画的思想空间, 让 SVG 的世界变得更美好。

我们将持续对可视化工具和类库做相关的分析和实验,我们会应用于各种数据媒体大屏和数据产品之中。 来吧! 我们一起努力、一起攻克、一起共建、一起分享, 我们在这儿等着你 [email protected]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK