コンテンツへスキップ
ウェブソケットを備えた高性能Angularグリッド

ウェブソケットを備えた高性能Angularグリッド

Angular Gridにリアルタイムでデータをプッシュするという要件に遭遇したことがあるかもしれません。ブラウザにデータをプッシュするには、WebSocketと呼ばれるテクノロジーが必要です。これは、NodeJS または ASP.NET SignalR を使用して実装できます。この記事では、NodeJSでWebソケットを使用します。

10min read

Angular Gridにリアルタイムでデータをプッシュするという要件に遭遇したことがあるかもしれません。ブラウザにデータをプッシュするには、WebSocketと呼ばれるテクノロジーが必要です。これは、NodeJS または ASP.NET SignalR を使用して実装できます。この記事では、NodeJSでWebソケットを使用します。

この記事の前半では、Web Socketを使ってクライアントにデータをプッシュするAPIを作成し、後半ではそれを消費するためのAngularアプリケーションを作成します。AngularアプリケーションではIgnite UI for Angularグリッドを使用します。 しかし、単純なHTMLテーブルを使ってウェブソケットからリアルタイムでデータを消費することも可能です。この記事では、NodeJS Web SocketのHTMLテーブルやAngular Data Grid Ignite UIデータをリアルタイムで消費する方法を学びます。これら二つのアプローチでパフォーマンスの違いが見られるでしょう。

Ignite UI for Angularについてさらに詳しく知ることができます。

NodeJS API

まずはNodeJS APIの作成から始めましょう。空白のフォルダを作成し、package.jsonというファイルを追加します。package.jsonでは、 の依存関係を加えます。

  • Core-js
  • Express
  • io

おおよそ、package.jsonファイルは以下のようになるはずです。

{
  "name": "demo1",
  "version": "1.0.0",
  "description": "nodejs web socket demo",
  "main": "server.js",
  "dependencies": {
    "core-js": "^2.4.1",
    "express": "^4.16.2",
    "socket.io": "^2.0.4"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Dhananjay Kumar",
  "license": "ISC"
}

リレーショナルデータベースやNo SQLデータベースなど、あらゆる種類のデータベースからデータを取得することができます。しかし、この投稿の目的のために、シンプルにしてdata.jsファイルにハードコーディングしたデータを含みます。このファイルはJSON配列をエクスポートし、Web Socketとタイマーを使ってプッシュします。

フォルダにdata.jsというファイルを追加し、その中に以下のコードを追加します。

data.js

module.exports = {
    data: TradeBlotterCDS()
};
 
function TradeBlotterCDS() {
    return [
        {
            "TradeId": "1",
            "TradeDate": "11/02/2016",
            "BuySell": "Sell",
            "Notional": "50000000",
            "Coupon": "500",
            "Currency": "EUR",
            "ReferenceEntity": "Linde Aktiengesellschaft",
            "Ticker": "LINDE",
            "ShortName": "Linde AG",
            "Counterparty": "MUFJ",
            "MaturityDate": "20/03/2023",
            "EffectiveDate": "12/02/2016",
            "Tenor": "7",
            "RedEntityCode": "DI537C",
            "EntityCusip": "D50348",
            "EntityType": "Corp",
            "Jurisdiction": "Germany",
            "Sector": "Basic Materials",
            "Trader": "Yael Rich",
            "Status": "Pending"
        }
        // ... other rows of data 
    ]
}

1200行のデータもこちらでご覧いただけます。

data.jsファイルからTradeBlotterのデータを返します。プロジェクトフォルダには、package.jsonとdata.jsの2つのファイルがあるはずです。

この時点で、npm installというコマンドを実行して、package.jsonファイルに記載されているすべての依存関係をインストールしてください。コマンドを実行すると、プロジェクトフォルダ内にnode_modulesフォルダが入ります。 また、プロジェクト内にファイルserver.js追加してください。 これらのステップを踏むと、プロジェクト構造には以下のファイルやフォルダがあるはずです。

  • js
  • js
  • Node_modules folder

server.jsでは、まず必要なモジュールのインポートから始めます。

const express = require('express'),
    app = express(),
    server = require('http').createServer(app);
io = require('socket.io')(server);
let timerId = null,
    sockets = new Set();
var tradedata = require('./data');

必要なモジュールがインポートされたら、以下のようにルート利用Expressを追加します。

app.use(express.static(__dirname + '/dist'));

ソケット接続時には以下の作業を行います。

  1. Fetching data
  2. 開始タイマー(この機能については後述します)
  3. 切断イベントでソケットを削除した場合
io.on('connection', socket => {
 
    console.log(`Socket ${socket.id} added`);
    localdata = tradedata.data;
    sockets.add(socket);
    if (!timerId) {
        startTimer();
    }
    socket.on('clientdata', data => {
        console.log(data);
    });
    socket.on('disconnect', () => {
        console.log(`Deleting socket: ${socket.id}`);
        sockets.delete(socket);
        console.log(`Remaining sockets: ${sockets.size}`);
    });
 
});

次に、startTimer()関数を実装します。この関数ではJavaScriptのsetInterval()関数を使い、各10ミリ秒の時間枠ごとにデータを出力しています。

function startTimer() {
    timerId = setInterval(() => {
        if (!sockets.size) {
            clearInterval(timerId);
            timerId = null;
            console.log(`Timer stopped`);
        }
        updateData();
        for (const s of sockets) {
            s.emit('data', { data: localdata });
        }
 
    }, 10);
}

関数updateData()を呼び出しており、これはデータを更新します。この関数では、ローカルデータをループさせ、範囲間のランダム数でCoponとNotionalの2つのプロパティを更新します。

function updateData() {
    localdata.forEach(
        (a) => {
            a.Coupon = getRandomInt(10, 500);
            a.Notional = getRandomInt(1000000, 7000000);
        });
}

We have implemented getRandomInit function as shown below:

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
}

すべてを組み合わせることで、sever.js次のコードを持つはずです

Server.js

const express = require('express'),
    app = express(),
    server = require('http').createServer(app);
io = require('socket.io')(server);
let timerId = null,
    sockets = new Set();
var tradedata = require('./data');
 
var localdata;
 
app.use(express.static(__dirname + '/dist'));
 
io.on('connection', socket => {
 
    console.log(`Socket ${socket.id} added`);
    localdata = tradedata.data;
    sockets.add(socket);
    if (!timerId) {
        startTimer();
    }
    socket.on('clientdata', data => {
        console.log(data);
    });
    socket.on('disconnect', () => {
        console.log(`Deleting socket: ${socket.id}`);
        sockets.delete(socket);
        console.log(`Remaining sockets: ${sockets.size}`);
    });
 
});
 
function startTimer() {
    timerId = setInterval(() => {
        if (!sockets.size) {
            clearInterval(timerId);
            timerId = null;
            console.log(`Timer stopped`);
        }
        updateData();
        for (const s of sockets) {
            s.emit('data', { data: localdata });
        }
 
    }, 10);
}
 
function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
}
 
function updateData() {
    localdata.forEach(
        (a) => {
            a.Coupon = getRandomInt(10, 500);
            a.Notional = getRandomInt(1000000, 7000000);
        });
}
 
server.listen(8080);
console.log('Visit http://localhost:8080 in your browser');

NodeJSでWeb Socketを作成し、10ミリ秒ごとにデータチャンクを返しています。

Creating Angular Application

このステップでは、アプリケーションを作成しAngular。CLIを使ってアプリケーションを作成し、その後グリッドを追加Angular Ignite UI for Angular。以下の記事に従ってAngularアプリケーションを作成し、アプリケーション内にIgnite UI for Angularグリッドを追加してください。

上記の記事に従っている場合は、APIを消費するためのサービスを作成するステップ3 Angular変更が必要です。

まずはAngularプロジェクトにsocket.io-clientをインストールすることから始めましょう。そのためにnpm installを実行してください。

npm i socket.io-client

NodeJS Web Socketとの接続を作成するためのAngularサービスを書きます。app.service.tsでは、まず輸入から始めましょう。

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { map, catchError } from 'rxjs/operators';
import * as socketIo from 'socket.io-client';
import { Socket } from './interfaces';

必要なモジュールはインポート済みです。後ほど、ソケットタイプがファイル内でどのように定義されているかinterface.ts見ていきます。次に、Web Socketに接続して応答から次のデータを取得しましょう。ウェブソケットから次のデータチャンクを返す前に、それをObservableに変換します。

getQuotes(): Observable < any > {
    this.socket = socketIo('http://localhost:8080');
    this.socket.on('data', (res) => {
        this.observer.next(res.data);
    });
    return this.createObservable();
}
 
createObservable(): Observable < any > {
    return new Observable<any>(observer => {
        this.observer = observer;
    });
}

上記の2つの関数は、ウェブソケットに接続し、データチャンクを取得し、それを可観測に変換します。すべてを合わせると、以下のapp.service.tsのようになります。

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { map, catchError } from 'rxjs/operators';
import * as socketIo from 'socket.io-client';
import { Socket } from './interfaces';
 
@Injectable()
export class AppService {
 
    socket: Socket;
    observer: Observer<any>;
 
    getQuotes(): Observable<any> {
        this.socket = socketIo('http://localhost:8080');
        this.socket.on('data', (res) => {
            this.observer.next(res.data);
        });
        return this.createObservable();
    }
 
    createObservable(): Observable<any> {
        return new Observable<any>(observer => {
            this.observer = observer;
        });
    }
 
    private handleError(error) {
        console.error('server error:', error);
        if (error.error instanceof Error) {
            let errMessage = error.error.message;
            return Observable.throw(errMessage);
        }
        return Observable.throw(error || 'Socket.io server error');
    }
 
}

サービスではSocketという型を使用しています。このタイプはファイルinterfaces.tsで作成しました。

export interface Socket {
    on(event: string, callback: (data: any) => void);
    emit(event: string, data: any);
}

Angularサービスが準備でき、NodeJS Web Socketに接続し、APIからデータを可観値として取得できます。

これは通常のAngularサービスであり、通常の方法でコンポーネントとして利用可能です。まずモジュール内でimportinを使い、下記のようにコンポーネントコンストラクタに注入します。

constructor(private dataService: AppService) { }

OnInitのライフサイクルでサービスメソッドを呼び出してデータを取得できます。

ngOnInit() {
    this.sub = this.dataService.getQuotes()
        .subscribe(quote => {
            this.stockQuote = quote;
            console.log(this.stockQuote);
        });
}

すべてを組み合わせると、コンポーネントクラスは以下のようになります。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { AppService } from './app.service';
import { Subscription } from 'rxjs/Subscription';
 
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
 
    stockQuote: number;
    sub: Subscription;
    columns: number;
    rows: number;
    selectedTicker: string;
 
    constructor(private dataService: AppService) { }
 
    ngOnInit() {
        this.sub = this.dataService.getQuotes()
            .subscribe(quote => {
                this.stockQuote = quote;
                console.log(this.stockQuote);
            });
    }
    ngOnDestroy() {
        this.sub.unsubscribe();
    }
}

注意しておきたい重要な点の一つは、OnDestroyのライフサイクルフックでコンポーネントの可観測リターンを解消していることです。 テンプレート上で、以下のように表にデータをレンダリングするだけです:

<table>
    <tr *ngFor="let f of stockQuote">
        <td>{{f.TradeId}}</td>
        <td>{{f.TradeDate}}</td>
        <td>{{f.BuySell}}</td>
        <td>{{f.Notional}}</td>
        <td>{{f.Coupon}}</td>
        <td>{{f.Currency}}</td>
        <td>{{f.ReferenceEntity}}</td>
        <td>{{f.Ticker}}</td>
        <td>{{f.ShortName}}</td>
    </tr>
</table>

通常のHTMLテーブルでリアルタイムでデータをレンダリングしているため、ちらつきやパフォーマンスの問題が発生することがあります。HTMLテーブルをグリッドに置き換えIgnite UI for Angular。

Ignite UI for Angularグリッドについて詳しくはこちらをご覧ください: https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid.html

以下のAngularのようにIgnite UIグリッドをアプリケーションに追加できます。igxGridのデータソースはデータプロパティバインディングで設定し、手動でグリッドに列を追加しました。

<igx-grid [width]="'1172px'" #grid1 id="grid1" [rowHeight]="30" [data]="stockQuote"
          [height]="'600px'" [autoGenerate]="false">
    <igx-column [pinned]="true" [sortable]="true" width="50px" field="TradeId" header="Trade Id" [dataType]="'number'"> </igx-column>
    <igx-column [sortable]="true" width="120px" field="TradeDate" header="Trade Date" dataType="string"></igx-column>
    <igx-column width="70px" field="BuySell" header="Buy Sell" dataType="string"></igx-column>
    <igx-column [sortable]="true" [dataType]="'number'" width="110px" field="Notional" header="Notional">
    </igx-column>
    <igx-column width="120px" [sortable]="true" field="Coupon" header="Coupon" dataType="number"></igx-column>
    <igx-column [sortable]="true" width="100px" field="Price" header="Price" dataType="number">
    </igx-column>
    <igx-column width="100px" field="Currency" header="Currency" dataType="string"></igx-column>
    <igx-column width="350px" field="ReferenceEntity" header="Reference Entity" dataType="string"></igx-column>
    <igx-column [sortable]="true" [pinned]="true" width="130px" field="Ticker" header="Ticker" dataType="string"></igx-column>
    <igx-column width="350px" field="ShortName" header="Short Name" dataType="string"></igx-column>
</igx-grid>

私たちが作成したグリッドで注目すべきポイントは以下の通りです:

  1. デフォルトではIgnite UI for Angularグリッド上で仮想化が有効になっています。
  2. ソート可能プロパティを設定することで、特定の列でのソートを有効にできます。
  3. ピン留めプロパティを設定することで、グリッドの左側に列をピン留めできます。
  4. データプロパティを設定することで、グリッドのデータソースを設定できます。
  5. <igx-column/>を使うことで手動でカラムを追加できます。
  6. <igx-column/>のフィールドとヘッダーは、フィールドプロパティと列のヘッダーを設定するために使われます。

アプリケーションを実行すると、グリッドはリアルタイムでデータ更新され、ちらつきもありません。グリッドは10ミリ秒ごとに更新されています。グリッドが稼働し、データはリアルタイムで更新されているはずです。以下のように:

グリッドランニングでリアルタイムでデータが更新される様子が示されています

このようにして、NodeJS Web Socket APIを使ってアプリケーション内でリアルタイムでデータをプッシュAngularできます。この記事が役に立てば幸いです。もしこの投稿が気に入ったら、ぜひシェアしてください。また、Infragistics Ignite UI for Angular Componentsをまだチェックしていない方は、ぜひチェックしてください!50+のMaterialベースのAngularコンポーネントがあり、ウェブアプリのコーディングをより速くサポートします。

デモを予約