AngularJS 相关问题学习 借鉴

[SignalR2] AngularJS 应用

由于手头有一个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 服务器地址。
  • queryParams 同 connection.qs
  • errorHandler 同 connection.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系列文章

AngularJS优化:bindonce 不是万能药,用时需谨慎

看过一次对 Misko Hevery 的专访,有说到限制2000个对象绑定,可以让页面刷新时间减少到5ms以下。而所谓的刷新时间是指 dirty-checking,因为一次 dirty-checking 时间将会影响 DOM 更新时间。dirty-checking 核心就是对所有 $watch 进行检查。而 bindonce 是为了减少这种不必要的 $watch

什么是 Watch

$watch就是监视数据的变化。这里的数据可能是指令或$scope

比如,以下创建两个$watch,一个文本框、一个名字输出:

<p>用户名:<input type="text" ng-model="name" /></p>
<p>我的用户名:{{name}}</p>

以上,当我在文本框输入字母 a 时,会立即启动一个检查(前期也会有很多工作,比如绑定keydown事件,用于触发事件,这里不再说太多,建议看源代码),会检查到 name 发生变化;这个过程叫dirty-checking,而对所有的 $watch 都检查一遍后,才会将主动权交给浏览器做DOM上的变化(即输出部分)。

试想当我们通过一个 ng-repeat 循环N条记录,而每一行有10个绑定对象,那意味者会有 N*10 个 $watch 等着我们。往往在一个复杂的页面,这种行绑定对象会多得多。

取消 $watch

对于像 ng-repeat,都是由官方提供的,限制是无法限制,所以我们只能在绑定的时候使用传统的方式。而 bindonce 就是这样子的,比如:bo-text 相当于binder.element.text(value);

注意:bo-text 是以一个指令出现,那么我们需要注意一个问题了,指令是在DOM被加载完全后开始执行,这里的执行顺序非常重要。当你的数据源已经加载完成,此时 bo-text 指令出现,他才能获取到数据,否则永远都是空的。因此这里得出第一个优化规则:

bindonce只能在数据存在时有效

所以对于像表单数据,正常我们都是从 $http.get 来获取,而DOM早就加载完成,所以这个时候再去使用 bindonce 已经没有意义了。

而像SPA很多时候我们会有一些共享的数据,比如版本号、APP名称、用户信息等等,这些信息如果你能够保证在 DOM 加载前就已经存在,那么就可以大胆去使用 bindonce

bindonce 更适合数据列表

$watch 最大的泛滥是数据列表,如我前所说它是N*M在增长。而我们又是通过 ng-repeat 来循环数据,ng-repeat 本身就是一个指令,所以本身会有一个 $watch 来监视数据变化,换句话说我们可以保证 规则一 原则,所以我们大可大胆在 ng-repeat 下去掉我们不需要监视的数据。

所以这里得出第二个优化规则:

ng-repeat下使用bindonce是安全的

在优化之前,我有几点建议:

  1. 绝对不要过早优化,因为NG已经够快的了。
  2. SPA应用加载顺序非常重要。
  3. bindonce适用于数据列表。
  4. 性能的限制因素是人,NG真的非常快。

AngularJS中的value和constant

Values

当我们需要一组数据,比如:当前用户登录信息或APP的一些配置,而这组数据又希望在 controller/service 使用它,那么它就是做这个事的。

value 支持string、number、date-time、array、object、function,比如:

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

app.value('user', {
    name: 'asdf',
    role: 'admin'
});

而当我们需要使用时,就像这样:

app.controller('MyCtrl', ['$scope', 'user', function($scope, user) {
    $scope.name = user.name;
}]);

Angular 会自动将 user 注入进来,同时我们可以随时修改数据,比如:

user.role = 'normal';

这样子其他再注入 user 时role的值变成normal。

Constants

value 与 constant 之间的差异只有两点:

  1. 可注入类型,对于前者只允许在 service 或 controller,而后者还包括模块配置函数。
  2. constant 是常量,无法被修改。

AngularJS在controller延迟加载自定义指令

有个几百KB的地址库,原则上我可以使用 http 按需加载数据来减少文件大小。可我就是太变态,因为地址的修改频率非常非常低,所以要一开始就把玩意儿加载放在那不使用,就不是一个滋味。这便是变态之原因一。

我的需求里面有一个订单详情页,但详情页可以直接修改地址,问题来了,详情页打开频率非常高,而修改频率极低【注意是极低】,所以我希望当我对其进行修改操作时,再延迟加载指令。其变态之原因二。

依赖

ocLazyLoad

简单点说就是将所需要的文件加载完毕后,并返回一个 promise,这样我们可以跟路由的resolve一起,达到一个完成的延迟加载 css/js etc.

同时,ocLazyLoad 提供一个指令,可以直接在 view 中延迟加载某个指令,也会重新编译该指令内的HTML元素 $compile(content)($scope)

<div oc-lazy-load="{name: 'TestModule', files: ['js/testModule.js', 'partials/lazyLoadTemplate.html']}">
    // Use a directive from TestModule
    <test-directive></test-directive>
</div>

看上去问题已经可以解决了,可总是那么不如意,ngIf 会打乱由于 DOM 被预编译且被缓存,那就蛋疼了。

即使重新编译也没用。

好,既然是变态做法,那自然不能够依赖 ocLazyLoad 提供的最方便免费午餐,需要一点点改进。

一个解决办法

大概分几步:

  1. 包裹一个 div,方便于只对该元素内进行重新编译,否则会出现循环编译。
  2. 调用 $ocLazyLoad.load 延迟加载所需要的文件。
  3. 已经加载过的,还需要重新编译,因为对于 ocLazyLoad 而言会保留自己的 module 信息。

那么,以下就是大概的一个方案了:

<div ng-init="cityLoad('#citySelect')">
    <city-select id="citySelect" ng-model="city.dis"></city-select>
</div>
var citySelectParam = {
    name: 'angular.city.select',
    files: ['angular-city-select/angular-city-select.min.js', 'angular-city-select/angular-city-select.min.css']
};
$scope.cityLoad = function (eleId) {
    try {
        angular.module(citySelectParam .name);
        $compile(angular.element(eleId))($scope);
    } catch (err) {
        $ocLazyLoad.load(citySelectParam ).then(function () {
        $compile(angular.element(eleId))($scope);
        });
    }
}

好了,大概就是这样子吧。希望您也有这么个变态做法。

弄懂AngularJS select的数据绑定ngOptions

虽然项目已经进行一大半了,但是 ngOptions 还真的一直保留着困惑,其实第一眼看我怎么都感觉像是 Linq 一般,所以弄懂他才能够真正活用。而官方文档也只是把各种方式罗列一下,也没有示例,所以还是认认真真的学习一遍。

那么,我需要开始假设有以下数据,由于数组和对象数据源有些不同,以下是我假定的两种数据源。完整的示例

    $scope.users = [
        { name: 'cipchk', gender: '男', age: 25 },
        { name: '卡色', gender: '男', age: 18 },
        { name: '外卖从来没遇到美女', gender: '女', age: 18 },
        { name: '今晚不知道要吃什么', gender: '女', age: 18 },
        { name: '其实.NET很好', gender: '女', age: 18 },
        { name: 'fhw r', gender: '女', age: 18 }
    ];

    // 相当于c# Dictionary<int, string>
    $scope.product = {
        1: 'KFC',
        2: '麦当苏',
        3: '鱼丸',
        4: '星巴克',
        5: '错了,星巴克放在前面'
    };

一、label for value in arraylabel for (key , value) in object

<select ng-model="first.user" ng-options="item.name for item in users" class="form-control"></select>
<select ng-model="first.product" ng-options="value for (key, value) in product" class="form-control"></select>
  1. value等同于源码中的item(key, value),相当于c#中 foreach(var item in object) 一样,即每次获取一个元素放入item中。
  2. label等同于源码中的item.namevalue,相当于 <option>item.name</option> 一样。

执行结果:
1

二、 as 关键词

<select ng-model="first.user" ng-options="item.gender as item.name for item in users" class="form-control"></select>
<select ng-model="first.product" ng-options="key as value for (key, value) in product" class="form-control"></select>

直白点讲就是将 as 前面的变量赋于 ng-model

执行结果:
2

三、 group by 关键词

<select ng-model="third.user" ng-options="item.name group by item.gender for item in users" class="form-control"></select>
<select ng-model="third.product" ng-options="key as value group by key for (key, value) in product" class="form-control"></select>
  1. group by xx位置必须是 for 关键词之后。
  2. 分组实际是生成一个 <optgroup label="男"></optgroup> 标签。

四、完整表达式

所以的关键词已经全部说到了,这里我来弄一个完整的表达式,其意:对数据源users,以年龄分组,下拉列表文本是姓名、值为年龄。

<select ng-model="full.user" ng-options="item.age as item.name group by item.gender for item in users" class="form-control"></select>

其实把表达式拆分开来说,回过头看文章,好像并没有什么特别,也相当容易理解,只不过刚开始在结合文档时很晕。

附录:

  1. 在线示例代码
  2. 官网文档

以上。

AngularJS如何下载Excel文件

在AngularJS中要下载一个Excel文件到底有多难呢?

最简单的方法

这当然是放一个 a 链接元素在页面搞定。

<a href="/path/file.xlsx" target="_blank">下载文件</a>

可如果我们涉及到一些身份验证,而且又是通过 Cookie,浏览器会很怪的一并发送到服务端,是不是一切都很好呢?

如果……

像上面说的如果我需要自定义请求头,例如:OWIN等身份验证的情况下,怎么办呢?

问题

也许我们可以非常简单的通过 ajax 发送一个 get 请求,并填写相应 headers,比如:

$http.get({
    url: '/path/file.xlsx',
    method: 'get',
    headers: {
        Authorization: 'Bearer pTVhzRZgA6yW-fp8c5vcxzBxr6vuIBYQrlo0ASIVxgkfN6'
    }
}).success(function (data) {
    // 怎么保存?
});

有一个办法就是我们可以通过 HTML5 的 a 元素,指定一段 Base64 数据编码,我们可以生成一个 a 链接,然后点击下载。

这种方式在我的实验中,发现对于 Excel 支持不好,对于大一点的文件,下载回来都是无法打开。

Blob

Blob 存储的是二进制,实则就是一个 JavaScript 下的一个 File 对象,目前被大部分流行浏览器所支持。

我这里还找到一个 FileServer.js 是对 Blob 保存的具体实现。

以下是我结合 FileServer.js 写的一个AngularJS指令,好了,废话不多说:

App.directive('downFile', ['$http',function ($http) {
    return {
        restrict: 'A',
        scope: true,
        link: function (scope, element, attr) {
            var ele = $(element);
            ele.on('click', function (e) {
                ele.prop('disabled', true);
                e.preventDefault();
                $http({
                    url: attr.downFile,
                    method: 'get',
                    responseType: 'arraybuffer'
                }).success(function (data, status, headers) {
                    ele.prop('disabled', false);
                    var type;
                    switch (attr.downFileType) {
                        case 'xlsx':
                            type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
                            break;
                    }
                    if (!type) throw '无效类型';
                    saveAs(new Blob([data], { type: type }), decodeURI(headers()["x-filename"]));  // 中文乱码
                }).error(function (data, status) {
                    alert(data);
                    ele.prop('disabled', false);
                });
            });
        }
    };
}]);

相对于 View 的具体实现:

<button down-file="/order/export/{{item.id}}" down-file-type="xlsx" class="btn btn-green btn-sm">导出</button>

以下是 ASP.NET API 的具体实现:

HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = HttpStatusCode.OK;
response.Content = new ByteArrayContent(pck.GetAsByteArray());
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
response.Content.Headers.Add("x-filename", System.Web.HttpUtility.UrlEncode(item.title, System.Text.Encoding.UTF8) + ".xlsx"); // 中文乱码
return response;

中文文件名乱码问题

文件名为中文时获取到的 x-filename 会是乱码,所以需要进行编码,在示例中已经标识鸟。

写文章时比较仓促,所以示例中只对 Excel 进行转化,可以根据需求加入各种文件格式类型。

以上,希望帮助到各位。

posted @ 2015-10-01 11:16  (*^__^*) 嘻嘻…  阅读(469)  评论(0编辑  收藏  举报