NCZ在他的同名博客《Feature detection is not browser detection》中,讲述了一直以来前端开发中的一个热门技术——检测用户的浏览器平台,并详细地叙说历史发展以及各种办法的优缺点。我大致翻译了部分文章,可能有理解错误的地方,敬请指正。值得一提的是,评论部分的争论亦值得一看。

特性检测

起初前端工程师们就极力反对浏览器检测,他们认为类似user-agent嗅探的方法是很不好的,理由是它并不是一种面向未来的代码,无法适应新版的浏览器。更好的做法是使用特性检测,就像这样:

if (navigator.userAgent.indexOf("MSIE 7") > -1){
    //do something
}

而更好的做法是这样:

if(document.all){
    //do something
}

这两种方式并不相同。前者是检测浏览器的特殊名称和版本;后者却是检测浏览器的特性。UA嗅探能够精确得到浏览器的类型和版本(至少能得知浏览器类型),而特性检测却是去确定浏览器是否拥有某个对象或者支持某个方法。注意这两者是完全不同的。

因为特性检测依赖于哪些浏览器支持,当出现新版本浏览器的时候需要繁琐的确认工作。例如DOM标准刚出现的时候,并不是所有浏览器都支持getElementById()方法,所以一开始代码可能是这样:

if(document.getElementById){  //DOM
    element = document.getElementById(id);
} else if (document.all) {  //IE
    element = document.all[id];
} else if (document.layers){  //Netscape < 6
    element = document.layers[id];
}

这是特性检测很好的一个例子,亮点在于当其它浏览器开始支持getElementById()方法时不必修改代码。

混合方式

后来前端工程师们考虑改进的写法,代码变化成这样:

//AVOID!!!
if (document.all) {  //IE
    id = document.uniqueID;
} else {
    id = Math.random();
}

这个代码的问题是通过检测document.all属性来确定是否是IE。当确定是IE后,假定使用私有的document.uniqueID属性也是安全的。然而,目前所作的只是确定是否支持document.all,并非是去辨识浏览器是否为IE。仅仅支持document.all的话也不意味着document.uniqueID是可用的。

后来人们开始这样写,用下面那行代替上面的:

var isIE = navigator.userAgent.indexOf("MSIE") > -1;
//下面这行代替上面那行
var isIE = !!document.all;

这些变化说明大家对“不要使用UA嗅探”存在误解——不再对浏览器的详细信息进行检测,取而代之的是通过特性的支持来推断。这种基于浏览器特性检测的方式非常不好。

后来前端们发现document.all并不可靠,更好的检测IE变为:

var isIE = !!document.all && document.uniqueID;

这种实现方式陷入歧途。不仅需要费时费事地去识别浏览器所增加的特性支持,另外也不能确定其它浏览器开始支持相同的特性。

如果你认为这样的代码并未被广泛使用,那么看看来自于老版本的Mootools代码片段吧:

//from MooTools 1.1.2
if (window.ActiveXObject) window.ie = window[window.XMLHttpRequest ? 'ie7' : 'ie6'] = true;
else if (document.childNodes && !document.all && !navigator.taintEnabled) window.webkit = window[window.xpath ? 'webkit420' : 'webkit419'] = true;
else if (document.getBoxObjectFor != null || window.mozInnerScreenX != null) window.gecko = true;

注意它是如何使用特性检测的。我可以指出它一系列的问题,比如通过检测window.ie会将ie8误认为ie7。

余波

随着浏览器的快速发展,使用特性检测变得越来越困难和不可靠。但是Mootools 1.2.4仍然使用这一方法,例如:getBoxObjectFor()

//from MooTools 1.2.4
var Browser = $merge({

	Engine: {name: 'unknown', version: 0},

	Platform: {name: (window.orientation != undefined) ? 'ipod' : (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase()},

	Features: {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)},

	Plugins: {},

	Engines: {

		presto: function(){
			return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925));
		},

		trident: function(){
			return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
		},

		webkit: function(){
			return (navigator.taintEnabled) ? false : ((Browser.Features.xpath) ? ((Browser.Features.query) ? 525 : 420) : 419);
		},

		gecko: function(){
			return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18);
		}

	}

}, Browser || {});

应该怎么做?

特性检测是个应该避免的方法,尽管直接进行特性检测是个很好的方法,并且大部分情况下能满足需求。一般只要在检测前知道这个特性是否被实现即可,而不会去考虑它们之间的关系。

我并非是说永远不使用浏览器特性检测而是基于UA嗅探,因为我相信它还是有很多用途的,然而我不相信它有很多合理的用途。如果你考虑UA嗅探的话,请先贯彻这一思想:唯一安全的方式是针对特定浏览器的特定版本,超出范围之外都是不可靠的——例如新出的浏览器版本。其实这样做也是个明智的办法,因为相较于向前兼容不确定的新版本而言,向后兼容老版本是最简单的做法。

十二月 27th, 2009

删除数组的重复项

9 Comments, 前端开发, by army8735.

怿飞在同名文章《删除数组中重复项(uniq)》中分享了他的方法,时间复杂度和空间复杂度均为O(n),但还无法解决弱类型的问题。凑巧我也思考过类似的方法,在此基础上修改一下,可以KO弱类型问题。

<script src="mootools-1.2.4-core-nc.js"></script>
<script>
Array.implement({
	distinct: function() {
		if(this.length < 2) {
			return this;
		}
		var hash = new Hash(), value;
		outer:
		for(var i = 0, len = this.length; i < len; i++) {
			if(hash.has(this[i])) {
				value = hash.get(this[i]);
				for(var j = 0, len2 = value.length; j < len2; j++) {
					if($type(this[i]) == value[j]) {
						this.splice(i, 1);
						i--;
						len--;
						continue outer;
					}
				}
				value.push($type(this[i]));
			}
			else {
				hash.set(this[i], [$type(this[i])]);
			}
		}
		return this;
	}
});
var arr = [0, 0, "0", '0', false, null, NaN, undefined, 1, "1", true];
alert(arr.distinct().join(", ")); //[0, "0", false, null, NaN, undefined, 1, "1", true]
</script>

基于mootools,我为Array类提供一个distinct方法,以供删除重复项。

基本思想是:遍历数组,建立一个hash用以统计出现过的每条项目。不过hash的key是数组每项的toString()值,value却是个类型数组。每遍历数组中的一个值时,首先检查hash中是否存在key,不存在设置value为一个新数组——并且里面有唯一的类型值(这个数组项的类型);如果hash中存在key,则遍历value,查找此数组项的类型是否在value中,在则说明重复,不在将类型push到value里面。

这样就解决了弱类型的问题,但如果数组中不仅仅有基本类型,还有数组、对象、arguments、hash等,则会更加复杂。

十二月 24th, 2009

960gs中浮动清除新变化

No Comments, 前端开发, by army8735.

昨天下了新版本960gs,瞅瞅里面有无新的变化,正巧发现其中.clearfix的新变化:

/* `Clear Floated Elements
----------------------------------------------------------------------------------------------------*/

/* http://sonspring.com/journal/clearing-floats */

.clear {
	clear: both;
	display: block;
	overflow: hidden;
	visibility: hidden;
	width: 0;
	height: 0;
}

/* http://perishablepress.com/press/2009/12/06/new-clearfix-hack */

.clearfix:after {
	clear: both;
	content: ' ';
	display: block;
	font-size: 0;
	line-height: 0;
	visibility: hidden;
	width: 0;
	height: 0;
}

/*
	The following zoom:1 rule is specifically for IE6 + IE7.
	Move to separate stylesheet if invalid CSS is a problem.
*/
* html .clearfix,
*:first-child+html .clearfix {
	zoom: 1;
}

关键在于最后几行的更新,用单独的样式为ie6和ie7设置了hack(应该是zoom:1触发hasLayout),来解决极少数情况下浮动清除的bug。至于争议性的.clearfix到底用不用,则实在是个鱼与熊掌的问题。

十二月 17th, 2009

JAse预览版

1 Comment, JAse, by army8735.

很高兴能为大家带来JAse的预览版,一款基于as+js的网页语法编辑器。在经历了jssc的静态DFA语法解析高亮的研究和JAte文本编辑器的失败之后,JAse终于能够吸取两者的经验和不足,慢慢地开发至今。开头不多说了,来看预览地址(暂且只有js解析器,其它的都是不存在的;基于Flash Player 10)。

两个都可以:http://jase.googlecode.com/svn/trunk/jase1/bin/index.htmlhttp://army8735.org/wp-content/uploads/jase/

它的下载地址:http://code.google.com/p/jase/downloads/list

它的svn:http://jase.googlecode.com/svn/trunk/jase1/

目前只能支撑千行级代码编辑,总体性能并不是卡在解析器上,而是卡在flash本身的input上。有过经验的人可能感受颇深,何时adobe能提供高性能的textfield?

JAse基本内容分为两块:编辑器提供基础编辑功能、解析器以插件形式提供高亮功能。两者之间通过一个接口连接,也就是说所有的外部解析器必须实现IParser接口,编辑器每次的更改也都是调用接口方法。如此实现了扩展语法的功能,只要实现了接口,你可以开发任意的高亮程序。当然,我自己也写了个AbstractParser基类,可以方便地在其基础之上拓展。

编辑器的undo、redo等功能还未和解析器链接上,因此使用这些button的话可能会造成高亮错误,这并不是程序本身原因。预览版放出只是展示基本输入修改已经ok了。其它将会在后续版本中逐渐开发。

JAse的整体流程是:寻找目标textarea(如果指定id直接获取,否则以document上第一个textarea为目标)=>隐藏textarea,将自己替换掉它的位置=>加载外部解析器(如果指定syntax直接加载,否则出现界面选择)=>编辑内容=>提交内容(如果指定url则使用urlloader,否则将内容放回textarea并寻找父级form提交)。

其间动态破损、修复、显示模型经历过3次算法变更得以成型,斜线动态区分perl正则和除法的算法也经历过2次变更,让我用图来简单说下吧。

jase-preview-1

在这种情况下,每行代码首先会被解析开始状态(如注释、字符串或者普通),同时渲染标明此行是否被高亮过。这两个状态List是和每行代码一一对应的,增加随之增加,修改随之修改,删除随之删除。而区块List则完全脱离于代码行索引,自成一家。

这样做的好处是分析和显示互相分开,每次编辑器内容被修改时先有破损和修复模型来完成行状态、渲染状态和区块的操作;然后由显示模型取得可视区域再从区块List中取出索引对每块进行高亮。

但坏处也很明显,每次操作对 区块List的改动太大了,前面增加一个字符,会导致所有后面的索引进行修改(自增1),因此很快就被废除,有了第2种想法。

jase-preview-2

废除区块索引。每次编辑器内容修改,都去计算行状态并且设置涉及行的渲染状态为否以便重新渲染。这个算法对分析阶段来说有了极大的便利,但是在显示阶段却叫苦不迭——在渲染每一行代码的时候,都要根据行状态重新对此行代码进行一遍DFA解析,而这个工作在前面的分析阶段已经做过了(大部分)。同样的功能需要两份代码显得冗余,要合并到一起的话又会变得难以维护,而且多分析一次也显得没有必要。

在经历了一段时间的纠结后,最终还是变更到现在这个样子。

jase-preview-3

区块List成为二维的,亦和每行对应。然后List中的List保存索引。不过和第1种方式略有不同,之前保存的是绝对索引(字符串的下标),而这里保存的是相对索引(相对于行代码第一个字符而言)。这样每次渲染的时候,只要知道行代码的第一个字符索引,然后遍历这行对应的区块List,相加得出绝对索引即可完成显示。而且每次修改后,后面行的相对索引都无需变更,综合了以上两种方法的优点。

至于斜线的动态区分,最终情况和这类似。静态DFA解析的情况下很容易确定一个斜线的含义究竟是除法还是正则——因为源代码扫猫是从头至尾的。动态情况下因为不确定性,不可能每次修改都从头遍历代码,代价太大了,最好能够从当前行或者前面几行判断出。最初我采用向前回溯的方式,这显得很愚蠢,而且前面行还可能存在单行注释、多行注释等情况干扰(词法分析无需考虑,因为注释空白一开始就被剔除了),基本上行不通。

后来我为每行增加一个布尔值来标明行起始状态如果出现斜线的话它的含义是什么,每次内容修改也会根据解析情况重置它,就和重置行状态和是否被渲染一样。

另外曾经让我非常头疼的一个地方是事件侦听部分。我希望的是每次编辑器内容修改时,所触发的侦听在修改后立刻执行,而as中所有的事件侦听都是在之前执行。这导致使用解析器进行分析显示的时候,实际上代码还根本未发生改变,显然这是个悖论。唯一的做法就是将侦听中模拟事件率先执行,然后通过preventDefault()来取消默认事件发生。这里有个问题,那就是像删除这样的事件(无论del还是backspace或者选区替换)是无法被取消的。所幸的是最终这些难题被一一解决——包括TAB键不能使用,感兴趣的可以查看编辑器源文件来了解是怎么做到的。只关心外部解析器的话不用考虑。

试用过程中有任何问题、反馈或者建议请不吝提出。在beta版完成之后,也会出系列文章来详细介绍JAse的所有技术,就像jssc一样。

十二月 15th, 2009

第一次兼PM心得

1 Comment, 其它, by army8735.

严格意义上来说这并不是我第一次带项目,第一次尝试Project Manager是不久前的Only Lady相册。不过那次是在产品设计早已完成的基础上自主进行的,仅由我和永赞两个人快速完成,需要协调和把握的东西很少。完成后想要出的前后端配合方式ppt也一直没来得及去弄~

这次则不一样,我不仅负责前端开发,还是带项目的,同时还涉及到部分产品经理的职责,可谓一次性扮演了3个角色,2个PM(Project + Product)。看过别人博客那么相关文章,这回也轮到我来喷一喷了,结合下实际情况,总结几点吧:

1.产品和项目

说实话,当产品和项目同时落到你头上的时候,你能够决策的地方就非常多了。省去产品和项目之间的沟通是件好事,在规模不大的情况下还能够承受,但一旦规模增长,它只会让你力不从心。因为人的时间和精力是有限的,所负责的东西越多,就要越抽象。潘潘前端时间就在会议上说过类似的情况:作为产品经理不仅需要做到本职工作,诸多大小琐碎事务都必须亲自动手,最后导致的结果就是把自己累死,手下可以利用的资源却一直空闲。这是个恶性的循环,会将导致自己越来越忙,而其它人一直赋闲。

我研究心理学也不少日子了,武志红老师曾经在《宽容自己,才能宽以待人》中举过关于诸葛亮的例子:

作为中国历史上的文人典范,诸葛亮也是“严于律己”的代表。治国上,他是“鞠躬尽瘁,死而后已”,同时,他似乎也做到了“宽以待人”,你很少会找到他没有道理地苛责别人。他杀马谡,废李严,设计斩魏延,仿佛都合情合理,都是依法办事,或是形势所迫。

然而,在我看来,他这些做法的内在逻辑一样是“严以待人”。这个逻辑也体现在他的“鞠躬尽瘁”上,蜀国大大小小的事件,他都要过手,意识上,他说是要对得起刘备的看重,但潜意识上,这里面有很深地对别人的不信任,他对人才的要求太高,这种高标准最终导致,因为缺乏锻炼机会,蜀国优秀的文臣武将越来越少。

这是过于“严于律己”的一个必然结果,诸葛亮对自己苛刻的同时,最终也苛刻地对待别人,尽管从大面上看,他并没有做错什么,但整体上,这形成了一种苛刻的气氛,令他和蜀国很难锻炼人才。

信任下属,重用下属,用人不疑。让别人参与进来,让专业人员作出专业决策,不仅能带动整个团队氛围,也能让所有人有归属感。

2.倾诉

由于项目时间紧张,似乎人人都有危机感。在第一天时,设计和开发人员就不停地向我抱怨、倾诉,诉说各种各样的不满。说实话,第一次接受这么多的牢骚真得让我很紧张。但是转念一想,这也说明了信任我。当其它人向我倾诉时是件好事,怕得是没人说出心里话。一旦所有人开始支持并理解你的时候,一切就事半功倍了。

在整个过程中我希望自己是个服务者,而不是高高在上的管理者。随着项目的进展,所有人都变得非常主动,终于感觉这次PM没白当。:)

3.攻人之过勿太严,要思其堪受;教人以善勿过高,当使其可从。

这是我最喜欢的名言之一。别人有错,职责时的方式该如何?不应过严,要考虑他的心理承受能力;引导别人,该怎样去衡量标准?不应过高,要考虑他的实际改善能力。我相信能做到这两点,就能至少赢得下属的尊重和信任了。

4.初期定位

初期的产品定位和策划不明确,是件很恐怖的事情。假如时间再紧,就更雪上加霜了。这次项目就有这个特点,中间进行修改使得很多人都不爽。细节的完善并不会影响到人的情绪,大方向的调整会给人一种“我白做了那么多”的感觉。这是要极力避免的现象。

5.交流工具

我想任何企业都有内部的交流工具,比如MSN、RTX什么的。但我感觉,除非很简单明了的事情,否则一切都不如当面叙说来得好!面对面的谈话所传达的信息非常多,也利于理解和加深双方的熟悉感。前端时间豆浆和我在改一个小东西时就因为误解邮件中的一句极简单的话造成许多无谓的错误和重复工作,如果最初当面花1分钟说清楚的话,一切就简单多了。

6.懂技术的产品经理

PM懂技术是优势,因为能更好地把握产品和理解开发进度。但是技术出身的PM也具备一般程序员的弱项——主观意识强、交谈口才欠缺等。世界上果然没有十全十美的东西。

暂时写这些,后续想到补充。