移动Web技术的春天来了?Facebook发布React Native,用JavaScript开发移动原生应用

几个小时前, React.js Conf 2015会议上,Facebook发布了React Native,可以基于目前大热的开源JavaScript库React.js来开发iOS和Android原生App。而且React Native已经用于生产环境——Facebook Groups iOS应用就是基于它开发的。Facebook也已确认,这个项目很快将会开源。

根据ProgVille的文章,React Native的原理是,在JavaScript中用React抽象操作系统原生的UI组件,代替DOM元素来渲染,比如以<View>取代<div>,以<Image>替代<img>等。

在幕后,React Native在主线程之外,在另一个背景线程里运行JavaScript引擎,两个线程之间通过一批量化的async消息协议来通信(有一个专门的React插件)。

UI方面React Native提供跨平台的类似Flexbox的布局系统,还支持CSS子集。可以用JSX或者普通JavaScript语言,还有CoffeeScript和TypeScript来开发。有评论说,React的UI层模型要比UIKit好很多。

更好的是,由于基于Web技术,开发起来可以像在浏览器里那样随时在仿真程序中查看应用运行情况,刷新一下就行,无需编译,爽吧。

只是不知道这种架构下App的性能、流畅度如何。更多信息,请持续关注本文。

更多详情可以参考会议视频:https://www.youtube.com/watch?v=KVZ-P-ZI6W4 (墙内可以看这里

阅读详情 -> 移动Web技术的春天来了?Facebook发布React Native,用JavaScript开发移动原生应用-CSDN.NET.

Categories: css3, html5, JavaScript

Gulp.js深入讲解

上周明河发了篇 《Gulp.js—比Grunt更易用的前端构建工具》 ,同时也贴在阿里的技术站上,引起了不少前端同学的讨论。

赤温 (开发工程师)说:这东西(Gulp.js与Grunt)差距不大,社区才是王道!

英布 (高级前端开发工程师)说:一个是以配置的方式,一个是以编程的方式,感觉各有千秋吧,复杂流程还是使用编程的方式比较好,简单的那就随便了。

诚冉 (前端开发工程师)说:曾经苦于将grunt的各个工具黏合在一起的过程,grunt这个东西生出来就不是给小项目用的,必须是一定规模的并且持续更新的项目,才舍得花这么大力气来“制造工具”。一点也不夸张,虽然grunt的组件越来越多,但概念就像import一样,称不上是框架,顶多是一种合作机制。Nodejs的世界里,好像不跟stream和pipe沾点边的概念都不好用似的,现在写Web服务框架,基本都跟connect一个风格,req和res一层一层的流经各个handler。刚一看到glup下意识的觉得有点俗气了,但是想想Unix的哲学,标准化的输入输出,不需要过多的元信息描述,简单实用,组件连接起来产生的巨大威力,任何人都无法拒绝!

猎隼 (资深前端开发工程师)说:基于node.js的工具有一点让人深恶痛绝:会创建一个文件夹,而且里边的文件有大量的冗余。这对项目文件是一种污染,很有可能让一些不明真相的工具把这些代码也一起搜索了,比如一些查找替换程序。比较喜欢ruby的管理方式,需要的包在本机只需要安装一次,而且不会像node那样创建一个文件夹

半边 同学推荐了一个讨论Gulp.js与Grunt的 issue ,明河也推荐一篇文章 《No Need To Grunt, Take A Gulp Of Fresh Air》 。

Gulp.js处理构建任务如下图:

B0B77QN

Grunt处理任务:

oeCGJUS

(上图来自 http://slid.es/contra/gulp )

理解Gulp.js的核心设计

streaming很容易联想到河流,河流有哪些特征?流动、源源不断、具有方向、有起点、有终点,而这些特征也是Gulp.js构建代码的特点。

Gulp.js 官网上的简介是 The streaming build system ,核心的词是streaming(流动式),如何理解这个词呢? 《Gulp.js—比Grunt更易用的前端构建工具》 ,明河讲到Gulp.js的精髓在于对Node.js中 Streams API 的利用,所以想要理解Gulp.js,我们必须理解Streams,streaming其实就是Streams的设计思想。

流(stream)的概念来自unix,核心思想是do one thing well,一个大工程系统应该是由各个小且独立的管子连接而成,如诚冉同学所说:Unix的哲学,标准化的输入输出,不需要过多的元信息描述,简单实用,组件连接起来产生的巨大威力,任何人都无法拒绝!,如下图:

gulp.js

推荐下 《stream-handbook》 ,非常详细地说明了如何使用Streams,也提供了很多Streams的第三方模块供参考。

我们以经典的Nodejs读取文件逻辑来说明Streams与传统方式的差异。

使用fs模块读取一个json文件

传统的方式:

var dataJson, fs;

fs = require('fs');

dataJson = './gallery-db/component-info.json';

exports.all = function(req, res) {
    fs.readFile(dataJson,function(err, data){
        if (err) {
            console.log(err);
        } else {
            res.end(data);
        }
    });
};

fs.readFile()是将文件全部读取放进内存,然后触发回调,这种方式存在二方面的瓶颈。

1.读取大文件时容易造成内存泄漏

比如一个上传视屏的服务,面对少者二三百M,多者七八G的文件,明显这种方式不可行,所以我们需要分断上传,如果使用传统方式,就会发现非常蛋疼,估计没人想这么干….

2.深恶痛绝的回调问题

这是我自己写NodeJs代码最深恶痛绝的地方,在提供大的服务时,比如读取4-5个文件,回调层层嵌套,可读性和维护性非常大,恶心指数陡增。

而且每次你都要对err进行处理:

if (err) {
    console.log(err);
} else {
    //...
}

代码啰嗦,不够简练。 koa 已经解决了这个问题,NodeJs的发展史就是跟回调斗争史啊。

流的方式:

var dataJson, fs;

fs = require('fs');

dataJson = './gallery-db/component-info.json';

exports.all = function(req, res) {
  var stream;
  stream = fs.createReadStream(dataJson);
  stream.on('error', function(err) {
    return console.log(err);
  });
  stream.on('data', function(chunk) {
    return console.log('got %d bytes of data', chunk.length);
  });
  return stream.pipe(res);
};

fs.createReadStream()创建一个 readable streams (只读的流),请看 《stream-handbook》 中的readable streams部分,readable streams是有状态的,比如可以监听其 data 事件,读取数据后会触发,比如 error 事件,读取数据失败后会触发。

stream.pipe(res) , pipe 方法是流对象的核心方法,这句代码可以理解为,res(结果集,实际上是一个writeable streams,写流)对象接收从stream而来的数据,并予以处理输出。所以Gulp.js中的pipe()方法并不是gulp的方法,而是流对象的方法。pipe()返回的是res的返回的对象。

上述代码会把 component-info.json 的内容打印出来。

流的种类有: readable streams、 writeable streams(比如Gulp.js中的gulp.dest(‘./build’)f方法)、 transform (明河很不喜欢这个词,词不达意,称之为 through 更为合适,表示这是读/写流,后面会详细讲解)streams, duplex streams(duplex可以理解为循环的流,比如服务器端和客户端的实时通信,由于与Gulp.js无关,不展开讲)。

如何开发个Gulp.js插件?

所有的Gulp.js插件基本都是through(后面不再使用transform这个词) streams,即是消费者(接收gulp.src() 传递出来的数据,然后进行处理加工处理),又是生产者(将加工后的数据传递出去),我们以 gulp-clean 为例(这个插件用于清理目录和文件)。

先看下gulp-clean的API用法:

var gulp = require('gulp');
var clean = require('gulp-clean');

gulp.task('default', function() {
    gulp.src('app/tmp')
        .pipe(clean());
});

非常简单的代码,目的是残忍地将tmp目录干掉。

gulp-clean的 源码传送门 。

引入依赖模块

var rimraf = require('rimraf');
var es = require('event-stream');
var gutil = require('gulp-util');
var path = require('path');

rimraf 模块用于删除目录。

gulp-clean插件使用了个through streams的包装模块: event-stream ,用于简化streams调用的api,明河推荐使用 through2 ,会更好用。

path 模块就不说了,大家太熟悉了。

定义供外部调用插件方法

module.exports = function (options) {
  return es.map(function (file, cb) {
    //具体业务逻辑
  });
};

es.map() 会创建一个through streams,我们只要关注写业务逻辑就好,不用关注如何创建streams。

file为上游读流产生的文件数据,cb是核心方法用于向下游传递处理后的数据。

return cb(null, file);

具体业务代码

// 获取文件绝对路径
var filepath = file.path;
var cwd = file.cwd;
var relative = path.relative(cwd, filepath);

// 防止路径错误
if (!(relative.substr(0, 2) === '..') && relative !== '' || (options ? (options.force && typeof options.force === 'boolean') : false)) {
  //删除目录
  rimraf(filepath, function (error) {
    if (!error) {
      return cb(null, file);
    } else {
      return cb(new Error('Unable to delete "' + filepath + '" file (' + error.message + ').'), file);
    }
  });
//打印错误消息
} else if (relative === '') {
  gutil.log('gulp-clean: Cannot delete current working directory. (' + filepath + ')');
  return cb(null, file);
}
//打印错误消息
else {
  gutil.log('gulp-clean: Cannot delete files outside the current working directory. (' + filepath + ')');
  return cb(null, file);
}

代码非常简单,获取文件(目录)路径,调用rimraf()删除之即可。

一个简单的Gulp.js插件就完成了,so easy!

总结

Gulp.js的使用和插件的开发都很简单,当然里面还有很多细节,明河就不展开讲了,请看Gulp.js的官方文档。

阅读详情 -> Gulp.js深入讲解 – 推酷.

Categories: JavaScript, Nodejs

Schema – 模块化,响应式的前端开发框架

  Schema 是一个模块化的,响应式的前端框架,方便,快捷地帮助您迅速启动你的 Web 项目。Schema 配备完整的创建多个视图的能力。从桌面显示器到移动设备,它的12列网格提供强大的灵活性。

  Schema 借助了强大的 LESS,这使得代码结构干净简洁,超级容易维护。无论你是试图快速的设计原型,或者是即将发布的应用程序,Schema 提供了基础和组件来轻松设计任何响应式的 Web 项目。

阅读详情 -> SCHEMA UI Frontend Framework| A powerfully light, responsive, and lean front-end UI framework built with Less. | CSS Framework | Less Framework.

Categories: css3, html5, JavaScript, 项目精选

移动H5前端性能优化指南

super630x3602

移动H5前端性能优化指南

移动H5前端性能优化指南[托尼托尼研究所]

概述

1. PC优化手段在Mobile侧同样适用
2. 在Mobile侧我们提出三秒种渲染完成首屏指标
3. 基于第二点,首屏加载3秒完成或使用Loading
4. 基于联通3G网络平均338KB/s(2.71Mb/s),所以首屏资源不应超过1014KB
5. Mobile侧因手机配置原因,除加载外渲染速度也是优化重点
6. 基于第五点,要合理处理代码减少渲染损耗
7. 基于第二、第五点,所有影响首屏加载和渲染的代码应在处理逻辑中后置
8. 加载完成后用户交互使用时也需注意性能
优化指南

[加载优化]

加载过程是最为耗时的过程,可能会占到总耗时的80%时间,因此是优化的重点

· 减少HTTP请求
因为手机浏览器同时响应请求为4个请求(Android支持4个,iOS 5后可支持6个),所以要尽量减少页面的请求数,首次加载同时请求数不能超过4个
a) 合并CSS、JavaScript
b) 合并小图片,使用雪碧图

· 缓存
使用缓存可以减少向服务器的请求数,节省加载时间,所以所有静态资源都要在服务器端设置缓存,并且尽量使用长Cache(长Cache资源的更新可使用时间戳)
a) 缓存一切可缓存的资源
b) 使用长Cache(使用时间戳更新Cache)
c) 使用外联式引用CSS、JavaScript

· 压缩HTML、CSS、JavaScript
减少资源大小可以加快网页显示速度,所以要对HTML、CSS、JavaScript等进行代码压缩,并在服务器端设置GZip
a) 压缩(例如,多余的空格、换行符和缩进)
b) 启用GZip

· 无阻塞
写在HTML头部的JavaScript(无异步),和写在HTML标签中的Style会阻塞页面的渲染,因此CSS放在页面头部并使用Link方式引入,避免在HTML标签中写Style,JavaScript放在页面尾

部或使用异步方式加载

· 使用首屏加载
首屏的快速显示,可以大大提升用户对页面速度的感知,因此应尽量针对首屏的快速显示做优化

· 按需加载
将不影响首屏的资源和当前屏幕资源不用的资源放到用户需要时才加载,可以大大提升重要资源的显示速度和降低总体流量
PS:按需加载会导致大量重绘,影响渲染性能
a) LazyLoad
b) 滚屏加载
c) 通过Media Query加载

· 预加载
大型重资源页面(如游戏)可使用增加Loading的方法,资源加载完成后再显示页面。但Loading时间过长,会造成用户流失
对用户行为分析,可以在当前页加载下一页资源,提升速度
a) 可感知Loading(如进入空间游戏的Loading)
b) 不可感知的Loading(如提前加载下一页)

· 压缩图片
图片是最占流量的资源,因此尽量避免使用他,使用时选择最合适的格式(实现需求的前提下,以大小判断),合适的大小,然后使用智图压缩,同时在代码中用Srcset来按需显示
PS:过度压缩图片大小影响图片显示效果
a) 使用智图( http://zhitu.tencent.com/ )
b) 使用其它方式代替图片(1. 使用CSS3 2. 使用SVG 3. 使用IconFont)
c) 使用Srcset
d) 选择合适的图片(1. webP优于JPG 2. PNG8优于GIF)
e) 选择合适的大小(1. 首次加载不大于1014KB 2. 不宽于640(基于手机屏幕一般宽度))

· 减少Cookie
Cookie会影响加载速度,所以静态资源域名不使用Cookie

· 避免重定向
重定向会影响加载速度,所以在服务器正确设置避免重定向

· 异步加载第三方资源
第三方资源不可控会影响页面的加载和显示,因此要异步加载第三方资源

[脚本执行优化]

脚本处理不当会阻塞页面加载、渲染,因此在使用时需当注意

· CSS写在头部,JavaScript写在尾部或异步

· 避免图片和iFrame等的空Src
空Src会重新加载当前页面,影响速度和效率

· 尽量避免重设图片大小
重设图片大小是指在页面、CSS、JavaScript等中多次重置图片大小,多次重设图片大小会引发图片的多次重绘,影响性能

· 图片尽量避免使用DataURL
DataURL图片没有使用图片的压缩算法文件会变大,并且要解码后再渲染,加载慢耗时长

[CSS优化]

· 尽量避免写在HTML标签中写Style属性

· 避免CSS表达式
CSS表达式的执行需跳出CSS树的渲染,因此请避免CSS表达式

· 移除空的CSS规则
空的CSS规则增加了CSS文件的大小,且影响CSS树的执行,所以需移除空的CSS规则

· 正确使用Display的属性
Display属性会影响页面的渲染,因此请合理使用
a) display:inline后不应该再使用width、height、margin、padding以及float
b) display:inline-block后不应该再使用float
c) display:block后不应该再使用vertical-align
d) display:table-*后不应该再使用margin或者float

· 不滥用Float
Float在渲染时计算量比较大,尽量减少使用

· 不滥用Web字体
Web字体需要下载,解析,重绘当前页面,尽量减少使用

· 不声明过多的Font-size
过多的Font-size引发CSS树的效率

· 值为0时不需要任何单位
为了浏览器的兼容性和性能,值为0时不要带单位

· 标准化各种浏览器前缀
a) 无前缀应放在最后
b) CSS动画只用 (-webkit- 无前缀)两种即可
c) 其它前缀为 -webkit- -moz- -ms- 无前缀 四种,(-o-Opera浏览器改用blink内核,所以淘汰)

· 避免让选择符看起来像正则表达式
高级选择器执行耗时长且不易读懂,避免使用

[JavaScript执行优化]

· 减少重绘和回流
a) 避免不必要的Dom操作
b) 尽量改变Class而不是Style,使用classList代替className
c) 避免使用document.write
d) 减少drawImage

· 缓存Dom选择与计算
每次Dom选择都要计算,缓存他

· 缓存列表.length
每次.length都要计算,用一个变量保存这个值

· 尽量使用事件代理,避免批量绑定事件

· 尽量使用ID选择器
ID选择器是最快的

· TOUCH事件优化
使用touchstart、touchend代替click,因快影响速度快。但应注意Touch响应过快,易引发误操作

[渲染优化]

· HTML使用Viewport
Viewport可以加速页面的渲染,请使用以下代码
<meta name=”viewport” content=”width=device-width, initial-scale=1″>

· 减少Dom节点
Dom节点太多影响页面的渲染,应尽量减少Dom节点

· 动画优化
a) 尽量使用CSS3动画
b) 合理使用requestAnimationFrame动画代替setTimeout
c) 适当使用Canvas动画 5个元素以内使用css动画,5个以上使用Canvas动画(iOS8可使用webGL)

· 高频事件优化
Touchmove、Scroll 事件可导致多次渲染
a) 使用requestAnimationFrame监听帧变化,使得在正确的时间进行渲染
b) 增加响应变化的时间间隔,减少重绘次数

· GPU加速
CSS中以下属性(CSS3 transitions、CSS3 3D transforms、Opacity、Canvas、WebGL、Video)来触发GPU渲染,请合理使用
PS:过渡使用会引发手机过耗电增加

参考资料

感谢你的阅读,本文由 腾讯ISUX 版权所有,转载时请注明出处,违者必究,谢谢你的合作。
注明出处格式:腾讯ISUX (http://isux.tencent.com/h5-performance.html)

阅读详情 -> 移动H5前端性能优化指南-腾讯ISUX – 社交用户体验设计 – Better Experience Through Design.

Categories: css3, html5, JavaScript

专栏:THREE.JS源码注释

  • 本专栏包括THREE.js整个代码库的源码注释,从数学库,几何对象,相机,材质,纹理,灯光,着色器,内核,详尽细致的注释了每段代码,在注释中,尽可能的包括本人学习wegbl过程中遇到的数学问题,着色器问题,甚至是各种图形学的技术,都进行了详细的补充.

阅读详情 -> 专栏:THREE.JS源码注释 – 博客频道 – CSDN.NET.

Categories: JavaScript

Web App的零框架解决方案

MVC框架(尤其是大型框架)有一个严重的缺点,就是会产生用户的重度依赖。一旦框架本身出现问题或者停止更新,用户的处境就会很困难,维护和更新成本极高。

ES6的到来,使得JavaScript语言有了原生的模块解决方案。于是,开发者有了另一种选择,就是不使用MVC框架,只使用各种单一用途的模块库,组合完成一个项目。

下面是可供选择的各种用途的模块列表,摘自Andy Walpole的《2015: The End of the Monolithic JavaScript Framework》

辅助功能库(Helper Libraries)

路由库(Routing)

Promise库

  • RSVP.js:ES6兼容的Promise库
  • ES6-Promise:RSVP.js的子集,但是全面兼容ES6
  • q:最常用的Promise库之一,AngularJS用了它的精简版
  • native-promise-only:严格符合ES6的Promise标准,同时兼容老式浏览器

通信库

  • fetch:实现window.fetch功能
  • qwest:支持XHR2和Promise的Ajax库
  • jQuery:jQuery 2.0支持按模块打包,因此可以创建一个纯Ajax功能库

动画库(Animation)

辅助开发库(Development Assistance)

  • LogJS:轻量级的logging功能库
  • UserTiming.js:支持老式浏览器的高精度时间戳库

流程控制和架构(Flow Control/Architecture)

数据绑定(Data-binding)

  • Object.observe():Chrome已经支持该方法,可以轻易实现双向数据绑定

模板库(Templating)

  • Mustache:大概是目前使用最广的不含逻辑的模板系统

微框架(Micro-Framework)

某些情况下,可以使用微型框架,作为项目开发的起点。

  • bottlejs:提供惰性加载、中间件钩子、装饰器等功能
  • Stapes.js:微型MVC框架
  • soma.js:提供一个松耦合、易测试的架构
  • knockout:最流行的微框架之一,主要关注UI

(完)

阅读详情 -> articles/2015-01-16-zero-framework.md at master · ruanyf/articles.

Categories: css3, html5, JavaScript, 前端资讯

撰写可测试的 JavaScript – 囧克斯

阅读详情 -> 撰写可测试的 JavaScript – 囧克斯.

译自:Writing Testable JavaScript – A List Apart

这篇文章算是 A List Apart 系列文章中,包括滑动门在内,令我印象最深刻的文章之一。最近有时间翻译了一下,分享给更多人,希望对大家有所帮助!


我们已经面对到了这一窘境:一开始我们写的 JavaScript 只有区区几行代码,但是它的代码量一直在增长,我们不断的加参数、加条件。最后,粗 bug 了…… 我们才不得不收拾这个烂摊子。

如上所述,今天的客户端代码确实承载了更多的责任,浏览器里的整个应用都越变越复杂。我们发现两个明显的趋势:1、我们没法通过单纯的鼠标定位和点击来检验代码是否正常工作,自动化的测试才会真正让我们放心;2、我们也许应该在撰写代码的时候就考虑到,让它变得可测试。

神马?我们需要改变自己的编码方式?是的。因为即使我们意识到自动化测试的好,大部分人可能只是写写集成测试(integration tests)罢了。集成测试的侧重点是让整个系统的每一部分和谐共存,但是这并没有告诉我们每个独立的功能单元运转起来是否都和我们预期的一样。

这就是为什么我们要引入单元测试。我们已经准备好经历一段痛苦的撰写单元测试的过程了,但最终我们能够撰写可测试的 JavaScript

单元与集成:有什么不同?

撰写集成测试通常是相当直接的:我们单纯的撰写代码,描述用户如何和这个应用进行交互、会得到怎样的结果就好。Selenium 是这类浏览器自动化工具中的佼佼者。而 Capybara 可以便于 Ruby 和 Selenium 取得联系。在其它语言中,这类工具也举不胜举。

下面就是搜索应用的一部分集成测试:

def test_search
    fill_in('q', :with => 'cat')
    find('.btn').click
    assert( find('#results li').has_content?('cat'), 'Search results are shown' )
    assert( page.has_no_selector?('#results li.no-results'), 'No results is not shown' )
end

集成测试对用户的交互行为感兴趣,而单元测试往往仅专注于一小段代码:

当我伴随特定的输入调用一个函数的时候,我是否收到了我预期中的结果?

我们按照传统思路撰写的程序是很难进行单元测试的,同时也很难维护、调试和扩展。但是如果我们在撰写代码的时候就考虑到我将来要做单元测试,那么这样的思路不仅会让我们发现测试代码写起来很直接,也会让我们真正写出更优质的代码。

我们通过一个简单的搜索应用的例子来做个示范:

当用户搜索时,该应用会向服务器发送一个 XHR (Ajax 请求) 取得相应的搜索结果。并当服务器以 JSON 格式返回数据之后,通过前端模板把结果显示在页面中。用户在搜索结果中点“赞”,这个人的名字就会出现在右侧的点“赞”列表里。

一个“传统”的 JavaScript 实现大概是这个样子的:

// 模板缓存,缓存的内容均为 jqXHR 对象
var tmplCache = {};

/**
 * 载入模板
 * 从 '/templates/{name}' 载入模板,存入 tmplCache
 * @param  {string} name 模板名称
 * @return {object}      模板请求的 jqXHR 对象
 */
function loadTemplate (name) {
  if (!tmplCache[name]) {
    tmplCache[name] = $.get('/templates/' + name);
  }
  return tmplCache[name];
}

/**
 * 页面主要逻辑
 * 1. 支持搜索行为并展示结果
 * 2. 支持点“赞”,被赞过的人会出现在点“赞”列表里
 */
$(function () {

  var resultsList = $('#results');
  var liked = $('#liked');
  var pending = false; // 用来标识之前的搜索是否尚未结束

  // 用户搜索行为,表单提交事件
  $('#searchForm').on('submit', function (e) {
    // 屏蔽默认表单事件
    e.preventDefault();

    // 如果之前的搜索尚未结束,则不开始新的搜索
    if (pending) { return; }

    // 得到要搜索的关键字
    var form = $(this);
    var query = $.trim( form.find('input[name="q"]').val() );

    // 如果搜索关键字为空则不进行搜索
    if (!query) { return; }

    // 开始新的搜索
    pending = true;

    // 发送 XHR
    $.ajax('/data/search.json', {
      data : { q: query },
      dataType : 'json',
      success : function (data) {
        // 得到 people-detailed 模板
        loadTemplate('people-detailed.tmpl').then(function (t) {
          var tmpl = _.template(t);

          // 通过模板渲染搜索结果
          resultsList.html( tmpl({ people : data.results }) );

          // 结束本次搜索
          pending = false;
        });
      }
    });

    // 在得到服务器响应之前,清空搜索结果,并出现等待提示
    $('<li>', {
      'class' : 'pending',
      html : 'Searching &hellip;'
    }).appendTo( resultsList.empty() );
  });

  // 绑定点“赞”的行为,鼠标点击事件
  resultsList.on('click', '.like', function (e) {
    // 屏蔽默认点击事件
    e.preventDefault();

    // 找到当前人的名字
    var name = $(this).closest('li').find('h2').text();

    // 清除点“赞”列表的占位元素
    liked.find('.no-results').remove();

    // 在点“赞”列表加入新的项目
    $('<li>', { text: name }).appendTo(liked);
  });

});

我的朋友 Adam Sontag 称之为“自己给自己挖坑”的代码:展现、数据、用户交互、应用状态全部分散在了每一行代码里。这种代码是很容易进行集成测试的,但几乎不可能针对功能单元进行单独的测试。

单元测试为什么这么难?有四大罪魁祸首:

  • 没有清晰的结构。几乎所有的工作都是在 $(document).ready() 回调里进行的,而这一切在一个匿名函数里,它在测试中无法暴露出任何接口。
  • 函数太复杂。如果一个函数超过了 10 行,比如提交表单的那个函数,估计大家都觉得它太忙了,一口气做了很多事。
  • 隐藏状态还是共享状态。比如,因为 pending 在一个闭包里,所以我们没有办法测试在每个步骤中这个状态是否正确。
  • 强耦合。比如这里 $.ajax 成功的回调函数不应该依赖 DOM 操作。

组织我们的代码

首当其冲的是把我们代码的逻辑缕一缕,根据职责的不同把整段代码分为几个方面:

  • 展现和交互
  • 数据管理和保存
  • 应用的状态
  • 把上述代码建立并串连起来

在之前的“传统”实现里,这四类代码是混在一起的,前一行我们还在处理界面展现,后两行就在和服务器通信了。

我们绝对可以写出集成测试的代码,但我们应该很难写出单元测试了。在功能测试里,我们可以做出诸如“当用户搜索东西的时候,他会看到相应的搜索结果”的断言,但是无法再具体下去了。如果里面出了什么问题,我们还是得追踪进去,找到确切的出错位置。这样的话功能测试其实也没帮上什么忙。

如果我们反思自己的代码,那不妨从单元测试写起,通过单元测试这个角度,更好的观察,是哪里出了问题。这进而会帮助我们改进代码,让代码变得更易于重用、易于维护、易于扩展。

我们的新版代码遵循下面几个原则:

  • 根据上述四类职责,列出每个互不相干的行为,并分别用一个对象来表示。对象之前互不依赖,以避免不同的代码混在一起。
  • 用可配置的内容代替写死的内容,以避免我们为了测试而复刻整个 HTML 环境。
  • 保持对象方法的简单明了。这会把测试工作变得简单易懂。
  • 通过构造函数创建对象实例。这让我们可以根据测试的需要复刻每一段代码的内容。

作为起步,我们有必要搞清楚,该如何把应用分解成不同的部分。我们有三块展现和交互的内容:搜索框、搜索结果和点“赞”列表。

我们还有一块内容是从服务器获取数据的、一块内容是把所有的内容粘合在一起的。

我们从整个应用最简单的一部分开始吧:点“赞”列表。在原版应用中,这部分代码的职责就是更新点“赞”列表:

var liked = $('#liked');
var resultsList = $('#results');

// ...

resultsList.on('click', '.like', function (e) {
  e.preventDefault();
  var name = $(this).closest('li').find('h2').text();
  liked.find( '.no-results' ).remove();
  $('<li>', { text: name }).appendTo(liked);
});

搜索结果这部分是完全和点“赞”列表搅在一起的,并且需要很多 DOM 处理。更好的易于测试的写法是创建一个点“赞”列表的对象,它的职责就是封装点“赞”列表的 DOM 操作。

var Likes = function (el) {
  this.el = $(el);
  return this;
};

Likes.prototype.add = function (name) {
  this.el.find('.no-results').remove();
  $('<li>', { text: name }).appendTo(this.el);
};

这段代码提供了创建一个点“赞”列表对象的构造函数。它有 .add() 方法,可以在产生新的赞的时候使用。这样我们就可以写很多测试代码来保障它的正常工作了:

var ul;

// 设置测试的初始状态:生成一个搜索结果列表
setup(function(){
  ul = $('<ul><li class="no-results"></li></ul>');
});

test('测试构造函数', function () {
  var l = new Likes(ul);
  // 断言对象存在
  assert(l);
});

test('点一个“赞”', function () {
  var l = new Likes(ul);
  l.add('Brendan Eich');

  // 断言列表长度为1
  assert.equal(ul.find('li').length, 1);
  // 断言列表第一个元素的 HTML 代码是 'Brendan Eich'
  assert.equal(ul.find('li').first().html(), 'Brendan Eich');
  // 断言占位元素已经不存在了
  assert.equal(ul.find('li.no-results').length, 0);
});

怎么样?并不难吧 :-) 我们这里用到了名为 Mocha 的测试框架,以及名为 Chai 的断言库。Mocha 提供了 test 和 setup 函数;而 Chai 提供了 assert。测试框架和断言库的选择还有很多,我们出于介绍的目的给大家展示这两款。你可以找到属于适合自己的项目——除了 Mocha 之外,QUnit 也比较流行。另外 Intern 也是一个测试框架,它运用了大量的 promise 方式。

我们的测试代码是从点“赞”列表这一容器开始的。然后它运行了两个测试:一个是确定点“赞”列表是存在的;另一个是确保 .add() 方法达到了我们预期的效果。有这些测试做后盾,我们就可以放心重构点“赞”列表这部分的代码了,即使代码被破坏了,我们也有信心把它修复好。

我们新应用的代码现在看起来是这样的:

var liked = new Likes('#liked'); // 新的点“赞”列表对象
var resultsList = $('#results');

// ...

resultsList.on('click', '.like', function (e) {
  e.preventDefault();
  var name = $(this).closest('li').find('h2').text();
  liked.add(name); // 新的点“赞”操作的封装
});

搜索结果这部分比点“赞”列表更复杂一些,不过我们也该拿它开刀了。和我们为点“赞”列表创建一个 .add() 方法一样,我们要创建一个与搜索结果有交互的方法。我们需要一个点“赞”的入口,向整个应用“广播”自己发生了什么变化——比如有人点了个“赞”。

// 为每一条搜索结果的点“赞”按钮绑定点击事件
var SearchResults = function (el) {
  this.el = $(el);
  this.el.on( 'click', '.btn.like', _.bind(this._handleClick, this) );
};

// 展示搜索结果,获取模板,然后渲染
SearchResults.prototype.setResults = function (results) {
  var templateRequest = $.get('people-detailed.tmpl');
  templateRequest.then( _.bind(this._populate, this, results) );
};

// 处理点“赞”
SearchResults.prototype._handleClick = function (evt) {
  var name = $(evt.target).closest('li.result').attr('data-name');
  $(document).trigger('like', [ name ]);
};

// 对模板渲染数据的封装
SearchResults.prototype._populate = function (results, tmpl) {
  var html = _.template(tmpl, { people: results });
  this.el.html(html);
};

现在我们旧版应用中管理搜索结果和点“赞”列表之间交互的代码如下:

var liked = new Likes('#liked');
var resultsList = new SearchResults('#results');

// ...

$(document).on('like', function (evt, name) {
  liked.add(name);
})

这就更简单更清晰了,因为我们通过 document 在各个独立的组件之间进行消息传递,而组件之间是互不依赖的。(值得注意的是,在真正的应用当中,我们会使用一些诸如 Backbone 或 RSVP 库来管理事件。我们出于让例子尽量简单的考虑,使用了 document 来触发事件) 我们同时隐藏了很多脏活累活:比如在搜索结果对象里寻找被点“赞”的人,要比放在整个应用的代码里更好。更重要的是,我们现在可以写出保障搜索结果对象正常工作的测试代码了:

var ul;
var data = [ /* 填入假数据 */ ];

// 确保点“赞”列表存在
setup(function () {
  ul = $('<ul><li class="no-results"></li></ul>');
});

test('测试构造函数', function () {
  var sr = new SearchResults(ul);
  // 断言对象存在
  assert(sr);
});

test('测试收到的搜索结果', function () {
  var sr = new SearchResults(ul);
  sr.setResults(data);

  // 断言搜索结果占位元素已经不存在
  assert.equal(ul.find('.no-results').length, 0);
  // 断言搜索结果的子元素个数和搜索结果的个数相同
  assert.equal(ul.find('li.result').length, data.length);
  // 断言搜索结果的第一个子元素的 'data-name' 的值和第一个搜索结果相同
  assert.equal(
    ul.find('li.result').first().attr('data-name'),
    data[0].name
  );
});

test('测试点“赞”按钮', function() {
  var sr = new SearchResults(ul);
  var flag;
  var spy = function () {
    flag = [].slice.call(arguments);
  };

  sr.setResults(data);
  $(document).on('like', spy);

  ul.find('li').first().find('.like.btn').click();

  // 断言 `document` 收到了点“赞”的消息
  assert(flag, '事件被收到了');
  // 断言 `document` 收到的点“赞”消息,其中的名字是第一个搜索结果
  assert.equal(flag[1], data[0].name, '事件里的数据被收到了' );
});

和服务器直接的交互是另外一个有趣的话题。原版的代码包括一个 $.ajax() 的请求,以及一个直接操作 DOM 的回调函数:

$.ajax('/data/search.json', {
  data : { q: query },
  dataType : 'json',
  success : function( data ) {
    loadTemplate('people-detailed.tmpl').then(function(t) {
      var tmpl = _.template( t );
      resultsList.html( tmpl({ people : data.results }) );
      pending = false;
    });
  }
});

同样,我们很难为这样的代码撰写测试。因为很多不同的工作同时发生在这一小段代码中。我们可以重新组织一下数据处理的部分:

var SearchData = function () { };

SearchData.prototype.fetch = function (query) {
  var dfd;

  // 如果搜索关键字为空,则不做任何事,立刻 `promise()`
  if (!query) {
    dfd = $.Deferred();
    dfd.resolve([]);
    return dfd.promise();
  }

  // 否则,向服务器请求搜索结果并把在得到结果之后对其数据进行包装
  return $.ajax( '/data/search.json', {
    data : { q: query },
    dataType : 'json'
  }).pipe(function( resp ) {
    return resp.results;
  });
};

现在我们改变了获得搜索结果这部分的代码:

var resultList = new SearchResults('#results');
var searchData = new SearchData();

// ...

searchData.fetch(query).then(resultList.setResults);

我们再一次简化了代码,并通过 SearchData 对象抛弃了之前应用程序主函数里杂乱的代码。同时我们已经让搜索接口变得可测试了,尽管现在和服务器通信这里还有事情要做。

首先我们不是真的要跟服务器通信——不然这又变成集成测试了:诸如我们是有责任感的开发者,我们已经确保服务器一定不会犯错等等,是这样吗?为了替代这些东西,我们应该“mock”(伪造) 与服务器之间的通信。Sinon 这个库就可以做这件事。第二个障碍是我们的测试应该覆盖非理想环境,比如关键字为空。

test('测试构造函数', function () {
  var sd = new SearchData();
  assert(sd);
});

suite('取数据', function () {
  var xhr, requests;

  setup(function () {
    requests = [];
    xhr = sinon.useFakeXMLHttpRequest();
    xhr.onCreate = function (req) {
      requests.push(req);
    };
  });

  teardown(function () {
    xhr.restore();
  });

  test('通过正确的 URL 获取数据', function () {
    var sd = new SearchData();
    sd.fetch('cat');

    assert.equal(requests[0].url, '/data/search.json?q=cat');
  });

  test('返回一个 promise', function () {
    var sd = new SearchData();
    var req = sd.fetch('cat');

    assert.isFunction(req.then);
  });

  test('如果关键字为空则不查询', function () {
    var sd = new SearchData();
    var req = sd.fetch();
    assert.equal(requests.length, 0);
  });

  test('如果关键字为空也会有 promise', function () {
    var sd = new SearchData();
    var req = sd.fetch();

    assert.isFunction( req.then );
  });

  test('关键字为空的 promise 会返回一个空数组', function () {
    var sd = new SearchData();
    var req = sd.fetch();
    var spy = sinon.spy();

    req.then(spy);

    assert.deepEqual(spy.args[0][0], []);
  });

  test('返回与搜索结果相对应的对象', function () {
    var sd = new SearchData();
    var req = sd.fetch('cat');
    var spy = sinon.spy();

    requests[0].respond(
      200, { 'Content-type': 'text/json' },
      JSON.stringify({ results: [ 1, 2, 3 ] })
    );

    req.then(spy);

    assert.deepEqual(spy.args[0][0], [ 1, 2, 3 ]);
  });
});

出于篇幅的考虑,这里对搜索框的重构及其相关的单元测试就不一一介绍了。完整的代码可以移步至此查阅。

当我们按照可测试的 JavaScript 的思路重构代码之后,我们最后用下面这段代码开启程序:

$(function() {
  var pending = false;

  var searchForm = new SearchForm('#searchForm');
  var searchResults = new SearchResults('#results');
  var likes = new Likes('#liked');
  var searchData = new SearchData();

  $(document).on('search', function (event, query) {
    if (pending) { return; }

    pending = true;

    searchData.fetch(query).then(function (results) {
      searchResults.setResults(results);
      pending = false;
    });

    searchResults.pending();
  });

  $(document).on('like', function (evt, name) {
    likes.add(name);
  });
});

比干净整洁的代码更重要的,是我们的代码拥有了更健壮的测试基础作为后盾。这也意味着我们可以放心的重构任意部分的代码而不必担心程序遭到破坏。我们还可以继续为新功能撰写新的测试代码,并确保新的程序可以通过所有的测试。

测试会在宏观上让你变轻松

看完这些的长篇大论你一定会说:“纳尼?我多写了这么多代码,结果还是做了这么一点事情?”

关键在于,你做的东西早晚要放到网上的。同样是花时间解决问题,你会选择在浏览器里点来点去?还是自动化测试?还是直接在线上让你的用户做你的小白鼠?无论你写了多少测试,你写好代码,别人一用,多少会发现点 bug。

至于测试,它可能会花掉你一些额外的时间,但是它到最后真的是为你省下了时间。写测试代码测出一个问题,总比你发布到线上之后才发现有问题要好。如果有一个系统能让你意识到它真的能避免一个 bug 的流出,你一定会心存感激。

额外的资源

这篇文章只能算是 JavaScript 测试的一点皮毛,但是如果你对此抱有兴趣,那么可以继续移步至:

Categories: JavaScript

CSS3 animation by less w3cmark

LESS动画优点

· 快速开发css3动画

· 采用less mixins写法,不会生成冗余css

· 已加入主流浏览器前缀,保证最大兼容性

· LESS-Animation 部分mixins支持传参,自定义动画幅度

(PS:sublime的less2css插件可能不支持编译本库的一些新写法,建议使用官方less编译、考拉、前端构建工具等方式编译。交流群:145423956

使用方法

本less文件主要包含两个功能:LESS-Prefixer和LESS-Animation。

· 下载 _animation.less 文件,git地址:

git@github.com:w3cmark/css3.git

· 在主less文件引入_animation.less

@import "_animation.less";

LESS-Prefixer

LESS-Prefixer是一组LESS mixins,可以让你在写css3时,去掉书写各个浏览器的前缀,简化代码书写。

如何使用

· 假如你写css3的transition:

div{
  -webkit-transition:all 0.2s ease-out;
  -o-transition:all 0.2s ease-out;
  -ms-transition:all 0.2s ease-out;
  -moz-transition:all 0.2s ease-out;
  transition:all 0.2s ease-out;
}

· 引入 _animation.less 后的写法:

div{
  .transition(all 0.2s ease-out);
}

支持的属性

.transition(@arg)
.transition-delay(@delay)
.transition-duration(@duration)
.transition-property(@property)
.transition-timing-function(@function)
.transition-delay(@delay)
.transition-delay(@delay)
.transform(@arg)
.transform-origin(@args)
.transform-style(@style)
.rotate(@deg)
.scale(@factor)
.translate(@x,@y)
.translate3d(@x,@y,@z)
.translateHardware(@x,@y)
.animation(@arg)
.animation-delay(@delay)
.animation-direction(@direction)
.animation-duration(@duration)
.animation-fill-mode(@mode)
.animation-iteration-count(@count)
.animation-name(@name)
.animation-play-state(@state)
.animation-timing-function(@function)
.flex(@arg)
.flexbox()
.opacity(@number)
.box-shadow(@arg)
.box-sizing(@arg)
.border-color(@arg)
.border-image(@arg)
.border-radius(@arg)
.background-origin(@arg)
.background-clip(@arg)
.background-size(@arg)
.columns(@args)
.column-count(@count)
.column-gap(@gap)
.column-width(@width)
.column-rule(@args)
.gradient(@default, @start, @stop)
.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2)
.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3)
.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4)
.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2)
.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3)
.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4)

LESS-Animation

mixin直接引用

LESS-Animation 是一个基于less,集成一些基础动画的库(所包含的动画可以在上面效果预览),直接调用相应的动画mixins即可。

动画帧名和mixin名一致,方便调用。

· 栗子一:引用 bounce 动画,只需在所在元素节点加入 .bounce();

.bounce{
  .bounce();
  .animation(bounce 1s ease-in-out);
}

· 栗子二:引用 bounceIn 动画,只需在所在元素节点加入 .bounceIn();

.box{
  .bounceIn();
  .animation(bounceIn 1s linear infinite);
}

mixin传参引用

ps:
· 新增部分传参功能,可以自定义动画幅度参数,实现同类型动画不同幅度;
· 传参功能不影响原来上面直接引用,参数都可为空,原来的动画幅度已经设为默认值;

具体动画名和参数说明

动画名 参数说明 栗子
.bounce(@t; @n) @t:抖动最小幅度(默认值4px);@n:动画帧名(默认值bounce) .bounce(5px; bounceA)
.pulse(@t; @n) @t:放大幅度(默认值1.05);@n:动画帧名(默认值pulse) .pulse(1.5; pulseA)
.shake(@x; @n) @x:抖动最小幅度(默认值10px);@n:动画帧名(默认值shake) .shake(20px; shakeA)
.swing(@d; @n) @d:旋转最小角度(默认值5deg);@n:动画帧名(默认值swing) .swing(5deg; swingA)
.wobble(@d; @n) @d:摇摆最小幅度(默认值1deg);@n:动画帧名(默认值wobble) .wobble(-2deg; wobbleA)
.fadeIn(@x; @y; @n) @x:x轴移动距离(默认值0);@y:y轴移动距离(默认值0);@n:动画帧名(默认值fadeIn) .fadeIn(-1000px; 0; fadeInA)
.fadeOut(@x; @y; @n) @x:x轴移动距离(默认值0);@y:y轴移动距离(默认值0);@n:动画帧名(默认值fadeOut) .fadeOut(-1000px; 0; fadeOutA)
.turnInDown(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnInDown) .turnInDown(700px; turnInDownA)
.turnInUp(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnInUp) .turnInUp(700px; turnInUpA)
.turnInLeft(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnInLeft) .turnInLeft(700px; turnInLeftA)
.turnInRight(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnInRight) .turnInRight(700px; turnInRightA)
.turnOutDown(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnOutDown) .turnOutDown(700px; turnOutDownA)
.turnOutUp(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnOutUp) .turnOutUp(700px; turnOutUpA)
.turnOutLeft(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnOutLeft) .turnOutLeft(700px; turnOutLeftA)
.turnOutRight(@p; @n) @p:perspective值,元素距视图的距离(默认值600px);@n:动画帧名(默认值turnOutRight) .turnOutRight(700px; turnOutRightA)

更新日志

· 20150110 增加turn Entrances(出现)和turn Exits(消失)系列动画

· 20150120 部分新增动画animation的mixin支持传参

参考:https://github.com/daneden/animate.css

阅读详情 -> CSS3 animation by less w3cmark_前端笔记_前端笔记网_为你提供最专业的前端笔记和前端资源.

Categories: css3