使用 SourceMap 来进行前端代码调试

今天在方凳会上做了一次 SourceMap 的分享。现在在博客上分享出来。

简介

什么是 SourceMap 呢?

在这个年代,对于前端开发来说,很少有用户浏览器执行的代码和我们写的 code 完全相同的情况。因为我们的代码一般要经过压缩、合并。另外现在还有 sass, less, stylus, coffscript, typescript 等等预编译语言。那么在这些情况下我们如何调试呢?SourceMap 就是为了解决这个问题而生的,虽然它还不够成熟,支持它的工具还不够多,但是我们能从它身上看到未来。

欲了解详情,请观看下面的 slide 吧。(有疑问或者建议可以在下面评论交流,或者微博 @allenm 进行交流)

sourcemap.001

sourcemap.003

sourcemap.004

sourcemap.005

Continue reading

如果你想在 python 项目中使用 stylus

Stylus 是用 node.js 写的,所以在 nodejs 项目中集成 Stylus 是一件很方便的事情,特别是 expressjs 这样的 web framework 直接配置下就可以用了。在 expressjs 中一旦你修改了 .styl 文件,立马就会被转换成同名的 .css 文件。

如果你想在 Python 项目中获得这样的特性,不妨来试试我写的一个 python package 吧: live-stylus

由于这个 package 依赖 python stylus ,python stylus 最终是要依赖 nodejs 和 stylus node package 的,所以你还是需要安装 nodejs 和 stylus node package 的。

如果你没有使用过 nodejs ,请在 nodejs 官网查看安装方法,然后:

npm install -g stylus

即可完成 stylus node package 的安装。然后:

pip install live-stylus

即可安装 live-stylus 。使用方法非常简单,比如在一个 Flask 项目中:

from flask import Flask
from live_stylus import ConvStylus

app = Flask(__name__)

from views import *

if __name__ == "__main__":
    app.debug = True
    ConvStylus()
    app.run()

这样就可以监控你项目中的所有 .styl 文件的变化,从而实时的转换成 .css 文件了。更多的说明和使用方式请参考此项目的github 主页

BASE64 编码规则

Base64 编码是一种可以把二进制文件编码成文本的编码规则。在很多地方地方都有用到,比如我们可以把图像转成 Base64 编码,然后内联到 HTML 或者 CSS 中。

Base64 编码用一些很常见的 ASCII 字符来表示 0-63 ,构成 6 个 bit 。用 A-Z 表示 0-25 ,a-z 表示 26-51 , 0-9 表示 52-61 , + 表示 62, / 表示 63 。那怎么把一个 byte(8bit) 用这种只能表示 6 bit 的字符表示呢?转换规则如下:

很简单吧,就是用4个 Base64 编码来表示 3 个 byte 的内容。

如果到末尾,凑不齐 3 byte 怎么办呢?如果只有一个 byte ,就在后面补4个 0 bit ,构成 2 个 Base64 编码,如果余下 2 byte ,就补 2 个 0bit ,构成 3 个 Base64 编码。理论上,这样做就足够判断最后还剩下几个 byte ,但是有些系统需要明确指明最后剩下的 byte ,需要用 = 号来把 Base64 编码的个数补全到 4 的倍数。也就是如果最后余下了一个 byte , 就在编码后的字符补上 == ,如果余下了 2 byte ,就补上 = .

有了这些知识就知道Base64的基本规则,想了解更多的请阅读:http://en.wikipedia.org/wiki/Base64

解放你的 ALT(CMD)+TAB 键吧

做前端开发的童鞋们,最常敲的一组键就是 alt(cmd)+tab 然后紧接着是 F5 或者 ctrl(cmd) + r 了吧。这是在干什么?写好一段样式看看效果啊。

那我们能不能边写样式,边让浏览器自动加载最新的 css 呢?当然是可以的了,业界已经有很多这样的工具了。比如国内前段时间曾经成功推广过的 F5 ,还有国外的 live.js xrefresh 等工具。

但是这些工具都没有适合我的。F5 需要用它提供的 HTTP 服务,但是对于一个大型站点来说,静态资源都是单独管理的,页面上一般都写的绝对 URL 使用 cookie free 的域名或者 CDN 的域名。为了开发方便,我们一般会用 apache 在本地搭一个 style 环境,像我们现在,因为经常需要跨应用访问 css, js 这些,本地环境不一定有这些文件,还需要用 url rewrite 把这些文件 rewrite 到一个公共环境去。这些复杂功能都是 F5 所不能提供的。live.js 只能对 css 和 page 同域才有效,而且还不支持 @import ,xrefresh 也需要使用它提供的 HTTP 服务。而且这些工具都有一个共同点,是都需要往页面插入一段它的 js 。

于是,我就自己造了一个,优先满足我的需求,那就是要做到服务器无关,编辑器无关。然后要支持 css 和 page 不同域,支持 @import 。而我平时又使用 chrome 开发,那就写一个 chrome 插件好了。而且由于使用了浏览器插件技术,所以我也可以做到不在页面插入任何 JS,不改变任何 DOM 结构,尽量保证安全,不影响页面功能。

现在这个项目的源码托管在 github :https://github.com/allenm/css-auto-reload , 欢迎各位提 BUG和建议,或者来写一个其他浏览器的插件。

如果你和我一样喜欢使用 chrome 开发,那就直接来这里 https://chrome.google.com/webstore/detail/fiikhcfekfejbleebdkkjjgalkcgjoip 安装,使用很简单,只要点击插件图标,就会点亮图标,表示正在对当前标签页进行监控,再次点击会关闭监控。

详细介绍和使用说明可以看上面 github 项目的介绍。另外我还录了一段操作视频,你可以先看看,再决定要不要试试这个工具。

 

如果使用后觉得好用,确实对你有所帮助,不要忘记向你身边的 web developer 们介绍下哦,有心情的话,可以去 chrome web store 上面给我个好评哦。

jQuery deferred 对象的 promise 方法

jQuery 从 1.5 版本引入了 deferred 对象,这是一个基于 CommonJS Promises/A 的设计,为了方便异步操作,大家都知道,在 js 中,异步代码是非常的多。

这篇博客不是来详细讲解 $.Dererred 的,阮一峰 的博客里有一篇博客详细介绍了这个。但是在这篇博客中,关于 promise() 这个方法的讲解却是错误的,我发现这个是因为我徒弟去学习这个的时候,看了这篇文章,然后我让他给我讲述的时候,发现了这个错误。所以我想写篇 blog 来说明下。

update(2012.1.2):  我针对此问题给一峰发了 email, 他已经针对下面的问题进行了更新,下面的引用来自他最初的版本。现在他博客中对 promise() 的讲解是正确的,如果想了解整个  Deferred 对象,建议直接移步他的博客中学习。同时感谢阮一峰发现的我这篇博客中的一个小错误,最后面的那段代码,现在已经修复。

下面是引用自阮一峰的博客:

这里有两个地方需要注意。

首先,最后一行不能直接返回dtd,必须返回dtd.promise()。原因是jQuery规定,任意一个deferred对象有三种执行状态—-未完成,已完成和已失败。如果直接返回dtd,$.when()的默认执行状态为”已完成”,立即触发后面的done()方法,这就失去回调函数的作用了。dtd.promise()的目的,就是保证目前的执行状态—-也就是”未完成”—-不变,从而确保只有操作完成后,才会触发回调函数。

下面是 jQuery 的官方文档

The deferred.promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request. The Promise exposes only the Deferred methods needed to attach additional handlers or determine the state (then,donefailalways,pipeprogress, and state), but not ones that change the state (resolverejectprogressresolveWithrejectWith, and progressWith).

也就是说,deferred.promise() 只是阻止其他代码来改变这个 deferred 对象的状态。可以理解成,通过 deferred.promise() 方法返回的 deferred promise 对象,是没有 resolve ,reject, progress , resolveWith, rejectWith , progressWith 这些可以改变状态的方法,你只能使用 done, then ,fail 等方法添加 handler 或者判断状态。

deferred.promise() 改变不了 deferred 对象的状态,作用也不是保证目前的状态不变,它只是保证你不能通过 deferred.promise() 返回的 deferred promise 对象改变 deferred 对象的状态。如果我们这个地方直接返回 dtd,也是可以工作的,.done 的处理函数还是会等到 dtd.resolve() 之后才会执行.

具体在那篇博客的例子, 如果我们把代码改成如下的形式:

var dtd = $.Deferred(); // 新建一个deferred对象
var wait = function(dtd){
    var tasks = function(){
        alert("执行完毕!");
        dtd.resolve(); // 改变deferred对象的执行状态
    };
    setTimeout(tasks,5000);
    return dtd;
};
$.when(wait(dtd))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

这样的执行结果和先前返回 dtd.promise 的结果是一样的。

差别在什么地方呢?如果我们把 $.when 的这块的代码改成这样的:

var d = wait(dtd);
$.when(d)
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
d.resolve();

我们会发现 alert(“哈哈,成功了!”) 会立即执行,“执行完毕”却需要5秒后才弹出来。

但是如果我们 wait 函数最后是 return dtd.promise() 这里 d.resolve() 就会报错了,因为对象 d 不存在 resolve() 方法。

同样如果我们把代码改成:

var dtd = $.Deferred(); // 新建一个deferred对象
var wait = function(dtd){
    var tasks = function(){
       alert("执行完毕!");
       dtd.resolve(); // 改变deferred对象的执行状态
   };
   setTimeout(tasks,5000);
   return dtd.promise();
};
dtd.resolve();
$.when( wait(dtd))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

我们也可以发现 alert(“哈哈,成功了!”) 会立即执行,因为 dtd 这个 deferred 对象在被传入 wait 之前,已经被 resolve() 了,而 deferred 对象一旦被 resolve 或者 reject 之后,状态是不会改变的。

然后我们再把 $.wait 这块的代码改成:

$.when( wait(dtd))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
dtd.resolve();

我们也会发现 alert(“哈哈,成功了!”); 被立即执行,虽然 wait(dtd) 执行的时候, dtd 还没有被 resolve,而且 wait 方法返回的是 dtd.promise(), 但是 dtd 这个原始的 deferred 对象是暴露在外面的,我们还是可以从外面改变它的状态。

于是,如果我们真的不想让其他代码能改变 wait 方法内部的 deferred 对象的状态,那我们应该写成这样:

var wait = function(){
    var dtd = $.Deferred(); // 新建一个deferred对象
    var tasks = function(){
        alert("执行完毕!");
       dtd.resolve(); // 改变deferred对象的执行状态
   };
   setTimeout(tasks,5000);
   return dtd.promise();
};
$.when( wait())
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

也就是不要把 deferred 直接暴露出来,最后返回 deferred.promise() ,让其他地方的代码只能添加 handler 。

WebSocket Protocol 介绍与实现

今天在方凳会上分享了 websocket protocol以及简单实现,PPT 和代码都放出来,有兴趣的可以围观

然后用 python 简单的实现了 websocket server,代码如下。在网上找的很多代码,都不再兼容 websocket draft 10, 因为 websocket 的草案进化太快,下面的代码是按照 draft10 写的。 chrome 14+ , firefox 7/8 都没有问题。

关于 websocket 草案,请阅读 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10

 

另外推荐一个 Python 的 websocket server http://www.tavendo.de/autobahn/home.html, 基于 twisted 的异步模型。

localStorage in HTML5(1)

HTML5 中的 localStorage 我相信 web developer 们,都听说过了吧。目前 chrome,firefox,opera,safari, IE8 都支持此属性,注意 IE8 也支持,那就是说,如果你的网站用户不是太低端,起码有一半以上的用户的浏览器是支持 localStorage 特性的。各个浏览器分配给每个“源(region)”的 localStorage 空间至少为 5M(具体数值请看稍后讨论),对于想开始使用 HTML5 的人们来说,这个是个不错的开始。

localStorage 使用方式也非常简单,有 setItem, getItem, removeItem,key, clear 5个方法,和 length 一个属性。定义如下:

interface Storage {
  readonly attribute unsigned long length;
  DOMString key(in unsigned long index);
  getter any getItem(in DOMString key);
  setter creator void setItem(in DOMString key, in any value);
  deleter void removeItem(in DOMString key);
  void clear();
};

localStorage 中都是以 key/value 的形式来存储数据的,存储的数据类型都是字符串,如果需要其他类型,需要自行转换。我们可以使用 setItem 方法来存储数据,例如 localStorage.setItem(‘foo’,’test data’); 这样来存储一个 key 为 foo 的数据,然后可以使用 getItem 来读取 value 值,即 localStorage.getItem(‘foo’); 如果 setItem 的时候,key 已经存在,则修改保存的数据为最新的数据,如果不存在,则新建 key。执行 removeItem() 可以删除已经存储的数据,比如localStorage.removeItem(‘foo’) 可以删除 key 为 foo 的数据。执行 clear() 会清除此“源”下的所有数据。key() 方法可以获取第 n 个数据的 key 值,例如 localStorage.key(0) 可以获取到第 0 个 key/value 对 的 key 值。这个配合 length 属性可以遍历出所有存储的值。length 属性返回已经存储的数据的条数。

上面简单的介绍了下 localStorage 的用法,然后我们来讨论各个浏览器分配了多少空间给 localStorage。

在各个浏览器中,只有 opera 可以清楚的看到用了多少空间,当这个容量到达 5M 的时候,浏览器就会提示用户是否增加容量,如果用户点击了拒绝,那此后就不会再提示,如果不删除这些数据,将不能再存储,抛出错误。如果用户点击了允许,那将获得以前两倍的空间,等到达了新的上限后,又会弹出提示,以 5M -> 10M -> 20M -> 40M 这样递增。

另外 IE8 的 localStorage 对象,有个 remainingSpace 的属性,可以查看剩余的可用空间,在 IE 中执行 alert(localStorage.remainingSpace) 可以看到 IE8 有5M 的最大存储空间。如果达到最大值,会抛出错误,不清除数据前将不能继续存储。

其他浏览器我还没找到办法查看最大容量,但是我用同样的脚本在不同的浏览器里跑,等到达最大值后,查看存储的键值对的数量,结果如下:opera 可以存储 1482 条记录,chrome 和 safari 可以存储1986条,MAC下的 firefox 3.6  可以存储3970条,IE8 下可以存储 3786条。WIN 下的 firefox 3.6 一直没得到结果,一直没有抛出错误,好像是无限的一样。(以上浏览器都是使用的截止到今天的最新的正式版)。 win 下的 firefox 3.6 的问题还有待验证,我使用的环境是虚拟机下的 windows7 系统。

从 IE8 的 remainingSpace 属性可以看出默认最大容量也是 5M ,和 opear 相同,但是存储的条目数量却相差很大,猜测可能是使用了压缩算法,因为存储的都是字符串,压缩率还是非常可观的。chrome 和 safari 具体的最大容量,从这里也没办法推断出来,不过既然 opera 存储的条目数最少,那我们就认为至少可以存储 5M 的数据吧。

有了 5M 这个数量,我们就可以判断哪些数据适合用这个来存储,哪些不适合。毕竟现在如果存储满了后,还没有什么好办法来清除一些不再使用的数据,因为我们从代码里不容易判断哪些是用户很少用到的。最好使用这个 feature 来实现一些用户体验加强的东西,而不是来实现关键性的东西。

另外在 IE8 以下的版本,我们可以使用其他技术来代替,比如 IE 特有的 userData,或者 flash store 。可以考虑封装一下,暴露出统一的接口给用户( developer)使用。

chrome.tabs.create 的 callback 不执行?

在写 chrome 插件的过程中,不知道你有没有遇到使用 chrome.tabs.create 创建一个新的 tab,但是这个方法的 callback 却不执行,具体说来,在调试的时候是可以执行的,关掉 chrome  的 DEBUG 工具后,就不可以了。

前几天我也遇到了这个麻烦,可能出现这种问题的情况为:在点击浏览器上插件的图标后,打开一个 popup 的页面,在这个页面里点击某按钮后执行 chrome.tabs.create 方法新建一个新的 tab 页,关掉 DEBUG 工具后,此方法的 callback 函数就无法执行。

我们先来观察这种 popup 的页面的行为,随便找一个会 popup 页面的扩展,点击,让页面弹出,然后你切换下浏览器上已经打开的 tab 页,看看发生了什么?是不是这个 popup 出来的页面会关闭掉?然后在那个扩展上右键,选择“审查弹出内容”,再切换下 tab,发现了什么?是不是这个弹出的页面依然打开?

这就是问题所在了,默认情况下,chrome.tabs.create 创建新的 tab 后,会自动切换到那个新建的 tab,在不打开 DEBUG 工具的情况下,原先弹出的页面会关闭,也就是被 unload 了,页面中 js 的宿主已经不在了,当然也就不会执行 callback 了。那应该怎么办呢?很简单,不切换 tab 就OK了。在 chrome.tabs.create 的第一个参数中,是有个 selected 的可选属性的,把这个属性置为 false,就不会自动切换到新窗口去了。例如下面的代码:

chrome.tabs.create( { url:chrome.extension.getURL('./html/testy.html'), index:(S.tab.index+1), selected:false }, function(tab){
				setTimeout(function(){_send2Spliter(tab); },50 );
});

我的 callback 中,使用了 setTimeout,是因为我遇到的另外一个问题,我需要在新的 tab 打开后,向新的 tab 发送一点数据,但是大部分时候会失败。经过测试,发送数据的程序是执行了,但是 tab 中接收数据的 callback 未能执行。于是猜测,是新的 tab 中的  chrome.extension.onRequest.addListener 还没来得及执行造成的,于是就稍稍延迟下,再发送数据,这样就OK了。

如果你遇到向新建的 tab 中发送数据失败的情况,不妨也试试这个方法。

使用SVN hook 让前端BUG少一点

你有没有发生过,发布的代码中含有 console ,然后在 IE 下报错?或者代码中有调试用的alert,用户点击某个东西后,就会弹出来?

这种错误 ,我相信前端开发者或多或少都会犯,要避免这类的BUG ,需要非常非常小心,但是人总会有疏忽的时候,这样的事情交给机器办比较合适,因为机器会按照指定的规则来检查你提交的代码,每次都完全按照规则来。

版本管理软件一般都有预置的hook,我们可以写一些脚本在我们对代码仓库做操作的时候来执行,这里,我将给出一个 TortoiseSVN 可以使用的 client side hook 脚本来做这种检测。

这个是目前我在实际生产环境使用过一段时间的,因为我们这里使用的 SVN 做版本控制,SVN 标准只需要 server 端实现 hook script,客户端不需要,由于大公司server端不那么容易去更改,恰巧 TortoiseSVN 实现了 clientside hook ,那就用这个来实现吧。

按照官方文档给的 pre commit hook script 修改而来,使用前,先要检查你的windows 是否用 WScript 关联了 .js 文件,绝大部分前端开发人员都关联了某个编辑器吧。

检测方法:随便新建一个 js 文件,里面写上”WScript.Echo(“Hello World!”);”,双击看能不能运行,如果你关联了编辑器,就肯定不行了,如果弹出个窗口“hello world”,那就可以跳过这一节了。

恢复windows 默认 .js 文件关联程序的方法:修改注册表,[HKEY_CLASSES_ROOT\.js] 项下的那个默认值改成 “JSFile”。修改完后看看刚才的测试文件是否正常运行。

然后,在本地磁盘任意位置新建 .js 文件,保存此代码:

/**
* author: Allen.M
* blog: http://blog.allenm.me
* date: 2010-12-13
* 根据 http://tortoisesvn.googlecode.com/svn/trunk/contrib/hook-scripts/client-side/PreCommit.js.tmpl 修改
* this script is a local pre-commit hook script.
*/
var objArgs,num;

objArgs = WScript.Arguments;
num = objArgs.length;
if (num != 4)
{
    WScript.Echo("Usage: [CScript | WScript] PreCommit.js path/to/pathsfile depth path/to/messagefile path/to/CWD ");
    WScript.Quit(1);
}

var paths = readPaths(objArgs(0));
var message = "list of paths selected for commit:\n";
var pattern = /^\s*(?!\/\/)(alert|console)/;
var haveWarning = false;
var i = 0;
while (i < paths.length)
{
	message = message + paths[i] + "\n";

	var content = readPaths(paths[i]);

	for(var m = 0, l = content.length; m < l; m++){
		if(pattern.test(content[m])){
			haveWarning = true;
			message = message + 'warning: ' + paths[i] + ' line ' + (m+1) + ' have "alert" or "console" \n';
		}
	}
	i = i + 1;
}
message = message + "path of message file is: " + objArgs(2) + "\n";

if(haveWarning){
	WScript.Echo(message);
}
WScript.Quit(0);

function readPaths(path)
{
	var retPaths = new Array();
	var fs = new ActiveXObject("Scripting.FileSystemObject");
	if (fs.FileExists(path))
	{
		var a = fs.OpenTextFile(path, 1, false);
		var i = 0;
		while (!a.AtEndOfStream)
		{
			var line = a.ReadLine();
			retPaths[i] = line;
			i = i + 1;
		}
		a.Close();
	}
	return retPaths;
}

然后按照下图所示配置脚本。

需要注意的是,指定脚本文件的地方,一定要在前面加上WScript,否则你 ci 代码的时候会得到一个出错信息。

此脚本不干涉你CI代码,即使有 alert ,console 等关键字,也只会给出个 warning ,代码还是可以 ci 成功,当出现 warning 的时候,你需要做的是检查代码是不是有问题。 //console 这样被注释掉的代码是被忽略的。另外你的代码要使用 windows格式的换行符,否则会有检测失败的情况。

检测代码的关键代码是中间的那行正则,你可以修改为适合你的。

warning 是这种形式显示的,如果无warning 信息,则会安安静静的。

Magic canvas & PNG

用 canvas 配合 PNG 可以做出什么好玩的东西呢?

昨天阿里中文站的方凳会,我为大家分享了这个主题。这是我第一次在方凳会上做大分享,也算迈出了这一步,以后会努力多在这个舞台上表现。

主要讲了运用 canvas 来做动态 favicon 和用 PNG 来打包字符串(js) 然后前端通过 js 对这个图片解码。

PPT 在 http://share.allenm.me/fd/ ,有兴趣的同学可以来看,我就不在这里重复一遍内容了。