2.2 CSS
CSS是一门非常简单同时也非常难的“编程语言”。CSS上手非常容易,它就像是HTML的“配置选项”,新手参照手册即可很迅速地“配置”出不错的UI效果,这也是造成前端岗位被普遍认为门槛低的原因之一。但是CSS几乎缺乏编程语言必须具备的所有要素:没有变量、没有命名空间[10]、缺乏计算能力等。事实上,CSS本来也不是一门编程语言,最起码不是高级编程语言。[11]W3C对CSS的定义是“一种用于描述HTML文档表现形式的计算机语言”,其全称为Cascading Style Sheets,级联样式表。名字中的“Cascading”正是其精华也是难点所在,一个CSS属性的最终表现形式除了自身的取值以外,往往还依赖其对应元素自身的样式以及其他CSS属性和祖先元素的CSS属性。这种错综复杂的组合关系可以产生各种缤纷绚丽的视觉效果,但同时也造成了CSS无规律可循、实现方案多样、难以复用等问题,令开发者叫苦不迭。
高容错
CSS并没有任何错误报告机制,不论属性名称错误还是赋值错误,浏览器在解析CSS代码时只会忽略错误的CSS代码,并不会抛出任何可见的错误和异常报告。
组合性
CSS的组合性体现在两个方面:
● 元素自身属性的组合
● 与祖先元素属性的组合
定位是一个可以体现上述两个方面的经典例子。只有position为非static时top/bottom/left/right的值才生效,并且left和right的优先级还取决于direction的值(值为ltr时,left的优先级高于right,值为rtl时反之),这体现了元素自身属性的组合。在默认的情况下,元素的position取值为fixed时会脱离文档流,以可视窗口为参考计算定位,但是如果存在transform不为none的祖先元素,则会打破这种局面,这便是与祖先元素属性的组合效果。
全局性
CSS规则并非针对某一个或者某些元素,而是应用于符合CSS选择器规则的所有元素。这个表述有些拗口,我们可以通过对比常规编程语言的机制来辅助理解。比如在JavaScript中,如果要针对某个对象的某个属性进行计算,通常的做法是先找到这个对象,然后检查其是否存在这个属性,若存在则将计算规则应用于此属性;若不存在则终止。这是一个不可逆的流程,除非添加了监听逻辑,否则即便此后为对象新增了这个属性也不会再次计算。而在CSS的规则下,假如我的本意是为现有版本HTML文档中class为name的input元素添加一条1像素的描边,如代码2-1所示:
代码2-1
但是在后续迭代中,HTML文档中新增了若干个class为name的div和table元素,你会发现这些元素同样被添加了描边。我们不妨将这种特性称为CSS规则的全局性[12],即不论元素的顺序、位置、类型,只要其与选择器规则匹配都会应用相同的样式。下面用一个现实生活中的通俗案例举例,某城市地铁进站的安检程序有这样一条规则:携带饮用水的乘客需要亲自喝一口以证明是非违禁品。在编程语言规则下,如果乘客没有携带饮用水,并且通过安检之后在站内的自动售货机购买了一瓶水则无须再次检查;而在CSS规则下,即便是刚买的水也需要再喝一口验明清白。全局性是一把双刃剑,倘若新增元素与已存元素的样式一致,则可以直接复用,否则必须时刻“提防”新样式受到已存样式的影响。如果项目历史悠久、缺乏规范、交接频繁,这类问题就会像堆积木一样越来越多,从而出现大量如代码2-2所示的“补丁代码”。只有开发者自己才知道隐藏在漂亮UI背后的CSS代码是多么臃肿和丑陋。
代码2-2
兼容性
CSS的浏览器兼容性是令前端开发者最头疼的问题之一,也是许多CSS新特性难以被广泛使用的症结。早些年,前端开发者最不想听到的一句话是“兼容IE6”,虽然声名狼藉的低版本IE逐渐退出了历史舞台,但智能移动设备系统和版本的碎片化、糟糕的WebView,还有有着“现代IE6”之称的Safari浏览器,造成时至今日兼容性仍然是CSS领域最热门的话题之一。近几年,JavaScript像是乘上了火箭一般飞速发展,并且在前端、后端、原生应用等领域多面开花,然而CSS却像沼泽中的步兵一样举步维艰。
CSS的兼容性主要归咎于浏览器厂商之间的争斗,这场没有硝烟的战争产生了“深远”的影响。各浏览器对CSS规范的支持程度不一、实现方案多样,熟悉各浏览器的CSS前缀以及支持的属性成为对前端工程师最基本的要求。比如,Firefox浏览器不支持zoom属性[13],实现缩放效果需要进行代码2-3所示的特殊处理:
代码2-3
大量兼容性的代码令CSS文件越来越臃肿、冗余,不仅增加了自身的开发和维护难度,也拖累了Web应用的性能。
2.2.1 从编程语言的角度思考CSS
前面提到的CSS的4种特性均是浏览器渲染引擎的解析规则,开发者无法干预,但是在开发或构建阶段借助合理的框架、工具可以在一定程度上提高CSS的开发和维护效率。目前几乎所有的框架、工具甚至开发规范均试图将常规编程语言的模式和方法论应用于CSS领域。虽然严格意义上CSS并非编程语言,但这并不妨碍我们从编程语言的角度去思考它,并以此改进CSS的开发模式。[14]
逻辑处理
在讨论逻辑性之前需要首先理解CSS的无状态性。当HTML元素的一系列属性、子元素(伪元素)和状态(伪类)符合CSS匹配器规则时,其会被应用指定的样式,而CSS规则本身是无状态的。[15]比如,存在如图2-8所示的列表和如代码2-4所示的CSS规则:
图2-8 示例:hover视觉转变
代码2-4
无操作时列表item仅符合第一条CSS匹配器规则;鼠标移至第一个item元素上方时,浏览器将此元素设置为hover状态,与第二条CSS匹配器规则匹配,从而表现出两条规则组合后的视觉效果,完整流程如图2-9所示。在这个过程中,hover状态是施加于item元素(更严谨的说法应该是item元素对应的DOM对象)而非CSS的。
之所以讨论CSS的无状态性是为了说明CSS无法实现类似if-else的逻辑。除此之外,CSS不支持变量、类型、函数等实现逻辑处理的必备要素,以致不仅CSS的解析规则令人困惑,同时其开发和维护也非常困难。所以,支持逻辑处理成为众多CSS框架和工具改进CSS开发模式的一个主要方向。
图2-9 示例:hover状态CSS与视觉转变完整流程
高复用性
对于历史悠久且缺乏规范的大型项目而言,动辄几百上千行的CSS代码混杂在同一个文件中。如果没有详细的文档说明,在迭代过程中确定新增UI是否可复用已存样式的一般流程是:人力寻找网页中是否存在相同或类似的UI组件,然后通过浏览器开发者工具查看此组件的CSS代码。而由于CSS的组合性和全局性,此组件的样式很有可能是多个class、id、自定义属性,甚至是一系列“补丁代码”的综合效果。另外,如果Web产品的某个功能或子页面过期需要删除冗余代码和文件,这种“all in one”的CSS代码的清理工作几乎是不可能完成的任务。这导致随着时间的推移和版本的迭代,CSS代码越来越臃肿、冗余,直至达到令人难以容忍的极限才会被彻底重构。而且即便是重构,JavaScript和HTML的重构难度也要远远低于CSS。这些问题在一定程度上固然要归咎于开发规范的缺乏,然而CSS的低复用性才是引起问题的根本原因。提高CSS的可复用性成为除加强其逻辑处理能力之外改进CSS开发模式的另一个主要方向。
CSS的@import
CSS在第一版的标准规范中便加入了@import功能,各浏览器对该功能的支持度也非常理想,但是时至今日其也并未被大规模使用,在生产环境中几乎看不见它的身影。很多CSS开发框架和工具使用相同的关键字实现模块化,语法也非常相近,但本质是开发阶段源码的一种静态模块机制,与原生@import并不相同。CSS原生@import可以在生产环境中使用,但是机制非常奇特:@import语句必须在除@chartset以外的所有CSS代码之前声明,但被引用的CSS文件却是在引用它的主文件加载完成之后加载的,并且在组合样式时CSS文件的优先级低于主文件中的同名样式。这种逻辑混乱的机制很可能是@import不被接纳的主要原因之一。
读者可以将其GitHub的源码[16]克隆到本地,通过浏览器开发者工具查看CSS原生@import所引用文件的加载顺序和优先级规则。
2.2.2 LESS和PostCSS
CSS预处理(preprocessor)是目前被应用最广泛的CSS开发模式之一,其也在一定程度上对CSS标准的演进产生了积极的推进作用。LESS是目前流行的CSS预处理语言之一。其实与其称LESS为一门“CSS预处理语言”,不如将其视为一个具备特殊语法规范、可编译为CSS的“开发框架”更为合适。与之类似的有CoffeeScript[17]。LESS弥补了CSS逻辑处理和复用性方面的不足,引入了变量、混合(mixins)、模块、继承等特性,同时支持更易于编写和维护的嵌套语法,其从细节和整体上提升了CSS的开发和维护效率。
CSS预处理语言是革命性的,然而与很多优秀的框架和工具一样,均难以避免被历史淘汰,jQuery如此,CoffeeScript如此,LESS同样如此。以当前时间节点的技术视角来看,CSS预处理虽然是一项非常成熟的开发模式,但仍然存在一些致命缺陷,如下所述。
● 大而全。一旦选择使用CSS预处理语言,则必须接受它的所有规范和功能。CSS并非JavaScript,它的逻辑非常简单。大型复杂项目的CSS开发可能会涉及作用域、判断、查找等较深入的功能,然而大多数项目只需要变量、混合、模块等便足以支撑,其余的功能便成为冗余。之所以“大而全”是一种缺陷,并非在于冗余的功能“碍眼”,而是代码规范难以约束。比如,团队制定的LESS代码规范不允许使用继承,倘若团队中的某个开发者未遵循此规范,并且团队缺乏严谨的代码审查制度,不规范的代码将越积越多。
● 难扩展。CSS预处理语言属于编译型语言[18],需要一个与之配套的编译器将源码编译为CSS代码,虽然大多数编译器的代码开源,但只有极少数编译器支持开发者扩展自定义插件,即便支持扩展也是后续版本追加的选项,核心架构过于封闭,缺少插件生态。
与LESS、SASS等日渐式微的CSS预处理语言相比,PostCSS这名后起之秀成为目前较流行的CSS编译工具。
PostCSS起源于Rework[19]的Autoprefixer插件[20],但是Autoprefixer的演进速度超出了Rework项目组的预期,这时在其基础之上开发的PostCSS便诞生了。PostCSS最初被开发组定位为“CSS后处理器(post-processor)”,这个称呼引起了很大的争议,最终开发组纠正[21]了这个错误,将其称为“CSS转化工具(tool for transforming CSS)”。
PostCSS最初被称为“后处理器”,一方面是因为开发组希望与LESS、SASS等CSS预编译器进行区分;另一方面是因为其最初也是流行的插件Autoprefixer的“后处理”理念。简单来说,虽然PostCSS最初的定位是后处理器,但是其后续的演进并未被束缚在后处理器的狭义范畴,而是逐渐进化成一个全面的CSS转化工具。作者的另一本书《前端工程化:体系设计与实践》中也将其称为“CSS后处理器”。
PostCSS的内核并不会对CSS做任何转化,而是将原始的CSS代码转化为抽象语法树(Abstract Syntax Tree,简称AST)并传递给各个插件,插件根据用户的配置对AST进行处理后还原为最终的CSS代码,如图2-10所示。换句话说,你想对CSS做哪些处理并不取决于PostCSS本身,而是取决于使用了哪些插件。“内核轻量化,功能插件化”的微内核架构令PostCSS具有高度的可定制性和可扩展性,减少了冗余功能,更利于开发团队制定统一的技术规范和构建流程。
图2-10 PostCSS的工作流程
其实从目前的时间节点来看,CSS预编译的改革意义要远甚于PostCSS,嵌套语法、混合、模块等概念对CSS开发模式的影响是革命性的,PostCSS的很多插件也或多或少地借鉴了CSS预编译的一些概念和模式。而PostCSS的意义在于它丰富了CSS工具生态,为进一步实现工程化提供了更多的可能性,比如Autoprefixer取代了通过手写混合代码来实现兼容,从而解放了生产力;CSSNext实现了“Use tomorrow's CSS syntax, today”[22];postcss-spirites解决了多年来困扰开发者的spirite图片难以维护的问题等。
2.2.3 CSS-in-JS
在JavaScript中编写CSS代码并不是新鲜的概念,jQuery早期版本便提供了相关API[23]。而此处将要讨论的CSS-in-JS是一种全新的模式,是继HTML-in-JS之后对All-in-JS开发模式的补充。同HTML-in-JS一样,CSS-in-JS的核心不同于jQuery简单地在JavaScript中编写CSS代码,而是利用JavaScript的语言特性和技术生态在一定程度上弥补CSS开发模式的不足。
模块
原生CSS和SASS、LESS的模块实质上仅仅是子文件,并非真正意义上的模块。而JavaScript中的模块,不论是CommonJS、AMD还是ES6 Modules,均具备独立的命名空间和局部作用域。模块对于编程的意义在于封装和解耦,是实现组件化必不可少的因素。将JavaScript的模块体系带入CSS开发领域可以有效地弥补CSS模块体系的不足,这正是CSS-in-JS的核心出发点之一。
命名空间
依前面所述,CSS并没有作用域的概念(准确地说只有一个全局作用域),与选择器匹配的所有元素均被应用对应的样式规则。CSS-in-JS在编译时(也有些框架是在运行时)为组件产生唯一的classname以及对应的选择器规则,将组件及其内部元素的样式限制在唯一的命名空间内,从而实现样式的隔离,如代码2-5所示的JavaScript代码(使用JSS框架)经编译后成为如代码2-6所示的HTML文档。虽然这种模拟方案并非严格意义上的局部作用域,但也能够在一定程度上弥补CSS在此方面的不足。
代码2-5
动态性
CSS-in-JS最令人兴奋的功能之一是样式规则与JavaScript逻辑的互操作性[24]。在其与React、Vue等框架配合使用时,将样式规则与组件的Props或者States绑定,当组件的动态数据改变时样式也可以被同步改变。这种动态性能够令组件实现高度的可定制性,同时避免了过度冗余的CSS代码。
除以上三点外,CSS-in-JS还能够在一定程度上减少无效的代码(Dead Code),更利于单元测试等。
CSS-in-JS确实为CSS开发带来了一些行之有效的模式,非常适用于组件化架构,但在决定使用它之前仍需要考虑一些难以避免的缺陷。上面提到的CSS-in-JS诸多优点的副作用是,其在一定程度上限制了代码的可移植性,同时昂贵的学习成本和额外的工具引入也是开发团队需要重点考虑的问题。此外,All-in-JS模式目前仍存在争议,在编程过程中需要于HTML、CSS、JavaScript三种上下文语境之间频繁切换,这在某种意义上背离了关注点分离原则。
2.2.4 Houdini
虽然浏览器的实现程度与ECMAScript规范仍然有很大差距,但是ES6甚至更新的ECMAScript特性已经被广泛应用在JavaScript开发领域,实现的方式要么在构建阶段使用Babel等转译工具将ES6语法转化为ES5,要么在运行时引入额外的polyfill[25]。第一种方式与CSS预编译的理念相同,将ES6编写的JavaScript视为编译型语言;第二种方式则在运行时改变了ES6语法的解析规则。而CSS预编译和CSS-in-JS的核心均围绕着提高CSS的可编程能力、复用性、消除冗余等,通过开发模式和逻辑架构的改进将这些问题在开发阶段解决,而并未对运行时的CSS做任何改变,浏览器未实现的CSS未来语法和特性仍然无法使用。[26]JavaScript可以使用polyfill得益于它是一种动态语言,可以“解释自己”,CSS却没有这种功能。然而JavaScript polyfill的思想却启发了各浏览器的开发者,由来自Apple、Mozilla、Google等浏览器厂商的工程师成立了专项小组,力图实现“CSS polyfill”——Houdini。
对于前端开发者来说,浏览器的解析和渲染是绝对封闭的,即便了解它的工作流程也没有任何可介入的能力。Houdini的思想是将浏览器CSS引擎的部分功能权限开放给开发者,以便开发者扩展和自定义CSS特性。目前处于草稿[27]阶段的部分Houdini特性包括如下几项。
● CSS属性/值API:可供开发者自定义或扩展已存在的CSS属性/值。
● CSS Typed OM:类比HTML元素可被转化为供JavaScript访问和操作的DOM,CSS Typed OM可以理解为供JavaScript访问和操作的CSSOM(CSS Object Model)。
● CSS Layout API:可供开发者自定义display布局模式。
● Worklets:类似于Web Worker[28]的独立线程脚本,可介入渲染阶段的逻辑。
除了以上特性之外,自定义字体规范、CSS语法扩展、滚动条扩展等API均被加入Houdini规范草案中。虽然目前这些特性尚未被确立为标准,何时能够在生产环境中使用也尚未可知,但可以看出Houdini工作组庞大的野心,对比各浏览器以十年计[29]的CSS规范实现速度而言,Houdini更值得期待。
为什么各浏览器厂商宁愿推出全新的Houdini也不实现CSS的标准规范
回顾近几年各浏览器的版本迭代内容可以大致看出,厂商们的侧重点偏向于对HTML5的支持,甚至超越了对ECMAScript新特性的支持程度。HTML5的诸多新特性可以丰富网站的功能,网站前端的核心竞争力从以前的视觉表现力逐渐过渡为功能丰富性,能够更全面地支撑这些功能便成了浏览器的竞争核心。对比之下,CSS的优先级自然相对较低。另外,CSS工作组确立了持续迭代(即频繁推出小版本)的策略,对CSS规范的实现必然是一个非常漫长且需要持续跟进的过程,而Houdini则是一个“一劳永逸”的方案。其实从Houdini草案可以看出浏览器厂商的真实意图:将CSS规范的实现交给开发者。这不仅减轻了浏览器厂商的工作压力,同时也能够丰富CSS生态,甚至会在一定程度上推动CSS规范的演进。一石三鸟,何乐而不为。