CSS|CSS Houdini(用浏览器引擎实现高级CSS效果)
vivo 互联网前端团队-Wei XingHoudini被称之为Magic of styling and layout on the web,看起来十分神秘,但实际上,Houdini并非什么神秘组织或者神奇魔法,它是一系列与CSS引擎相关的浏览器API的总称。
一、Houdini 是什么 在了解之前,先来看一些Houdini能实现的效果吧:
反向的圆角效果(Border-radius):
文章图片
动态的球形背景(Backgrond):
文章图片
彩色边框(Border):
文章图片
神奇吧,要实现这些效果使用常规的CSS可没那么容易,但对CSS Houdini来说,却很easy,这些效果只是冰山一角,CSS Houdini能做的有更多。(这些案例均来自Google Chrome Labs,更多案例可以通过 Houdini Samples 查看)。
看完效果,再来说说Houdini到底是什么。
首先,Houdini 的出现最直接的目的是为了解决浏览器对新的CSS特性支持较差以及Cross-Browser的问题。我们知道有很多新的CSS特性虽然很棒,但它们由于不被主流浏览器广泛支持而很少有人去使用。
随着CSS规范在不断地更新迭代,越来越多有益的特性被纳入进来,但是一个新的CSS特性从被提出到成为一个稳定的CSS特性,需要经过漫长地等待,直到被大部分浏览器支持时,才能被开发者广泛地使用。
而 Houdini 的出现正是洞察和解决了这一痛点,它将一系列CSS引擎API开放出来,让开发者可以通过JavasScript创造或者扩展现有的CSS特性,甚至创造自己的CSS渲染规则,给开发者更高的CSS开发自由度,实现更多复杂的效果。
二、JS Polyfill vs Houdini 有人会问,实际上很多新的CSS特性在被浏览器支持之前,也有可替代的JavaScript Polyfill可以使用,为什么我们仍然需要Houdini呢?这些Polyfill不是同样可以解决我们的问题吗?
要回答这个问题也很简单,JavaScript Polyfill相对于Houdini有三个明显的缺陷:
1.不一定能实现或实现困难。
CSSOM开放给JavaScript的API很少,这意味着开发者能做的很有限,只能简单地操纵DOM并对样式做动态计算和调整,光是去实现一些复杂的CSS新特性的Polyfill就已经很难了,对于更深层次的Layout、Paint、Composite等渲染规则更是无能为力。所以当一个新的CSS特性被推出时,通过JavaScript Polyfill不一定能够完整地实现它。2.实现效果差或有使用限制。
JavaScript Polyfill是通过JavaScript来模拟CSS特性的,而不是直接通过CSS引擎进行渲染,通常它们都会有一定的限制和缺陷。例如,大家熟知的css-scroll-snap-polyfill就是针对新的CSS特性Scroll Snap产生的Polyfill,但它在使用时就存在使用限制或者原生CSS表现不一致的问题。3.性能较差。
JavaScript Polyfill可能造成一定程度的性能损耗。JavaScript Polyfill的执行时机是在DOM和CSSOM都构建完成并且完成渲染后,通常JavaScript Polyfill是通过给DOM元素设置内联样式来模拟CSS特性,这会导致页面的重新渲染或回流。尤其是当这些Polyfill和滚动事件绑定时,会造成更加明显的性能损耗。
文章图片
Houdini的诞生让CSS新特性不再依赖于浏览器,开发者通过直接操作CSS引擎,具有更高的自由度和性能优势,并且它的浏览器支持度在不断提升,越来越多的API被支持,未来Houdini必然会加速走进web开发者的世界,所以现在对它做一些了解也是必要的。
在本文,我们会介绍Houdini的APIs以及它们的使用方法,看看这些API当前的支持情况,并给出一些在生产环境中使用它们的建议。
Houdini的名称与一位著名美国逃脱魔术师Harry Houdini的名称一样,也许正是取逃脱之意,让CSS新特性逃离浏览器的掌控。三、Houdini APIs 上文提到CSS Houdini提供了很多CSS引擎相关的API,根据Houdini提供的规范说明文件,API共分为两种类型:high-level APIs 和 low-level APIs 。
文章图片
high-level APIs:顾名思义是高层次的API,这些API与浏览器的渲染流程相关。
- Paint API
提供了一组与绘制(Paint)过程相关的API,我们可以通过它自定义的渲染规则,例如调整颜色(color)、边框(border)、背景(background)、形状等绘制规则。
- Animation API
提供了一组与合成(composite)渲染相关的API,我们可以通过它调整绘制层级和自定义动画。
- Layout API
提供了一组与布局(Layout)过程相关的API,我们可以通过它自定义的布局规则,类似于实现诸如flex、grid等布局,自定义元素或子元素的对齐(alignment)、位置(position)等布局规则。low-level APIs:低层次的API,这些API是high-level APIs的实现基础。
- Typed Object Model API
- CSS Properties & Values API
- Worklets
- Font Metrics API
- CSS Parser API
文章图片
对比下图2018年底的情况,Houdini目前得到了更广泛的支持,我们也期待图里更多绿色的板块被逐渐点亮。
文章图片
大家可以访问 Is Houdini ready yet? 看到Houdini的最新支持情况。
下文中,我们会着重介绍Typed Object Model API、CSS Properties & Values API、Worklets和Paint API、Animation API,因为它们目前具有比其他API更好的支持度,且它们的特性已经趋于稳定,在未来不会有很大的变更,大家也能在了解它们之后直接将它们使用在项目中。
四、 Typed Object Model API 在Houdini出现以前,我们通过JavaScript操作CSS Style的方式很简单,先看看一段大家熟悉的代码。
// Before Houdini
const size = 30
target.style.fontSize = size + 'px' // "20px"const imgUrl = 'https://www.exampe.com/sample.png'
target.style.background = 'url(' + imgUrl + ')' // "url(https://www.exampe.com/sample.png)"target.style.cssText = 'font-size:' + size + 'px;
background: url('+ imgUrl +')'
// "font-size:30px;
background: url(https://www.exampe.com/sample.png)"
我们可以看到CSS样式在被访问时被解析为字符串返回,设置CSS样式时也必须以字符串的形式传入。开发者需要手动拼接数值、单位、格式等信息,这种方式非常原始和落后,很多开发者为了节省性能损耗,会选择将一长串的CSS Style字符串传入cssText,可读性很差,而且很容易产生隐蔽的语法错误。
Typed Object Model与TypeScript的命名类似,都增加了Type这个前缀,如果你使用过TypeScript就会了解到,TypeScript增强了类型检查,让代码更稳定也更易维护,Typed Object Model也是如此。相比于上面晦涩的传统方法,Typed Object Model将CSS属性值包装为Typed JavaScript Object,让每个属性值都有自己的类型,简化了CSS属性的操作,并且带来了性能上的提升。通过JavaScript对象来描述CSS值比字符串具有更好的可读性和可维护性,通常也更快,因为可以直接操作值,然后廉价地将其转换回底层值,而无需构建和解析 CSS 字符串。
在Typed Object Model中CSSStyleValue是所有CSS属性值的基类,在它之下的子类用于描述各种CSS属性值,例如:
- CSSUnitValue
- CSSImageValue
- CSSKeywordValue
- CSSMathValue
- CSSNumericValue
- CSSPositionValue
- CSSTransformValue
- CSSUnparsedValue
- 其它
{
value: 30,
unit: "px"
}
可以看到,通过对象来描述CSS属性值确实比传统的字符串更易读了。
要访问和操作CSSStyleValue还需要借助两个工具,分别是attributeStyleMap和computedStyleMap(),前者用于处理内联样式,可以进行读写操作,后者用于处理非内联样式(stylesheet),只有读操作。
// 获取stylesheet样式
target.computedStyleMap().get("font-size");
// { value: 30, unit: "px"}// 设置内联样式
target.attributeStyleMap.set("font-size", CSS.em(5));
// stylesheet样式仍然返回20px
target.computedStyleMap().get("font-size");
// { value: 30, unit: "px"}// 内联样式已经被改变
target.attributeStyleMap.get("font-size");
// { value: 5, unit: "em"}
当然attributeStyleMap和computedStyleMap()还有更多可用的方法,例如clear、has、delete、append等,这些方法都为开发者提供了更便捷和清晰的CSS操作方式。
五、CSS Properties & Values API 根据MDN的定义,CSS Properties & Values API也是Houdini开放的一部分API,它的作用是让开发者显式地声明自定义属性(css custom properties),并且定义这些属性的类型、默认值、初始值和继承方法。
--my-color: red;
--my-margin-left: 100px;
--my-box-shadow: 3px 6px rgb(20, 32, 54);
在被声明之后,这些自定义属性可以通过var()来引用,例如:
// 在:root下可声明全局自定义属性
:root {
--my-color: red;
}
#container {
background-color: var(--my-color)
}
了解了自定义属性的基本概念和使用方式后,我们来考虑一个问题,我们能否通过自定义属性来帮助我们完成一些过渡效果呢?
例如,我们希望为一个div容器设置背景色的transition动画,我们知道CSS是无法直接对background-color做transition过渡动画的,那我们考虑将transition设置在我们自定义的属性--my-color上,通过自定义属性的渐变来间接完成背景的渐变效果,是否能做到呢?根据刚才的自定义属性简介,也许你会尝试这么做:
// DOM
container
// Style
:root {
--my-color: red;
}
#container {
transition: --my-color 1s;
background-color: var(--my-color)
}
#container:hover {
--my-color: blue;
}
这看起来是个符合逻辑的写法,但实际上由于浏览器不知道该如何去解析--my-color这个变量(因为它并没有明确的类型,只是被当做字符串处理),所以也无法对它采用transition的效果,因此我们并不能得到一个渐变的背景色动画。
文章图片
但是,通过CSS Properties & Values API提供的CSS.registerProperty()方法就可以做到,就像这样:
// DOM
container
// JavaScript
CSS.registerProperty({
name: '--my-color',
syntax: '',
inherits: false,
initialValue: '#c0ffee',
});
// Style
#container {
transition: --my-color 1s;
background-color: var(--my-color)
}
#container:hover {
--my-color: blue;
}
与上面的不同之处在于,CSS.registerProperty()显式定义了--my-color的类型syntax,这个syntax告诉浏览器把--my-color当做color去解析,因此当我们设置transition: --my-color 1s时,浏览器由于提前被告知了该属性的类型和解析方式,因此能够正确地为其添加过渡效果,得到的效果如下图所示。
文章图片
CSS.registerProperty()接受一个参数对象,参数中包含下面几个选项:
- name: 变量的名字,不允许重复声明或者覆盖相同名称的变量,否则浏览器会给出相应的报错。
- syntax: 告诉浏览器如何解析这个变量。它的可选项包含了一些预定义的值等。
- inherits: 告诉浏览器这个变量是否继承它的父元素。
- initialValue: 设置该变量的初始值,并且将该初始值作为fallback。
@property --my-color{
syntax: '',
inherits: false,
initialValue: '#c0ffee',
}
六、Font Metrics API 目前 Font Metrics API 还处于早期的草案阶段,它的规范在未来可能会有较大的变更。在当前的specification文件中,说明了 Font Metrics API 将会提供一系列API,允许开发者干预文字的渲染过程,创建文字或者动态修改文字的渲染效果等。期待它能在未来被采纳和支持,为开发者提供更多的可能。
七、CSS Parser API 目前 Font Metrics API 也处于早期的草案阶段,当前的specification文件中说明了它将会提供更多CSS解析器相关的API,用于解析任意形式的CSS描述。
八、Worklets 【CSS|CSS Houdini(用浏览器引擎实现高级CSS效果)】Worklets是轻量级的 Web Workers,它提供了让开发者接触底层渲染机制的API,Worklets的工作线程独立于主线程之外,适用于做一些高性能的图形渲染工作。并且它只能被使用在HTTPS协议中(生产环境)或通过localhost来启用(开发调试)。
Worklets不像Web Workers,我们不能将任何计算操作都放在Worklets中执行,Worklets开放了特定的属性和方法,让我们能处理图形渲染相关的操作。我们能使用的Worklet类型暂时有如下几种:
- PaintWorklet - Paint API
- LayoutWorklet - Animation API
- AnimationWorklet - Layout API
- AudioWorklet - Audio API(处于草案阶段,暂不介绍)
九、Paint API Paint API允许开发者通过Canvas 2d的方法来绘制元素的背景、边框、内容等图形,这在原始的CSS规则中是无法做到的。
Paint API需要结合上述提到的PaintWorklet一起使用,简单来说就是开发者构建一个PaintWorklet,再将它传入Paint API就可以绘制相应的Canvas图形。如果你熟悉Canvas,那Paint API对你来说也不会陌生。
使用Paint API的过程简述如下:
- 使用registerPaint()方法创建一个PaintWorklet。
- 将它添加到Worklet模块中,CSS.paintWorklet.addModule()。
- 在CSS中通过paint()方法使用它。
文章图片
其中registerPaint()方法用于创建一个PaintWorklet,在这个方法中开发者可以利用Canvas 2d自定义图形绘制。
可以通过Google Chrome Labs给出的一个paint API案例checkboardWorklet来直观看看它的具体使用方法,案例中利用Paint API为textarea绘制彩色的网格背景,它的代码组成很简单:
/* checkboardWorklet.js */
class CheckerboardPainter {
paint(ctx, geom, properties) {
const colors = ['red', 'green', 'blue'];
const size = 32;
for(let y = 0;
y < geom.height/size;
y++) {
for(let x = 0;
x < geom.width/size;
x++) {
const color = colors[(x + y) % colors.length];
ctx.beginPath();
ctx.fillStyle = color;
ctx.rect(x * size, y * size, size, size);
ctx.fill();
}
}
}
}
// 注册checkerboard
registerPaint('checkerboard', CheckerboardPainter);
/* index.html *//* index.html */
textarea {
background-image: paint(checkerboard);
// 使用paint()方法调用checkboard绘制背景
}
通过上述三个步骤,最终生成的textarea背景效果如图所示:
文章图片
感兴趣的同学可以访问 houdini-samples查看更多官方样例。
十、Animation API 在过去,当我们想要对DOM元素执行动画时,通常只有两个选择:CSS Transitions和CSS Animations。这两者在使用上虽然简单,也能满足大部分的动画需求,但是它们有两个共同的缺点:
- 仅仅依赖时间来执行动画(time-driven):动画的执行仅和时间有关。
- 无状态(stateless):开发者无法干预动画的执行过程,获取不到动画执行的中间状态。
文章图片
Animation API却可以帮助我们轻松做到。
在功能方面,它是CSS Transitions和CSS Animations的扩展,它允许用户干预动画执行的过程,例如结合用户的scroll、hover、click事件来控制动画执行,像是为动画增加了进度条,通过进度条控制动画进程,从而实现一些更加复杂的动画场景。
在性能方面,它依赖于AnimationWorklet,运行在单独的Worklet线程,因此具有更高的动画帧率和流畅度,这在低端机型中尤为明显(当然,通常低端机型中的浏览器内核还不支持该特性,这里只是说明Animation API对动画的视觉体验优化是很友好的)。
Animation API的使用和Paint API一样,也同样遵循Worklet的创建和使用流程,分为三个步骤,简述如下:
- 使用registerAnimator()方法创建一个AnimationWorklet。
- 将它添加到Worklet模块中,CSS.animationWorklet.addModule()。
- 使用new WorkletAnimation(name, KeyframeEffect)创建和执行动画。
文章图片
/* myAnimationWorklet.js */
registerAnimator("myAnimationWorklet", class {
constructor(options) {
/* 构造函数,动画示例被创建时调用,可用于做一些初始化 */
}//
animate(currentTime, effect) {
/* 干预动画的执行 */
}
});
/* index.html */
await CSS.animationWorklet.addModule("path/to/myAnimationWorklet.js");
;
/* index.html */
/* 传入myAnimationWorklet,创建WorkletAnimation */
new WorkletAnimation(
'myAnimationWorklet', // 动画名称
new KeyframeEffect(// 动画timeline(对应于步骤一中animate(currentTime, effect)中的effect参数)
document.querySelector('#target'),
[
{
transform: 'translateX(0)'
},
{
transform: 'translateX(200px)'
}
],
{
duration: 2000, // 动画执行时长
iterations: Number.POSITIVE_INFINITY// 动画执行次数
}
),
document.timeline // 控制动画执行进程的数值(对应于步骤一中animate(currentTime, effect)中的currentTime参数)
).play();
可以看到步骤一的animate(currentTime, effect)方法有两个参数,就是它们让开发者能够干预动画执行过程。
- currentTime:
document.timeline是每个页面被打开后从0开始递增的时间数值,可以简单理解为页面被打开的时长,初始时document.timeline === 0,随着时间不断递增。
- effect:
如果不修改effect.localTime或者设置effect.localTime = currentTime,那么动画会随着document.timeline正常匀速执行,线性动画。但是如果将effect.localTime设置为某个固定值,例如effect.localTime = 1000ms,那么动画将会定格在1000ms时对应的帧,不会继续执行。
为了更好理解effect.localTime,可以来看看effect.localTime和动画执行之间的关系,假设我们创建了一个2000ms时长的动画,并且动画没有设置delay时间。
文章图片
通过上面的描述,大家应该get到如何做一个简单的滚动驱动(scroll-driven)的动画了,实际上有个专门用于生成滚动动画的类:ScrollTimeline,它的用法也很简单:
/* myWorkletAnimation.js */
new WorkletAnimation(
'myWorkletAnimation',
new KeyframeEffect(
document.querySelector('#target'),
[
{
transform: 'translateX(0)'
},
{
transform: 'translateX(500px)'
}
],
{
duration: 2000,
fill: 'both'
}
),
new ScrollTimeline({
scrollSource: document.querySelector('.scroll-area'), // 监听的滚动元素
orientation: "vertical", // 监听的滚动方向"horizontal"或"vertical"
timeRange: 2000 // 根据scroll的高度,传入0 - timeRage之间的数值,当滚动到顶端时,传入0,当滚动到底端时,传入2000
})
).play();
文章图片
这样一来,通过简单的几行代码,一个简单的滚动驱动的动画就做好了,它比任何CSS Animations或CSS Transitions都要顺畅。
接下来再看看最后一个同样有潜力的API:**Layout API **。
十一、Layout API Layout API允许用户自定义新的布局规则,创造类似flex、grid之外的布局。
但创建一个完备的布局规则并不简单,官方的flex、grid布局是充分考虑了各种边界情况,才能确保使用时不会出错。同时Layout API使用起来也比其它API更为复杂,受限于篇幅,本文仅简单展示相关的API和使用方式,具体细节可参考官方描述。
Layout API和其它两个API相似,使用步骤同样分为三个步骤,简述如下:
- 通过registerLayout()创建一个LayoutWorklet。
- 将它添加到Worklet模块中,CSS.layoutWorklet.addModule()。
- 通过display: layout(exampleLayout)使用它。
文章图片
Google Chrome Labs案例如下所示,通过Layout API实现了一个瀑布流布局。
虽然通过Layout API自定义布局较为困难,但是我们依然可以引入别人的优秀开源Worklet,帮助自己实现复杂的布局。
十二、新特性检测 鉴于当前Houdini APIs的浏览器支持度仍然不是很完美,在使用这些API时需要先做特性检测,再考虑使用它们。
/* 特性检测 */
if (CSS.paintWorklet) {
/* ... */
}
if (CSS.animationWorklet) {
/* ... */
}
if (CSS.layoutWorklet) {
/* ... */
}
想要在chrome中调试,可以在地址栏输入chrome://flags/#enable-experimental-web-platform-features,并勾选启用Experimental Web Platform features。
文章图片
十三、总结 Houdini APIs让开发者有办法接触到CSS渲染引擎,通过各种API实现更高性能和更复杂的CSS渲染效果。虽然它还没有完全准备好,很多API甚至还处于草案阶段,但它给我们带来了更多可能性,并且诸如paint API、Typed OM、Properties & Values API这些新特性也都被广泛支持了,可以直接用于增强我们的页面效果。未来Houdini APIs一定会慢慢走进开发者的世界,大家可以期待并做好准备迎接它。
参考文献:
- W3C Houdini Specification Drafts
- State of Houdini (Chrome Dev Summit 2018)
- Houdini’s Animation Worklet - Google Developers
- Interactive Introduction to CSS Houdini
- CSS Houdini Experiments
- Interactive Introduction to CSS Houdini
- Houdini Samples by Google Chrome Labs
推荐阅读
- SpringBoot快速整合通用Mapper
- 测试用例|实操自动生成接口自动化测试用例
- linux利用火焰图进行性能分析
- kubernetes RBAC相遇--介绍常规用法集群默认
- 前端学习从青铜到王者—HTML常用标签
- web学习的碎片之CSS的Sprite方法
- 用肉身的长度丈量灵魂的高度
- Spring学习笔记(4)Spring|Spring学习笔记(4)Spring 事件原理及其应用
- 启发式|元启发式如何跳出局部最优()
- Day15:营业利润率与营业费用率