破狼 Blog

Write less got more.

最新Angular2案例rebirth开源

在过去的几年时间里,Angular1.x显然是非常成功的.但由于最初的架构设计和Web标准的快速发展,逐渐的显现出它的滞后和不适应。这些问题包括性能瓶颈、滞后于急速发展的Web标准、移动化等多平台发展,学习难度等。

所以Angular团队最终决定全新方式构建Angular2框架。Angular2框架现在已经进入RC6版本,很快将进入最终的发布版。Angular2带来了很多不错的特性,它们包括跨平台、高性能、高效开发等。

由于在Angular中引入了render层隔离设计,所以它也很容易实现跨平台的拓展。理论上只需要实现目标平台的render层处理。目前在Angular2的生态圈中已有的跨平台框架如下:

Angular2架构的重新设计,使得其在性能方面也得到了巨大的改善:

  • 组件树和单向的@Input、@Output使得变更的影响变得可预知,不再需要Angular1那样多次的digest直到稳定;以及结合ChangeDetectionStrategy.OnPush与immutablejs或者是Rxjs,Angular2的变更检查算法复杂度降为了log(n)。相对于Angular1,得到了至少5倍的性能提升;
  • Universal服务端渲染能够更好提升首屏加载的性能;
  • AOT技术引入,能够让组件处理的生成代码提前到构建时期;再加上TypeScript的tree shaking等技术,能够更大化的减小加载JavaScript文件大小和提升运行时性能;
  • Web端Web Worker的实现,能够更好的解放我们的UI线程,得到更好的而用户体验,以及性能的提升;

不仅仅这些,Angular2还有很多的优秀特性。如:基于TypeScript的静态类型检查、拥抱web标准(Shadow dom,fetch API)等等。

总之,Angular2是一门值得我们学习的优秀前端框架。随着Angular2进入了RC6版本,意味发布版将不远了。学习Angular2的时候已到。

========= 未来即将来到

同时笔者也开源了自己的rebirth项目供大家学习。它是一个利用Angular2开发的博客系统前端部分。它涉及到的Angular2知识点非常的全面,包括:组件化,自定义directive,路由,HTTP交互,Template drive form和Reactive form,异步路由,jwt token认证,资源权限控制,动态加载component,jQuery插件集成等常用知识点。

同时rebirth项目也集成了很多前端优秀的技术实践:

  • Angular2 + rxjs
  • bootstrap-sass
  • codemirror + markdownit(online markdown文档编辑器)
  • webpack2 + DashboardPlugin(代码打包)
  • TypeScript2 + @types
  • stubby(数据mock框架)
  • tslint + codelyzer(ts代码和Angular2组件静态检查)
  • angular2-template-loader(Angular2 component的html、css打包)
  • karma + phantomjs(TDD开发)
  • sass + postcss(css样式组织)
  • typedoc(ts文档)
  • fontgen-loader(icon font)

在这里为大家放上几张rebirth效果图,供大家预览:

移动端样式:

PC端样式:

https://github.com/greengerong/rebirth/raw/master/shortscreens/rebirth-index.png

https://github.com/greengerong/rebirth/raw/master/shortscreens/rebirth-manage-list.png

https://github.com/greengerong/rebirth/raw/master/shortscreens/rebirth-manage-edit.png

希望大家能喜欢。有任何的问题可以在笔者的github提issue,笔者会在空闲时间为大家解答。

Zone.js - 暴力之美

在ng2的开发过程中,Angular团队为我们带来了一个新的库 – zone.js。zone.js的设计灵感来源于Dart语言,它描述JavaScript执行过程的上下文,可以在异步任务之间进行持久性传递,它类似于Java中的TLS(thread-local storage: 线程本地存储)技术,zone.js则是将TLS引入到JavaScript语言中的实现框架。

那么zone.js能为我们解决什么问题呢?在回答这个问题之前,博主更希望回顾下在JavaScript开发中,我们究竟遇见了什么难题?

问题引入

我们先来看一段常规的同步JavaScript代码:

var foo = function(){ ... },
    bar = function(){ ... },
    baz = function(){ ... };

foo();
bar();
baz();

这段代码并没有什么特殊之处,它的执行顺序也并无什么特殊之处,完全在我们的预知之内:foo –> bar –> baz。对它做性能监测也很容易,我们只需要在执行上下文前后记录执行时间即可。

var start, 
    timer = performance ? performance.now.bind(performance) : Date.now.bind(Date);

start = timer();

foo(); 
bar(); 
baz(); 

console.log(Math.floor((timer() - start) * 100) / 100 + 'ms');

但在JavaScript的世界并不全是这么简单,众所周知的JavaScript单线程执行的。因此为了不阻塞UI界面的用户体验,在JavaScript执行的很多耗时操作都被封装为了异步操作,如:setTimeout、XMLHttpRequest、DOM事件等。由于浏览器的寄宿限制,JavaScript中异步操作是与生俱来的特性,被深深的印在了骨髓之中。这也是Ryan Dahl博士选择JavaScript开发Node.js平台的原因之一。关于JavaScript单线程执行可以参考博主的另一篇博文:JavaScript单线程和浏览器事件循环简述

那么对于下面这段异步代码,我们又如何做性能监测呢?

var foo = function(){ setTimeout(..., 2000); },
    bar = function(){ $.get(...).success(...); },
    baz = function(){ ... };

foo();
bar();
baz();

在这段代码中,引入了setTimeout和AJAX异步调用。其中AJAX回调和setTimeout回调时间顺序很难确定,因此给这段代码引入性能检测代码并不像上面的顺序执行代码一样那么简单了。如果我们需要强行加入性能的检测,则会在setTimeout和$.get回调中插入相关的hook代码并并记录执行时间,这样我们的业务代码也会变得非常混乱,就像一团“意大利拉面”一样(What the fuck!)。

zone.js简介

在本文开篇提到zone.js为JavaScript提供了执行上下文,可以在异步任务之间进行持久性传递。该是zone.js上场的时候了。zone.js采用猴子补丁(Monkey-patched)的暴力方式将JavaScript中的异步任务都包裹了一层,使得这些异步任务都将运行在zone的上下文中。每一个异步的任务在zone.js都被当做为一个Task,并在Task的基础上zone.js为开发者提供了执行前后的钩子函数(hook)。这些钩子函数包括:

  • onZoneCreated:产生一个新的zone对象时的钩子函数。zone.fork也会产生一个继承至基类zone的新zone,形成一个独立的zone上下文;
  • beforeTask:zone Task执行前的钩子函数;
  • afterTask:zone Task执行完成后的钩子函数;
  • onError:zone运行Task时候的异常钩子函数;

并且zone.js对JavaScript中的大多数异步事件都做了包裹封装,它们包括:

  • zone.alert;
  • zone.prompt;
  • zone.requestAnimationFrame、zone.webkitRequestAnimationFrame、zone.mozRequestAnimationFrame;
  • zone.addEventListener;
  • zone.addEventListener、zone.removeEventListener;
  • zone.setTimeout、zone.clearTimeout、zone.setImmediate;
  • zone.setInterval、zone.clearInterval

以及对promise、geolocation定位信息、websocket等也进行了包裹封装,你可以在这里找到它们https://github.com/angular/zone.js/tree/master/lib/patch

下面我们先来看一个简单的zone.js示例:

var log = function(phase){
    return function(){
        console.log("I am in zone.js " + phase + "!");
    };
};

zone.fork({
    onZoneCreated: log("onZoneCreated"),
    beforeTask: log("beforeTask"),
    afterTask: log("afterTask"),
}).run(function(){
    var methodLog = function(func){
        return function(){
            console.log("I am from " + func + " function!");
        };
    },
    foo = methodLog("foo"),
    bar = methodLog("bar"),
    baz = function(){
        setTimeout(methodLog('baz in setTimeout'), 0);
    };

    foo();
    baz();
    bar();
});

执行这段示例代码的输出是:

I am in zone.js beforeTask!
I am from foo function!
I am from bar function!
I am in zone.js afterTask!

I am in zone.js onZoneCreated!
I am in zone.js beforeTask!
I am from baz in setTimeout function!
I am in zone.js afterTask!

从上面的输出结果,我们能够看出在zone.js中将run方法块分为了两个Task,它们分别是方法体运行时的Task和异步setTimeout的Task。并且我们能够在这些Task的创建,执行前后拦截并做一些有意义的事情。

在zone.js中fork方法会产生一个继承至zone的子类,并在fork函数中可以配置特定的钩子方法,形成独立的zone上下文。而run方法则是启动执行业务代码的对外接口。

同时zone也支持父子继承,以及它也定义了一套DSL语法,支持$、+、-的前缀。

  • $会传递父类zone的钩子函数,便于对zone钩子函数执行的控制;
  • -代表在父zone的钩子函数之前运行本钩子函数;
  • +则与之相反,代表在父zone的钩子函数之后运行本钩子函数

更多的语法使用,请参考zone.js github首页文档https://github.com/angular/zone.js

引入zone.js

有了上面的这些关于zone.js的基础知识,在本文开始的遗留问题我们就可以迎刃而解了。下面这段代码是来自zone.js项目的示例代码:https://github.com/angular/zone.js/blob/master/example/profiling.html

var profilingZone = (function () {
    var time = 0,
        timer = performance ?
                    performance.now.bind(performance) :
                    Date.now.bind(Date);
    return {
      beforeTask: function () {
        this.start = timer();
      },
      afterTask: function () {
        time += timer() - this.start;
      },
      time: function () {
        return Math.floor(time*100) / 100 + 'ms';
      },
      reset: function () {
        time = 0;
      }
    };
  }());

  zone.fork(profilingZone).run(function(){

     //业务逻辑代码

  });

这里在beforeTask中启动了时间计算,并在afterTask中计算出当前累积的花费的时间。因此我们在业务代码的逻辑中就可以随时利用zone.time()来获取当前耗时了。

zone.js的实现

了解了zone.js的时候之后,或许你会像我一样感觉很神奇,它是如何实现的呢?

下面是zone.js中browser.ts的代码片段(https://github.com/angular/zone.js/blob/master/lib/patch/browser.ts):

export function apply() {
  fnPatch.patchSetClearFunction(global, global.Zone, [
    ['setTimeout', 'clearTimeout', false, false],
    ['setInterval', 'clearInterval', true, false],
    ['setImmediate', 'clearImmediate', false, false],
    ['requestAnimationFrame', 'cancelAnimationFrame', false, true],
    ['mozRequestAnimationFrame', 'mozCancelAnimationFrame', false, true],
    ['webkitRequestAnimationFrame', 'webkitCancelAnimationFrame', false, true]
  ]);

  fnPatch.patchFunction(global, [
    'alert',
    'prompt'
  ]);

  eventTargetPatch.apply();

  propertyDescriptorPatch.apply();

  promisePatch.apply();

  mutationObserverPatch.patchClass('MutationObserver');
  mutationObserverPatch.patchClass('WebKitMutationObserver');

  definePropertyPatch.apply();

  registerElementPatch.apply();

  geolocationPatch.apply();

  fileReaderPatch.apply();
}

从这里我们能看到,zone.js对浏览器中的setTimeout、setInterval、setImmediate、以及事件、promise、地理信息geolocation都做了特殊处理。那么这些处理是怎么处理的呢?下面是关于fnPatch.patchSetClearFunction的实现代码,来自zone.js中functions.ts(https://github.com/angular/zone.js/blob/master/lib/patch/functions.ts)的代码片段:

export function patchSetClearFunction(window, Zone, fnNames) {
  function patchMacroTaskMethod(setName, clearName, repeating, isRaf) {
    //浏览器原生方法留存
    var setNative = window[setName];
    var clearNative = window[clearName];
    var ids = {};

    if (setNative) {
      var wtfSetEventFn = wtf.createEvent('Zone#' + setName + '(uint32 zone, uint32 id, uint32 delay)');
      var wtfClearEventFn = wtf.createEvent('Zone#' + clearName + '(uint32 zone, uint32 id)');
      var wtfCallbackFn = wtf.createScope('Zone#cb:' + setName + '(uint32 zone, uint32 id, uint32 delay)');

      // 对浏览器原生方法的包裹封装
      window[setName] = function () {
        return global.zone[setName].apply(global.zone, arguments);
      };

      // 对浏览器原生方法的包裹封装
      window[clearName] = function () {
        return global.zone[clearName].apply(global.zone, arguments);
      };


      // 创建自己包裹方法,由上面的wind[setName]转移到这里执行.
      Zone.prototype[setName] = function (fn, delay) {

        var callbackFn = fn;
        if (typeof callbackFn !== 'function') {
          // force the error by calling the method with wrong args
          setNative.apply(window, arguments);
        }
        var zone = this;
        var setId = null;
        // wrap the callback function into the zone.
        arguments[0] = function() {
          var callbackZone = zone.isRootZone() || isRaf ? zone : zone.fork();
          var callbackThis = this;
          var callbackArgs = arguments;
          return wtf.leaveScope(
              wtfCallbackFn(callbackZone.$id, setId, delay),
              callbackZone.run(function() {
                if (!repeating) {
                  delete ids[setId];
                  callbackZone.removeTask(callbackFn);
                }
                return callbackFn.apply(callbackThis, callbackArgs);
              })
          );
        };
        if (repeating) {
          zone.addRepeatingTask(callbackFn);
        } else {
          zone.addTask(callbackFn);
        }
        setId = setNative.apply(window, arguments);
        ids[setId] = callbackFn;
        wtfSetEventFn(zone.$id, setId, delay);
        return setId;
      };
      ......

    }
  }
  fnNames.forEach(function(args) {
    patchMacroTaskMethod.apply(null, args);
  });
};

在上面的代码中,首先会将浏览器的原生方法保存在setNative中以便将会重用。紧接着zone.js就开始了它的暴力行为,覆盖window[setName]和window[clearName]然后将对setName的调用转到自身的zone[setName]的调用,zone.js就是如此暴力的对浏览器原生对象实现了拦截转移。然后它会在Task执行的前后调用自身的addRepeatingTask、addTask以及wtf事件来应用注册上的所有钩子函数。

到这里相信作为读者的你已经明白了zone.js的实现机制了,是不是和笔者一样有种“简单粗暴”的感觉?但是它真的很强大,为我们实现了对异步Task的跟踪、分析等。

zone.js应用场景

zone.js能实现异步Task跟踪,分析,错误记录、开发调试跟踪等,这些都是zone.js场景的应用场景。你也可以在https://github.com/angular/zone.js/tree/master/example看见更多的示例代码,以及Brian在ng-conf 2014关于zone.js的演讲视频: https://www.youtube.com/watch?v=3IqtmUscE_U.

当然对于一些特定的业务分析zone.js也有它很好的运用场景。如果你使用过Angular1的开发,那么也许你还能记忆犹新的想起:使用第三方事件或者ajax却忘记$scope.$apply的场景吧。在Angular1中如果在非Angular的上下文改变数据Model,Angular是无法预知的,因此也不会触发界面的更新。所以我们不得不显示的调用$scope.$apply或者$timeout来触发界面的更新。Angular框架为了更多的获知变化的事件,不得不为封装了一整套框架内置的服务和指令,如ngClick、ngChange、$http,$timeout等,这也增加了Angular1的学习成本。

也是为了解决Angular1的这一些列问题,Angular2团队引入了zone.js,放弃自定义这类服务和指令,相反而是拥抱浏览器的原生对象和方法。所以在Angular2中可以使用浏览器的任何事件了,只需要括号模板语法的标识:(eventName),等价于on-eventName;也可以直接使用浏览器的原生对象了,如setTimeout,addEventListener、promise、fetch等。

当然,zone.js也能应用于Angular1的项目之中。示例代码如下(http://jsbin.com/kenilivuvi/edit?html,js,output):

angular.module("com.ngbook.demo", [])
    .controller("DemoController", ['$scope', function($scope){

        zone.fork({
            afterTask: function(){
                var phase = $scope.$root.$$phase;
                if(['$apply', '$digest'].indexOf(phase) === -1) {
                    $scope.$apply();
                 }
            }
        }).run(function(){

            setTimeout(function(){
                $scope.fromZone = "I am from zone with setTimeout!";
            }, 2000);
        });

    }]);

在示例代码中,在每次Task的完成后都会尝试$scope.$apply,强制将Model数据的改变更新到UI界面。对于在Angular1中使用zone.js更多的地方应该是在Directive中,同时也可以将zone的创建过程封装为服务(工厂方法,每次返回一个全新的zone对象)。在Angular2中也有同样zone的封装,它被称为ngZone(https://github.com/angular/angular/blob/master/modules/angular2/src/core/zone/ng_zone.ts)。

《AngularJS深度剖析与最佳实践》简介

由于年末将至,前阵子一直忙于工作的事务,不得已暂停了微信订阅号的更新,我将会在后续的时间里尽快的继续为大家推送更多的博文。毕竟一个人的力量微薄,精力有限,希望大家能理解,仍然能一如既往的关注和支持shuang_lang_shuo[破狼]微信号,同时也欢迎大家的高质量文章的投稿。

在2015年一年时间中,我、雪狼大叔、彭洪伟一起共同编写了《AngularJS深度剖析与最佳实践》这本前端Angular.js框架的进阶书籍。在写作期间也得到很多人的支持,特别是在Angularjs中文社区群中的各位群友的持续关注。中途由于写作、出版流程等因素,花费了大家很长的等待时间,就在昨天《AngularJS深度剖析与最佳实践》这本书籍终于上市了,大家现在可以在京东上预订书籍了,相信出版社也会在很快的时间内送到大家手中。

http://item.jd.com/11845736.html#none

链接地址:http://item.jd.com/11845736.html#none

双狼的写作感谢

我和雪狼的本次合作起于机械工业出版社编辑吴怡的邀请。作为ThoughtWorks的Tech Lead,双狼都有很多工作任务,原定6个月的写书计划,被拖到了8个月,感谢吴怡的耐心等待与支持。

还有很多ThoughtWorker为本书做出了贡献:   

张逸,资深ThoughtWorker,很多技术书籍的作者或译者。一直在鼓励我们,并给了我们很多帮助。
彭洪伟,本书的第三作者。在交稿压力最大的时候,承担了“工具”篇的撰写工作,保障了本书的尽早交稿。
陈嘉,幕后的贡献者,全栈式工程师。帮我们设计了“双狼说”微信公众号的Logo,从技术的角度帮我们审稿,并提了一些非常有用的建议。

还有很多ThoughtWorker和社区朋友帮助我们从技术层面和语言层面进行修改。他们有的是Angular专家,有的是新手,给了我们比较全面的反馈。能将枯燥、乏味的技术平易近人地展现在这本书中,一定要感谢他们所作出的奉献。他们是(排名不分先后):

冯尔东、朱本威、李科伟、杨琛、彭琰、叶志敏、ng群as。

   还要感谢Angular中文社区QQ群和关注“双狼说”微信号的网友们,是你们的鼓励给了我们写作的信心和动力!

书籍的阅读指南

Angular的学习曲线大概是这样的:入门非常容易,中级的时候会发现需要深入理解很多概念,高级的时候需要掌握Angular的工作原理,而想成为专家则很难,需要经过很多工程实践的磨练。

本书的主体结构也是针对这样的学习曲线设计的:

首先,初级阶段,轻松入门

我们会带你在实战中逐步体验Angular的开发过程,并随着进度的推进,逐步引入所需的技术和概念。

然后,中级阶段,概念介绍

在实战中提到的一些概念不会就地展开,而是只做简介,到了这里,会对概念进行深入讲解:是什么,为什么,怎么用,什么时候用,什么时候不用等。

接下来,高级阶段,工作原理

学习了这些概念,我们还要把它们串起来,向读者揭示Angular的工作原理,看看这些概念之间是如何协作的。

最后,专家阶段:最佳实践,技巧

前面主要是入门和理论,而这部分将主要以实战经验为主。

只把Angular用熟了是不够的,我们还要把它整合进更宏观的开发过程中,不但要考虑开发,更要考虑维护。我们要如何开发容易维护的Angular程序?请看“最佳实践”一节。

专家还需要掌握一些技巧去把复杂问题简单化,把一些不常用但很有用的API发掘出来,把看起来平淡无奇的框架特性运用得出神入化,“使用技巧”一节将集中展现这一点。

在前面的章节中零零散散提到了一些需要注意的地方,但是这样不方便查阅,所以我们把它作为独立的一大章,把我们帮别人解决过的一些典型问题收集在一起。 当然,我们也会在读者社区继续维护并更新这些“坑”,而不是等再版时才发布。 我们希望能把这本书做成“活的”,让这本书更加物超所值,不辜负读者对我们的信任。

工具

工欲善其事,必先利其器。充分发挥工具的力量是开发人员的重要素质,日常用到的工具你真的用熟了吗?有没有更好地工具?我们会把实战中觉得对自己帮助最大的工具及其使用经验分享给你。

更多

在实战中,有很多需求是不显眼但很重要的,比如SEO、访问统计等,在实际的项目中,这些往往是不能忽视的。 我们会专门开一章来讲解如何结合Angular和第三方软件来干净漂亮的解决这些问题。

Hybrid应用和手机Web越来越普及,手机版开发的需求也越来越高,在Angular的基础上,开发手机版变得容易多了。而且,也已经有了比较成熟的工具和框架,我们会简要讲解一下手机版开发的方法和框架。

附录

软件开发需要很多综合技能,但本书容量有限,我们也不可能是每个领域的专家。因此,我们会“授人以渔”,给出一些在线资源和书单,供大家深入学习或作为备查资料。

关于随书代码

书中所摘录的只是全部代码的一小部分,大部分代码都放在了Github上。

如果你查看Git历史,会发现总的提交数并不多。这是因为要方便教学,所以在提交前进行了合并。所保留的这些提交大都和书中的主要进度有关,略去了细节提交。所以,本书中代码的提交粒度不能代表实际项目中的提交粒度,在实际项目中,其提交粒度通常比本书中所示范的更小。阅读代码时请记住这一点,以免养成“大粒度提交”的坏习惯。

另外,文中的js代码(包括摘引的angular源码)全都使用了两格缩进模式,这主要是考虑到图书排版问题,希望少一些不必要的换行。你们在现实项目中愿意用两格或四格均可,只要项目组内保持一致。

(译)你应该知道的jQuery技巧

帮助提高你jQuery应用的简单小技巧。

  1. 回到顶部按钮
  2. 图片预加载
  3. 判断图片是否加载完
  4. 自动修补破损图像
  5. Hover切换class类
  6. 禁用输入
  7. 停止正在加载的链接
  8. toggle fade/slide
  9. 简单的手风琴
  10. 使两个DIV同等高度
  11. 在浏览器标签/新窗口打开外部链接
  12. 根据文本获取元素
  13. 可见变化的触发
  14. Ajax调用错误处理
  15. 链式操作

回到顶部按钮

利用jQuery里的animate和scrollTop方法,你便不需要使用插件创建简单的滚动到顶部动画。

// Back to top
$('.top').click(function (e) {
  e.preventDefault();
  $('html, body').animate({scrollTop: 0}, 800);
});
<!-- Create an anchor tag -->
<a class="top" href="#">Back to top</a>

通过scrollTop的值来改变你想要滚动到的位置。其实你就是做了:在接下来的800毫秒中让页面滚动,直到它滚动到文档的顶部。

备注:来看一些scrollTop的调皮行为 。

图片预加载

如果你的网页使用了很多隐藏图片文件(例如:鼠标悬停展示的图片),那么图片的预加载是有意义的:

$.preloadImages = function () {
  for (var i = 0; i < arguments.length; i++) {
    $('<img>').attr('src', arguments[i]);
  }
};

$.preloadImages('img/hover-on.png', 'img/hover-off.png');

判断图片是否加载完

有时候你可能需要检查图像是否已经加载完成,以便于可以继续执行相应的js代码:

$('img').load(function () {
  console.log('image load successful');
});

你还可以检查一个特定的图片是否加载完并且被带有Id或者class的<img>标签代替。

自动修补破损图像

如果你碰巧发现在你的网站上发现破损的图像链接,一个个去替代他们是痛苦的。这个简单的代码可以节省很多的麻烦:

$('img').on('error', function () {
  if(!$(this).hasClass('broken-image')) {
    $(this).prop('src', 'img/broken.png').addClass('broken-image');
  }
});

即使你没有任何断开的链接,加入这代码也不会有任何影响。

Hover切换class类

比方说,当用户将鼠标悬停在你页面上的元素时,你想改变其视觉效果。当用户鼠标悬停在元素上,你可以在该元素上添加一个class类,当鼠标停止悬停事件时移除此class类:

$('.btn').hover(function () {
  $(this).addClass('hover');
}, function () {
  $(this).removeClass('hover');
});

如果你想要一个更简单的方式使用toggleClass方法,则仅仅需要添加必要的CSS:

$('.btn').hover(function () {
  $(this).toggleClass('hover');
});

备注:CSS在这种情况下使用是一个快速的解决方案,但要知道这点知识依旧是值得去了解下的。

禁用输入

有时你可能需要用表单的提交按钮或者某个输入框直到用户执行了某个动作(比如:检查“我已阅读条款”复选框)。在你的输入框上设置disabled属性,然后当你需要的时候启用该属性:

$('input[type="submit"]').prop('disabled', true);

你需要做的只是需要在输入框上再次运行prop方法,但设置的被禁用值是false:

$('input[type="submit"]').prop('disabled', false);

停止正在加载的链接

有时你不想链接到特定的网页或者重新载入页面;你可能想让他们做一些其他事情,如触发一些其他的脚本。这是防止违约行动的技巧:

$('a.no-link').click(function (e) {
  e.preventDefault();
});

toggle fade/slide

滑动和淡入/淡出 是我们在jQuery中经常大量使用的动画。你可能仅仅想在用户做某些点击事件的时候显示一个元素,这时候需要淡入/淡出或者滑动方法。但是如果你需要那个元素在你第一次点击的时候出现,在第二次点击的时候消失,代码如下:

// Fade
$('.btn').click(function () {
  $('.element').fadeToggle('slow');
});

// Toggle
$('.btn').click(function () {
  $('.element').slideToggle('slow');
});

简单的手风琴

这是个简单快速的方法创建一个手风琴:

// Close all panels
$('#accordion').find('.content').hide();

// Accordion
$('#accordion').find('.accordion-header').click(function () {
  var next = $(this).next();
  next.slideToggle('fast');
  $('.content').not(next).slideUp('fast');
  return false;
});

通过添加这个脚本,你需要做的则是必要的HTML操作在你的页面上。

使两个DIV同等高度

有时你会想要两个DIV有相同的高度,无论他们都有什么内容:

$('.div').css('min-height', $('.main-div').height());

这个例子设置了DIV的最小高度,这意味着它的高度只可以比这个设置的高度大而不能小。然而,一个更灵活的方法是循环的一组元素,并设置将最高元素的高度作为高度:

var $columns = $('.column');
var height = 0;
$columns.each(function () {
  if ($(this).height() > height) {
    height = $(this).height();
  }
});
$columns.height(height);

如果你想要所有的列有相同的高度:

var $rows = $('.same-height-columns');
$rows.each(function () {
  $(this).find('.column').height($(this).height());
});

在浏览器标签/新窗口打开外部链接

在新的浏览器标签或窗口中打开外部链接,并确保在同一个标签或窗口中打开的是同一个源的链接:

$('a[href^="http"]').attr('target', '_blank');
$('a[href^="//"]').attr('target', '_blank');
$('a[href^="' + window.location.origin + '"]').attr('target', '_self');

备注:window.location.origin 在IE10不工作。

根据文本获取元素

通过jQuery中的contains()选择器,你能找到一个元素内的文本内容。如果文本不存在,则这个元素将被隐藏:

var search = $('#search').val();
$('div:not(:contains("' + search + '"))').hide();

可见变化的触发

当用户不再聚焦或者重新聚焦一个标签时触发javascript脚本:

$(document).on('visibilitychange', function (e) {
  if (e.target.visibilityState === "visible") {
    console.log('Tab is now in view!');
  } else if (e.target.visibilityState === "hidden") {
    console.log('Tab is now hidden!');
  }
});

Ajax调用错误处理

当一个Ajax调用返回一个404或500的错误时,将执行该错误处理。如果该处理未定义,则其他jQuery代码便可能不会执行了。定义一个全局Ajax错误处理程序:

$(document).ajaxError(function (e, xhr, settings, error) {
  console.log(error);
});

链式操作

jQuery允许通过链式操作来减轻反复查询DOM和创建多个jQuery对象的过程。比如下面是你的方法调用:

$('#elem').show();
$('#elem').html('bla');
$('#elem').otherStuff();

这代码可以通过链式大大的提高:

$('#elem')
  .show()
  .html('bla')
  .otherStuff();

另一个方法是在一个可变的元素缓存($作为前置):

var $elem = $('#elem');
$elem.hide();
$elem.html('bla');
$elem.otherStuff();

链式和jQuery缓存方法是最好的做法,导致更短、更快的代码。

翻译:野兽

英文原文地址:https://github.com/AllThingsSmitty/jquery-tips-everyone-should-know

JavaScript多线程之HTML5 Web Worker

桥和多线路电线

在博主的前些文章Promise的前世今生和妙用技巧JavaScript单线程和浏览器事件循环简述中都曾提到了HTML5 Web Worker这一个概念。在JavaScript单线程和浏览器事件循环简述中讲述了JavaScript出于界面元素访问安全的考虑,所以JavaScript运行时一直是被实现为单线程执行的;这也意味着我们应该尽量的避免在JavaScript中执行较长耗时的操作(如大量for循环的对象diff等)或者是长时间I/O阻塞的任务,特别是对于CPU计算密集型的操作。

例如在JavaScript中尝试计算像fibonacci这类计算密集型的操作,就会导致整个页面体验被blocked。HTML5 Web Worker的出现让我们在不阻塞当前JavaScript线程的情况下,在当前的JavaScript执行线程中可利用Worker这个类新开辟一个额外的线程来加载和运行特定的JavaScript文件,这个新的线程和JavaScript的主线程之间并不会互相影响和阻塞执行的;并且在Web Worker中提供这个新线程和JavaScript主线程之间数据交换的接口:postMessage和onmessage事件。它和C# WinForm中的BackgroundWorker很类似。

Web Worker实现fibonacci计算

利用HTML5 Web Worker实现fibonacci可像如下所示(plnkr在线demo):

fibonacci.js Worker JavaScript文件:

(function() {
  var fibonacci = function(n) {
    return n < 2 ? 1 : (fibonacci(n - 1) + fibonacci(n - 2));
  };

  onmessage = function(event) {
    postMessage({
      input: event.data,
      result: fibonacci(event.data)
    });
  };

})();

在fibonacci.js中利用onmessage方法来监听主线程发送的fibonacci计算请求,和利用postMessage返回计算的结果到请求线程。

script.js 主线程JavaScript文件:

$(function() {
  var $input = $('#input'),
    $btn = $('#btn'),
    $result = $('#result'),
    worker = new Worker('fibonacci.js'),
    timeKey = function(val) {
      return 'fibonacci(' + val + ')';
    };

  worker.onmessage = function(event) {
    console.timeEnd(timeKey(event.data.input));
    $result.text(event.data.result);
  };

  $btn.on('click', function() {
    var val = parseInt($input.val(), 10);
    if (val) {
      console.time(timeKey(val));
      $result.text('?')
      worker.postMessage(val);
    }
  });
});

在这个JavaScript文件中,利用new Worker('fibonacci.js')方式来创建Web Worker对象,并利用Worker对象上的postMessage方法发送请求计算请求,以及利用Worker对象的onmessage的方法接受Worker线程的返回结果,并显示在UI界面上。同时我们也利用了console最新的time API来统计计算所花费的时间。

其显示效果如下:

html5 web worker demo

在console中打印的时间信息为:

fibonacci(10): 1.022ms
fibonacci(20): 1.384ms
fibonacci(30): 22.065ms
fibonacci(40): 1744.352ms
fibonacci(50): 202140.027ms

从这里时间输出可以看出,在计算n为40的fibonacci 开始时间开始急速的加长,在UI中返回结果的时间也逐渐变长;但是在Web Worker后台计算的时候,它并不会阻塞我们的UI界面的其他交互。

Web Worker总结

Web Worker在这类耗时计算密集型操作中,显得特别实用。在Web Worker中我们可以实现:

  1. 可以加载一个JS进行大量的复杂计算而不挂起主进程,并通过postMessage,onmessage进行通信;
  2. 可以在worker中通过importScripts(url)加载另外的脚本文件;
  3. 可以使用 setTimeout(),clearTimeout(),setInterval(),clearInterval();
  4. 可以使用XMLHttpRequest来发送请求,以及访问navigator的部分属性。

但是它也存在一些来自浏览器安全沙盒的限制:

  1. 不能加载跨域的JavaScript文件;
  2. 如文件开始所说,考虑到JavaScript操作DOM的安全性问题,在Web Worker中不能访问界面中的DOM信息,对于DOM的访问操作都必须委托给JavaScript主线程来操作;因此HTML5 Web Worker的出现的出现,并没有改变JavaScript单线程执行的这个事实;
  3. 还有就是Web Worker的浏览器兼容性问题。它的浏览器兼容性图如下:

html5 web worker浏览器兼容性

更多关于Web Worker的资料,请参考https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

Angular移除不必要的$watch之性能优化

Angular-apply-and-浏览器-event-loop

双向绑定是Angular的核心概念之一,它给我们带来了思维方式的转变:不再是DOM驱动,而是以Model为核心,在View中写上声明式标签。然后,Angular就会在后台默默的同步View的变化到Model,并将Model的变化更新到View。

双向绑定带来了很大的好处,但是它需要在后台保持一只“眼睛”,随时观察所有绑定值的改变,这就是Angular 1.x中“性能杀手”的“脏检查机制”($digest)。可以推论:如果有太多“眼睛”,就会产生性能问题。在讨论优化Angular的性能之前,笔者希望先讲解下Angular的双向绑定和watchers函数。

双向绑定和watchers函数

为了能够实现双向绑定,Angular使用了$watch API来监控$scope上的Model的改变。Angular应用在编译模板的时候,会收集模板上的声明式标签 —— 指令或绑定表达式,并链接(link)它们。这个过程中,指令或绑定表达式会注册自己的监控函数,这就是我们所说的watchers函数。

下面以我们常见的Angular表达式({{}})为例。

HTML:

1
2
3
4
<body ng-app="com.ngnice.app" ng-controller="DemoController as demo">
    <div>hello : {{demo.count}}</div>
    <button type="button" ng-click="demo.increase ();">increase ++</button>
</body>

JavaScript:

1
2
3
4
5
6
7
8
9
angular.module('com.ngnice.app')
.controller('DemoController', [function() {
  var vm = this;
  vm.count = 0;
  vm.increase = function() {
    vm.count++;
  };
  return vm;
}]);

这是一个自增长计数器的例子,在上面的代码我们用了Angular表达式({{}})。表达式为了能在Model的值改变的时候你能及时更新View,它会在其所在的$scope(本例中为DemoController)中注册上面提到的watchers函数,监控count属性的变化,以便及时更新View。

上例中在每次点击button的时候,count计数器将会加1,然后count的变化会通过Angular的$digest过程同步到View之上。在这里它是一个单向的更新,从Model到View的更新。如果处理一个带有ngModel指令的input交互控件,则在View上的每次输入都会被及时更新到Model之上,这里则是反向的更新,从View到Model的更新。

Model数据能被更新到View是因为在背后默默工作的$digest循环(“脏检查机制”)被触发了。它会执行当前scope以及其所有子scope上注册的watchers函数,检测是否发生变化,如果变了就执行相应的处理函数,直到Model稳定了。如果这个过程中发生过变化,浏览器就会重新渲染受到影响的DOM来体现Model的变化。

在Angular表达式({{}})背后的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
  var nodeType = node.nodeType,
    attrsMap = attrs.$attr,
    match,
    className;

  switch (nodeType) {
    case 1:
      /* Element */
      ...
      break;
    case 3:
      /* Text Node */
      addTextInterpolateDirective(directives, node.nodeValue);
      break;
    case 8:
      /* Comment */
      ...
      break;
  }

  directives.sort(byPriority);
  return directives;
}

function addTextInterpolateDirective(directives, text) {
  var interpolateFn = $interpolate(text, true);
  if (interpolateFn) {
    directives.push({
      priority: 0,
      compile: function textInterpolateCompileFn(templateNode) {
        // when transcluding a template that has bindings in the root
        // then we don't have a parent and should do this in the linkFn
        var parent = templateNode.parent(),
          hasCompileParent = parent.length;
        if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding');

        return function textInterpolateLinkFn(scope, node) {
          var parent = node.parent(),
            bindings = parent.data('$binding') || [];
          bindings.push(interpolateFn);
          parent.data('$binding', bindings);
          if (!hasCompileParent) safeAddClass(parent, 'ng-binding');
          scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
            node[0].nodeValue = value;
          });
        };
      }
    });
  }
}

Angular会在compile阶段收集View模板上的所有Directive。Angular表达式会被解析成一种特殊的指令:addTextInterpolateDirective。到了link阶段,就会利用scope.$watch的API注册我们在上面提到的watchers函数:它的求值函数为$interpolate对绑定表达式进行编译的结果,监听函数则是用新的表达式计算值去修改DOM Node的nodeValue。可见,在View中的Angular表达式,也会成为Angular在$digest循环中watchers的一员。

在上面代码中,还有一部分是为了给调试器用的。它会在Angular表达式所属的DOM节点加上名为‘ng-binding’的调试类。类似的调试类还有‘ng-scope’,‘ng-isolate-scope’等。在Angular 1.3中我们可以使用compileProvider服务来关闭这些调试信息。

1
2
3
4
app.config(['$compileProvider', function ($compileProvider) {
  // disable debug info
  $compileProvider.debugInfoEnabled(false);
}]);

其它指令中的watchers函数

不仅Angular的表达式会使用$scope.$watch API添加watchers,Angular内置的大部分指令也一样,下面再举几个常用的Angular指令。

ngBind:它和Angular表达式很类似,都是绑定特定表达式的值到DOM的内容,并保持与scope的同步。不同之处在于它需要一个HTML节点并以attribute属性的方式标记。简单来说,我们可以认为Angular表达式就是ngBind的特定语法糖。当然,还是有一点区别的,详情参见“使用技巧”一章的“防止Angular表达式闪烁”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ngBindDirective = ngDirective({
  compile: function(templateElement) {
    templateElement.addClass('ng-binding');
    return function (scope, element, attr) {
      element.data('$binding', attr.ngBind);
      scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
        // We are purposefully using == here rather than === because we want to
        // catch when value is "null or undefined"
        // jshint -W041
        element.text(value == undefined ? '' : value);
      });
    };
  }
});

这里也能清晰的看见$scope.$watch的注册代码:监控器函数为ngBind attribute的值,处理函数则是用表达式计算的结果去更新DOM的文本内容。

ngShow/ngHide: 它们是根据表达式的计算结果来控制显示/隐藏DOM节点的指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ngShowDirective = ['$animate', function($animate) {
  return function(scope, element, attr) {
    scope.$watch(attr.ngShow, function ngShowWatchAction(value){
      $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
    });
  };
}];

var ngHideDirective = ['$animate', function($animate) {
  return function(scope, element, attr) {
    scope.$watch(attr.ngHide, function ngHideWatchAction(value){
      $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide');
    });
  };
}];

这里同样用到了$scope.$watch,到这里你应该明白$watch的工作原理了吧。

再回到上面所提的性能问题。

如果有太多watcher函数,那么在每次$digest循环时,肯定会慢下来,这就是Angular“脏检查机制”的性能瓶颈。在社区中有个经验值,如果超过2000个watcher,就可能感觉到明显的卡顿,特别在IE8这种老旧浏览器上。有什么好的方案可以解决这个问题呢?最明显的方案是:减少$watch,尽量移除不必要的$watch。

慎用$watch和及时销毁

要想提高Angular页面的性能,那么在开发的时候,就应该尽量减少显式使用$scope.$watch函数,Angular中的很多内置指令已经能够满足大部分的业务需求。特别是如果能复用ng内置的UI事件指令(ngChange、ngClick…),那么就不要添加额外的$watch。

对于不再使用的$watch,最好尽早将其释放,$scope.$watch函数的返回值就是用于释放这个watcher的函数,如下面的单次绑定实现(one-time):

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('com.ngnice.app')
.controller('DemoController', ['$scope', function($scope) {
  var vm = this;
  vm.count = 0;
  var textWatch = $scope.$watch('demo.updated', function(newVal, oldVal) {
    if (newVal !== oldVal) {
      vm.count++;
      textWatch();
    }
  });
  return vm;
}]);

one-time绑定

在开发中,经常会遇见很多有静态数据构成的页面,如静态的商品、订单等的显示,他们在绑定了数据之后,在当前页面中Model不再会被改变。试想我们需要显示一个培训会议Sessions的预约的展示页面,常规的Angular方案应该是用ng-repeat来产生这个列表:

HTML:

1
2
3
4
5
6
7
8
9
10
11
<ul>
    <li ng-repeat="session in sessions">
        <div class="info">
            {{session.name}} - {{session.room}} - {{session.hour}} - {{session.speaker}}
        </div>
        <div class="likes">
            {{session.likes}} likes!
            <button ng-click="likeSession(session)">Like it!</button>
        </div>
    </li>
</ul>

JavaScript:

1
2
3
4
5
6
7
angular.module('com.ngnice.app')
.controller('MainController', ['$scope', function($scope) {
  $scope.sessions = [...];
  $scope.likeSession = function(session) {
    // Like the session
  }
}]);

用Angular来实现这个需求,很简单。但假设这是一个大型的预约,一天会有300个Sessions。那么这里会产生多少个$watch?这里每个Session有5个绑定,额外的ng-repeat一个。这将会产生1501个$watch。这有什么问题?每次用户“like”一个Session,Angular将会去检查name、room等5个属性是不是被改变了。

问题在于,除了例外的“like”外,所有数据都是静态数据,这是不是有点浪费资源?我们知道数据Model是没有被改变的,既然这样为什么让Angular要去检查是否改变呢?

因此,这里的$watch是没必要的,它的存在反而会影响$digest的性能,但这个$watch在第一次却是必要的,它在初始化时用静态信息填充了我们的DOM结构。对于这类情况,如果能换为单次(one-time)绑定应该是最佳的方案。

Angular中的单次(one-time)绑定是在1.3后引入的。在官方文档描述如下:

单次表达式在第一次$digest完成后,将不再计算(监测属性的变化)。

1.3中为Angular表达式({{}})引入了新语法,以“::”作为前缀的表达式为one-time绑定。对于上面的例子可以改为:

1
2
3
4
5
6
7
8
9
10
11
<ul>
    <li ng-repeat="session in sessions">
        <div class="info">
            {{::session.name}} - {{::session.room}} - {{::session.hour}} - {{::session.speaker}}
        </div>
        <div class="likes">
            {{session.likes}} likes!
            <button ng-click="likeSession(session)">Like it!</button>
        </div>
    </li>
</ul>

在1.3之前的版本没有提供这个语法,我们应该如何实现one-time绑定呢?在开源社区中有个牛人在我们之前也问了自己这个问题,并创建了一系列指令来实现它:Bindonce https://github.com/Pasvaz/bindonce。用Bindonce实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul>
    <li bindonce ng-repeat="session in sessions">
        <div class="info">
            <span bo-text="session.name"></span> -
            <span bo-text="session.room"></span> -
            <span bo-text="session.hour"></span> -
            <span bo-text="session.speaker"></span>
        </div>
        <div class="likes">
            {{session.likes}} likes!
            <button ng-click="likeSession(session)">Like it!</button>
        </div>
    </li>
</ul>

为了让示例能够工作,需要引入bindonce库,并依赖pasvaz.bindonce module。

angular.module('com.ngnice.app', ['pasvaz.bindonce']);

并把Angular表达式改成bo-text指令。该指令将会绑定到Model,直到更新DOM,然后自动释放watcher。这样,显示功能仍然工作,但不再使用不必要的$watch。在这里每个Session只有一个$watch绑定,用301个绑定替代了1501个绑定。

恰当的使用bingonce或者1.3的one-time绑定能为应用one程序减少大量不必要$watch绑定,从而提高应用性能。

滚屏加载

另外一种可行的性能解决方案就是滚屏加载,又称”Endless Scrolling,“ “unpagination”,这是用于大量数据集显示的时候,又不想表格分页,所以一般放在页面最底部,当滚动屏幕到达页面底部的时候,就会尝试加载一个序列的数据集,追加在页面底部。在Angular社区有开源组件ngInfiniteScroll http://binarymuse.github.io/ngInfiniteScroll/index.html实现滚屏加载。下面是官方Demo:

HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div ng-app='myApp' ng-controller='DemoController'>
  <div infinite-scroll='reddit.nextPage()' infinite-scroll-disabled='reddit.busy' infinite-scroll-distance='1'>
    <div ng-repeat='item in reddit.items'>
      <span class='score'>{{item.score}}</span>
      <span class='title'>
        <a ng-href='{{item.url}}' target='_blank'>{{item.title}}</a>
      </span>
      <small>by {{item.author}} -
        <a ng-href='http://reddit.com{{item.permalink}}' target='_blank'>{{item.num_comments}} comments</a>
      </small>
      <div style='clear: both;'></div>
    </div>
    <div ng-show='reddit.busy'>Loading data...</div>
  </div>
</div>

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var myApp = angular.module('myApp', ['infinite-scroll']);

myApp.controller('DemoController', ['$scope', 'Reddit', function($scope, Reddit) {
  $scope.reddit = new Reddit();
}]);

// Reddit constructor function to encapsulate HTTP and pagination logic
myApp.factory('Reddit', ['$http', function($http) {
  var Reddit = function() {
    this.items = [];
    this.busy = false;
    this.after = '';
  };

  Reddit.prototype.nextPage = function() {
    if (this.busy) return;
    this.busy = true;

    var url = 'http://api.reddit.com/hot?after=' + this.after + '&jsonp=JSON_CALLBACK';
    $http.jsonp(url).success(function(data) {
      var items = data.data.children;
      for (var i = 0; i < items.length; i++) {
        this.items.push(items[i].data);
      }
      this.after = 't3_' + this.items[this.items.length - 1].id;
      this.busy = false;
    }.bind(this));
  };

  return Reddit;
}]);

可以在这里http://binarymuse.github.io/ngInfiniteScroll/demo_async.html访问这个例子。其使用很简单,有兴趣的读者可以查看其官方文档。

其它

当然对于性能解决方案还有很多,如客户端分页、服务端分页、将其它更高效的jQuery插件或者React插件合理的封装为ng组件等。当封装第三方非Angular组件时需要注意scope和model的同步,以及合理的触发$apply更新View。另外在开源社区中也有ngReact可以简化将React组件应用到Angular应用中,在这里可以了解到关于它的更多信息:https://github.com/davidchang/ngReact

此刻,我猜你一定正是心中默默嘀咕着:Angular“脏检查机制”一定很慢,一个“肮脏”的家伙。但这是错误的。它其实很快,Angular团队为此专门做了很多优化。相反,在大多数场景下,Angular这种特殊的watcher机制,反而比很多基于JavaScript模板引擎(underscore、Handlebars等)更快。因为Angular并不需要通过大范围的DOM操作来更新View,它的每次更新区域更小,DOM操作更少。而DOM操作的代价远远高过JavaScript运算,在有些浏览器中,修改DOM的速度甚至会比纯粹的JavaScript运算慢很多倍!

而且,在现实场景中,我们的大多数页面都不会超出2000个watcher,因为过多的信息对使用者是非常不友好的,好的设计师都懂得限制单页信息的展示量。但是如果超过了2000个watcher,那么你就得仔细思考如何去优化它了,应该优先选择从用户体验方面改进,实在不行就用上面提到的技巧来优化你的应用程序。

最后,随着Angular 2.0框架对“脏检查机制”的改进,运行性能将会得到显著地提高,特别是针对Mobile开发的ionic这类框架,将直接受益。

JavaScript单线程和浏览器事件循环简述

JavaScript 单线程 火车轨道

JavaScript单线程

在上篇博客《Promise的前世今生和妙用技巧》的开篇中,我们曾简述了JavaScript的单线程机制和浏览器的事件模型。应很多网友的回复,在这篇文章中将继续展开这一个话题。当然这里是博主的一些理解,如果还存在什么纰漏的话,请不吝指教。

JavaScript这门语言运行在浏览器中,是以单线程的方式运行的。说到单线程,就得从操作系统进程开始说起。进程和线程都是操作系统的概念。进程是应用程序的执行实例,每一个进程都是由私有的虚拟地址空间、代码、数据和其它系统资源所组成;进程在运行过程中能够申请创建和使用系统资源(如独立的内存区域等),这些资源也会随着进程的终止而被销毁。而线程则是进程内的一个独立执行单元,在不同的线程之间是可以共享进程资源的,所以在多线程的情况下,需要特别注意对临界资源的访问控制。在系统创建进程之后就开始启动执行进程的主线程,而进程的生命周期和这个主线程的生命周期一致,主线程的退出也就意味着进程的终止和销毁。主线程是由系统进程所创建的,同时用户也可以自主创建其它线程,这一系列的线程都会并发地运行于同一个进程中。

在多线程操作的情况下可以实现应用的并行处理,而提高整个应用程序的性能和吞吐量,更大粒度的榨取本机的CPU利用率,特别是现代很多语言都支持了多核并行处理技术。然后JavaScript居然还是单线程执行,为什么呢?

这是因为JavaScript这门脚本语言诞生的使命所致:JavaScript为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JavaScript是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,而线程1要求浏览器删除DOM节点,线程2却希望修改这个节点的某些样式风格。这个时候浏览器就无法裁决采用哪一种策略了。当然我们可以为浏览器引入“排它锁”或者是“乐观锁”来解决这些冲突,但为了避免引入了更大的复杂性,所以JavaScript从诞生开始就选择了单线程执行。

因为单线程执行,所以对于JavaScript的任务而言,在同一时间内只能执行一个特定的任务,并且它会阻塞其他的任务执行。那么JavaScript的执行不会很慢吗?特别是对于长时间任务执行的时候,那么其他的任务就得不到执行。然而在软件开发中,特别是应用软件开发中,对于I/O设备的访问都是一些及其耗时的操作。在这些耗时任务执行的时候,其实并没必要等待它的完成,在I/O任务完成之前JavaScript完全可以继续执行其他的任务,直到I/O任务完成后再继续执行该任务的处理就行。JavaScript在设计之初,就意识这一点。所以在JavaScript中将这些耗时的I/O等操作封装为了异步的方法,等到这些任务完成后就将后续的处理操作封装为JavaScript任务放入执行任务队列中,等待JavaScript线程空闲的时候被执行。因此这里形成了另一个话题“浏览器的事件循环”机制,将在后续中详细阐述。

因为在JavaScript语言中,和其他大多数语言不一样之处:JavaScript中耗时的I/O操作都被处理为异步操作,以及回调注册机制。异步和回调仿佛和JavaScript就是“与生俱来”的一样。如Nodejs创始人Ryan Dahl所言,JavaScript语言的非阻塞的异步I/O事件驱动模型,以及JavaScript在Chrome推进下的多次性能优化、具有函数式等高级语言特性,因此最终Nodejs选择JavaScript。由于Nodejs最终选择了JavaScript,从此也大大的推动了JavaScript在非浏览器领域的急速扩展。

下面的文字是来自Nodejs官网:

nodejs-javascript-简介

当然对于非I/O的操作耗时操作如上篇博文《Promise的前世今生和妙用技巧》所说,在HTML5中也提出了新的解决方案,它就是Web Worker。Web Worker就是在当前JavaScript的执行主线程中利用Worker类新开辟一个额外的线程来加载和运行特定的JavaScript文件,这个新的线程和JavaScript的主线程之间并不会互相影响和阻塞执行的;并且在Web Worker中提供这个新线程和JavaScript主线程之间数据交换的接口:postMessage和onMessage事件。但在HTML5 Web Worker中是不能操作DOM的,任何需要操作DOM的任务都需要委托给JavaScript主线程来执行,所以虽然引入HTML5 WebWorker但仍然没有改线JavaScript单线程的本质。对于HTML5的Web Worker和在C# WinForm设计中的BackgroundWorker很类似,对于这类GUI(图形化界面)操作的应用程序中,对于UI界面的操作都需要委托给UI主线程来执行,避免多线程情况下UI操作的安全性和避免不必要的多线程访问控制的复杂度。

浏览器事件循环

在上面已经提到JavaScript中为了不阻塞UI的渲染,很多JavaScript任务都是异步的,它们包括键盘、鼠标I/O输入输出事件、窗口大小的resize事件、定时器(setTimeout、setInterval)事件、Ajax请求网络I/O回调等。当这些异步任务发生的时候,它们将会被放入浏览器的事件任务队列中去。在浏览器内部中存在一个消息循环池,也叫Event Loop(事件循环),JavaScript引擎在运行时后单线程的处理这些事件任务。例如用户在网页中点击了button事件,则它们会被放入在这个事件循环池中,需要等到JavaScript运行时执行线程空闲时候才会按照队列先进先出的原则被一一执行。对于setTimeout这类定时任务也是一样的,只有当定时时刻达到的时候,它们才会被放入浏览器的事件队列中等待被执行;由于此时的JavaScript主线程也许并不空闲,所以它将并不会被JavaScript引擎所立即执行,因为在JavaScript语言设计中setTimeout这类定时任务的执行时间并不是精确的。在前端开发中经常会发现setTimeout(func, 0)很有用,因为这并不是立即执行,而是将当前执行回调函数放入浏览器的事件队列中,等待当前其他任务的完成,然后在执行它;所以setTimeout(func, 0)具有改变当前代码执行顺序的作用,让浏览器有机会完成UI界面渲染等任务后在执行这段回调函数。当然对于老式浏览器这里具有16ms的差距,HTML5规定为4ms,以及关于动画操作中的requestAnimationFrame,请读者参见MDN资料https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

浏览器事件循环如下图所示:

浏览器事件模型

虽然JavaScript是单线程执行的,但是浏览器并不是单线程执行的,它们有JavaScript的执行线程、UI节点的渲染线程,图片等资源的加载线程,以及Ajax请求线程等。在Chrome设计中,为了防止因一个Tab window的奔溃而影响整个浏览器,它的每一个Tab被设计为一个进程;在Chrome设计中存在很多的进程,并利用进程间通讯来完成它们之间的同步,因此这也是Chrome快速的法宝之一。对于Ajax的请求也需要特殊线程来执行,当需要发送一个Ajax请求的时候,浏览器会开辟一个新的线程来执行HTTP的请求,它并不会阻塞JavaScript线程的执行,HTTP请求状态变更事件会被作为回调放入到浏览器的事件队列中等待被执行。

总结

写到这里,本文也进入了尾声。希望这篇文章能给阅读本文的读者一些启发,同时如果本文中存在不足的地方,也希望你能不吝指教。另外,同时也欢迎关注博主的微信公众号[破狼](微信二维码位于博客右侧),这里将会为大家第一时间推送博主的最新博文,谢谢大家的支持和鼓励。

微信订阅号

Promise的前世今生和妙用技巧

浏览器事件模型和回调机制

JavaScript作为单线程运行于浏览器之中,这是每本JavaScript教科书中都会被提到的。同时出于对UI线程操作的安全性考虑,JavaScript和UI线程也处于同一个线程中。因此对于长时间的耗时操作,将会阻塞UI的响应。为了更好的UI体验,应该尽量的避免JavaScript中执行较长耗时的操作(如大量for循环的对象diff等)或者是长时间I/O阻塞的任务。所以在浏览器中的大多数任务都是异步(无阻塞)执行的,例如:鼠标点击事件、窗口大小拖拉事件、定时器触发事件、Ajax完成回调事件等。当每一个异步事件完成时,它都将被放入一个叫做”浏览器事件队列“中的事件池中去。而这些被放在事件池中的任务,将会被javascript引擎单线程处理的一个一个的处理,当在此次处理中再次遇见的异步任务,它们也会被放到事件池中去,等待下一次的tick被处理。另外在HTML5中引入了新的组件-Web Worker,它可以在JavaScript线程以外执行这些任务,而不阻塞当前UI线程。

浏览器中的事件循环模型如下图所示:

浏览器事件模型

由于浏览器的这种内部事件循环机制,所以JavaScript一直以callback回调的方式来处理事件任务。因此无所避免的对于多个的JavaScript异步任务的处理,将会遇见”callback hell“(回调地狱),使得这类代码及其不可读和难易维护。

asyncTask1(data, function (data1){

    asyncTask2(data1, function (data2){

        asyncTask3(data2, function (data3){
                // .... 魔鬼式的金字塔还在继续
        });

    });

});

Promise的横空出世

Promise承诺

因此很多JavaScript牛人开始寻找解决这回调地狱的模式设计,随后Promise(jQuery的deferred也属于Promise范畴)便被引入到了JavaScript的世界。Promise在英语中语义为:”承诺“,它表示如A调用一个长时间任务B的时候,B将返回一个”承诺“给A,A不用关心整个实施的过程,继续做自己的任务;当B实施完成的时候,会通过A,并将执行A之间的预先约定的回调。而deferred在英语中语义为:”延迟“,这也说明promise解决的问题是一种带有延迟的事件,这个事件会被延迟到未来某个合适点再执行。

Promise/A+规范

  • Promise 对象有三种状态: Pending – Promise对象的初始状态,等到任务的完成或者被拒绝;Fulfilled – 任务执行完成并且成功的状态;Rejected – 任务执行完成并且失败的状态;
  • Promise的状态只可能从“Pending”状态转到“Fulfilled”状态或者“Rejected”状态,而且不能逆向转换,同时“Fulfilled”状态和“Rejected”状态也不能相互转换;
  • Promise对象必须实现then方法,then是promise规范的核心,而且then方法也必须返回一个Promise对象,同一个Promise对象可以注册多个then方法,并且回调的执行顺序跟它们的注册顺序一致;
  • then方法接受两个回调函数,它们分别为:成功时的回调和失败时的回调;并且它们分别在:Promise由“Pending”状态转换到“Fulfilled”状态时被调用和在Promise由“Pending”状态转换到“Rejected”状态时被调用。

如下面所示:

promises 流程图

根据Promise/A+规范,我们在文章开始的Promise伪代码就可以转换为如下代码:

asyncTask1(data)
    .then(function(data1){
        return asyncTask2(data1);
    })
    .then(function(data2){
       return asyncTask3(data2);
    })
    // 仍然可以继续then方法

Promise将原来回调地狱中的回调函数,从横向式增加巧妙的变为了纵向增长。以链式的风格,纵向的书写,使得代码更加的可读和易于维护。

Promise在JavaScript的世界中逐渐的被大家所接受,所以在ES6的标准版中已经引入了Promise的规范了。现在通过Babel,可以完全放心的引入产品环境之中了。

另外,对于解决这类异步任务的方式,在ES7中将会引入async、await两个关键字,以同步的方式来书写异步的任务,它被誉为”JavaScript异步处理的终极方案“。这两个关键字是ES6标准中生成器(generator)和Promise的组合新语法,内置generator的执行器的一种方式。当然async、await的讲解并不会在本文中展开,有兴趣的读者可以参见MDN资料

Promise的妙用

如上所说Promise在处理异步回调或者是延迟执行任务时候,是一个不错的选择方案。下面我们将介绍一些Promise的使用技巧(下面将利用Angular的$q$http为例,当然对于jQuery的deferred,ES6的Promise仍然实用):

多个异步任务的串行处理

在上文中提到的回调地狱案例,就是一种试图去将多个异步的任务串行处理的结果,使得代码不断的横向延伸,可读性和维护性急剧下降。当然我们也提到了Promise利用链式和延迟执行模型,将代码从横向延伸拉回了纵向增长。使用Angular中$http的实现如下:

$http.get('/demo1')
 .then(function(data){
     console.log('demo1', data);
     return $http.get('/demo2', {params: data.result});
  })
 .then(function(data){
     console.log('demo2', data);
     return $http.get('/demo3', {params: data.result});
  })
 .then(function(data){
     console.log('demo3', data.result);
  });

因为Promise是可以传递的,可以继续then方法延续下去,也可以在纵向扩展的途中改变为其他Promise或者数据。所以在例子中的$http也可以换为其他的Promise(如$timeout$resource …)。

多个异步任务的并行处理

在有些场景下,我们所要处理的多个异步任务之间并没有像上例中的那么强的依赖关系,只需要在这一系列的异步任务全部完成的时候执行一些特定逻辑。这个时候为了性能的考虑等,我们不需要将它们都串行起来执行,并行执行它们将是一个最优的选择。如果仍然采用回调函数,则这是一个非常恼人的问题。利用Promise则同样可以优雅的解决它:

$q.all([$http.get('/demo1'),
        $http.get('/demo2'),
        $http.get('/demo3')
])
.then(function(results){
    console.log('result 1', results[0]);
    console.log('result 2', results[1]);
    console.log('result 3', results[2]);
});

这样就可以等到一堆异步的任务完成后,在执行特定的业务回调了。在Angular中的路由机制ngRouteuiRoute的resolve机制也是采用同样的原理:在路由执行的时候,会将获取模板的Promise、获取所有resolve数据的Promise都拼接在一起,同时并行的获取它们,然后等待它们都结束的时候,才开始初始化ng-viewui-view指令的scope对象,以及compile模板节点,并插入页面DOM中,完成一次路由的跳转并且切换了View,将静态的HTML模板变为动态的网页展示出来。

Angular路由机制的伪代码如下:

    var getTemplatePromise = function(options) {
         // ... 拼接所有template或者templateUrl
    };

    var getResolvePromises = function(resolves) {
        // ... 拼接所有resolve
    };

    var controllerLoader = function(options, currentScope, tplAndVars, initLocals) {
        // ...

        ctrlInstance = $controller(options.controller, ctrlLocals);
        if (options.controllerAs) {
            currentScope[options.controllerAs] = ctrlInstance;
        }

        // ...

        return currentScope;
    };

    var templateAndResolvePromise = $q.all([getTemplatePromise(options)].concat(getResolvePromises(options.resolve || {})));

    return templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
        var currentScope = currentScope || $rootScope.$new();
        controllerLoader(options, currentScope, tplAndVars, initLocals);
        // ... compile & append dom 
    });

对于这类路由机制的使用,在博主上篇博文《自定义Angular插件 – 网站用户引导》中的ng-trainning插件中也采用了它。关于这段代码的具体分析和应用将在后续单独的文章中,敬请大家期待。

对于同步数据的Promise处理,统一调用接口

有了Promise的处理,因为在前端代码中最多的异步处理就是Ajax,它们都被包装为了Promise .then的风格。那么对于一部分同步的非异步处理呢?如localStorage、setTimeout、setInterval之类的方法。在大多数情况下,博主仍然推荐使用Promise的方式包装,使得项目Service的返回接口统一。这样也便于像上例中的多个异步任务的串行、并行处理。在Angular路由中对于只设置template的情况,也是这么处理的。

对于setTimeout、setInterval在Angular中都已经为我们内置了$timeout和$interval服务,它们就是一种Promise的封装。对于localStorage呢?可以采用$q.when方法来直接包装localStorage的返回值的为Promise接口,如下所示:

    var data = $window.localStorage.getItem('data-api-key');
    return $q.when(data);

整个项目的Service层的返回值都可以被封装为统一的风格使用了,项目变得更加的一致和统一。在需要多个Service同时并行或者串行处理的时候,也变得简单了,一致的使用方式。

对于延迟任务的Promise DSL语义化封装

在前面已经提到Promise是延迟到未来执行某些特定任务,在调用时候则给消费者返回一个”承诺“,消费者线程并不会被阻塞。在消费者接受到”承诺“之后,消费者就不用再关心这些任务是如何完成的,以及督促生产者的任务执行状态等。直到任务完成后,消费者手中的这个”承诺“就被兑现了。

对于这类延迟机制,在前端的UI交互中也是极其常见的。比如模态窗口的显示,对于用户在模态窗口中的交互结果并不可提前预知的,用户是点击”ok“按钮,或者是”cancel“按钮,这是一个未来将会发生的延迟事件。对于这类场景的处理,也是Promise所擅长的领域。在Angular-UI的Bootstrap的modal的实现也是基于Promise的封装。

$modal.open({
    templateUrl: '/templates/modal.html',
    controller: 'ModalController',
    controllerAs: 'modal',
    resolve: {
    }
})
    .result
    .then(function ok(data) {
        // 用户点击ok按钮事件
    }, function cancel(){
        // 用户点击cancel按钮事件
    });

这是因为modal在open方法的返回值中给了我们一个Promise的result对象(承诺)。等到用户在模态窗口中点击了ok按钮,则Bootstrap会使用$qdeferresolve来执行ok事件;相反,如果用户点击了cancel按钮,则会使用$qdeferreject执行cancel事件。

这样就很好的解决了延迟触发的问题,也避免了callback的地狱。我们仍然可以进一步将其返回值语义化,以业务自有的术语命名而形成一套DSL API。

 function open(data){
    var defer = $q.defer();

    // resolve or reject defer;

    var promise = defer.promise;
    promise.ok = function(func){
        promise.then(func);
        return promise;
    };

    promise.cancel = function(func){
        promise.then(null, func);
        return promise;
    };

    return promise;
};

则我们可以如下方式来访问它:

$modal.open(item)
   .ok(function(data){
        // ok逻辑
   })
   .cancel(function(data){
       // cancel 逻辑
   });

是不是感觉更具有语义呢?在Angular中$http的返回方法success、error也是同样逻辑的封装。将success的注册函数注册为.then方法的成功回调,error的注册方法注册为then方法的失败回调。所以success和error方法只是Angular框架为我们在Promise语法之上封装的一套语法糖而已。

Angular的success、error回调的实现代码:

  promise.success = function(fn) {
    promise.then(function(response) {
      fn(response.data, response.status, response.headers, config);
    });
    return promise;
  };

  promise.error = function(fn) {
    promise.then(null, function(response) {
      fn(response.data, response.status, response.headers, config);
    });
    return promise;
  };

利用Promise来实现管道式AOP拦截

在软件设计中,AOP是Aspect-Oriented Programming的缩写,意为:面向切面编程。通过编译时(Compile)植入代码、运行期(Runtime)动态代理、以及框架提供管道式执行等策略实现程序通用功能与业务模块的分离,统一处理、维护的一种解耦设计。 AOP是OOP的延续,是软件开发中的一个热点,也是很多服务端框架(如Java世界的Spring)中的核心内容之一,是函数式编程的一种衍生范型。 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。 AOP实用的场景主要有:权限控制、日志模块、事务处理、性能统计、异常处理等独立、通用的非业务模块。关于更多的AOP资料请参考http://en.wikipedia.org/wiki/Aspect-oriented_programming

在Angular中同样也内置了一些AOP的设计思想,便于实现程序通用功能与业务模块的分离、解耦、统一处理和维护。$http中的拦截器(interceptors)和装饰器($provide.decorator)是Angular中两类常见的AOP切入点。前者以管道式执行策略实现,而后者则通过运行时的Promise管道动态实现的。

首先回顾一下Angular的拦截器实现方式:

// 注册一个拦截器服务
$provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
  return {
    // 可选方法
    'request': function(config) {
      // 请求成功后处理
      return config;
    },

    // 可选方法
   'requestError': function(rejection) {
      // 请求失败后的处理
      if (canRecover(rejection)) {
        return responseOrNewPromise
      }
      return $q.reject(rejection);
    },



    // 可选方法
    'response': function(response) {
      // 返回回城处理
      return response;
    },

    // 可选方法
   'responseError': function(rejection) {
      // 返回失败的处理
      if (canRecover(rejection)) {
        return responseOrNewPromise
      }
      return $q.reject(rejection);
    }
  };
});

// 将服务注册到拦截器链中
$httpProvider.interceptors.push('myHttpInterceptor');


// 同样也可以将拦截器注册为一个工厂方法。 但上一中方式更为推荐。
$httpProvider.interceptors.push(['$q', function($q) {
  return {
   'request': function(config) {
       // 同上
    },

    'response': function(response) {
       // 同上
    }
  };
}]);

这样就可以实现对Angular中的$http或者是$resource的Ajax请求拦截了。但在Angular内部是是如何实现这种拦截方式的呢?Angular使用的就是Promise机制,形成异步管道流,将真实的Ajax请求放置在request、requestError和response、responseError的管道中间,因此就产生了对Ajax请求的拦截。

其源码实现如下:

var interceptorFactories = this.interceptors = [];

var responseInterceptorFactories = this.responseInterceptors = [];

this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
  function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {

    var defaultCache = $cacheFactory('$http');

    var reversedInterceptors = [];

    forEach(interceptorFactories, function(interceptorFactory) {
      reversedInterceptors.unshift(isString(interceptorFactory) ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
    });

    forEach(responseInterceptorFactories, function(interceptorFactory, index) {
      var responseFn = isString(interceptorFactory) ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory);

      reversedInterceptors.splice(index, 0, {
        response: function(response) {
          return responseFn($q.when(response));
        },
        responseError: function(response) {
          return responseFn($q.reject(response));
        }
      });
    });
    ...

function $http(requestConfig) {
  ...

  var chain = [serverRequest, undefined];
  var promise = $q.when(config);

  // apply interceptors
  forEach(reversedInterceptors, function(interceptor) {
    if (interceptor.request || interceptor.requestError) {
      chain.unshift(interceptor.request, interceptor.requestError);
    }
    if (interceptor.response || interceptor.responseError) {
      chain.push(interceptor.response, interceptor.responseError);
    }
  });

  while (chain.length) {
    var thenFn = chain.shift();
    var rejectFn = chain.shift();

    promise = promise.then(thenFn, rejectFn);
  }

  promise.success = function(fn) {
    promise.then(function(response) {
      fn(response.data, response.status, response.headers, config);
    });
    return promise;
  };

  promise.error = function(fn) {
    promise.then(null, function(response) {
      fn(response.data, response.status, response.headers, config);
    });
    return promise;
  };

  return promise;
};

在上面紧接着在$get注入方法之后,Angular会将interceptorsresponseInterceptors反转合并到一个reversedInterceptors的拦截器内部变量中保存。最后在$http函数中以[serverRequest, undefined]serverRequest是Ajax请求的Promise操作)为中心,将reversedInterceptors中的所有拦截器函数依次加入到chain链式数组中。如果是request或requestError,那么就放在链式数组起始位置;相反如果是response或responseError,那么就放在链式数组最后。

注意添加在chain的request和requestError或者response和responseError都一定是成对的,换句话说可能注册一个非空的request与一个为undefined的requestError,或者是一个为undefined的request与非空的requestError。就像chain数组的声明一样(var chain = [serverRequest, undefined];),成对的放入serverRequest和undefined对象到数组中。因为后面的代码将利用Promise的机制注册这些拦截器函数,实现管道式AOP拦截机制。

在Promise中需要两个函数来注册回调,分别为成功回调和失败回调。在这里request和response会被注册成Promise的成功回调,而requestError和responseError则会注册成Promise的失败回调。所以在chain中添加的request和requestError,response或responseError都是成对出现的,这是为了能在接下来的循环中简洁地注册Promise回调函数。 这些被注册的拦截器链,会通过$q.when(config) Promise启动,它会首先传入$http的config对象,并执行所有的request拦截器,依次再到serverRequest这个Ajax请求,此时将挂起后边所有的response拦截器,直到Ajax请求响应完成,再依次执行剩下的response拦截器回调; 如果在request过程中有异常失败则会执行后边的requestError拦截器,对于Ajax请求的失败或者处理Ajax的response拦截器的异常也会被后面注册的responseError拦截器捕获。

从最后两段代码也能了解到关于$http服务中的success方法和error方法,是Angular为大家提供了一种Promise的便捷写法。success方法是注册一个传入的成功回调和为undefined的错误回调,而error则是注册一个为null的成功回调和一个传入的失败回调。

总结

写到这里,本文也进入了尾声。希望大家能够对Promise有一定的理解,并能够”信手拈来“的运用于实际的项目之中,增强代码的可读性和可维护性。在本文中所用到的例子,你都可以在博主的jsbinhttp://jsbin.com/bayeva/edit?html,js,output中找到它们。

另外,同时也欢迎关注博主的微信公众号[破狼](微信二维码位于博客右侧),这里将会为大家第一时间推送博主的最新博文,谢谢大家的支持和鼓励。

自定义Angular插件 - 网站用户引导

最近由于项目进行了较大的改版,为了让用户能够适应这次新的改版,因此在系统中引入了“用户引导”功能,对于初次进入系统的用户一些简单的使用培训training。对于大多数网站来说,这是一个很常见的功能。所以在开发这个任务之前,博主尝试将其抽象化,独立于现有系统的业务逻辑,将其封装为一个通用的插件,使得代码更容易扩展和维护。

无图无真相,先上图:

training demo

关于这款trainning插件的使用很简单,它采用了类似Angular路由一样的配置,只需要简单的配置其每一步training信息。

  • title:step的标题信息;
  • template/templateUrl: step的内容模板信息。这类可以配置html元素,或者是模板的url地址,同时templateUrl也支持Angular route一样的function语法;
  • controller: step的控制器配置;在controller中可注入如下参数:当前step – currentStep、所有step的配置 – trainnings、当前step的配置 – currentTrainning、以及下一步的操作回调 – trainningInstance(其中nextStep:为下一步的回调,cancel为取消用户引导回调);
  • controllerAs: controller的别名;
  • resolve:在controller初始化前的数据配置,同Angular路由中的resolve;
  • locals:本地变量,和resolve相似,可以传递到controller中。区别之处在于它不支持function调用,对于常量书写会比resolve更方便;
  • placement: step容器上三角箭头的显示方位;
  • position: step容器的具体显示位置,这是一个绝对坐标;可以传递{left: 100, top: 100}的绝对坐标,也可以是#stepPanelHost配置相对于此元素的placement位置。同时它也支持自定义function和注入Angular的其他组件语法。并且默认可注入:所有step配置 – trainnings,当前步骤 – step,当前step的配置 – currentTrainning,以及step容器节点 – stepPanel;
  • backdrop:是否需要显示遮罩层,默认显示,除非显示声明为false配置,则不会显示遮罩层;
  • stepClass:每一个step容器的样式信息;
  • backdropClass: 每一个遮罩层的样式信息。

了解了这些配置后,并根据特定需求定制化整个用户引导的配置信息后,我们就可以使用trainningService的trainning方法来在特定实际启动用户引导,传入参数为每一步step的配置信息。并可以注册其done或者cancel事件:

trainningService.trainning(trainningCourses.courses)
    .done(function() {
        vm.isDone = true;
    });

下面是一个演示的配置信息:

    .constant('trainningCourses', {
                courses: [{
                    title: 'Step 1:',
                    templateUrl: 'trainning-content.html',
                    controller: 'StepPanelController',
                    controllerAs: 'stepPanel',
                    placement: 'left',
                    position: '#blogControl'
                },{
                    title: 'Step 3:',
                    templateUrl: 'trainning-content.html',
                    controller: 'StepPanelController',
                    controllerAs: 'stepPanel',
                    placement: 'top',
                    position: {
                        top: 200,
                        left: 100
                    }
                },
                    ...
                {
                    stepClass: 'last-step',
                    backdropClass: 'last-backdrop',
                    templateUrl: 'trainning-content-done.html',
                    controller: 'StepPanelController',
                    controllerAs: 'stepPanel',
                    position: ['$window', 'stepPanel', function($window, stepPanel) {
                        // 自定义函数,使其屏幕居中
                        var win = angular.element($window);
                        return {
                            top: (win.height() - stepPanel.height()) / 2,
                            left: (win.width() - stepPanel.width()) / 2
                        }
                    }]
                }]
            })

本文插件源码和演示效果唯一codepen上,效果如下:

See the Pen ng-trainning by green (@greengerong) on CodePen.

在trainning插件的源码设计中,包含如下几个要点:

  • 提供service api。因为关于trainning这个插件,它是一个全局的插件,正好在Angular中所有的service也是单例的,所以将用户引导逻辑封装到Angular的service中是一个不错的设计。但对于trainning的每一步展示内容信息则是DOM操作,在Angular的处理中它不该存在于service中,最佳的方式是应该把他封装到Directive中。所以这里采用Directive的定义,并在service中compile,然后append到body中。
  • 对于每一个这类独立的插件应该封装一个独立的scope,这样便于在后续的销毁,以及不会与现有的scope变量的冲突。
  • $q对延时触发的结果包装。对于像该trainning插件或者modal这类操作结果采用promise的封装,是一个不错的选择。它取代了回调参数的复杂性,并以流畅API的方式展现,更利于代码的可读性。同时也能与其他Angular service统一返回API。
  • 对于controller、controllerAs、resolve、template、templateUrl这类类似路由的处理代码,完全可以移到到你的同类插件中去。它们可以增加插件的更多定制化扩展。关于这部分代码的解释,博主将会在后续文章中为大家推送。
  • 利用$injector.invoke动态注入和调用Angular service,这样既能获取Angular其他service注入的扩展性,也能获取到函数的动态性。如上例中的屏幕居中的自定义扩展方式。

这类设计要点,同样可以运用到想modal、alert、overload这类全局插件中。有兴趣的读者,你可以在博主的codepen笔记中阅读这段代码http://codepen.io/greengerong/pen/pjwXQW#0

smartcrop.js智能图片裁剪库

今天将为大家介绍一款近期github上很不错的开源库 – smartcrop.js。它是一款图片处理的智能裁剪库。在很多项目开发中,经常会遇见上传图片的场景,它可能是用户照片信息,也可能是商品图片等。然而在网页布局中,为了更好的用户体验,它们往往都需要一些宽度和高度的限制。对于不合适的图片,常常需要为用户提供一种裁剪方式,以此来满足网站更好的用户体验。但是图片默认的裁剪区域往往被显示在一个固定的位置,而这个位置却往往又不是精准的用户裁剪位置。因此今天为大家介绍的这一款开源库,就是为了解决这类问题,并为用户提供更好的用户体验的。

首先我们可以使用npm install smartcrop或者bower install smartcrop来下载它。然后像如下方式使用它:

SmartCrop.crop(image, {
        width: 100,
        height: 100
    }, 
    function(result){
        console.log(result); // {topCrop: {x: 300, y: 200, height: 200, width: 200}}
    });

它会输出一个比较好的最佳图片裁剪位置,如{topCrop: {x: 300, y: 200, height: 200, width: 200}}的数据。

下面是一副来自它的展示网站的案例,请欣赏:

smartcrop-图片裁剪-案例

更多案例:

  1. http://29a.ch/sandbox/2014/smartcrop/examples/testsuite.html:这里拥有超过1000个图片效果的展示(流量用户请谨慎点击,图片众多);
  2. http://29a.ch/sandbox/2014/smartcrop/examples/testbed.html:这里允许上传本地的图片,并体验其效果;
  3. http://29a.ch/sandbox/2014/smartcrop/examples/slideshow.html:在这里可以尝试用它创建幻灯片。

最后,更多关于smartcrop.js的信息,请参见其github:https://github.com/jwagner/smartcrop.js