论坛首页 Web前端技术论坛

面向未来的CSS实践

浏览 14785 次
该帖已经被评为精华帖
作者 正文
   发表时间:2007-08-15  
CSS
本文源于http://ued.taobao.com/blog/2007/08/12/css-notes/的讨论。淘宝UED团队的小马对taobao的CSS编程原则描述如下:
小马 写道
* 尽量不使用hack
* 尽量不使用ie6不支持的选择符
能符合这两个条件的最简洁的写法,就是我们的目标。


由此展开,我论述了在CSS实践上的另一种思路。这是我自去年年中至今年4月在SNDA进行商城开发过程中对于前端web设计编程的思考和实践的首次书面整理。

如下:

对于taobao网站css的原则,我个人认为这两条原则是较为保守的,当然对于taobao这样的网站,采用比较保守的策略是很合情合理的。

我谈一下我对着两个原则的一般看法。

对于hack,我觉得要区别对待。对于使用selector或利用其他特定浏览器的bug来做hack的,需要谨慎。因为这类hack没有向后兼容性,很可能碰到下个版本的浏览器,支持了原先不支持的selector,或者修复了原先的bug,这就惨了。MSIE7就是一个典型例子。实际上90%的hack都是为IE准备的,而对于IE来说,最好用condition comments,这是IE团队推荐的方法 —— 它的优点除了向后兼容性的保证之外,还有就是可以把IE特定的代码写在单独的stylesheet里(其他浏览器可以不load它从而节约带宽),但是缺点也是这个,就是同一个效果,要在两个样式表里维护。

对于第二条原则,即不使用IE6不支持的selector,我觉得对多数网站来说,就过于保守。

我推崇一种面向未来的CSS实践。即大胆采用CSS2.1甚至部分CSS3的特性。因为绝大多数特性,Firefox、Opera、Safari等都已经很好的支持了。MSIE7也改进了许多,将来IE也无疑终究会完全支持CSS2.1。对于目前的IE,除了graceful degradation的方式(实际上整个内容样式分离的原则和良好的CSS设计可以确保这点,比如淘宝以前的“裸体”所体现的),可以考虑通过特定手段来patch之。

在这点上,我必须说,我原来也是一直坚持只用ie6的selector的。是什么改变了我?就是Dean Edwards的IE7!它的出现不仅在于实践价值——即提供了一个对于IE的补丁,让开发者可以直接写CSS2甚至CSS3。对我来说,它更是观念上的革新,原来事情可以这样做!

所以,尽管DE的IE7在大型商业网站上还是存在一些问题的(主要是Ajax下的样式刷新带来的性能问题),但是它启发了我们可以从另一个角度来思考CSS的运用。

比如说,从实践的角度出发,所有IE6不支持的各种CSS selector中,最不可缺少的是什么?

由我个人的经验来看,最有用的就是多class。其次是一些伪类。

一个最常见的例子是,当button获得focus的时候,我们希望改变它的样式。

CSS2.1下可以:
input[type=button] {…}
input[type=button]:focus {…}

对于IE,我们可以给input加一个class来表明它是button,我们也可以通过脚本来给当前focus的元素增加一个pc-focus类(pc前缀表示伪类)。但是同时有button和focus怎么办涅?IE不支持多class,意味着,你不能这样写:

input.button {…}
input.button.pc-focus {…}

IE会把上述代码错误的解释为:

input.button {…}
input.pc-focus {…}

结果你为button准备的focus效果可能会跑到其他input上,例如radiobox、checkbox上。

所以通常会看到有人会给出两个class一个是button,一个是button_focus,onfocus的时候,把button替换成button_focus。

input.button {…}
input.button_focus {…}

当然它可以工作,对于一向只用一个class的人来说,甚至可以用不太严谨的方式,在onfocus的时候className += '_focus',在onblur的时候className.replace('_focus', ''),这段代码可以通用,而不必为button写一遍,又为radio或者checkbox再写一遍。但是总的来说,这恐怕不是一个好方法。例如button_focus不能复用button的样式特性。如果你要复用,必须写成:

input.button, input.button_focus {…}
input.button_focus {…}

每一处类似的情况都要记住这个写法(且两句顺序不能颠倒)。
特别讨厌的是,即使在另一个地方,你不想要focus效果,因此只需要input.button,而不需要input.button_focus,但你也要记得第一句的写法,否则一旦有了焦点,input.button样式就失效了!

那么我们可以考虑另一种方式,即不是把button替换成button_focus,而是两个并存,onfocus的时候addClass('button_focus'),onblur的时候removeClass('button_focus')。写CSS的时候要注意优先级一致和顺序问题:必须保持.button_focus的样式声明在.button之后。

对于input的其他情况,也照例:

input.radio {…}
input.radio_focus {…}

onfocus的时候addClass('radio_focus'),onblur的时候removeClass('radio_focus')。

一般来说,在IE里我们就到此为之了。注意,对于一个元素的class属性里包含更多class例如3个或者4个class的情况,要用这种方式是非常麻烦的。因此遵循小马所说原则的开发者会尽量避免使用多class。

多个class能够让我们以正交的方式处理问题,而避免多class,实际上是强迫我们尽量把问题平面化,降低了我们对于设计的表达能力。而在实际需求的逼迫下,开发者往往会不得不作出一些作为特例的代码(例如上面的button到button_focus的替换法)。在团队开发中,假如团队缺乏一些处理这类问题的通用“模式”的话,结果会更麻烦。

总之,避免多个class的selector,就是一种典型的实现工具对设计方法的不合理约束,对于设计的简单性、可维护性、可复用性都可能造成伤害。

我们能否换一种思路思考呢?

我们不是削足适履,仅仅在没办法的时候才用一些特别代码来达到本质上可以用多个class的selector来表达的效果。而是确认,多个class的selector是我们的基本需求。问题就变成了,怎样让IE也支持多个class。

让我们回顾在前面的若干个focus/blur事件处理函数,本质是相同的。我们能否避免写那么多本质相同但可能很复杂的focus/blur处理函数?这是可行的,例如对于一个class属性包含若干个class的情况,你可以给所有的class X都add一份对应的X_focus。比方说,A[class='x y']如果获得focus,那就可以改为A[class='x y x_focus y_focus']。这个事件处理函数是通用的,也就是不管是什么元素,只要获得焦点,我就根据该元素所具有的class进行变换。好,既然我们可以捕捉到既是x又获得focus的A,我们为什么不能捕捉一个既是x又是y的A呢(A.x.y)。我们可以改成A[class='x y x_y x_focus y_focus']。

这里我们要迈出重要一步。获得focus本身,其实可以看作增加了一个focus伪类(记做pc-focus),所以A[class='x y']获得focus,就得到A[class='x y pc-focus'],按照我们前面的变换,并把既是x又是y并且获得focus的情况(A.x.y:focus)也考虑进来,最后我们可以得到:
A[class='
  x
  y
  pc-focus
  x_y
  x_pc-focus
  y_pc-focus
  x_y_pc-focus
']

如果我们推而广之,就能发现这其实就是在IE下模拟多类的效果。对于任何一个class='a b c d…'的情况,我们只要把class的值改为a b c d … a_b a_c a_d … b_c b_d … c_d … a_b_c a_b_d … a_c_d … a_b_c_d,然后在写css的时候遵循一定的规则:
多个class按照字母顺序书写,即把X.a.b.c, X.a.c.b, X.c.b.a统一写做X.a_b_c
按照优先级顺序书写,即先写X.a然后写X.a_b和X.c_d,最后写X.a_b_c_d;
就可以了。

实际上,我正在酝酿一个开源项目,遵循这个思路,并把所有这些变换自动化(通过htc来override className属性,能把class的转换自动化;自动产生focus,hover,first-child等伪类;通过css解析处理工具,能把CSS2.1的多class selector自动转换为等价的IE形式)。这样,开发者就可以自由一点的写CSS,而不必束手束脚了。相比较Dean Edwards的IE7,这种方法所提供的改进有限,并不能给予开发者完整的CSS2/3的支持,但是边际效用很大,更轻量级。因为本质上是使用IE自己的引擎,而不是自己实现的CSS Parser,所以对Ajax应用是透明的,在实际应用中性能也几乎没有损失。因此这一方案应能适用于大型商业网站。

虽然我的项目尚处于计划阶段,但是原理是很简单的,任何人都可以付诸实践。
   发表时间:2007-08-15  
我宁愿通过javascript来hack css.
毕竟现在 javascript sector很多,性能\提升都很好.


如果用你的htc方案,整个dom树循环可能避免不了吧?
0 请登录后投票
   发表时间:2007-08-15  
性能是一个关键问题。因此我不会做任何dom循环,也不会用任何js selector。而是使用IE本身的能力。

实现上,最核心的一点就是override className。
0 请登录后投票
   发表时间:2007-08-15  
一个雏形实现(以下代码以LGPL条款发布):
class.htc
<public:component lightweight="true">
<public:property name="className" get="getClass" put="setClass"/>
<public:method name="hasClass"/>
<public:method name="addClass"/>
<public:method name="removeClass"/>
</public:component>
<script>
if ('nodeType' in element) {
	MultiClassPatch.setClass(element, element.getAttributeNode('class').value);
}
function hasClass(name) {
	return MultiClassPatch.hasClass(element, name);
}
function getClass() {
	return MultiClassPatch.getClass(element);
}
function setClass(name) {
	return MultiClassPatch.setClass(element, name);
}
function addClass(name) {
	return MultiClassPatch.addClass(element, name);
}
function removeClass(name) {
	return MultiClassPatch.removeClass(element, name);
}
</script>


class.js
var MultiClassPatch = (function ()  {

	var MULTICLASS_MAX = 3;

	function getClass(e) {
		return e.getAttributeNode('class').value.
			replace(/\S*?[.]\S*/g, '');
	}

	function setClass(e, value) {
		log.trace('entry setClass: $1 $2', nodeInfo(e), value);
		
		var values = value.replace(/\S*?[.]\S*/g, '').split(/\s+/);
		var a = [];
		for (var i = 0, size = values.length, cache = {}; i < size; i++) {
			var v = values[i];
			if (!cache.hasOwnProperty(v)) {
				a.push(v);
				cache[v] = true;
			}
		}
		if (a.length > 8 /*avoid javaeye bug*/ ) {
			log.warn('Performance Warning: Too many ($1) class values.', a.length);
		}
		
		var mc = [];
		var s = a.join(' ');
		mc.push(s);
		
		names = a;
		
		var n = names.length < MULTICLASS_MAX ? names.length : MULTICLASS_MAX;
		while(n > 1) {
			n--;
			var a = [];
			for (var i = 0; i < names.length; i++) {
				var name = names[i];
				var hasName = new RegExp('(?:^|\\s+)(?:\\S*[.])?(?:' + escapeRegExp(name) + ')(?:[.]\\S*)?(?=\\s+|$)', 'g');
				a.push(s.replace(hasName, '').replace(/\S+/g, name.replace('$', '$$') + '.$&'));
			}
			s = a.join(' ');
			mc.push(s);
		}
		
		var v = mc.join(' ');
		e.getAttributeNode('class').value = v;
		
		log.trace('exit setClass');
	}

	function hasClass(e, value) {
		var re = new RegExp('(?:^|\\s)' + escapeRegExp(value.replace(/\s+/g, '.')) + '(?:\\s|$)');
		return re.test(e.getAttributeNode('class').value);
	}

	function addClass(e, value) {
		if (hasClass(e, value)) return false;
		setClass(e, getClass(e) + ' ' + value);
		return true;
	}

	function removeClass(e, name) {
		var hasName = new RegExp('(?:^|\\s+)(?:\\S*[.])?(?:' + escapeRegExp(name) + ')(?:[.]\\S*)?(?=\\s+|$)', 'g');
		var attr = e.getAttributeNode('class');
		attr.value = attr.value.replace(hasName, '');
	}

	function escapeRegExp(s) {
		return s.replace(/[(){}.*+?^$|\[\]\\]/g, '\\$&');
	}

	return {
		getClass:getClass,
		setClass:setClass,
		hasClass:hasClass,
		addClass:addClass,
		removeClass:removeClass,
		VERSION:'2.0'
	};
	
})();




测试代码:

<html>
<head>
<script src="../src/scripts/lang.js"></script>
<script src="../src/scripts/system.js"></script>
<script src="../src/scripts/log.js"></script>
<script src="../src/class.js"></script>

<style id="ie-class-patch">
* html body, * html body * { behavior:url(../src/class.htc); }
</style>

<style>
body { color:white; background:gray; }
.a { background:navy }
.b { background:maroon; }
.c { border:thin dotted; }
html>body  .a.b { background:green; }
* html     .a\.b { background:green; }
html>body  .a.b.c { border-width:medium; }
* html     .a\.b\.c { border-width:medium; }
html>body  .c.d.e { color:yellow; }
* html     .c\.d\.e { color:yellow; }
</style>
</head>
<body>
	<p class="a">a  background:navy</p>
	<p class="b">b  background:maroon</p>
	<p class="c">c  border:thin dotted</p>
	<p class="a b">a b  background:green</p>
	<p class="a b c">a b c  background:green; border:medium dotted</p>
	<p class="a b c d">a b c d  background:green; border:medium dotted</p>
	<p class="a b c d e">a b c d e  background:green; border:medium dotted; color:yellow</p>
	<p class="
		test-multiple-class-01 test-multiple-class-02 test-multiple-class-03
		test-multiple-class-04 test-multiple-class-05
	">multiple class 5</p>
	<p class="
		test-multiple-class-01 test-multiple-class-02 test-multiple-class-03
		test-multiple-class-04 test-multiple-class-05 test-multiple-class-06
	">multiple class 6</p>
	<p class="
		test-multiple-class-01 test-multiple-class-02 test-multiple-class-03
		test-multiple-class-04 test-multiple-class-05 test-multiple-class-06
		test-multiple-class-07
	">multiple class 7</p>
	<p class="
		test-multiple-class-01 test-multiple-class-02 test-multiple-class-03
		test-multiple-class-04 test-multiple-class-05 test-multiple-class-06
		test-multiple-class-07 test-multiple-class-08
	">multiple class 8</p>
	
	<p class="
		test-multiple-class-01 test-multiple-class-02 test-multiple-class-03
		test-multiple-class-04 test-multiple-class-05 test-multiple-class-06
		test-multiple-class-07 test-multiple-class-08 test-multiple-class-09
		test-multiple-class-10 test-multiple-class-11 test-multiple-class-12
		test-multiple-class-13 test-multiple-class-14 test-multiple-class-15
	">multiple class 15</p>
</body>
</html>


注意,这里没有包括所有代码,但是包括了关键的概念和实现方法。
0 请登录后投票
   发表时间:2007-08-15  
默认上支持最多3个class,也就是在CSS中可以写:
X.a.b.c,但是不支持X.a.b.c.d。对class属性里的个数没有限制(当然越多性能越差,但是没有css上class个数的影响大)。

也可以改变默认设置到4个、5个甚至更多,但是性能会有所下降。我测试下来,在目前的中低端机器上,8个以上就不太可以接受了。

但是实践当中,css中多class,一般都是2个或者3个,很少人会用到更多。所以这个方法在实践中是有效的。
0 请登录后投票
   发表时间:2007-08-15  
我很感兴趣。希望快点看到楼主的劳动成果啊。
0 请登录后投票
   发表时间:2007-08-15  
被评为精华帖了,所以把原文又润色了一下,现在应该更容易理解了。

另附上在淘宝UED blog上的comments更新部分:

问题是怎么理解“复杂化”。实际上,更好的CSS支持必然能让事情变得更合理、简单、清晰、可维护。我之前对于table布局的讨论,对于多class selector在实际中运用的讨论,其实都说明了这一点。

那么什么让CSS变复杂了?其实正是那许多trick。我仍旧是拿段王爷的这个分隔线的例子。请比较我提出的方法和(稍作变形的)段王爷的方法:

ul { padding:0; margin:0; }
li { display:inline; }
li ~ li:before { content:url(sep.bmp); }

vs

ul { padding:0; margin:0; overflow:hidden; zoom:1; }
li {
display:inline;
background:url(sep.bmp) left center no-repeat;
zoom:1;
margin-left:-8px;
padding:0 8px;
}

sep.bmp是一个8px宽的图片。两段代码效果上是几乎等价的。并且对代码做了最大简化,去除了所有无关代码。

很容易看出哪个更简单。这种简单,不仅在于代码的量的多少,而且更关键的是在于对于意图的表述。前者很清晰。后者就算一个css老手,也得花点精神才看的出来。而且后者带有一些意图之外的副作用,例如获得了hasLayout,又如对background, padding和margin的征用。请注意,这还是一个我们都认为很合用,也蛮清晰的trick。如果是更复杂的trick呢?

当我们有大量样式的时候,css trick的累加所造成的可维护性的下降,是很可观的。这是“意图丢失”所造成的。当然良好的注释可以改善一下这个状况。

所以,请考虑一下意图清晰的CSS2.1代码所带来的好处。

然后我们考虑前者如何运用到IE中。
第一个方法,不管IE,在IE下自然graceful degradation。
第二个方法,用Dean Edwards的IE7或类似项目。
第三个方法,也是按照我计划项目(暂时称作IEPatch)的思路下的做法(虽然目前还是设想,但绝对可以实现):

ul { padding:0; margin:0; }
li { display:inline; }
/*因为暂时不能模拟sibling selector,所以换用了一种写法*/
li::before { content:url(sep.bmp); }
li:first-child::before { content:'’; }

/* for IE */
li { pe-before:enabled; }
li .pe-before { content:url(sep.bmp); }
li.pc-first-child .pe-before { content:''; }

设想中的IEPatch会使用一个htc重载className,元素会自动获得一些伪类,例如pc-first-child等价于:first- child伪类。同时,实现content属性,并通过一个扩展的pe-before属性来表示是否为一个元素产生::before伪元素。注意,这听上去很困难,但实际上确实是可以实现,而且不会对呈现性能造成影响的。

这样,上述的三行代码就表示:对于li启用::before伪元素,li的::before伪元素使用sep.bmp作为其内容,li如果是first-child则::before伪元素内容置空。

看上去for IE的部分似乎复杂了,但是其实并不复杂,因为它与前面的标准CSS代码是一一对应的(除了用作辅助的第一句)。

表示意图的代码,始终只有一份,就是以标准CSS书写的那份。for IE部分,只是遵循规则就可以得到的简单的转换,而不是复杂的trick。如果有工具帮助,更是很容易自动产生的(实际上可以纳入到整个项目的building流程中)。

这是我对于“复杂性”的理解。

CSS实践之所以复杂,绝大多数时候来自于trick。也正是trick,导致CSS实践有时候甚至变成了近乎于艺术的工作。然而,发现一个 trick所获得的快感其实是一种慢性毒药,因为CSS本身不该是这样的。它不应该让我们被迫带着枷锁跳舞,它应该易于使用,很好的反映我的设计意图。
0 请登录后投票
   发表时间:2007-08-16  
相当不错,鼓励鼓励!

是否能让for IE的部分由IEPatch自动生成呢?
0 请登录后投票
   发表时间:2007-08-16  
birdjavaeye 写道
相当不错,鼓励鼓励!

是否能让for IE的部分由IEPatch自动生成呢?


这正是目标之一。不过最近还没有时间开展项目。
0 请登录后投票
   发表时间:2007-10-26  
有的时候  麻烦的是老项目  而不是新项目  

维护的时候受到的约束最多了
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics