原文
Philip Walton
2016年3月24日
CSS Techniques
你是否曾经试过想使用某个CSS特性但是却因为他没有被所有浏览器支持而不能用?又或者更糟糕的,他被全部浏览器支持,但是这种支持充满了bug、表现不一致甚至是不完全兼容的?如果这些事情曾经在你身上发生过——并且我打赌他们绝对发生过——那么你就需要关注一下 Houdini 。
Houdini是一个新的W3C工作组,他们致力于让这些问题永远消失。他们计划通过引入一整套API来让开发者首次拥有扩展CSS的权利,并且会提供出一套工具来 与浏览器的渲染引擎的样式与布局 进行挂钩。
但是这意味着什么呢?这是一个好的提议吗?这会如何帮助我们开发者在现今与未来构建网页呢?
在这篇文章里,我将尝试去回答这些问题。但是在我回答之前,弄清楚今天我们遇到了什么问题和为什么需要这样的变革是很重要的。然后我会更详尽地去解释Houdini如何去解决这些问题,并且列出一些在现今开发上十分令人振奋的特征。最后,我会提供一些具体的,我们的开发者现在可以去完成的事情来让Houdini变成现实。
每次我为一些全新的CSS特征编写文章或者构建一个demo,必然会有人在评论或者推特上说,“这真的很棒!不过我们可能未来十年都不会用到它们。”
像这些烦人且毫无建设性的评论,我可以理解那种情绪。从历史上来看,一个功能建议要花费数年才能被广泛使用。其原因在于,纵观整个web的历史,唯一能让一个新特征加入CSS的方法就是走完整个标准过程。
标准过程步骤( 观看大图 )
当然我不是反对标准过程,但是不可否认的是他花费太多时间了。
例如, flexbox 在2009年第一次被提出,然而直至今天,开发者仍然抱怨因为缺乏浏览器的支持而不能使用它。当然,这个问题正在慢慢的被解决因为现在几乎所有现代浏览器都会自动更新;但是即使是现代浏览器,从功能提议到全面上市仍然会有延迟。
有趣的是,并不是所有web的领域都是这样。看一下在JavaScript上事情最近是怎么发生的:
polyfill过程的步骤 ( 观看大图 )
在这种情况下,可能只需要花费几天就能把一个想法运用到生产中去。我的意思是,我已经在生产中使用 async
/ await
了,但是这个功能仍然未被加入到任何一个浏览器中。
你还可以看到这两个社区的普遍情绪上的巨大差异。在JavaScript社区,你会阅读到人们抱怨变化太快的文章。而在CSS社区,相反你听到的是人们哀叹学习新东西的徒劳因为他们不知道什么时候才能使用他们。
第一个想法就是,编写更多CSS polyfills似乎是解决问题的方法。如果有了良好的polyfills,CSS就可以像JavaScript那样快速前进了,不是吗?
然后悲伤的是,并没有那么简单。对CSS进行polyfill是难以置信的困难,大部分情况下,你不能在完全不破坏性能的情况下去完成polyfill。
JavaScript是一个动态语言,这意味着你可以使用JavaScript来polyfill JavaScript。又因为他是动态的,所以他特别容易被拓展。反之,CSS很难被用来polyfill CSS。某些情况下,你可以在编译步骤里面转化编译CSS( PostCSS 负责这种工作);但是如果你想polyfill任何基于DOM结构的或者是元素布局位置的属性,你必须要在客户端运行你的polyfill逻辑。
不幸的是,在浏览器上完成这项任务并不容易。
下图勾勒出了你的浏览器如何把一个HTML文件展现到屏幕上的过程。用蓝色标注的部分是JavaScript有权利控制的部分:
JavaScript所能控制的渲染工作流 ( 观看大图 )
图片显示的结果相当惨淡。作为一个开发者,你并不能控制你的浏览器如何解析HTML和CSS也不能控制他如何把HTML和CSS转化成 DOM 和 CSS对象模型 (CSSOM)。你无法控制级联。你无法掌控浏览器如何在DOM里布局元素,如何在屏幕上绘制元素。你也无法控制合成器。
唯一一个你可以全部控制的进程就是DOM。CSSOM部分开放;然而,引述自Houdini的网站,这种开放是“尚未被确认的,在浏览器上是不一致的,并且缺少关键功能。”
例如,如今的浏览器的CSSOM不会展示你的跨域样式表,而且他会轻易丢弃掉任何他不认识的CSS规则或者声明,这意味着如果你想polyfill一个浏览器尚不支持的功能,你不能使用CSSOM。相反,你必须通过DOM,找到 and/or
标记,拿到CSS样式,解析、重写再把它添加到DOM上。
当然,更新DOM通常意味着整个浏览器必须重新完成所有的级联、布局、绘制和合成的步骤。
利用JavaScript polyfill浏览器的渲染过程。( 观看大图 )
尽管完全重绘一个页面对性能的影响看起来并不会特别大(特别是某些网站),但是思考一下这种事情发生的频率。如果你的polyfill逻辑是需要对滚动事件、窗口大小调整、鼠标动作、键盘事件进行响应的话——基本相当于任何时候都在改变——那么事情将会十分明显,有的时候甚至是接近极限,十分缓慢。
当你意识到今天绝大部分的CSS polyfill包含他们自己的CSS解析器和他们自己的级联逻辑,你会发现状况变得更加的糟糕。又因为解析和级联都是十分复杂的事情,这些polyfills通常不是太大就是充满了bug。
简单的总结一下刚才我所说的:如果你想让你的浏览器(因为你给予的CSS)做一些它自身不支持的事情,那么你必须要亲自通过更新和修改DOM来伪造出这种效果。你在其余的渲染步骤里没有权限。
这对于我来说,绝对是整篇文章中最重要的问题。所以如果你一直都是快速浏览的话,这部分必须缓慢而且仔细的阅读!
在看到最后一节后,我相信部分人会认为“我不需要这些!我只是想构建一个正常的网页。我并不想侵入我的浏览器内部去创造一些十分花哨的、试验性的甚至可能会造成问题的效果。”
如果你是这么想的话,我强烈建议你先退一步然后认真思考一下我们这些年用在网站建设上面的技术。想要访问或者和浏览器的样式化过程挂钩并不只是去建设花哨的样例——这是为开发者和框架作者提供动力去完成两件重要的事情:
正常化浏览器之间的差异
发明并且polyfill一些新功能让人们可以使用。
如果你曾经使用一些JavaScript库诸如jQuery,那么你会从这种能力里面获益良多!事实上,这是现今几乎所有前端库或者框架的主要卖点。五个Github上面最流行的JavaScript和DOM资料库——AngularJS, D3, jQuery, React和Ember——都在减少跨浏览器间的差异上做了很多工作所以你不用去考虑他们。他们都只暴露出一个API,然后这个API正常工作。
现在,回想一下CSS和他那些跨浏览器问题。即使是最流行的CSS框架如Bootstrap和Foundation,尽管他们生成自己具有跨平台的兼容性,但是他们并没有消除跨浏览器的bug——他们只是避免触发bug的做法。而CSS跨浏览器bug并不仅仅是过去的问题。甚至在拥有新的布局模式的如 flexbox 的今天,我们仍然会面对很多 跨浏览器不兼容的问题 。
底线是,想像一下如果你可以使用任何CSS属性,并且知道他们一定会生效,在每个浏览器里面表现都是一样,那么你的开发工作会变得多轻松。再想象一下,你在其他blog文章上面或者在会议上知道的新特性——如 CSS grids , CSS snap points 和 sticky positioning 。如果你今天就可以使用它们并且它们的性能表现的就像原生的CSS功能一样。而你所有需要做的就是从Github上面获取代码。
这就是Houdini的梦想。这就是他们期望的未来。
所以,即使你并不打算去写一个CSS polyfill或者开发一个实验性的功能,你可能会想让其他人可以这么做——因为一旦这些polyfill存在,每个人都可以从中获益。
我在上面提到开发者对于浏览器的渲染过程没有什么控制权。事实上,唯一可以控制的是DOM并且一部分的CSSOM。
为了解决这个问题,Houdini工作组引入了几个新规范。这将会首次让开发者对渲染过程里的其他部分拥有访问的权利。下表展示了在新规范中渲染过程哪部分可以被修改。(注意灰色的规范是在计划中但是尚未被编写。)
新规范融入浏览器渲染过程的部分。 ( 观看大图 )
下面几个部分将会简短地浏览一下每一个新规范,介绍他们提供了什么。我应该注意到在这篇文章中有另外一个规范未被提及;你可以从 GitHub repository of Houdini’s drafts 上面观看完整的列表。
CSS解析器API 尚未被编写;所以我所说的大部分都很容易被改变。但是基本想法是让开发者拓展CSS解析器并且告知他们新的结构——例如新的媒体规则、新的伪类、嵌套、 @extends
、 @apply
等等。
一旦解析器知道这些新的结构,它就可以把它们放到CSSOM里面的正确位置上去,而不是抛弃掉他们。
CSS已经具有自定义属性了,就像 我之前提到的那样 ,我对他们被解开的可能性感到十分兴奋。而 CSS属性和值API 则在自定义属性上更进一步,通过添加类型让他变得更加有用。
在自定义属性上添加类型会带来很多好处,不过最大的卖点或许就是这些类型会让开发者可以为自定义属性添加transition和animate这些我们今天不能使用的功能。
看一下以下例子:
body { --primary-theme-color: tomato; transition: --primary-theme-color 1s ease-in-out; } body.night-theme { --primary-theme-color: darkred; }
在上面的代码中,如果 night-theme
类别添加到 `元素上,页面上每个具有
--primary-theme-color 属性的元素的值都会慢慢的从
tomato 转变成
darkred`。如果你想在今天完成这些,你必须要手动地为每个元素写transition,因为你不能对自定义属性进行transition变换。
这个API另一个十分有希望的特点是可以注册一个“应用钩”(apply hook),这让开发者可以在级联步骤完成后仍然可以修改一个自定义属性的值,这对于polyfill可能是一个十分有用的功能。
CSS Typed OM 可以被认为是现在使用的CSSOM的第二个版本。他的目标是解决很多现有模型的问题并且会引入新的CSS解析器API和CSS属性和值API的特性。
Typed OM的另一个主要目标是改进性能。将当前CSSOM的字符串值转化成有意义的类型化的JavaScript表达式会产生显著的性能提升。
CSS布局API 让开发者可以编写他们自己的模型。通过“布局模型”,基本上所有东西都能被传入到CSS display
属性中。这会让开发者第一次可以像原生的布局模型 display:flex
和 display:table
那样高效地进行布局。
作为一个示例, Masonry布局库 展示了开发者开发者有多愿意去完成仅依靠CSS无法搭建的布局。尽管这些布局令人印象深刻,但是不幸的是,他们充满了性能问题,特别是那些不那么强劲的设备上。
CSS布局API会给予开发者一个 registerLayout
方法来接受布局名称(这在稍后的会被用到)和一个JavaScript类来引入所有布局逻辑。下面是一个基本例子告诉你如何通过 registerLayour
来定义 masonry
:
registerLayout('masonry', class { static get inputProperties() { return ['width', 'height'] } static get childrenInputProperties() { return ['x', 'y', 'position'] } layout(children, constraintSpace, styleMap, breakToken) { // Layout logic goes here. } }
如果你看不懂上面的例子,不要担心。主要需要关心的是下面的代码。一旦你下载了 masonry.js
文件,并且将它添加到你的网站上,你可以像下面那样编写CSS然后他们就会工作:
body { display: layout('masonry'); }
CSS绘制API和上面的布局API十分相似。它提供了一个 registerPatint
方法,操作方式和 registerLayout
方法一样。开发者然后可以在CSS中任何地方需要CSS图像的地方使用 paint()
函数,只要传入他们注册的名称就好。
下面是一个简单绘制有颜色的圆的例子:
registerPaint('circle', class { static get inputProperties() { return ['--circle-color']; } paint(ctx, geom, properties) { // Change the fill color. const color = properties.get('--circle-color'); ctx.fillStyle = color; // Determine the center point and radius. const x = geom.width / 2; const y = geom.height / 2; const radius = Math.min(x, y); // Draw the circle /o/ ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI, false); ctx.fill(); } });
在CSS中它可以这样使用
.bubble { --circle-color: blue; background-image: paint('circle'); }
现在, .bubble
元素会展示一个蓝色的圆形作为背景。圆形自身会被放置在中间,并且大小回合元素本身一样,无论发生什么事。
上面列举的许多规范都展示了代码示例(例如, registarLayout
和 registerPaint
)。如果你好奇你应该在哪里放置这些代码,答案就是 worklet 脚本。
Worklets和web workers十分相似,他们都允许你引入脚本文件并且运行JavaScript代码。而且他可以在渲染过程中的不同地方被调用,并且独立于主线程。
Worklet脚本会为了保证高性能严重限制你可以用的操作类型。
尽管仍然没有官方的 合成滚动和动画 规范,这仍然是实际上比较知名和高度受关注的Houdini功能。最终的API会允许开发者在合成worklet中执行逻辑运算,而且这是独立于主线程的,可以去修改一些指定的DOM元素属性。这些属性只会是那些可以在不需要重绘的情况下进行更改的属性。(如transform、opacity、scroll offset)。
这会让开发者可以创造高效的滚动动画或者基于输入的动画,如粘滚动的标题和时差效果。你可以在Github上挂看更多这个API尝试去解决的 样例 。
尽管尚未有官方的文档,Chrome上已经开始了实验性的开发。事实上,Chrome团队正在使用这个API最终会暴露的方法来添加 CSS捕捉点(CSS snap points) 和 粘定位(sticky positioning) 。这是十分惊人的,因为这意味着Houdini的API足够高效,以至于新的Chrome特性依据他们来构建。如果你仍然害怕Houdini不如原生的快,这个事实会说服你。
Surma 录制了一个在Chrome上运行 视频样例 。这个demo模仿了Twitter原生移动应用的滚动标题欣慰。可以点击 源码 查看他是怎么工作的。
就像我之前提到的那样,我认为每个网站构建的人都应该关注Houdini;他将会在未来让我们生活的更加方便。即时你从来没有直接使用过Houdini规范,你也必然会使用一些基于他们完成的模块。
尽管这个未来不是马上就到来,但是他会比很多人想象想的要接近。所有的主要浏览器厂商都参加了今年稍早时候悉尼举办的最新Houdini面对面会议。他们对于要做什么如何构建这些问题产生的分歧还是比较少。
我认为的是,问题不是Houdini会否被实现,而是什么时候被实现。这也是需要你们的地方。
浏览器厂商,就像软件开发商一样,会对新功能进行分级。而这些优先级则一般都是用户对其的渴望程度。
所以,如果你关注web上面的样式和布局的拓展性。如果你希望以后你可以不用等待漫长标准过程就能使用新的CSS功能。那么就和你使用的浏览器的开发者们说,告诉他们你想要这个功能。
另一种帮忙的方法就是提供现实中的用例。那些你现在很难通过样式和布局实现的效果。有些 Github上的草稿 都有用例文档,你可以提交一个pull request来贡献你的想法。如果文档不存在,你可以创造一个。
Houdini工作组(和W3C一样)十分希望得到web开发者的建议。许多参加规范编写的人员都是浏览器开发工程师。他们自身不是专业的web开发者,这意味着他们通常不知道痛点何在。
这需要我们去告诉他们。
最新的Houdini公开的草稿版本。
规范更新和开发的官方Github资料库。
代码样例展示和利用可能的API进行的实验。
问问题的地方。
特别感谢Houdini成员 Ian Kilpatrick 和 Shane Stephens 对本文的审校
扫码关注w3ctech微信公众号