浏览 14785 次
锁定老帖子 主题:面向未来的CSS实践
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2007-08-15
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应用是透明的,在实际应用中性能也几乎没有损失。因此这一方案应能适用于大型商业网站。 虽然我的项目尚处于计划阶段,但是原理是很简单的,任何人都可以付诸实践。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2007-08-15
我宁愿通过javascript来hack css.
毕竟现在 javascript sector很多,性能\提升都很好. 如果用你的htc方案,整个dom树循环可能避免不了吧? |
|
返回顶楼 | |
发表时间:2007-08-15
性能是一个关键问题。因此我不会做任何dom循环,也不会用任何js selector。而是使用IE本身的能力。
实现上,最核心的一点就是override className。 |
|
返回顶楼 | |
发表时间: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> 注意,这里没有包括所有代码,但是包括了关键的概念和实现方法。 |
|
返回顶楼 | |
发表时间: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个,很少人会用到更多。所以这个方法在实践中是有效的。 |
|
返回顶楼 | |
发表时间:2007-08-15
我很感兴趣。希望快点看到楼主的劳动成果啊。
|
|
返回顶楼 | |
发表时间: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本身不该是这样的。它不应该让我们被迫带着枷锁跳舞,它应该易于使用,很好的反映我的设计意图。 |
|
返回顶楼 | |
发表时间:2007-08-16
相当不错,鼓励鼓励!
是否能让for IE的部分由IEPatch自动生成呢? |
|
返回顶楼 | |
发表时间:2007-08-16
birdjavaeye 写道 相当不错,鼓励鼓励!
是否能让for IE的部分由IEPatch自动生成呢? 这正是目标之一。不过最近还没有时间开展项目。 |
|
返回顶楼 | |
发表时间:2007-10-26
有的时候 麻烦的是老项目 而不是新项目
维护的时候受到的约束最多了 |
|
返回顶楼 | |