由于手头有一个AngularJS项目,而其中消息模块就是采用SignalR来推送。AngularJS就像天生需要SignalR一样,因为他们都需要很快。其实更在意的是WebSocket在AngularJS的应用,而当前只是一个小试验,但效果真的非常好。这一篇文章,我们围绕AngularJS中如何应用SignalR,所以你看到的更多会是JavaScript脚本。

项目是以ASP.NET API作为服务端、OAuth2票据令牌认证、AngularJS做为前端、中间全部采用JSON作为数据传输。

依赖jQuery.SignalR.js

这是必须的,而SignalR客户端脚本库又是以插件的形式,所以不管未来如何更新,都不担心脚本的变动而倒置AngularJS的改动,要知道AngularJS项目的JS脚本管理要控制得非常严谨。

Hub代理脚本

其实我在SignalR JavaScript 客户端已经非常详细的描述过关于如果创建代理脚本,但有个缺点就是他是全局式的,而对于AngularJS而言不建议使用这种全局式的,所以我们需要手动的创建代理脚本部分;并且把它当作一个 factory,这样对于多个Controller是可以共享数据的。

这里有个github的项目,完全可以满足我们的需求,非常简单,我把代理贴出来:

angular.module('SignalR', [])
.constant('$', $)
.factory('Hub', ['$', function ($) {
    //This will allow same connection to be used for all Hubs
    //It also keeps connection as singleton.
    var globalConnections = [];

    function initNewConnection(options) {
        var connection = null;
        if (options && options.rootPath) {
            connection = $.hubConnection(options.rootPath, { useDefaultPath: false });
        } else {
            connection = $.hubConnection();
        }

        connection.logging = (options && options.logging ? true : false);
        return connection;
    }

    function getConnection(options) {
        var useSharedConnection = !(options && options.useSharedConnection === false);
        if (useSharedConnection) {
            return typeof globalConnections[options.rootPath] === 'undefined' ?
            globalConnections[options.rootPath] = initNewConnection(options) :
            globalConnections[options.rootPath];
        }
        else {
            return initNewConnection(options);
        }
    }

    return function (hubName, options) {
        var Hub = this;

        Hub.connection = getConnection(options);
        Hub.proxy = Hub.connection.createHubProxy(hubName);

        Hub.on = function (event, fn) {
            Hub.proxy.on(event, fn);
        };
        Hub.invoke = function (method, args) {
            return Hub.proxy.invoke.apply(Hub.proxy, arguments)
        };
        Hub.disconnect = function () {
            Hub.connection.stop();
        };
        Hub.connect = function () {
            return Hub.connection.start(options.transport ? { transport: options.transport } : null);
        };

        if (options && options.listeners) {
            angular.forEach(options.listeners, function (fn, event) {
                Hub.on(event, fn);
            });
        }
        if (options && options.methods) {
            angular.forEach(options.methods, function (method) {
                Hub[method] = function () {
                    var args = $.makeArray(arguments);
                    args.unshift(method);
                    return Hub.invoke.apply(Hub, args);
                };
            });
        }
        if (options && options.queryParams) {
            Hub.connection.qs = options.queryParams;
        }
        if (options && options.errorHandler) {
            Hub.connection.error(options.errorHandler);
        }

        //Adding additional property of promise allows to access it in rest of the application.
        Hub.promise = Hub.connect();
        return Hub;
    };
}]);

使用时先把 SignalR 模块依赖到你的应用中,然后再需要的地方创建 var hub = new Hub('hubname',options);,就可以了。

其中 options 会有一些参数:

  • listeners 服务端调用客户端方法。
  • methods 客户端调用服务端方法。
  • rootPath 服务器地址。
  • queryParamsconnection.qs
  • errorHandlerconnection.error
  • logging 是否启用日志,这里的日志是指 console.log,如果是chrome,则F12就可以查看到了。
  • useSharedConnection 是否共享一个连接,当有两个不同的服务器地址时,还是会按两个连接的。
  • transport 指定使用哪一个方法进行数据传输,比如:'longPolling'['webSockets', 'longPolling']

当然以上是针对大部分项目来设计的,我的项目里面根据这个做法,做了更适合自身项目的修改,但基本上如同上面一样。

票据字符串

在创建连接时把会票据字符串加入 qs中,可能别的项目不是采用OAuth2票据令牌认证,但是作为ASP.NET API项目这种认证方式是无可挑剔。

服务端验证票据

这一点,请详见另一篇文章认证和授权,这里就不再重复。

一个完整的示例

1、首先创建一个factory的Hub代理类

app.constant('$', $).factory('hub', ['$', '$localStorage', function ($, $localStorage) {
    var authData = $localStorage.authorizationData;
    //This will allow same connection to be used for all Hubs
    //It also keeps connection as singleton.
    var globalConnection = null;

    function initNewConnection(options) {
        var connection = null;
        if (options && options.rootPath) {
            connection = $.hubConnection(options.rootPath, {
                useDefaultPath: false,
                qs: { Bearer: authData.token }
            });
        } else {
            connection = $.hubConnection();
        }

        connection.logging = (options && options.logging ? true : false);
        return connection;
    }

    function getConnection(options) {
        var useSharedConnection = !(options && options.useSharedConnection === false);
        if (useSharedConnection) {
            return globalConnection === null ? globalConnection = initNewConnection(options) : globalConnection;
        }
        else {
            return initNewConnection(options);
        }
    }

    return function (hubName, options) {
        var Hub = this;

        Hub.connection = getConnection(options);
        Hub.proxy = Hub.connection.createHubProxy(hubName);

        Hub.on = function (event, fn) {
            Hub.proxy.on(event, fn);
        };
        Hub.invoke = function (method, args) {
            return Hub.proxy.invoke.apply(Hub.proxy, arguments)
        };
        Hub.disconnect = function () {
            Hub.connection.stop();
        };
        Hub.connect = function () {
            Hub.connection.start();
        };

        if (options && options.listeners) {
            angular.forEach(options.listeners, function (fn, event) {
                Hub.on(event, fn);
            });
        }
        if (options && options.methods) {
            angular.forEach(options.methods, function (method) {
                Hub[method] = function () {
                    var args = $.makeArray(arguments);
                    args.unshift(method);
                    return Hub.invoke.apply(Hub, args);
                };
            });
        }
        if (options && options.errorHandler) {
            Hub.connection.error(options.errorHandler);
        }
        //Adding additional property of promise allows to access it in rest of the application.
        Hub.promise = Hub.connection.start();
        return Hub;
    };
}]);

由于票据字符串是放在LocalStorage当中,所以我注入了 $localStorage 来获取我的 token,并把他放在 qs 中。其他并没有什么不一样。

2、Controller层使用

首先我需要将我创建的factory注入到我需要的使用的Controller中,比如:

app.controller('DemoCtrl', [ 'Hub', function(Hub) {
    var msgHub = new Hub('msgHub', {
        logging: true, // 启用日志
        rootPath: 'app', // 服务器地址
        listeners: { // 服务端调用客户端方法
            get: function (r) {
                console.log(r);
            }
        },
        methods: ['echo'], // 客户端调用服务端方法。
        errorHandler: function (error) { // 异常处理
            console.error(error);
        }
    });
}]);

总结

这里唯一我们要注意的就是你所采用的登录认证的方式,因为这里我的确吃了一些亏,所以务必要清楚。

查看 SignalR系列文章