原文链接: Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of
想要使用某种 CSS 特性,但是因为浏览器兼容性问题而没法使用?更糟糕一点,所有浏览器都支持这种特性,但支持度不完全,在某些情况下会有 bug 出现、支持性不一致,甚至于完全不兼容。如果你曾经遇到过上述情况(我肯定你一定遇到过),那你得好好关注 Houdini 。
Houdini 是 W3C 新成立的一个任务小组,它的终极目标是实现 css 属性的完全兼容。Houdini 提出了一个前无古人的的设想:开放 CSS 的 API 给开发者,开发者可以通过这套接口自行扩展 CSS,并提供相应的工具 允许开发者介入浏览器渲染引擎的样式和布局流程中 。
但是……这意味着什么呢?这个方案靠谱吗?不管是现在还是将来,Houdini 对开发者进行万展开发有什么帮助呢?
我将在后面的文章里尽可能地回答上面这三个问题。但在开始回答之前,要先搞清楚我们现在面临的问题是什么,以及为什么出现 Houdini 这样的解决方案。弄明白问题是什么之后,我再告诉大家 Houdini 将会如何解决这些问题,以及它目前的进展。最后,开发者们,你们还能了解具体如何做才能让 Houdini 尽早落地。
写文章也罢做 demo 也罢,每一次当我想要炫耀点 CSS 新花样的时候,总有人会说“这效果真是屌炸天!然而我们现在并不能好好用它,至少等个 10 年吧”。
虽然每每我都觉得这样的评论让人生气又没啥建设性,但依然承认大家的担心有理有据。纵观 CSS 历史,每一份特性草案都是经过了许多年才被广泛应用。而之所以会是“许多年”,就是因为要允许一个新特性被写入 CSS 标准需要经过一整套标准制定流程,然而就过了很多年……
我对于这个标准制定的流程肯定是毫无异议的,但你得承认,走完整个流程花儿都谢了。
flexbox 大概就是最好的例子,2009年,关于 flexbox 的草案第一次被提出,但直到今天开发者们还在抱怨着这个属性的浏览器兼容性问题。不过感谢上帝,随着现代浏览器的自动更新机制,这个问题总有解决的那一天。但是,按照现在的新属性发布流程,最新的浏览器也会和新属性提案仍然存在一个时间差。
不过同样在 web 世界里,对于如今的 JavaScript 来说好像不是什么事儿了:
在上面的流程图中,我们可以看到从想到一个新的 js 特性到运用在生产环境,大概只需要几天时间。讲真,我已经在使用 async
和 await
了,然而目前没有浏览器天生支持这两个方法吧。
估计现在你也大概感受到了 CSS 社区和 JS 社区的画风不同了:在 JS 社区里,你总能听到人们在抱怨它一天一个样 -- 跑得太快追得太累;而 CSS 社区,人们却在叹息着花精力去学习新事物是件多么吃力不讨好的事儿 -- 天知道什么时候才能用上新玩意呢。
脑海里闪过的第一个解决方案就是 CSS polyfill,只要 CSS polyfill 足够强大,相信 CSS 的发展速度赶超 JavaScript 不是梦。
然而,给 CSS 打补丁做起来有多难你都想不到,而且在大部分情况下,只要这么做了,性能肯定会受到影响。
JavaScript 是一种动态语言,它是如此之“动态”以至于有着让人惊叹的可扩展性,所以用 JavaScript 给 JavaScript 打补丁是可以轻松实现的,但 CSS 不是动态的呀。在某些情况下,你可以在构建过程中将一种形式的 CSS 转译成另一种形式( PostCSS 就是一个典型的例子)。而一旦你的补丁是依赖于的 DOM 结构、某一个元素的布局或者它的定位的话,那补丁程序就需要在本地运行了。
不幸的是,要在浏览器中实现这种方案挺难的。
让我们来看看一个 HTML 文档从被浏览器接收到显示在屏幕上的全过程,下面这张图里被标为蓝色的部分就是 JavaScript 可以染指的环节了:
作为开发者,看着这张图心都凉了,我们根本控制不了浏览器解析 HTML 和 CSS 的过程,只能看着它生成 DOM 和 CSS object model (CSSOM)。没法控制级联(cascade)、没法控制浏览器布局元素的方式(layout)、也没法控制元素在屏幕上显示的过程(paint)、最后的合成(composite)也无能为力。
整个过程中,开发者能完全控制的唯一环节就是 DOM,另外 CSSOM 也可以部分控制到。即使如此,引用 Houdini 官网上的话来说,这种程度的暴露是“不确定的、兼容性不稳定的以及缺乏对关键特性的支持的”。
举个例子,浏览器中的 CSSOM 是不会告诉你它是如何处理跨源的样式表的,而且对于浏览器无法解析的 CSS 语句它的处理方式就是不解析了,也就是说——如果你要用 CSS polyfill 让浏览器去支持它尚且不支持的属性,那就不能在 CSSOM 这个环节做,只能自行解析一遍 DOM 树,找到 <style>
和 <link rel="stylesheet">
标签,获取其中的 CSS 样式、解析、重写,最后再加回 DOM 树中。
DOM 树都刷新了,得,渲染页面步骤重新走一遍。
(上图括号里面的文字:JavaScript Polyfills 只能去更改 DOM 和 CSSOM,大部分这样的操作,都会导致页面重新渲染。)
好吧,可能你会说这种别无选择的方法,也并不会对性能造成多大伤害(对某些网站来说,是的),但想想这个重渲染过程会多么频繁地发生,如果你的 polyfill 是需要应对页面上的所有交互呢?scroll 事件、窗口缩放、鼠标移动还有键盘输入……随时都会被触发的重渲染会把页面拖得无敌慢,用户绝对会发现的。
雪上加霜的是……大部分的 CSS polyfills 都是各有各的解析器和层级逻辑,而且“解析器”和“层级逻辑”又是两个非常复杂的东西,所以这些 polyfills 不是文件太大就是有太多 bug。
简要概括一下上面的内容:如果你想要浏览器做出它本来做不到事情(比如让它解析你给的样式,不管它能不能实现该样式),而渲染流程里你无法插手其他步骤,所以只能通过手动更新和改变 DOM 的方式。
我认为,这个问题是这篇文章的关键所在,如果你草草略过了前文,千万要在这里停下!仔细看这部分!
在看完上面那一节之后,我确定有些人干脆因噎废食地想“我不需要这个!我只想要中规中矩地写页面,并不想侵入浏览器内核然后实现一些特别前卫的效果”。
如果你是这么想的,那我强烈建议你看看自己这些年用于实现页面效果的技术。我们想要“干涉”浏览器解析样式的目的并不仅仅是为了炫技,更是为了框架作者以及开发者们能做到下面两件事情:
如果你曾使用过像 jQuery 那样的 JS 库,那你已经从中受惠了!事实上,良好的兼容性正是绝大多数前端库活着框架的卖点之一。Github 上受欢迎度排名前五的 JavaScript 和 DOM 仓库 — AngularJS、D3、 jQuery、 React 还有 Ember,面对使用它们的人来说,只要搞明白如何使用那些 API,就能成功达到想要的目的了,但是它们背后,在兼容各浏览器上下了多少功夫,恐怕是使用者几乎从未考虑过的。
现在,想想 CSS 在跨浏览器上的问题。甚至像 Bootstrap 或者 Foundation 这样宣称兼容性良好的 CSS 框架也都没有把浏览器 bug 标准化,而只是尽量去避免它们。不要以为 CSS 的兼容性问题只是个老毛病,就拿 flexbox 来说,我们也还面对着各种各样的 跨浏览器兼容问题 。(译者注:flexbox 的兼容问题,现在在主流的移动端页面开发上,已经有所缓解,译者曾整理过一个 gist 用于移动端 html 5 页面的 flexbox 效果,欢迎使用纠错)
试想一下,你可以随心所欲地使用想用的 CSS 属性,在每个浏览器上,你的页面长得和你设想的一样(,这职业生涯得过的多欢脱啊。你看到的那些酷炫的属性都能在 保证性能的前提下 使用,比如网格布局( CSS grids )、对齐( CSS snap points )还有 sticky 定位 …… 而要实现这一切,你只需要把代码从 Github 上下载下来而已。
好吧,上面描绘的是 Houdini 的蓝图,Houdini 小组想要将这个梦想实现。
也许你从未想过写个 CSS polyfill 或者开发一些实验性的特性,但你可能会希望其他人能做这些事情,毕竟一旦有了这些工具,受益的可是全部开发者啊。
在前面,我曾提到过开发者是很难干涉浏览器的渲染过程的,除了 DOM 和 CSSOM 这两个环节外。
Houdini 小分队为了解决这个问题引入了一些新的标准,首次给了开发者介入另外几个渲染环节的权限。下面这张图片展示的是每个环节对应的新标准,开发者可以用这些标准来控制对应的环节。(注意:灰色区块是还在实现中的标准,目前暂时无法使用。)
在接下来的几节内容中,我将大概介绍一下每一种新规范以及它们能做到什么,其他规范这里就不再赘述了,感兴趣的朋友可以看 这里 。
CSS Parser API 还没有被写入规范,所以下面我要说的内容随时都会有变化,但是它的基本思想不会变:允许开发者自由扩展 CSS 词法分析器,引入新的结构(constructs),比如新的媒体规则、新的伪类、嵌套、 @extends
、 @apply
等等。
只要新的词法分析器知道如何解析这些新结构,CSSOM 就不会直接忽略它们,而是把这些结构放到正确的地方。
我曾 提过 CSS 已经有自定义属性了,这太让人兴奋了,CSS 将解锁多少新技能啊。 CSS Properties and Values API 的出现进一步推动了自定义属性,它还允许自定义属性添加不同的类型,大大增强了自定义属性的能力。
在自定义属性中加入不同的类型是很棒啦,不过这个 API 最大的的卖点是,开发者可以在自定义属性上做!动!画!而仅凭现在的技术,我们是做不到的……
来看看这个例子:
body { --primary-theme-color: tomato; transition: --primary-theme-color 1s ease-in-out; } body.night-theme { --primary-theme-color: darkred; }
当 night-theme
类被加到 <body>
元素上之后,页面所有有 --primary-theme-color
的元素属性值都会慢慢从 tomato
变成 darked
。如果今天你想要在自己的页面上实现这个效果,那就需要费劲儿的一个个给元素添加过渡动画。
译者注:为什么我满脑子想的都是性能,页面全部重绘似乎是不可避免得了,毕竟从 body 元素的颜色都换了嘛。不过话又说回来,主题都换了,reflow 也是理所应当的
这个 API 的另一个卖点是注册一个 “apply hook”,也就是开发者可以在 cascade 步骤结束的时候,通过一个钩子更改一个元素的自定义属性值,这个特性对于 polyfills 开发可是很有意义的。
你可以把 CSS Typed OM 视为 CSSOM 2.0,它的目的在于解决目前模型的一些问题,并实现 CSS Parsing API 和 CSS 属性与值 API 相关的特性。
提升性能是 Typed OM 的另一重任。将现在 CSSOM 的字符串值转成有意义的 JS 表达式可以有效的提升性能。
开发者可以通过 CSS Layout API 实现自己的布局模块(layout module),这里的“布局模块”指的是 display
的属性值。也就是说,这个 API 实现以后,开发者首次拥有了像 CSS 原生代码(比如 display:flex
、 display:table
)那样的布局能力。
让我们来看一个用例,在 Masonry layout library 上大家可以看到开发者们是有多想实现各种各样的复杂布局,其中一些布局光靠 CSS 是不行的。虽然这些布局会让人耳目一新印象深刻,但是它们的页面性能往往都很差,在一些低端设备上性能问题犹为明显。
CSS Layout API 暴露了一个 registerLayout
方法给开发者,接收一个布局名(layout name)作为后面在 CSS 中使用的属性值,还有一个包含有这个布局逻辑的 JavaScript 类。假如你想要用这个方法定义一个 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 你就能得到一个 masonry 布局的样式了:
body { display: layout('masonry'); }
CSS Paint API 和上面说到的 Layout API 非常相似。它提供了一个 registerPaint
方法,操作方式和 registerLayout
方法也很相似。当想要构建一个 CSS 图像的时候,开发者随时可以调用 paint()
函数,也可以使用刚刚注册好的名字。
下面这段代码展示时如何画一个有颜色的圆型:
registerPaint('circle', class { static get inputProperties() { return ['--circle-color']; } paint(ctx, geom, properties) { // 改变填充颜色 const color = properties.get('--circle-color'); ctx.fillStyle = color; // 确定圆心和半径 const x = geom.width / 2; const y = geom.height / 2; const radius = Math.min(x, y); // 画圆 ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI, false); ctx.fill(); } });
在 CSS 中可以这样使用它:
.bubble { --circle-color: blue; background-image: paint('circle'); }
你将在页面上看到一个以蓝色圆形为背景的元素,它的类是 .bubble
,这个圆形背景将始终占满 .bubble
元素。
你已经看过了一些和规范相关的代码(比如 registerLayout
和 registerPaint
),估计现在你想知道的是,这些代码得放在哪里呢?答案就是 worklet 脚本(工作流脚本文件)。
Worklets 的概念和 web worker 类似,它们允许你引入脚本文件并执行特定的 JS 代码,这样的 JS 代码要满足两个条件:第一,可以在渲染流程中调用;第二,和主线程独立。
Worklet 脚本严格控制了开发者所能执行的操作类型,这就保证了性能。
虽然关于 composited scrolling and animation 还没有官方的规范出来,但它可以算是 Houdini 项目中相当广为人知且颇被期待的特性之一。在设想中,这个 API 将会使得开发者能在合成器(compositor)的 worklet (而不是在主线程)中执行程序,还能更改一个 DOM 元素的属性,不过是不会引起渲染引擎重新计算布局或者样式的属性,比如 transform
、 opacity
或者滚动条位置(scroll offset)。
开发者可以通过这个 API 创建高性能的滚动和输入动画,比如滚动头效果、视差效果。你可以在 Github 上看到更多这个 API 试图实现的 效果 。
虽说正式规范还没有确定,但 Chrome 已经在实验性工具中加上了它。事实上 Chrome 的工程师们正在使用这些 API 最终会暴露的语言基元(primitives)来实现 CSS snap points 和 sticky 定位 。这说明了什么?Houdini API 的性能已经足够说服 Chrome 在它之上实现新特性了。单单这一点应该就能说服一直在担心性能问题的的你了吧。
Surma 在 Youtube 上发布了一个模拟 Twitter 应用头部滚动效果的 demo ,源码可以在 这里 查看。
在开头我就说了,我认为所有的 web 开发者都应该关注 Houdini,这个项目将会大大改善我们的未来。即使你可能不会直接接触到 Houdini 规范,但你肯定也会间接享受到它为你带来的便利,毕竟很多特性将基于它被构建出来。
虽然所描绘的美妙未来暂时还不会到来,但它也不会有你想象中那么遥远。今年早些时候,所有主流浏览器厂商都派代表参加了 Houdini 在悉尼的线下会议,在那次会议上,厂商们对于 Houdini 的方向和进程基本都达到了一致。
相信从我说的这些话里,你应该能相信 Houdini 的到来只是时间问题,它一定会成为正式的规范。
浏览器也是软件之一,它当然不可能一次性把所有的特性全加上,肯定是有给特性划分重要程度的。而这个划分方式常常是取决于用户对某个特性的需求度。
所以如果你真的很在意浏览器上样式和布局的可扩展性,如果你真的很想活在 CSS 新特性一出就能直接用进项目的世界里,快去和浏览器的开发者团队联系,告诉他们你真的很需要 Houdini。
另一种参与方式是,把现在不容易或根本不可能实现的、但你希望有一天可以用 CSS 实现的效果列出来, W3C 有提供一个 用例文档 ,你可以向那个 repo 提 pr。如果有的浏览器没有提供那样的文档,那干脆你来帮他们新建一个!
Houdini 项目小组(也可以说是 W3C 的所有成员)真的非常希望听到全世界 web 开发者的声音。事实上很多开发浏览器的人他们本身并不是职业的 web 开发者,像 c++ 程序员是真的不容易明白 web 开发者的痛点啊。
他们需要我们!
CSS-TAG Houdini Editor Drafts , W3C
CSS-TAG Houdini Editor Drafts , W3C The latest public version of all Houdini drafts
CSS-TAG Houdini Task Force Specifications , GitHub
CSS-TAG Houdini Task Force Specifications , GitHub The official Github repository where specification updates and development takes place
Houdini Samples , GitHub
Houdini Samples , GitHub Code examples showcasing and experimenting with possible APIs
Houdini mailing list , W3C
Houdini mailing list , W3C A place to ask general questions
特别鸣谢 Houdini 小组成员 Ian Kilpatrick 和 Shane Stephens 帮我 review 这篇文章。
外刊君: “不用去硅谷,来打一场码农的翻身仗吧~”