暂时试着用Angular2绘制SVG

首先

我一直以来都在使用SVG进行数据可视化工作。
在2013年左右,我开始使用D3.js构建SVG元素,但考虑到性能、编写简易度和可维护性等方面的平衡,我最近开始将库迁移到AngularJS和React上。
AngularJS是我喜欢的库,但在用于数据可视化方面,我对性能有些担忧。
因为听说Angular2正在为发布做准备,我决定尝试使用性能改善的Angular2来绘制SVG。

我們將在 Angular1 和 Angular2 中實現相同的功能並進行比較。
兩者都是使用 TypeScript 進行實現。
我們將使用 Angular1 的版本 1.4.8 和 Angular2 的版本 2.0.0-alpha.53。

使用Angular1来实现

首先来说常规的一些事情。

const app = angular.module('app', []);

我們將進入本體的實現,為了實現類似於基於組件的Angular2的實現,我們也會在Angular1中使用指令。本次我們將構建由兩個獨立組件Component1和Component2組成的結構。

首先是Component1的实现。

const component1Template = `
<div>
  <h2>Component 1</h2>
  <p>{{message}}</p>
  <input ng-model="c1.message"/>
</div>
`;

class Component1 {
  message = 'hello';
}

app.directive('component1', () => ({
  template: component1Template,
  controllerAs: 'c1',
  controller: Component1
}));

我正在使用ng-model进行双向绑定的信息。

下一步是Component2的实现。

class Point {
  x: number;
  y: number;
}

const component2Template = `
<div>
  <h2>Component 2</h2>
  <div>
    <input type="number" min="1" max="10" step="1" ng-model="c2.r"/>
  </div>
  <div>
    <svg ng-attr-width="{{c2.size}}" ng-attr-height="{{c2.size}}">
      <circle ng-attr-r="{{c2.r}}" ng-attr-cx="{{c2.cx(point)}}" ng-attr-cy="{{c2.cy(point)}}" ng-repeat="point in c2.points"/>
    </svg>
  </div>
</div>
`;

class Component2 {
  r = 5;
  size = 500;
  points: Point[] = [];

  constructor() {
    var n = 10;
    for (var i = 0; i < n; ++i) {
      this.points.push({
        x: Math.random() * this.size,
        y: Math.random() * this.size
      });
    }
  }

  cx(point: Point) : number {
    return point.x; // 何かすごい計算をする
  }

  cy(point: Point) : number {
    return point.y; // 何かすごい計算をする
  }
}

app.directive('component2', () => ({
  template: component2Template,
  controllerAs: 'c2',
  controller: Component2
}));

正在将10个圆绘制到SVG上。
在构造函数中随机生成圆的坐标,并将其绑定到circle元素的cx和cy上。
cx和cy通过Controller的方法进行坐标转换处理。
为了简化说明,这个例子中直接返回了初始坐标。
请注意,方法中可能会进行复杂的计算。
另外,用于表示圆的半径r可以通过input元素进行更改。

下一个是一个应用程序,它是Component1和Component2的父组件。

const appTemplate = `
<div>
  <component1></component1>
  <component2></component2>
</div>
`;

class App {
}

app.directive('myApp', () => ({
  template: appTemplate,
  controllerAs: 'app',
  controller: App
}));

使用Angular2进行实现。

我们将在Angular 2中实现与Angular 1版相同的功能。首先是模块的导入。

import {Component, Input, ChangeDetectionStrategy} from 'angular2/core'
import {CORE_DIRECTIVES} from 'angular2/common'
import {bootstrap} from 'angular2/platform/browser'

从alpha.53开始,angular2不再支持从angular2/angular2进行导入,而是改为从angular2/core、angular2/common等单独进行导入。可以查看https://angular.io/docs/ts/latest/api/等网址来了解每个模块的具体位置。

Component1的实现如下所示。

const component1Template = `
<div>
  <h2>Component 1</h2>
  <p>{{message}}</p>
  <input [(ngModel)]="message"/>
</div>
`;

@Component({
  selector: 'component1',
  template: component1Template
})
class Component1 {
  message = 'hello';
}

我們使用 [(ngModel)]=”message” 來實現雙向數據綁定。

Component2的实现如下所示。

class Point {
  x: number;
  y: number;
}

const component2Template = `
<div>
  <h2>Component 2</h2>
  <div>
    <input type="number" min="1" max="10" step="1" [(ngModel)]="r"/>
  </div>
  <div>
    <svg [attr.width]="size" [attr.height]="size">
      <circle [attr.r]="r" [attr.cx]="cx(point)" [attr.cy]="cy(point)" *ngFor="#point of points"/>
    </svg>
  </div>
</div>
`;

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'component2',
  template: component2Template
})
class Component2 {
  r = 5;
  size = 500;
  points: Point[] = [];

  constructor() {
    var n = 10;
    for (var i = 0; i < n; ++i) {
      this.points.push({
        x: Math.random() * this.size,
        y: Math.random() * this.size
      });
    }
  }

  cx(point: Point) : number {
    return point.x; // 何かすごい計算をする
  }

  cy(point: Point) : number {
    return point.y; // 何かすごい計算をする
  }
}

在上面的代码中,通过[attr.r]=”r”将circle元素的属性进行数据绑定。在Angular1中,ng-repeat=”point in c2.points”等同于*ngFor=”#point of points”。

另外,在@Component的参数中,我写了changeDetection: ChangeDetectionStrategy.OnPush。这会改变Angular2的变动检测策略的默认设置。关于变动检测方面,@laco0416先生已经写得非常详细了。 http://qiita.com/laco0416/items/78edd53f5da8ead02e75

最后,要进行App的实现。

const appTemplate = `
<div>
  <component1></component1>
  <component2></component2>
</div>
`;

@Component({
  selector: 'my-app',
  template: appTemplate,
  directives: [Component1, Component2, CORE_DIRECTIVES]
})
class App {
}

bootstrap(App);

Angular1和Angular2的差异。

让我们简单比较一下Angular 1版和Angular 2版的实现。
尽管有意识地以Angular 2为目标实现了Angular 1版,但基本上可以看出它们能够实现相似的功能。

在Angular1中,用directive来实现Component,但是directive被认为具有高度的通用性,但API复杂。
我认为在Angular2中通过使用@Component变得更加简单。
此外,模板语法中,属性数据绑定的ng-attr-r=”{{c1.r}}” 简化为 [attr.r]=”r”。

关于关键性能问题,如果在Component2的cx方法中插入console.log等功能,你会发现无论何时Component1的message更新,Angular1都会每次调用cx方法,即使Component2与Component1是独立的。

如果在cx中进行重型处理,那么它会经常被调用并导致性能下降。因此,在Angular1中经常出现像构造函数中预先计算重型处理代码的情况。在这种情况下,虽然我们在Component2中生成了points,但如果要从父组件接收它,则还需要额外的工作来使用Scope.$watch监视其变化。在这种情况下,代码量会增加,并且会导致数据的重复管理,所以不是很智能。

在Angular2中,默认情况下,似乎会执行相同的操作,但是可以在Component2的装饰器中使用changeDetection选项来跳过元素的更新,如果Component2的状态没有改变。但是,如果像这次的r变化一样,对Component2的状态进行了更改,那么更新将会被正确地执行。

最后

在本文中,我尝试使用Angular2来绘制SVG,并确认了在Angular1中不满意的问题是否得到解决。
在目前的官方教程中,alpha.44版本无法很好地构建SVG,但在alpha.45、46、49版本中,SVG的支持得到了改进,现在的alpha.53版本已经可以成功地绘制SVG了。
可能还存在一些细小的问题,但随着发布的逐步调整,将会进展顺利。

此外,在性能方面,数据可视化方面,Angular 1 应用程序经常使用的调优技巧,例如减少 $watch 的目标,在 Angular 2 中不太适用,因此非常感激有了类似 Change Detection 的细致调优空间。

bannerAds