【译】你应该使用哪个JavaScript测试库?QUnit vs Jasmine vs Mocha

不论你是在写浏览器端javascript还是后端的nodejs,总存在那么一个问题:我该使用什么单元测试库去确保我的代码如预期的运行呢?有一些流行的框架可供选择。如果你正在考虑Qunit、Jasmine或者Mocha,那么恰好我这有一些他们的优缺点信息介绍,你可能感兴趣。

qunit_logo.png

qunit肯定是我列表中最古老的三个,2008年第一次正式发布。因此,多年来它已经获得了良好的用户基础。它在jQuery中流行使用,并有很多地方有很好的支持。

它发展的如何呢?真的不让人满意...

优点:

从Q&A到CI服务器有很多地方支持

缺点:

缺乏流利的语法

配置很头痛,必须不断维护
使得包括第三方库(如断言库)相对困难
异步测试可能有点头痛
没有与原生的命令行支持

Jasmine.png

Jasmine稍微更新一些,在qUnit之后2年发布。它已经有足够的时间成熟,同时还学习了其他JavaScript测试框架的经验教训。 它的建立是很容易设置和使用在几乎任何情况下。在大多数情况下在大多数情况下它需要一个运行者,如Karma或Chutzpah,但一些发行版(如jasmine-node)有内建的runner。

它怎么样? 这对于大多数情况下是你可能想要的,异步代码是主要的问题。

优点:

对于node来说通过jasmine-node是很好安装的

运行测试无需浏览器用户界面
内置流畅漂亮的语法,完美兼容其它测试库
有许多CI服务器(如TeamCityp,CodeShip等)和一些本身不支持插件的服务器支持(jenkins有一个Maven插件)
可描述性的BDD范例

缺点:

异步测试可能有点头痛

所有测试文件都有个确切的后缀(默认*spec.js)

mock.png

Mocha是专为测试nodeJs模块而开发的,它是2012年发布的第一个主要版本,它的API与Jasmine的相似,添加了一些语法糖,使其具有更广的应用场景,如BDD。内建有runner,所以你不用担心runner了。它与Jasmine不同,它对测试异步方法有非常好的支持,使用done()函数:如果你的测试使用它,测试不会传递直到done()被调用,如果它不使用它,当测试到达测试方法结束时,测试将通过。

我的印象?非常适合我!

优点:

易于安装

运行测试无需浏览器用户界面
允许任何能够抛出失败异常测试库的使用
部分CI服务器和其它插件的支持
功能上更多是面向行为驱动开发或者行为面向测试驱动开发
高扩展性
轻而易举的进行异步测试

缺点:

较新的领域,因此在某些领域可能缺乏支持

TL;DR

看看上面的三个框架,我的选择是明确的:Mocha和其他相比最灵活、最可用和最简单的配置。开箱即用,它提供了你需要的一切,并如此优雅。

如果你有兴趣看到这三个框架的例子,请查看我在研究时创建的repo:
https://github.com/thegrtman/javascript-test-framework-comparison

原文地址

动画那点事

先说说前端动画种类吧:CSS动画,JS动画,Canvas动画,SVG动画、PNG动画。
注:这里我们讨论的动画都为2D动画。

衡量动画的标准
衡量动画性能高低的是“帧数”,在动画进行过程中,每一幅静止的画面就叫一帧,所以说帧数越高,动画效果看起来越流畅。一般来说显示器的帧数是60HZ,50-60Hz是效果最好的,如果20Hz以下就会有可能感到卡顿。但是一般帧数低点没关系,毕竟硬件条件总是有限的,但是最怕丢帧,经常玩游戏的同学可能体会比较深。

一、CSS动画

1.Transition属性

顾名思义Transition(过渡),从开始状态过渡到结束状态。
特点:

  • 能够很方便的制作简单的放大缩小旋转的动画,注意一个transition规则,只能定义一个属性的变化(all属性除外)。
  • 需要事件触发,且每次触发只能执行一次。
  • 只能定义开始状态和结束状态,不能定义中间状态,而且状态必须是具体数值,不能为auto或none。

2.Animation属性

animation相比Transition多了关键帧概念,弥补了Transition动画只有开始结束两个状态的不足。此外animation还可以定义循环播放及循环次数,动画播放方向等。
我们需要先通过 @keyframes 定义关键帧的,然后在animation属性引用关键帧的名称

div:hover {
  animation: 1s keyframesName;
}
@keyframes keyframesName {
  0% { background-color: yellow; }
  100% { background: blue; }
}

用CSS就不得不提兼容性,transition和animation都支持IE10+,总的来说还算不错了。

二、CSS动画优化

首先回顾一下页面绘制的过程:

构造DOM模型/解析CSS规则 → 生成渲染树 → 排版/重排Layout → 绘图/重绘Painting

1486146410achieve-60-fps-mobile-animations-css3-01.png

如果动画中的CSS导致整个页面重新绘制(比如 left/top/right/bottom 属性的转换),就可能带来潜在的性能问题。对于哪些CSS属性会使浏览器生重排或重绘,不同内核有不同的结果,具体可以参考CSS Triggers

CSS动画优化主要就是靠GPU加速。当浏览器某个DOM元素开启硬件加速之后,它会为此元素单独创建一个“层”。当有单独的层之后,此元素的repaint操作将只需要更新自己,不会影响到其他元素,减小了重绘面积也就提高了动画性能。

<p data-height="500" data-theme-id="21453" data-slug-hash="secwi" data-user="njmcode" data-embed-version="2" data-pen-title="SVG and CSS animation performance" class="codepen">See the Pen SVG and CSS animation performance by Neil McCallion (@njmcode) on CodePen.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js";></script>

观察上面例子,在Chrome中打开 Rendering 菜单,然后勾选Painting FlashLayer Border选项,触发浏览器重绘的区域就会被绿色覆盖,而浏览器创建的硬件加速层会被棕色边框圈起来。

层创建标准

3D 或透视变换(perspective transform) CSS 属性
使用will-change 属性的元素

使用加速视频解码的元素
拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
混合插件(如 Flash)
对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素
拥有加速 CSS 过滤器的元素
元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
元素有一个 z-index 较低且有在一个复合层渲染的兄弟元素(换句话说就是该元素在复合层上面渲染,这一点需要特别注意!)

需要注意的是层的创建和更新也是消耗资源的,而且CPU和GPU之前的总线带宽也是一定的,所以太多的层也会适得其反,而且不仅仅是卡顿,还可能会将页面卡死。

使用will-change的注意事项:

will-change的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。
动画开始之前要提前添加will-change属性,动画结束时,will-change 的值要及时被更新为 auto,实际操作时我们会根据Hover事件添加will-change。
IE所有版本包括Edge都不支持will-change属性。

三、JS动画

JS实现动画动画效果其实就是定时修改CSS属性,所以我们肯定首先想到用setTimeout,每隔16.667ms触发一次重绘逻辑。
使用setTimeout的一些问题,因为浏览器是单线程的,JS定时器的执行时间并不会很准确,所以帧数会抖动。

再考虑一个问题,如果我们设置每10ms执行一次setTimeout会怎样。
IC550966.png
动画过度绘制,之前我们提到过显示器一般为60Hz,即每 16.7ms 刷新一次,显示器在一个周期内只能显示最后一帧,在图中的方框内出现了两帧就会导致前一帧丢失。而且还会浪费 CPU 周期以及消耗额外的电能等问题。更重要的是切换到其他选项卡或浏览器已最小化时,动画依然会执行。

为了解决上述问题,W3C推出了requestAnimationFrame方法,从根本上解决了动画触发的时序控制。

<p data-height="300" data-theme-id="21453" data-slug-hash="RobmMz" data-default-tab="js,result" data-user="zhangchen915" data-embed-version="2" data-pen-title="requestAnimationFrame 测试" class="codepen">See the Pen requestAnimationFrame 测试 by zhangchen (@zhangchen915) on CodePen.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js";></script>

从此测试结果可以看出requestAnimationFrame一般小于5000m一点,而setTimeout总是大于requestAnimationFrame用时且大于5000ms。

四、PNG动画

你一定会问,PNG怎么可以制作动画?其实很简单,动画不是由帧构成的吗,那我在一张PNG图里放上动画所有的帧,然后像跑马灯一样切换每一帧,画面就动起来了。所以,这种动画的适用场景也非常有限,但是对于非常复杂而且面积较小的动画不失为一种高效的解决办法。
实现方法就是利用了animation-timing-function:steps()

在阿里云首页中就用到了这种动画方式
GIF.gif

动画的每一帧都是由类似下面的一张张图片做成的
ali.png

下面的例子是Twitter的“喜欢”按钮:
<p data-height="265" data-theme-id="0" data-slug-hash="LpXVJb" data-default-tab="css,result" data-user="yisi" data-embed-version="2" data-pen-title="Twitter heart button animation" class="codepen">See the Pen Twitter heart button animation by 一丝 (@yisi) on CodePen.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js";></script>
可以看下SVG的实现方式,使用了第三方组件库,还需要JS控制,对于快速迭代的前端来说项目复杂度和开发时间都是必须考虑在内的,显然为一个动画显然不值得,值得一提的是这个效果还有利用box-shadows实现的纯CSS版本,读者可以自行搜索。

五、SVG动画

SVG是使用XML文档描述来绘图,所以当内联进HTML时可以像操作DOM一样操作SVG,同时也这也说明SVG可以借助CSS和JS实现更多交互性动画,再加上SVG自身也支持一些动画属性和图形效果(如裁剪和遮罩、背景虚化模式和滤镜等等),所以SVG做出来的效果也不容小觑。
从这点来看:SVG更适合用来做动态交互,而且SVG绘图很容易编辑,只需要增加或移除相应的元素就可以了。

svg基于路径,所以非常适合描边这种的动画。

GIF.gif

主要应用:
代替字体图标、文字特效、图标动画(如loading效果)、描边动画
但需要注意的是,在安卓4.4以下对svg的支持并不好。

六、Canvas动画

Canvas是基于像素的,只能通过脚本驱动。
之前你一定看过一些网站的背景动画,比如知乎登录页的背景,其实就是用Canvas实现的。

IC628910.png

一般显示器大小差别都不大,所以主要差别在于对象数。所以Canvas非常绘制适合复杂的动画,所以地图、图表类应用(如谷歌地图、百度Echart)都采用的Canvas实现。

关于Svg和Canvas可以去CodePen搜索,可以很直观的感受二者应用场景的区别。

七、GIF/webP/webM动画

千万别一谈动画就想代码,代码是做出来是很厉害。
很多时候在快速的业务迭代下,我们需要在实现成本、效果和性能之间做出平衡。

如果有你是炉石玩家,并经逛官方论坛的话,你一定见过下面的效果。

<video id="headerFlash" autoplay="autoplay" loop="true" preload="auto" width="500" height="124">

        <source src="http://hearthstone.nos.netease.com/3/home/20151114/header_v2.webm" type="video/webm">
        <source src="http://hearthstone.nos.netease.com/3/home/20151114/header_v2.mp4" type="video/mp4">
</video>

这其实就是直接用的 webM 实现的,当然 webM 存在一些兼容,但是我们为不兼容的浏览器提供 mp4 格式的动画。实现方式可以直接看这里的源码。

参考阅读:
Accelerated Rendering in Chrome
CSS3硬件加速也有坑
will-change
Web动画性能指南
CSS动画的性能优化
基于脚本的动画的计时控制(“requestAnimationFrame”)
SVG 与 Canvas:如何选择

图片懒加载实践

图片再怎么压缩,也不能从根本上解决问题,所以我们需要一个终极解决方案——懒加载,只要看不到的图片都不加载。这样就极大的提高了首屏加载速度,同时也节省了大量流量。所以目前大型网站几乎都用到了懒加载技术,懒加载思路也很简单,监听resize和scroll事件,判断img是否在视窗内就可以了。

一、使用 getBoundingClientRect

getBoundingClientRect用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置,该函数返回一个Object对象,该对象有6个属性:top,lef,right,bottom,width,height

var lazy = document.getElementsByClassName('lazy');

window.addEventListener('load', lazyLoad);
window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);

function lazyLoad(){
    for(var i=0; i<lazy.length; i++){
        if(isInViewport(lazy[i])){
            if (lazy[i].getAttribute('data-src')){
                lazy[i].src = lazy[i].getAttribute('data-src');
                lazy[i].removeAttribute('data-src');
            }
        }
    }
    
    cleanLazy();
}

function cleanLazy(){
    lazy = lazy.filter(function(l){ return l.getAttribute('data-src');});
}

function isInViewport(el){
    var rect = el.getBoundingClientRect();
    
    return (
        rect.bottom >= 0 && 
        rect.right >= 0 && 
        rect.top <= (window.innerHeight || document.documentElement.clientHeight) && 
        rect.left <= (window.innerWidth || document.documentElement.clientWidth)
     );
}

二、使用 IntersectionObserver

虽然之前我们通过getBoundingClientRect达到了计算img元素是否在视口内的效果,但是这种实现方式性能极差,每次调用 getBoundingClientRect() 都会强制浏览器重新计算整个页面的布局,而且对于在iframe里的元素,getBoundingClientRect就根本无能为力了。

所以 IntersectionObserver API 出现了,它能够能轻松决监视某个元素是否滚动进了视口或者滚动进了它的某个祖先元素的可视区域内,而且解决了性能问题,还无需监听滚动事件。

IntersectionObserver 可以异步观察目标元素与祖先元素或视口的交集变化。

let img = document.querySelector('img');
const IntersectionObserver =  new IntersectionObserver(entries => { 
  console.log('我出现了') 
},{
  root: null,               // 用于计算相交区域的根元素,默认使用最上级文档的视口
  threshold: [0, 0.5, 1],   // 触发回调函数的临界值,用 0 ~ 1 的比率指定,也可以是一个数组。
  rootMargin: "50px, 0px"
});

IntersectionObserve.observe(img);
//其他三个实例方法
//IntersectionObserve.unobserve()
//IntersectionObserve.disconnect()
//IntersectionObserve.takeRecords()

当然功能强大意味着兼容性就不好,目前只能在Chrome51以上使用,就目前来说我们可以使用lazyload.js插件,能在性能和兼容上做一个折中。

拓展阅读
1.IntersectionObserver’s Coming into View
2.IntersectionObserver API

webp实践

现在网站消耗流量最大的也就是图片了,而解决图片的体积过大的终极解决方案就是Webp图片了,虽然目前浏览器中只有chrome内核支持webp,但是考虑到chrome的用户量,我们也值得单独为chrome提供webp图片。

由于我们使用的是又拍云存储图片,所以最简单的办法就是使用又拍云提供的图片处理功能,直接在链接后加上 !/format/webp,又拍云就会提供转换后的图片。还有一个支持让浏览器支持webp的插件webpjs,但是这个插件性能很差,所以并不推荐使用。

所以我写了一个简单的指令替换图片的src属性

app.directive('imgWebp', function () {
    return {
        restrict: 'AE',
        link: function ($scope, $element, $attrs) {
            $attrs.$observe('imgWebp', function (value) {
                if (supportWebP) {
                    $element[0].setAttribute('src', value + '!/format/webp');
                } else {
                    $element[0].setAttribute('src', value);
                }
            });
        }
    };
});

关键点在于如何检测浏览器对webp的支持,简单的方法可以直接根据浏览器的UV来判断,但是最稳妥的办法还是对浏览器做特征检测。

var WebP = new Image();
        WebP.onload = WebP.onerror = function(){
            window.supportWebP = WebP.height == 1;
        };
WebP.src = "data:image/webp;base64,UklGRiYAAABXRUJQVlA4IBoAAAAwAQCdASoBAAEAAAAMJaQAA3AA/v89WAAAAA==";

原理也很简单加载一个1×1的wenp图片,然后在加载完成时检测图盘的高度是否为1.

Angular 中的 $watch , $watchGroup() 与 $watchCollection()

一、$watch

在Angular中大部分指令都依靠watch函数来监听Model变化,watch也是Angular的“脏检查”的核心之一。

$watch(watchExp, listener, objectEquality)

1.watchExp 很灵活,可以是一个$scope上的一个属性名,也可以是一个函数或一个字符串形式的表达式;
2.listener 是检测到变化时执行的函数,这个函数接受 newValue, oldValue 两个参数;
3.objectEquality 是一个可选布尔值,决定比较的深浅,默认是false,此时 watch 使用 === 进行浅比较,所以它对数组或 Object 进行比较是检查的是引用,也叫导致了数组或对象内容改变是并不会触发回调,同时两个内容相同的表达式也会判定为不同。当此项设置为 true 是,watch 会用 angular.equals() 进行深比较,这种比较方法会遍历数组或对象,这也导致了性能会比较差。

二、$watchGroup() 与 $watchCollection()

因为 watch 深比较性能较差,所以 Angular 还提供了 $watchGroup([watchExp], listener)$watchCollection(obj, listener) 方法来分别监听数组和对象。
$watchGroup 其实是使用 watch 监听一组 watchExp ,所以 watchGroup 不支持深比较
$watchCollection 比 $watch 进一步,但是基于性能考虑它只向内关注 1 层,对数组重新赋值,或是对数组元素进行新增、删除、修改时,回调会被调用,注意只要是修改就会调用,如果给数组赋的值和之前一样也会触发回调。如果某个数组元素内部的某个属性被更新时,回调不会被调用。

参考阅读:
1.在 AngularJS 中用 $watch / $watchCollection 监听数组变化的研究
2.Angular文档:$rootScope.Scope
3.How to use a $watchGroup with object equality or deep-watch the properties in the group?