Node稳定性的研究心得

目前大部分Web服务器,如Apache,都使用多线程的方式响应多用户请求,即一个线程服务一个用户请求。这种模式其中一个好处是,当某个请求的线程上抛出的异常没被捕获,只会影响当前这个线程,不会影响其他请求。

由于Node执行在单线程上,一旦线程上抛的异常没有被捕获,就会引起整个进程的崩溃。所以对Node服务的异常处理、保证进程的稳定性非常重要。

再好的程序员也不敢保证自己的代码不出现异常,除了尽可能捕获可能出现的异常,我们还需要通过一些规范减少异常发生,通过单元测试辅助我们验证代码,通过一些工具保证服务的稳定性。下面我从这几个方面探讨如何保证Node的稳定性。

一 异常捕获

提升稳定性最直接的方式就是尽可能的捕捉异常,Node提供3种方式。

1.1 try/catch

在大多数语言中,try/catch是捕获异常的好手,能确保我们的代码不进入不可控流程。但是由于Node回调/异步的特性,我们无法通过try/catch来捕捉所有的异常,看下面的示例:

1
2
3
4
5
6
7
8
try {
    process.nextTick(function () {
        throw new Error("error");
    });
} catch (err) {
    //can not catch it
    console.log(err);
}
1
2
3
4
5
6
7
8
try {
    setTimeout(function(){
        throw new Error("error");
    },1)
} catch (err) {
    //can not catch it
    console.log(err);
}

上面的代码没有像预期的那样帮我们捕获异常,后果就是这个未被捕获的异常导致整个Node进程crash,所以Node中try/catch的方式不是那么管用。
如果有一种方法能帮我们全局捕获异常,Node服务就不会轻易挂掉了。Node确实提供了这种方式,不过却不能完全满足我们的需求。

1.2 uncaughtException

当一个异常未被捕获,冒泡回归到事件循环中就会触发uncaughtException事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
process.on('uncaughtException', function(err) {
    console.error('Error caught in uncaughtException event:', err);
});


try {
    setTimeout(function(){
        throw new Error("error");
    },1)
} catch (err) {
    //can not catch it
    console.log(err);
}

只要给uncaughtException配置了回调,Node进程不会异常退出,但异常发生的上下文已经丢失,我们无法给出友好的返回,比如告诉用户哪里出问题了。而且由于uncaughtException事件发生后,会丢失当前环境的堆栈,可能导致Node不能正常进行内存回收,从而导致内存泄露。所以,uncaughtException的正确使用姿势是,当uncaughtException触发,记录error日志,然后结束Node进程,我们通过日志监控,报警及时解决异常。

1
2
3
4
5
6
process.on('uncaughtException', function(err) {
    // 记录日志
    logger(err);
    // 结束进程
    process.exit(1);
});

1.3 domain

为了弥补try/catch和uncaughtException的不足,在node v0.8+版本的时候,发布了一个模块domain,这个模块能捕捉异步回调中出现的异常。
看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var d = domain.create();

process.on('uncaughtException', function(err) {
    console.error(err);
});

d.on('error', function(err) {
    console.error('Error caught by domain:', err);
});

d.run(function() {
    process.nextTick(function() {
        throw new Error("test domain");
    });
});

运行代码我们会发现,异常会被domain捕获到,uncaughtException不会被触发。虽然我们对domain模块寄予厚望,不过目前domain模块的评级为“Unstable”,因为存在不少性能以及稳定性问题。
关于domain的详细介绍,可以看看以下几篇文章:

二 阻止异常发生

我们当然无法阻止异常的发生,这里说的阻止异常发生,是希望大家能养成良好的编码习惯,严谨的思维逻辑,来尽可能减少代码抛出未捕获异常。

2.1 良好的异常处理习惯

  • 异步API编写规范:由于异步调用中回调函数里的异常无法被外部捕获,所以我们将API内部发生的异常作为第一个参数传递给回调函数,包括NodeJS官方的API是遵循这个规范的。
1
2
3
4
fs.readFile('/t.txt', function (err, data) {
  if (err) throw err;
  console.log(data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// 不推荐的做法
function fun(options,callback){
    if(!options{
        throw new Error("..")
    }
}

// 推荐的做法
function fun(options,callback){
    if(!options){
        callback(err,null)
    }
}
  • 严格校验用户的输入
1
2
3
4
5
function doSth(cb){
    if(typeof cb === 'function'){
        cb();
    }
}
  • 使用try/catch处理可能出现异常的代码
1
2
3
4
5
6
var obj;
try{
    obj = JSON.parse('')
}catch(e){
    obj = {};
}
  • 不要直接在controller中抛异常,应该用500等状态更友好的返回错误
1
2
3
4
5
6
// 不推荐的做法
app.get('/item.html', function (req, res, next) {
    if(!query["id"]){
        throw new Error('no item id');
    }
});
1
2
3
4
5
6
// 推荐的做法
app.get('/item.html', function (req, res, next) {
    if(!query["id"]){
        next();
    }
});

2.2 单元测试

单元测试的重要性想必所有人都清楚,JS代码跑在浏览器端的时候我们未必会做,因为异常通常只会影响部分人使用的部分功能,不足以引起很多人的重视。JS代码跑在服务端情况完全不一样了,一旦出现异常,影响的是所有的用户,所以单元测试就显得非常重要。

至于如何在NodeJS中写单元测试,可以看看两位大神的分享:

2.3 记录日志

还是那句说,谁也不敢保证自己的代码不出现异常,因为有运行环境,网络环境等各种不稳定因素,所以建立健全的排查和跟踪机制就显得很重要,而日志就是实现这种机制的关键。
阿里线上环境已经有完善的日志监控体现,我们要做的就是去学会如何使用他。

ali-logger:http://search.npm.taobao.net/package/ali-logger

三 多进程架构

3.1 cluster

cluster模块用于创建共享端口的多进程模式,这种模式使多个进程间共享一个监听状态的socket,并由系统将accept的connection分配给不同的子进程。
文档上有一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  // Workers can share any TCP connection
  // In this case its a HTTP server
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

利用cluster,我们可以根据CPU的数量创建多个worker进程,用户的请求会被分配到不同的进程上,如果某个进程出现异常,可以直接将这个进程crash掉,而不会影响其他进程。
关于cluster实现原理的介绍文章非常多,想深入了解的可以自行搜索一下。

3.1.1 graceful + recluster

数据产品中使用graceful + recluster两个模块实现多进程和服务器稳定性的工作,分享一下使用方法:

  • graceful : 当uncaughtException触发后,延迟一段时间退出进程
  • recluster : 实现多进程,并且当有进程退出实现自动重启。

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var app = require('../app');
var graceful = require('graceful');

var server = app.listen(app.get('port'), function() {
    debug('Express server listening on port ' + server.address().port);
});

graceful({
    server: server,
    killTimeout: 30000,
    error:function(e){
        logger.error(e);
    }
});

server.js

1
2
3
4
5
6
7
8
9
10
11
12
var recluster = require('recluster'),
    path = require('path');

var cluster = recluster(path.join(__dirname, 'app.js'));
cluster.run();

process.on('SIGUSR2', function() {
    console.log('Got SIGUSR2, reloading cluster...');
    cluster.reload();
});

console.log("spawned cluster, kill -s SIGUSR2", process.pid, "to reload");

开发环境,直接通过app启动:

1
$ node app.js

生产环境, 启动多个 worker:

1
$ node server.js

3.1.2 pm2

如果你觉得graceful + recluster的方式太复杂,那么pm2肯定是你最理想的选择。
pm2非常强大,生产环境使用pm2启动你的Node服务是个不错的选择,他能自动利用你的多核cup,完善的监控,日志记录等等..

pm2使用非常简单:

1
$ npm install pm2@latest -g
1
$ pm2 start app.js
1
$ pm2 list

pm2

如果你想更多的了解pm2,可以直接看pm2的文档:https://github.com/Unitech/pm2

不过正是由于pm2功能太过强大,我们没有选择pm2,因为他太复杂了,感觉还没有能力驾驭他,特别是万一出现问题,我们还不知道如何解决。也许我们的顾虑是多余的,不过需要一点时间去了解他。

小结

刚开始学习Node的使用,对Node确实有点小担心,因为使用别的语言做Web服务,根本不用担心因为一个错误导致整个服务crash的问题。
随着对Node的了解,我掌握了一些技巧,也打消了一些顾虑。
不过毕竟Node应用经验有限,所以欢迎大家一起探讨,积累更多宝贵的经验。

也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

随着不同终端(pad/mobile/pc)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本。为了提升开发效率,前后端分离的需求越来越被重视,后端负责业务/数据接口,前端负责展现/交互逻辑,同一份数据接口,我们可以定制开发多个版本。

这个话题最近被讨论得比较多,阿里有些BU也在进行一些尝试。讨论了很久之后,我们团队决定探索一套基于NodeJS的前后端分离方案,过程中有一些不断变化的认识以及思考,记录在这里,也希望看到的同学参与讨论,帮我们完善。

一、什么是前后端分离?

最开始组内讨论的过程中我发现,每个人对前后端分离的理解不一样,为了保证能在同一个频道讨论,先就什么是”前后端分离”达成一致。

大家一致认同的前后端分离的例子就是SPA(Single-page application),所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现。
从某种意义上来说,SPA确实做到了前后端分离,但这种方式存在两个问题:

  • WEB服务中,SPA类占的比例很少。很多场景下还有同步/同步+异步混合的模式,SPA不能作为一种通用的解决方案。
  • 现阶段的SPA开发模式,接口通常是按照展现逻辑来提供的,有时候为了提高效率,后端会帮我们处理一些展现逻辑,这就意味着后端还是涉足了view层的工作,不是真正的前后端分离。

SPA式的前后端分离,是从物理层做区分。(认为只要是客户端的就是前端,服务器端的就是后端)这种分法已经无法满足我们前后端分离的需求,我们认为从职责上划分才能满足目前我们的使用场景:

  • 前端:负责View和Controller层
  • 后端:只负责Model层,业务处理/数据等。

为什么去做这种职责的划分,后面会继续探讨。

二、为什么要前后端分离?

关于这个问题,玉伯的文章Web研发模式演变中解释得非常全面,这里我根据自己的理解概括一下:

2.1 现有开发模式的适用场景

玉伯提到的几种开发模式,各有各的适用场景,没有哪一种完全取代另外一种。

  • 比如后端为主的MVC,做一些同步展现的业务效率很高,但是遇到同步异步结合的页面,与后端开发沟通起来就会比较麻烦。
  • Ajax为主SPA型开发模式,比较适合开发app类型的场景,但是只适合做app,因为SEO等问题不好解决,对于很多类型的系统,这种开发方式也过重。

2.2 前后端职责不清

在业务逻辑复杂的系统里,我们最怕维护前后端通吃那种人写的代码,因为没有约束,M-V-C每一层都可能出现别的层的代码,日积月累,完全没有维护性可言。
虽然前后端分离没办法完全解决这种问题,但是可以大大缓解。因为从物理层次上保证了你不可能这么做。

2.3 开发效率问题

淘宝的web基本上都是基于MVC框架webx,架构决定了前端只能依赖后端。
所以我们的开发模式依然是,前端写好静态demo,后端翻译成vm模版,这种模式的问题就不说了,被吐槽了很久。
直接基于后端的项目开发也很痛苦,一是后端开发环境很重,配置安装使用都很麻烦,而且慢。为了解决这个问题,我们发明了各种工具,比如vmarket,但是前端还是要写vm,而且依赖后端数据,效率依然不高。
另外,后端也没法摆脱对展现的强关注,从而专心于业务逻辑层的开发。
套页面,引脚本就不说了,还要经常让他们改个时间戳,搞个vmcommon,他们也非常痛苦。

2.4 对前端发挥的局限

例如,性能优化如果只在前端做空间非常有限,于是我们经常需要后端合作才能碰撞出火花。但是后端由于框架限制,有时候很难做,比如bigpipe,长链接之类的。

为了解决以上提到的一些问题,我们进行了很多尝试,开发了各种工具,但始终没有太多起色,主要是因为我们只能在后端给我们划分的那一小块空间去发挥。只有真正做到前后端分离,把选择权交还给前端,我们才能彻底解决以上问题。

三、怎么做前后端分离?

怎么做前后端分离,其实第一节中已经有了答案:

  • 前端:负责View和Controller层
  • 后端:只负责Model层,业务处理/数据等。

MVC分

试想一下,如果前端掌握了Controller,我们可以做url design,我们可以根据场景决定在服务端同步渲染,还是根据view层数据输出json数据,我们还可以根据表现层需求很容易的做bigpipe,comet,socket等等,完全是需求决定使用方式。

3.1 基于NodeJS“全栈”式开发

如果想实现上图的分层,就必然需要一种web服务帮我们实现以前后端做的事情,于是就有了标题提到的“基于NodeJS的全栈式开发”

Node 带来的全栈时代

这张图看起来简单而且很好理解,但没尝试过,会有很多疑问。

  • SPA模式中,后端已供了所需的数据接口,view前端已经可以控制,为什么要多加NodeJS这一层?
  • 多加一层,性能怎么样?
  • 多加一层,前端的工作量是不是增加了?
  • 多加一层就多一层风险,怎么破?
  • NodeJS什么都能做,为什么还要JAVA?

这些问题要说清楚不容易,下面说下我的认识过程。

3.2 为什么要增加一层NodeJS?

现阶段我们主要以后端MVC的模式进行开发,这种模式严重阻碍了前端开发效率,也让后端不能专注于业务开发。
解决方案是让前端能控制Controller层,但是如果在现有技术体系下很难做到,因为不可能让所有前端都学java,安装后端的开发环境,写VM。
NodeJS就能很好的解决这个问题,我们无需学习一门新的语言,就能做到以前开发帮我们做的事情,一切都显得那么自然。

3.3 性能问题

分层就涉及每层之间的通讯,肯定会有一定的性能损耗。但是合理的分层能让职责清晰、也方便协作,会大大提高开发效率。分层带来的损失,一定能在其他方面的收益弥补回来。
另外,一旦决定分层,我们可以通过优化通讯方式、通讯协议,尽可能把损耗降到最低。

举个例子:
detail静态化之后,还是有不少需要实时获取的信息,比如优化、物流、促销等等,因为这些信息在不同业务系统中,所以需要前端发送5,6个异步请求来回填这些内容。
有了NodeJS之后,前端可以在NodeJS中去代理这5个异步请求,还能很容易的做bigpipe,这块的优化能让整个渲染效率提升很多。
可能在PC上你觉得发5,6个异步请求也没什么,但是在无线端,在客户手机上建立一个http请求开销很大,有了这个优化,性能一下提升好几倍。

淘宝详情基于NodeJS的优化我们正在进行中,上线之后我会分享一下优化的过程。

3.4 前端的工作量是否增加了?

相对于只切页面/做demo,肯定是增加了一点,但是当前模式下有联调、沟通环节,这个过程非常花时间,也容易出bug,还很难维护。
所以,虽然工作量会增加一点,但是总体开发效率会提升很多。

另外,测试成本可以节省很多。以前开发的接口都是针对表现层的,很难写测试用例。如果做了前后端分离,甚至测试都可以分开,一拨人专门测试接口,一拨人专注测试UI(这部分工作甚至可以用工具代替)。

3.5 增加Node层带来的风险怎么控制?

这个担心有点多余,随着Node大规模使用,SCM/PE/安全,都会解决进行。
他们会帮助我们在申请,测试,发布,风控几个层面保证稳定性

3.6 Node什么都能做,为什么还要JAVA?

我们的初衷是做前后端分离,如果考虑这个问题就有点违背我们的初衷了。即使用Node替代Java,我们也没办法保证不出现今天遇到的种种问题,比如职责不清。我们的目的是分层开发,专业的人,专注做专业的事。基于JAVA的基础架构已经非常强大而且稳定,而且更适合做现在架构的事情。

四、淘宝基于Node的前后端分离

淘宝基于NodeJS的前后端分离

上图是我理解的淘宝基于Node的前后端分离分层,以及Node的职责范围。简单解释下:

  • 最上端是服务端,就是我们常说的后端。后端对于我们来说,就是一个接口的集合,服务端提供各种各样的接口供我们使用。因为有Node层,也不用局限是什么形式的服务。对于后端开发来说,他们只用关心业务代码的接口实现。
  • 服务端下面是Node应用。
  • Node应用中有一层Model Proxy与服务端进行通讯。这一层主要目前是抹平我们对不同接口的调用方式,封装一些view层需要的Model。
  • ( Node层还能轻松实现原来vmcommon,tms等需要)
  • Node层要使用什么框架由开发者自己决定。不过推荐使用express+xTemplate的组合,xTemplate能做到前后端公用。
  • 怎么用Node大家自己决定,但是令人兴奋的是,我们终于可以使用Node轻松实现我们想要的输出方式:JSON/JSONP/RESTful/HTML/BigPipe/Comet/Socket/同步、异步,想怎么整就怎么整,完全根据你的场景决定。
  • 浏览器层在我们这个架构中没有变化,也不希望因为引入Node改变你以前在浏览器中开发的认知。
  • 引入Node,只是把本该就前端控制的部分交由前端掌控。

这种模式我们已经有两个项目在开发中,虽然还没上线,但是无论是在开发效率,还是在性能优化方面,我们都已经尝到了甜头。

五、我们还需要要做什么?

  • 把Node的开发流程集成到淘宝现有的SCM流程中。
  • 基础设施建设,比如session,logger等通用模块。
  • 最佳开发实践
  • 线上成功案例
  • 大家对Node前后端分离概念的认识
  • 安全
  • 性能

技术上不会有太多需要去创新和研究的,已经有非常多现成的积累。其实关键是一些流程的打通和通用解决方案的积累,相信随着更多的项目实践,这块慢慢会变成一个稳定的流程。

六、“中途岛”

虽然“基于NodeJS的全栈式开发”模式很让人兴奋,但是把基于Node的全栈开发变成一个稳定,让大家都能接受的东西还有很多路要走,我们正在进行的“中途岛”项目就是为了解决这个问题。虽然我们起步不久,但是离目标已经越来越近!!

移动开发前端框架选型

随着公司无线ALL IN,前端同学们或多或少都在接触一些无线的业务。
即使没做过无线的同学,基本上也听过关于无线开发的一些难点,比如省电、流量、速度、兼容性等等

这些点给我们的印象就是,我们开发的webapp就一定得更小,更快!
我觉得这个没问题,在PC上,我们也一直在探索怎么把这两点做到极致!

所以,很多之前用KISSY做PC开发的同学会有一点疑虑:做无线开发我们还用KISSY吗?
也有一些同学问我,KISSY有没有针对移动开发的版?
已经用了KISSY做移动开发的同学,KISSY的体积成为了他们的心病,因为zepto才9.2KB(when gzipped)

其实几周前,我也在纠结KISSY那几十KB的大小
还发起过一个讨论无线web性能衡量的一些标是什么?
经过一段时间的实践,关于这个问题我有一些自己的看法。
下面我通过一些数据和我的经验聊下我的看法。

关于兼容性代码

KISSY是一款全终端全浏览器支持的JavaScript框架。
所以大家一定会想,KISSY中一定有大量的兼容性代码,对于移动开发,这些代码都是多余的。
KISSY有兼容性代码没错,但是!做移动开发,其实我们只是不用兼容低版本IE,其他的兼容都还是需要的。
如果大家看过KISSY源码,KISSY是有条件加载的,在高级浏览器中,KISSY不会载入IE哪些兼容性代码。
虽然不是非常彻底,但是真没多出多少代码。
如果你不相信,认为彻底去掉IE的兼容性代码可以让体积压缩很多,我们可以看看JQuery。

jquery两个版本大小对比

  • jquery-1.10.2
    • 压缩前:273KB
    • 压缩后:93KB
  • jquery-2.0.3
    • 压缩前:242KB
    • 压缩后:84KB

大家都知道juqery2.x是不支持IE6/7/8的
压缩后体积相差9KB,这个差值比我想象的要少很多。
在功能相当的情况下,指望不兼容IE6/7/8就能让代码体积较少很多,貌似也不是很现实。

关于体积

KISSY常用的功能有seed,dom,evnet,node,anim,ajax,我看了下大小

  • KISSY Core(seed+dom+event+node+anim+ajax)
    • 压缩后:131(42+89)KB
    • GZIP: 47.9KB

功能类似的组合有seajs+jquery,seajs+zepto,我与KISSY core对比下大小

KISSY core,JQuery,ZEPTO,

zepto,jquery都加上了seajs 6.6KB

咋一看,在体积上KISSY位居榜首
不过这里得强调下,虽然功能类似,KISSY功能比另外两个强大很多。

很多是经常用到的功能,当然你也可以自己实现,但是你能保证稳定性,兼容性,以及向后兼容吗?

47.9到底有多大呢,有参照物或许能让你感觉更直观,以下数据都是gzip之后的数据。

KISSY core大小参照

KISSY core的体积与大部分网站的总体积比起来也还好
像etao这样的就显得略蛋疼,网站23%的大小是KISSY core的大小。

对于“大小”这事,看了上面的图,大家是不是有了更直观的认识?

有些同学说KISSY还是太大,这么大的体积,可能会导致手机运行变慢
这个假设我暂时没法反驳
但是,咱们来看看大名鼎鼎的sencha touch

打开这个demo你可以看到sencha touch的体积
http://cdn.sencha.io/touch/sencha-touch-2.3.1/built-examples/touchtomatoes/index.html

sencha-touch-all-debug.js(2.3.1)

  • 3.5M
  • 912KB(gzip)

是的,你没看错,是3.5M,gzip之后也有将近1M。

有些同学可能会说,环境不一样,老外用的手机好,网络也好!
手机好不好不是很清楚,网络确实比我们好。
但是,关于网络,我也正好有想吐槽的。

关于2G网络

2g网络真的很慢,可以去我写的demo体验一下
http://www.atatech.org/article/detail/11922/786

有些同学规划产品前,把2G网络当作一个首要的衡量标准,认为一定要保证2G网络的体验。
为了省几十KB的流程,甚至不惜砍功能,改方案!
真的有必要吗亲?

2G用户一个月30M流量,刷微信都来不够
你的产品对于这部分用户真的这么重要吗?
真的有必要为了他们牺牲3g,wifi用户的体验么?
我觉得可以多掂量一下!

有时候就应该分清主次,学会放弃,大踏步的迈出步子!!
据说即将发布的淘宝主客户端4.0版本不支持android2.3,android开发的同学是不是因此可以不用996了!

适合移动开发的前端框架

这个节标题是很多同学的一个疑问,但是这种说法是有问题的。
做框架选型,首先得看你的需求,只有适合某个需求的框架:

  • 如果仅仅是一个资讯类的适合手机终端的站点,例如sina.cn,那么你要考虑的是加载速度
  • 如果是一个功能复杂的webapp,你得考虑整体的架构和性能
  • 如果你要的是一个跨终端、跨浏览器的响应式解决方案,那么你就得考虑兼容性

如果你想找一个库同事解决上面所有问题,貌似不太显示,就是鱼和熊掌不可兼得。
简单分析下,很容易找到适合自己场景的:

  • 如果是第一种需求,zepto这种小库比较适合,反正也不会用到很多功能,就算是原生写,也没多少代码
  • 第二种需求,JQueryMobile,KISSY,Sencha这样的框架是个不错的注意。你需要用到大量的特性,组件支持,还要保证项目的架构稳定。
  • 第三种需求,我暂时还没有想到比KISSY更好的 :)

反正很难有一个库满足所有需求,所以一切皆权衡!!

关于KISSY for mobile

有一个好消息,风驰团队发起了一个KISSY MINI的分支项目,目标是功能比seajs+zepto强大,但是体积与他们相当。
等这个项目发布,做一套这样简单的展示型的网站再也不用纠结了

按照承玉的思路,KISSY本身近期应该还是会以全终端全浏览器兼容的思路发展。
不过承玉近期主要的工作都是在做移动相关的组件支持,到时候会提供一整套webapp的解决方案,有需求的同学敬请期待吧!

最后

上面说了这么多,真的不是在忽悠大家用KISSY
只是想让大家有一个清醒的,直观的认识。

如果你觉得seajs,zepto,jquery,sencha中的任何一个更适合你的需要场景
放心大胆的用,不用纠结这纠结哪的
那个产品不是几个月重构一次,有问题了咱再改
不要为了几十KB的体积纠结得饭都吃不下
放心大胆大踏步的去做!
阿里无线起步晚,现阶段,我们最重要的任务是把步子迈出去,把产品做出来!!


文章同时发布在http://blog.kissyui.com/

viewport笔记

对于准备做手机h5开发的同学来说,页面布局应该用多大,是困扰我们的第一个问题。
我们看到iphone5s的参数介绍里写着 主屏分辨率: 1136x640像素
这个分辨率能赶上很多PC机19寸显示器的分辨率了,但是按照这个分辨率来布局显然不行

有些同学看过资料,知道加上下面这句,手机上看网页内容会变得“正常”

1
<meta name="viewport" content="width=device-width,initial-scale=1">

那么这个设置到底做了哪些事情?
相关的介绍非常多,不过一直没仔细研究,很多问题都是一知半解。
这两天花了点时间梳理了一下,搞明白了一些概念。
以下是我的学习笔记:


一、主屏分辨率和主屏尺寸

主屏分辨率主屏尺寸是我们最关注的手机pad等设备的参数,这两个参数是设备硬件参数。例如 IPHONE 5S :

  • 主屏分辨率: 1136x640像素
  • 主屏尺寸: 4.0英寸

在PC显示器上,我们通常都是按照设备主屏的分辨率来做页面布局的。例如之前的主流分辨率是1024*768,我们基本上都采用定宽950px居中布局。
不过PC机的显示器是19英寸,如果在手机上也按照主屏分辨率布局,后果就是把以前19英寸上显示的内容缩小到4英寸的大小,显然我们无法看清显示的内容。
所以,首先我们得了解分辨率以及物理尺寸两个概念。

1.1 主屏分辨率

主屏分辨率也叫设备分辨率(Device Resolution)

我们通常说的分辨率就是指设备分辨率,这个分辨率是指每英寸的面积上可产生的像素点,分辨率越高代表可以将画面显示得更精细。
(有时候获取的显示器分辨率,其实是指桌面设定的分辨率,而不是显示器的物理分辨率。大多数情况下,我们设置的分辨率与物理分辨率一致,显示效果才最佳。)

度量单位 PPI(pixels per inch)

显示器的分辨率,我们一般都是按照例如1024768这种说法,但其实分辨率是有自己的单位的。可能是大家觉得1024768这种说法比PPI直观。
换算也很简单:

假设显示器的屏幕大小为14英寸,即对角线长度为14英寸,
纵横比为3:4,
则水平方向长度为 14*(4/5)=11.2英寸
从而显示分辨率为 1024/11.2 = 91.4 PPI。

所谓Retina屏,其实就是PPI高到人眼识别的极限,下面会简单讲一下。

DPI和PPI的区别

DPI(dots per inch)是一个专属于打印机的墨点级分辨率单位,其意指每英寸所打印的墨点的数目。打印机输出的每个像素都是由若干个不同颜色的墨点拼合而成的。可以认为墨点是更小的像素,称为子像素。
关于DPI与PPI的区别三两句也讲不清楚,有兴趣的可以搜索下相关资料。

设备分辨率

  • 可以通过screen.width/screen.height来查看设备的分辨率。(上面提到过,在PC上获取的设备分辨率有可能是用户设置的桌面分辨率)
  • 设备的分辨率与浏览器设置无关,比如我们进行了缩放。
  • 手机的设备分辨率,与我们写页面布局,几乎没有什么关系。
  • 某些场景下设备分辨率又被称为“设备像素”,下面还有一个关于“CSS像素”的概念

1.2 屏幕尺寸

屏幕尺寸就是指屏幕的物理尺寸,一般都是以英寸为单位,也有以寸为单位的。
注意英寸和寸的区别:1英寸=0.762寸。记得以前很多奸商在这个单位上做文章,忽悠消费者。
屏幕尺寸根我们要讨论的页面布局几乎没有什么关系,所以不详细讨论。

1.3 Retina

Retina显示屏(英文:Retina Display)具备足够高像素密度而使到人体肉眼无法分辨其中单独像素点的液晶屏,最初采用该种屏幕的产品iPhone 4由执行长史蒂夫·乔布斯于WWDC2010发布,其屏幕分辨率为960×640(每英寸像素数326ppi)。这种分辨率在正常观看距离下足以使人肉眼无法分辨其中的单独像素。

Retina的标准为:人眼看不到看不见单个物理像素点。

因为不同设备在使用状态下与人眼的距离不同,所以不同设备满足Retina标准的PPI也会不同。距离越小,PPI要求越高。

在iPad(3rd-Gen)发布会上,苹果给出了Retina设计标准的公式:

-
-

其中a代表人眼视角,h代表像素间距(pixel pitch),其实就是一个像素的大小,d代表肉眼与屏幕的距离。符合以上条件的屏幕可以使肉眼看不见单个物理像素点。

而人眼视角小于或等于1角分(π/10800 弧度)的情况下,无法看见单个像素点。于是可得出Retina标准的PPT计算公式为:-

所以假如人眼与屏幕的尺寸为11英寸,那么通过公式可得PPI = 312,而实际上 iPhone 4/4S 的 PPI 是 330,符合retina的要求。

二、像素、设备像素、CSS像素

要搞清楚文章最开始提出的问题,这个三个名词一定要弄明白。
hax写了一篇文章像素(px)到底是个什么单位,非常有助于我们理解这三个名词,建议读一读。
下面我也按照我的理解分析一下。

2.1 像素

引用维基百科-像素

像素,又称画素,为图像显示的基本单位,译自英文“pixel”,pix是英语单词picture的常用简写,加上英语单词“元素”element,就得到pixel,故“像素”表示“图像元素”之意,有时亦被称为pel(picture element)。每个这样的信息元素不是一个点或者一个方块,而是一个抽象的采样。仔细处理的话,一幅图像中的像素可以在任何尺度上看起来都不像分离的点或者方块;但是在很多情况下,它们采用点或者方块显示。每个像素可有各自的颜色值,可采三原色显示,因而又分成红、绿、蓝三种子像素(RGB色域),或者青、品红、黄和黑(CMYK色域,印刷行业以及打印机中常见)。照片是一个个采样点的集合,故而单位面积内的像素越多代表分辨率越高,所显示的图像就会接近于真实物体。

2.2 设备像素

设备像素其实就是指上面讲过的设备分辨率。之前听有些同学理解手机上的布局,说要以设备像素为准,其实是不对的,应该是visual viewport,后面会讲到。

2.3 CSS像素

CSS像素就是指我们在写页面布局用得最多的单位(px)。
CSS像素是浏览器使用的抽象单位,用来在网页上绘制内容,CSS像素占的物理空间大小是不确定的。
在PC上,浏览器会给我们默认计算好1px占多少物理尺寸,以便让我们看得更舒服。
浏览器对页面进行缩放,其实是缩放CSS像素占的物理空间大小

另外,hax的文章中提到,有些同学不建议使用px作为布局单位其实存疑的,因为不同的设备、浏览器会计算好1px所占的物理空间大小,我们无需关心太多。

三、viewport

3.1 viewport

viewport产生的背景

几年前,我们做的网站都是为PC浏览器而设计的,几乎没考虑到手机屏幕,所以用手机浏览为PC定制的网站会出现问题。
固定宽度的网站,比如经典950的,网页会出现横向滚动条,这还好,至少可以浏览。
但如果是浏览流动布局的网页那情况会非常糟糕,设想一个宽度为 30% 的侧边栏对于 320px 手机屏幕而言也就 96px,只能容纳八个 12px 的汉字,可阅读性非常差。

为了让手机也能获得良好的网页浏览体验,Apple 找到了一个办法:在移动版的 Safari(iOS平台)中定义了 viewport meta 标签,它的作用就是创建一个虚拟的窗口(viewport),而且这个虚拟窗口的分辨率接近于桌面显示器,Apple 将其定位为 980px。

我们通常把这个虚拟的窗口叫,布局窗口(layout viewport)。
例如我们写一个宽度为100%的页面在iPhone5s上浏览,页面宽度是980px。

在Apple实现viewport后,其他浏览器也加入了对viewport meta的支持,但彼此间还是有些差异,差异最大的是layout viewport的大小上:

1
2
3
4
Safari iPhone: 980px
Opera: 850px
Android WebKit: 800px
IE: 974px

在没有viewport设置的情况下,我们很容易可以拿到布局窗口的值:

1
2
console.log(document.documentElement.clientWidth);
console.log(document.documentElement.clientHeight);

设置viewport

1
2
3
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
</head>

这个是最常见的一条 viewport meta 代码,将 viewport 定义为:宽度为设备宽度,初始缩放比例为 1 倍,禁止用户缩放。

viewport 全部属性如下:

1
2
3
4
5
6
width: viewport宽度
height: viewport高度
initial-scale: 初始缩放比例
maximum-scale: 最大缩放比例
minimum-scale: 最小缩放比例
user-scalable: 是否允许用户缩放

详细介绍可以看看Safari对viewport的实现

虽然布局窗口(layout viewport)解决了手机浏览PC定制网页的问题,但是移动时代,很多网站就是为移动定制的。
所以,在无线时代,可视窗口(visual viewport)的大小才是我们真正关系的。

layout viewport (布局窗口) & visual viewport (可视窗口)

关于什么是layout viewport 什么是visual viewport,两个viewport的故事《1》《2》 中有比较详细的介绍

引用他们的两个图,便于我们理解:

Alt text
Alt text

顺便引用下他们的比如:

把layout viewport想像成为一张不会变更大小或者形状的大图。现在想像你有一个小一些的框架,你通过它来看这张大图。(译者:可以理解为「管中窥豹」)这个小框架的周围被不透明的材料所环绕,这掩盖了你所有的视线,只留这张大图的一部分给你。你通过这个框架所能看到的大图的部分就是visual viewport。当你保持框架(缩小)来看整个图片的时候,你可以不用管大图,或者你可以靠近一些(放大)只看局部。你也可以改变框架的方向,但是大图(layout viewport)的大小和形状永远不会变。

layout viewport 和 visual viewport 是我们理解viewport的关键。但是看了各种文章,也都没给他们一个确切的定义,而且都比较绕。
我是这么理解这两个概念的:

在没有viewport这个概念之前,页面布局只有visual viewport这一个概念,无论是手机还是PC机。PC上的visual viewport通常是1280这么大的分辨率,但是由于手机屏幕比较小,通常只有320。为了在PC上定制的网页在手机上浏览有比较好的体验,于是引用了layout viewport这个概念。

四、width=device-width

字面意思是设置宽度(布局宽度)等于设备宽度,但在实际中大多数浏览器都给出了个定值320px;
这句话里面有三个关键词:

  • 大多数: 大多数包括safari,老一点android上的chrome,随着4,5寸屏的市场份额增加,这个值也在变大。
  • 320px: 为什么是320px呢,据说这个值源于 Apple,因为早期 iPhone 的分辨率为 320px × 480px,大量为 iPhone 量身定制的网站都设置了width=device-width,并且按照宽度 320px 来设计制作,所以其他浏览器加入 viewport 支持时为了兼容性也将 device-width 定义为了 320px。
  • 浏览器: 最开始我以为device-width就是设备的某个值,至少同一个设备这个值是滚动的,测试后发现,同一个设备不同的浏览器这个值竟然不同。比如我的三星E120L(刷了小米系统),原生浏览器是320px,但是UC,QQ都是360px;

不同型号的手机这么多,浏览器也很多,意味着device-width的值也会很多,手机页面到底应该按照什么宽度来设计呢?

估计很多同学只关心这个问题的结论,说下我的观点吧:
我觉得这和PC时代的思路是一样的,只是我们又走了一次这个轮回。
记得最早PC显示器的分辨率是800600,于是多数网页是按照760的定宽来设计的
后来1024
768成了主流,于是950定宽成了设计标准。

现在手机主流就是320px,如果做定宽的设计,320px这个取值没错。
不过遇到更大分辨率的手机,比如480px,大家似乎不太能接受固定320px然后居中对齐,所以我们通常是100%宽度来布局的。

大家似乎也都是这么做的!


终于整理完了,虽然自己读起来感觉也不是那么通顺,不过如果大家决定彻底弄懂相关的概念,我的大纲到是可以参考下。
另外,以上很多内容都是引用网上的,加上了一些自己的理解,可能有些结论有误,欢迎探讨和指正。

请各位小伙伴们帮忙做一个简单的测试

大家都说2g/3g网络慢,这个测试就是想直观的看看到底有多“慢”。测试步骤很简单:

1. 关闭手机wifi
为了保证测试数据的准确性,请确保你已经关闭了wifi.

2. 扫描二维码
测试链接

3. 开始测试”
2g网络下可以能需要等待1分钟,会消耗你几百KB的流量。

4. 等待测试完成,提交测试结果
提前请选择正确的测试结果。

如果最后能拿到有用的测试数据和结果,你也将会成为测试结果的受益人,再次感谢大家!

CSS3 Transitions 你可能不知道的知识点

如何临时让transition失效

我们给一个element设置了transition效果,但某些特殊情况,我们希望让transition临时失效。
我们第一反应就是先移除transition设置,等其他属性设置完成之后再还原transition设置。
但浏览器有时候会让我们感觉事与愿违
看下面这段代码,你觉得会不会有transition动画效果?

1
2
3
4
5
6
7
8
<div id="test"></div>
<script>
    window.onload = function(){
        var div = document.getElementById("test");
        div.style.width="500px";
        div.style.transition="all 2s ease";
    };
</script>

答案是有的。
之所以会出现这种情况,与javascript单线程有关,给dom设置style,只是一个赋值的过程,浏览器引擎不会立即去渲染,所以会看到动画效果。
那么遇到这种情况,如何去解决呢?
大家可能首先会想到 setTimeout(),但感觉不是那么自然。

还有另外一种更好的方法,实用getComputedStyle()方法强制让当前设置生效

1
2
3
4
5
6
7
8
9
10
<div id="test"></div>
<script>
    window.onload = function(){
        var div = document.getElementById("test");
        div.style.width="500px";
        //获取计算后的宽度
        window.getComputedStyle(div).width;
        div.style.transition="all 2s ease";
    };
</script>

transitionend 事件

大家都知道,KISSY1.4中支持了transition动画,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KISSY.add(function(S,Node,Anim){
    Node.all("#test").animate({
            transform: "translate3d(-100px,0,0)"
        }, {
            duration: .3,
            //增加useTransition配置即可
            useTransition:true,
            easing: "ease-out",
            complete: function(){
                //your code
            }
        });
},{
    requires:['node','anim']
})

刚开始很好奇,觉得肯定需要不少代码才能实现支持原生动画,看了源码之后才发现其实挺简单,关键点是transitionend事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
#test {
    width:200px;
    height: 200px;
    padding:10px;
    background-color: #8bb8f3;
    transition: all 1s ease;
}
#test:hover {
    background-color: #ff5500;
}
</style>
<div id="test">touch me</div>
<script>
    document.getElementById("test").addEventListener("transitionend",function(ev){
        console.log(ev);
        alert(1);
    })
</script>

transition与visibility

http://gtms01.alicdn.com/tps/i1/T1GjgcFm4eXXbWpdou-1000-852.png

1
-webkit-transition: visibility 0s linear .2s;

在研究google phone版导航菜单效果的时候,无意中发现css中有上面这么一段。
visibility不就是用来实现显示、隐藏效果的吗,与过度有什么关系呢?
不过直觉告诉我,google的工程师不会这么无聊,写一段毫无用处的代码

用相关的关键字搜索了一下,果然暗藏玄机
不过用一两句话说不明白,直接看这篇文章 http://www.zhangxinxu.com/wordpress/2013/05/transition-visibility-show-hide/

看来任何细节深挖下去都会有收获。

启用硬件加速

这个大家可能都知道,方法也有好几种,介绍的文章也多,这里顺便记录一下。

变化某些属性,比如 width,left,浏览器会重新计算每一帧的显示效果,这严重影响速度,尤其是在移动设备上。解决办法就是让 GPU 来做这些运算,简单的说,就是将元素转化为图片再制作变化效果,而不是重新计算每一帧的 CSS 样式。

1
2
3
4
.test{
    //触发GPU加速
    transform: translate3d(0,0,0);
}

教你如何耍流氓

我承认我是标题党 :)
其实不是什么新鲜玩意,觉得有意思就发出来玩玩。

一行代码“劫持”你的浏览器

直接看代码

1
window.opener.location.href = "http://www.taobao.com";

这段代码非常简单,window.opener也是一个非常常见的对象,上面这种用法可能很多人用过
如果你还没明白为什么这是一段流氓代码,那说明你是一个纯洁的同志?

试想一下:
假如你访问了淘宝的detail,点了一个外链的广告页面,这个页面中执行了一句:
window.opener.location.href = “钓鱼网站”;
后果是不是很严重呢?
据说很多人都使用过这种方法来劫持流量

演示:http://sandbox.runjs.cn/show/d7d7lugq

强制留在本页

这个没什么好说的,看代码你就知道很流氓

1
2
3
window.onbeforeunload = function(){
    setTimeout( function(){ location.href = "#"; }, 0 );
};

除了用来耍流氓,不知道大家有没有想到比较正常的使用场景

演示:http://sandbox.runjs.cn/show/84aftk1b

跨终端实践:丢掉你的数据线吧(售后维权图片上传功能增强改造)

背景

在“我的淘宝”中发起售后维权后,需要上传举证图片。我们通常的做事是,先手机拍好照片,然后用数据线导入到电脑中,最后上传提交。淘宝成立10年了,大家一直都这么操作,觉得没什么问题。

“举个栗子”

某天,你愤怒的用手机拍了几张卖家寄过来假货的照片,准备作为凭证提交。
突然,发现数据线忘带了,真是悲剧!!
肿么办?肿么办?
找同事借一根啊!好吧,你狠。
要是你的micro usb接口坏了呢,你肿么办??告诉我你要肿么办??

当然有办法,而且非常简单。
下面就来告诉你,我们是怎么做到的!

二维码传图

通过手机扫描二维码后上传。
是不是很简单,是不是有种恍然大悟、如沐春风、醍醐灌顶、茅塞顿开、大彻大悟的感觉?

什么??没弄明白如何使用二维码上传??
好吧,看来我有必要详细解释下我们的方案。

继续看图

1、首先,在需要上传图片的地方嵌入一个与该页面有特殊关联的二维码。比如你已经发起了一个维权,那么这个URL里面就需要带上维权编号。我们还可以通过token、session失效时间、加密等方式实现免登录和防止出现安全漏洞。

2、用户通过手机或者PAD扫描该二维码,进入图片上传界面。说到这里不得不给大家讲下我在开发过程中遇到的各种坑。

开始我做了一个还比较酷的界面:http://satans17.github.io/fragment/upload.html
放到手机上测试,发现各种兼容性问题。

  • 部分手机(android2.3下的360,UC)浏览器不能fire file控件的click事件,所以没办法使用fileAPI实现预览,更没办法使用formData,XMLHttpRequest实现异步上传。建议直接使用最原始的form表单提交的方式。
  • 使用android2.3下的微信二维码扫描,也不能使用file控件,别的版本没试过。初步估计是微信定制了一些系统自带浏览器的功能,目前还不明真相。 这个比较好解决,就推荐他使用淘宝APP或者一淘火眼,还能顺便推广下我们自己的应用。
  • 部分浏览器没有支持拍照功能,只能选择已有的图片,比如android下的360,chrome表现得比较好,可惜android4.0才能装。

由于设备有限,可能还有其他问题。如果有人遇到,请一起讨论下。

3、用户在手机上提交图片后,提示他返回电脑上继续操作。如果想用户体验做得更好,可以在网页上轮询,看看是否有新图片提交,如果有,就直接显示在网页上。粗暴点的做法就是手机上传成功后,提示他返回刷新电脑上的网页。

思路有了,功能实现起来不难。
从有了这个想法到产品上线,我们只有了三天时间。
下面的截图是上线后的效果

第一步:扫描二维码

第二步:手机上传相册中的图片 或者 直接拍照

第三步:凭证上传成功

小结

几年前,PC机是我们的主要终端设备,相机、麦克、扫描仪、摄像头等都是以PC机的辅助设备出现的。
随着科技的发展,出现了智能手机、iTV、PAD、google glass、kindle等产品,他们都能以独立的终端形式出现,每种设备都有自己的特点和特长,能满足用户的不同需求。而且这些设备越来越多的被人接受和普及,很多人往往在同时使用好几种终端。
例如:你可能在用PC机看电影的同事,还在用手机微信泡妹子。

利用这些不同的终端,不同终端的特性,我们一定可以创造出更多更棒的跨终端操作的用户体验!