SignalRのクライアントサイドをTypeScript で強い型付けにする。

ASP.NET MVC5 + SignalR 2.0 + TypeScript 1.4 でリアルタイムWeb入門

最近、SignalRを使ったWebアプリケーションを作りたいと思い、お勉強を始めました。
クライアントサイドには(こちらも入門したばかりの)TypeScriptを使おうかと考えています。

という事で、今回は ASP.NET MVC + SignalR の組み合わせにTypeScriptを導入する方法をまとめたいと思います。

ASP.NET/SignalR チュートリアル

サンプルコードとして、ASP.NET公式サイトのチュートリアル Tutorial: Getting Started with SignalR 2 and MVC 5 | The ASP.NET Site を使用します。
このサンプルコードのクライアントサイドをTypeScriptに置き換えて行こうと思います。

なお、チュートリアルではVisual Studio2012を使用していますが、今回はVisual Studio2013(Ultimate)で実装と動作確認を行っています。

TypeScriptのインストール

Visual Studio 2013 に Update4 を当てていればv1.3 がインストールされていると思いますが、最新版にアップデートしておきます。

[ツール]-[拡張機能と更新プログラム...]で"TypeScript"を検索して最新版(現時点ではv1.4)をインストールします。 f:id:pierre3:20150407222334p:plain

SignalRの型定義 signalr.d.ts のインストール

NuGetでインストールします。

PM> Install-Package signalr.TypeScript.DefinitelyTyped

(この時、SignalRが依存しているJQueryの型定義(jquery.d.ts)も同時にインストールされます。)

ここで、TypeScriptのバージョンが古いと、以下の記事にあるようなエラーが発生するようです。orzmakoto.hatenablog.com

Hub による接続

チュートリアルの詳細はリンク先を見て頂く事にして、ここではHubを使用した通信の実装部分をざっくりと確認しておきます。

サーバー側(C#)

サーバー側では、Hubクラスを継承したクラスを定義します。

  • Client側から呼んでもらうメソッドを定義
    • ChatHub.Send()
  • サーバーから呼ぶClient側の処理は、実行する箇所を記述するだけ。記述したメソッドは実行時に解決される。
    • Clients.All.AddNewMessageToPage()
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        //クライアントからチャットメッセージを受け取る
        public void Send(string name , string message)
        {
            //受け取ったメッセージを接続している全てのクライアントへ送信
            Clients.All.AddNewMessageToPage(name, message);
        }
    }
}

クライアント側(JavaScript)

クライアント側では、自動生成されるHubProxyを使ってサーバーとの通信を行います。

window.onload = function (ev: Event) {
    $('#displayname').val(prompt('Enter your name:', ''));
    $('#message').focus();

    var chatHub = $.connection.chatHub;
    //サーバーから呼ばれるメソッドをここで定義
    chatHub.client.addNewMessageToPage = function (name: string, message: string){
        appendMessage("#discussion", name, message);
    };
    //サーバーとの接続が完了したタイミングでClickイベントにハンドラを設定
    $.connection.hub.start().done(function (){
        $("#sendmessage").click(function(ev) {
            //サーバーにメッセージを送信
            chatHub.server.send($("#displayname").val(), $("#message").val());
            $("#message").val("");
        });
    });
};
// メッセージをHtmlに埋め込む
function appendMessage(selector, name, message) {
    $(selector).append(
        '<li>'
        + '<strong>'
        + $('<div />').text(name).html()
        + '</strong>: '
        + $('<div />').text(message).html()
        + '</li>');
}

クライアントサイドをTypeScriptに置き換える。

プロジェクトに空のTypeScriptファイルを追加(App.ts とします)したら、ファイルの先頭にSignalRとJQueryの型定義への参照を追加します。

コードは上のJavaScriptのコードをそのままコピペします。

//(App.ts)
/// <reference path="../typings/signalr/signalr.d.ts" />
/// <reference path="../typings/jquery/jquery.d.ts" />

window.onload = function (ev: Event) {
    $('#displayname').val(prompt('Enter your name:', ''));
    $('#message').focus();

    //...(略)...

ひとまずは、これで動くはず...
と思ったのですが、「型SignalRにchatHubなんてプロパティはありません。」と怒られてしまいました。

f:id:pierre3:20150410211403p:plain

HubProxy (chatHub) 自体は、実行時に$connection のプロパティとして実装されるのですが、TypeScriptでの型の定義が無いためにコンパイルエラーとなります。

HubProxyの型定義を記述する

では、chatHub の型を定義してみましょう。 SignalR という型ですが、$.connection の型としてsignalr.d.ts で定義されています。

//(chatHubProxy.d.ts)
/// <reference path="../typings/signalr/signalr.d.ts" />
/// <reference path="../typings/jquery/jquery.d.ts" />

//$.connection の型。 ここにHubProxyが追加される
interface SignalR {
    chatHub : ChatHub;
}
//HubProxyの定義。プロパティにserver, client を持つ。
interface ChatHub {
    server : ChatHubServer;
    client : ChatHubClient;
}
//サーバー側のメソッドを定義
interface ChatHubServer {
    send(name : string, message : string) : JQueryPromise<void>;
}
//クライアント側のメソッドを定義
interface ChatHubClient
{
    addNewMessageToPage : (name : string, message : string) => void;
}

このファイル(chatHubProxy.d.ts)の参照を App.tsに記述します。

/// <reference path="hubproxy/chatHubProxy.d.ts" />

これで、無事コンパイルできるようになりました。
インテリセンスもちゃんと効きます。

f:id:pierre3:20150417214344p:plain

補足

サーバー側のメソッドは非同期実行

サーバー側のメソッド(sendメソッド)は、戻り値にPromise(JQueryPromise<T>)を返す非同期メソッドとなっています。 チュートリアルのサンプルでは同期的に実行されているように見えますが、実際は、非同期実行の結果を受け取らずに投げっぱなしにしているだけです。

試しに、以下の様に記述してみると、ボタンクリック直後に"start"が表示され、サーバーからの結果を受信後に"complete"が表示されるはずです。

$("#sendmessage").click(ev => {
    chatHub.server
        .send($("#displayname").val(), $("#message").val())
        .then(() => $("#message").val("complete"));
    $("#message").val("start");
});
型定義を書かない方法

以下の様に、HubProxyの基底クラス が持つ汎用的なメソッドを使う方法もあります。
但し、Hub名やメソッド名を文字列で指定する必要があります。

//HubProxyの取得
var hub = $.connection.hub.createHubProxy("chatHub");
//サーバーに呼んでもらうメソッドの定義
hub.on("addNewMessageToPage",(name: string, message: string) : void => {
    appendMessage("#discussion", name, message);
});

$.connection.hub.start().done(() => {
    $("#sendmessage").click(ev => {
        //サーバー側にメッセージを送信
        hub.invoke("send", $("#displayname").val(), $("#message").val());
        $("#message").val("");
   });
});

まとめ

これで、クライアント側のコードをTypeScriptに置き換えることが出来ました。

なのですが、型定義をHub毎に書かなくてはならないのは面倒です。
しかも、型定義自体の誤りはチェックされませんので、動的に記述するのと大して変わりません。 サーバー側とクライアント側で同じ定義を2度記述しなくてはならないのもイケてないです。

サーバー側のコード(C#)から、型定義を自動生成出来たらいいのに! という事で、次回へ続く