了解一下CSS Houdini API
Houdini, in essence, gives developers lower level access to CSS itself.
- Worklets, TypedOM, Custom Properties
- Paint API, Animation API, Layout API
CSS Houdini API
TL;DR
Houdini API主要包含一下六个部分:
类别 | 简称 | Spec全称 | 特点 |
---|---|---|---|
多线程 | Worklets | Worklets Level 1 | 使用多线程执行渲染引擎的指定类型任务,提高整体性能 |
CSS对象模型 | TypedOM | CSS Typed OM Level 1 | 使用JS操作CSSOM属性 |
自定义属性 | Custom Properties | CSS Properties and Values API Level 1 | 在JS主进程中定义与类型化CSS属性 |
绘制 | Paint API | CSS Painting API Level 1 | 使用类canvas上下文在元素背景与mask中自由绘制 |
动画 | Animation API | CSS Animation Worklet API | 使用JS与worklet操作元素帧动画的效果与时间 |
布局 | Layout API | CSS Layout API Level 1 | 通过类逻辑属性与worklet自由地布局元素 |
注意: 目前在chrome使用这些API可能需要手动开启web实验性功能选项。
开启方法: 进入
chrome://flags/
地址,将Experimental Web Platform features
启用即可。
Worklet
渲染引擎的扩展模块,类似web worker,也是开启新的线程去执行指定任务。
具有以下两个核心概念:
- 并行设计 - 每个Worklet必须拥有2个及以上的实例,每个实例都会在被调用时实际运行
- 域的限制 - 只能访问一些全局域下的方法或对象,比如
setTimeout
(这一点类似worker)
如何使用
Worklet属于JS模块,通过调用worklet对象的addModule
函数来添加,该函数会返回一个Promise。
// 在浏览器上下文中加载worklet
await demoWorklet.addModule('path/to/script.js');
// 也可以一次性加载多个worklet
Promise.all([
demoWorklet1.addModule('script1.js'),
demoWorklet2.addModule('script2.js'),
]).then(results => {
// 待worklet都加载完成,可以基于此做一些其他工作
});
worklet文件的内容结构大同小异,均有一个在全局作用域的注册函数,该函数接收一个注册的名称和一个针对指定类型worklet设计的类。
// 一个worklet大概长这个样子
registerDemoWorklet('name', class {
// 每种worklet中都需要定义渲染引擎所需的函数
process(arg) {
// 具体功能取决于worklet的种类
// 有时可能会有返回值,有时也许会直接在参数上进行操作
return !arg;
}
});
生命周期
- 从渲染引擎开始,启动一个主线程
- 接着会启动多个worklet所运行的worklet进程。为了不阻塞主线程,这些进程理论上是与主线程分离的。
- 在主线程中加载浏览器的JS脚本
- JS中调用
worklet.addModule
异步加载一个worklet - 一旦捕获到空闲的worklet进程,则worlet会加载到两个或更多的worklet进程中
- 当需要时,渲染引擎会从已加载的worklet中调用合适的进程来执行任务,这个调用可以是任何并行的worklet实例
TypedOM
Typed OM
是对于已存在的CSSOM的扩展,它使用类型化JS对象暴露了CSS值,而不是像现在的一个简单的字符串,将字符串转换成有意义的类型会消耗很多性能,因此采用类型化JS对象的方式可以更有效率的处理CSS属性值。
在Typed OM
中,CSS Value现在是一个新基类CSSStyleValue
的成员变量,CSSStyleValue
中包含大量用来描述具体CSS属性的子类:
CSSKeywordValue
- CSS关键字或其他标识符(比如inhert
或grid
)CSSPositionValue
- 坐标属性(x
,y
)值CSSImageValue
- 表示图像属性的对象CSSUnitValue
- 数值型值。可以由一个数值与单位表示(比如50px),也可以由无单位的数值或百分比表示CSSMathValue
- 复杂的数值型值。比如使用了calc
,min
,max
等函数得到结果,包含很多子类:CSSMathSum
,CSSMathProduct
,CSSMathMin
,CSSMathMax
,CSSMathNegate
,CSSMathInvert
CSSTransformValue
- 由CSSTransformComponents组成的一个CSS变换列表,包含CSSTranslate, CSSRotate, CSSScale, CSSSkew, CSSSkewX, CSSSkewY, CSSPerspective 或 CSSMatrixComponent
如何使用
与Typed OM相关的两个重要方法:attributeStyleMap
和computedStyleMap
attributeStyleMap
- 用来get与set类型化样式computedStyleMap
- 用来get一个元素完整的Typed OM样式
attributeStyleMap
在设置属性值时,有多个数值函数可以用来处理它们(与关键字值不同)
myElement.attributeStyleMap.set('font-size', CSS.em(2));
myElement.attributeStyleMap.get('font-size'); // CSSUnitValue { value: 2, unit: 'em' }
myElement.attributeStyleMap.set('opacity', CSS.number(.5));
myElement.attributeStyleMap.get('opacity'); // CSSUnitValue { value: 0.5, unit: 'number' };
computedStyleMap
一个css片段
.foo {
background-position: center bottom 10px;
transform: translateX(1em) rotate(50deg) skewX(10deg);
vertical-align: baseline;
width: calc(100% - 3em);
}
使用computedStyleMap获取对应关键字的完整属性值,对应的js操作如下
const cs = $('.foo').computedStyleMap();
cs.get('vertical-align');
// CSSKeywordValue {
// value: 'baseline',
// }
cs.get('background-position').x;
// CSSUnitValue {
// value: 50,
// unit: 'percent',
// }
cs.get('background-position').y;
// CSSMathSum {
// operator: 'sum',
// values: CSSNumericArray {
// 0: CSSUnitValue { value: -10, unit: 'px' },
// 1: CSSUnitValue { value: 100, unit: 'percent' },
// },
// }
cs.get('width');
// CSSMathSum {
// operator: 'sum',
// length: 2,
// values: CSSNumericArray {
// 0: CSSUnitValue { value: -90, unit: 'px' },
// 1: CSSUnitValue { value: 100, unit: 'percent' },
// },
// }
cs.get('transform');
// CSSTransformValue {
// is2d: true,
// length: 3,
// 0: CSSTranslate {
// is2d: true,
// x: CSSUnitValue { value: 20, unit: 'px' },
// y: CSSUnitValue { value: 0, unit: 'px' },
// z: CSSUnitValue { value: 0, unit: 'px' },
// },
// 1: CSSRotate {
// is2d: true,
// angle: CSSUnitValue { value: 50, unit: 'deg' },
// x: CSSUnitValue { value: 0, unit: 'number' },
// y: CSSUnitValue { value: 0, unit: 'number' },
// z: CSSUnitValue { value: 1, unit: 'number' },
// },
// 2: CSSSkewX {
// is2d: true,
// ax: CSSUnitValue { value: 10, unit: 'deg' },
// },
// }
Custom Properties
目前的CSS Variables
特性提供了一种用户定义CSS属性值的方式,但限制了var()
的外部引用(仅在CSS中使用),并且不能被严格的定义(类型等)。
针对这些问题产生了CSS Properties & Values API Level 1
,即自定义属性,扩展了CSS Variables
,允许我们注册属性并定义它们的类型、初始值和继承属性,极大提高了CSS属性变量的操作能力和灵活性。
对比
目前的CSS Variables
.thing {
--my-color: green;
--my-color: url('not-a-color'); // 它只是一个变量,什么都不懂
color: var(--my-color); // 这就尴尬了
}
Houdini自定义属性
window.CSS.registerProperty({
name: '--my-color',
syntax: '<color>', // 现在它定义了一个color,url这种值就会被跳过
});
示例
CSS.registerProperty({
name: '--foo', // String,自定义属性的名称
syntax: '<color>', // String, 解析该属性的方式,默认为*
inherits: false, // Boolean, 若为true则会继承自DOM树
initialValue: 'black', // String, 该属性的初始值
});
在注册一个自定义属性时,有很多支持的syntax值可供使用:<length>
, <number>
, <percentage>
, <length-percentage>
, <color
, <image>
, <url>
, <integer>
等等
可以在使用空格分隔的列表中使用+
表示包含多个syntax值(比如length),使用|
来分隔不同的可选synatx值。
Paint API
CSS Painting API Level 1, 也被称为Houdini Paint API
,它在CSS中使用图像的地方(背景、遮罩等)为我们提供了一种新的实现绘制自定义图样的方式。
使用全新的paint()
函数与Paint API worklet
,我们可以:
- 使用类似2D Canvas的绘制上下文
- 基于元素尺寸缩放绘制的图像(重绘亦是)
- 通过自定义属性设置绘制样式
支持情况
Paint API的polyfill:css-paint-polyfill
如何使用
画一个圆
比如现在我们想使用Paint API在某个元素的背景中画一个圆,主要分为如下三部分
-
CSS部分
section { background-image: paint(awesomePattern); };
在css的属性值中使用
paint()
函数加载指定worklet模块,参数为之后起的worklet模块名称。 -
JS部分
CSS.paintWorklet.addModule('patternWorklet.js');
使用
CSS.paintWorklet
的addModule()
载入worklet。 -
worklet部分
// patternWorklet.js class Shape { paint(ctx, geom, properties) { let x = geom.width / 2; let y = geom.height / 2; ctx.strokeStyle = 'white'; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(x, y, 50, 0, 2 * Math.PI); ctx.stroke(); ctx.closePath(); } } // Register our class under a specific name registerPaint('awesomePattern', Shape);
主要是
paint()
与registerPaint()
这两个函数。paint()
中会依次传入三个参数:ctx
,geom
,properties
ctx
(PaintRenderingContext2D) - 绘制上下文,与canvas类似,不同的是无法像在canvas中那样获取图像中的像素数据geom
(PaintSize) - 表示绘制区域的尺寸,包含height
与width
属性properties
(StylePropertyMapReadOnly) - 一个包含自定义属性与样式属性的只读Map,属性在inputProperties
中导入
registerPaint()
是Paint worklet的注册函数
使用自定义属性
在paint worklet中使用自定义属性仅需两步:
-
CSS部分
在CSS中定义属性
section { --circle-scale: 2; // 定义缩放比例 --circle-color: red;// 定义颜色 ... };
-
worklet部分
在worklet中取得属性值并绘制出来
class Shape { static get inputProperties() { return ['--circle-scale','--circle-color']; } paint(ctx, geom, properties) { ... const scale = parseInt(properties.get('--circle-scale').toString()) || 1; const color = properties.get('--circle-color').toString() || 'white'; ... ctx.strokeStyle = color; ctx.arc(x, y, 50 * scale, 0, 2 * Math.PI); ... } } ...
Animation API
CSS Animation Worklet API,即Animation API(注意,与Web Animations
不同), 这个API提供了一种能力,使我们可以通过一种标准的、非阻塞的方式,根据用户的输入(比如滚动等)来驱动关键帧动画。
支持情况
如何使用
要使用Animation API,需要创建一个WorkletAnimation
实例来控制动画
-
JS部分
// 1. 载入animation worklet await CSS.animationWorklet.addModule('sample-animation-worklet.js'); // 2. 获取滚动的元素和生成与之绑定的timeline对象 const scrollSource = document.scrollingElement; // 分解的动画步数 const timeRange = 1000; // 监听scrollSource元素的滚动事件,将行为分解成timeRange所设置的步数,可以自定义滚动的起始`startScrollOffset`和终止`endScrollOffset`偏移量 const scrollTimeline = new ScrollTimeline({ scrollSource, timeRange, }); // 3. 想要执行动画的元素及对应的关键帧动画效果 const elem = document.querySelector('#my-elem'); const effectKeyframes = new KeyframeEffect( elem, // 帧动画效果 [ {transform: 'scale(1)'}, {transform: 'scale(.25)'}, {transform: 'scale(1)'} ], { // 效果的持续时间 // 取值 timeRange 则变化与元素滚动行为的速度保持一致 // 取值 0-timeRange 则变化化相对滚动更快 // 取值 >timeRange 则变化相对滚动更慢 // 取值 0 组不会执行该动画 duration: timeRange, }, ); // 创建一个WorkletAnimation实例 new WorkletAnimation( // 使用的Animation Worklet名称 'sample-animator', // 使用的帧动画效果,也可以是一个KeyframeEffect序列 effectKeyframes, // 使用的执行动画的timeline scrollTimeline, // 传入Worklet构造器的其他参数 {}, ).play(); // 创建完后执行动画
-
worklet部分
registerAnimator('sample-animator', class { constructor(options) { // 在这里配置每个animator所使用的参数等 } animate(currentTime, effect) { // currentTime - timeline中定义的current time // effect - 正在进行的效果组 // 在这里执行帧动画的效果逻辑 // 通常会在这里设置动画效果的执行时间 effect.localTime = currentTime; } });
WorkletAnimation
WorkletAnimation中包含一个Effect和一个Timeline对象,它所具有的current time
不会直接作为动画效果发生的local time
,而与其关联的Animator实例可以控制动画实际发生的local time
。
下面介绍一下与WorkletAnimation相关的四个参数
-
Animator
实际上是一个Animation Worklet,对动画实际发生时间进行处理,类似
requestAnimationFrame()
与setTimeout()
-
AnimationEffect
用来设置关键帧动画效果,是一个KeyframeEffect实例,可以理解为CSS中@keyframe内容的JS对象体现。
const avatarEffect = new KeyframeEffect( document.querySelector('.avatar'), [ {transform: `translateY(0) scale(1)`}, {transform: `translateY(0px) scale(${0})`, offset: 0}, {transform: `translateY(${0}px) scale(${0})`}, ], { duration: 1000, fill: 'both', } );
构建KeyframeEffect实例需要DOM元素、变换序列及计算的时间属性。
-
AnimationTimeline
提供current time的对象,在WorkletAnimation中有一个特殊的timeline概念:
ScrollTimeline
,它可以根据当前在容器中滚动的位置来决定时间值。 -
options
可选参数,其他需要在worklet内部使用的数据。
线程与时间模型
线程模型
- 主线程执行帧动画
- 动画线程(worklet)执行animator,计算local time
- 并行的worklet执行上下文与主线程JS上下文之间动画效果值的同步,必须要在执行动画关键帧回调之前进行
时间模型
worklet animation的时间模型与web animation略有不同,具有如下特点:
- worklet animation在ready状态的判断中会比其他的animation多一个条件:user agent是否完成了创建worklet animation对应的animator实例所需的步骤。
- worklet animation不会将current time作为动画效果的local time,而是交由animator实例直接处理
Layout API
CSS Layout API Level 1,简称Layout API
,使我们可以像玩俄罗斯方块一样对页面上的元素进行布局,释放无限可能的创造力。 使用它,我们可以:
- 真正的去操作元素的显示相关属性
- 写一个布局spec的polyfill,或者创造一个
- 创建一个不会影响性能的瀑布流布局,人见人爱
基本概念
-
Current Layout
表示在当前盒模型中执行的布局算法
-
Parent Layout
作为执行布局算法的父级元素,会应用形如
display: layout(custom)
的计算属性,加载layout worklet中的布局算法。执行了布局算法后的父级元素布局即为Parent Layout
-
Layout Edges
在
Parent Layout
中包含的边框、内边距与滚动条等属性,都被收集在了Layout Edges
中 -
Layout Constraint
在
Parent Layout
中移除掉Layout Edges
,剩下用来布局子元素的区域叫做Layout Constraint
-
Child Layout
Child Layout
表示Current Layout
中Layout Child
的布局算法 -
LayoutChild
LayoutChild
是一个用来内部计算的容器,本身不具有布局信息,只能通过确定条件
生成,可以被用来生成实际执行布局的Fragment
-
Fragment
用来执行具体布局算法的元素
如何使用
-
CSS部分
.some-element { display: layout(custom); }
在目标元素的display属性上使用
layout()
计算属性加载自定义的layout模块 -
JS部分
CSS.layoutWorklet.addModule('customLayout.js');
-
worklet部分
// customLayout.js registerLayout('custom', class { // 传入样式属性与配置选项 static inputProperties = ['--foo']; static childrenInputProperties = ['--bar']; static layoutOptions = { childDisplay: 'normal', sizing: 'block-like' }; async intrinsicSizes(children, edges, styleMap) { // 在这里计算固有尺寸 const childIntrinsicSize = await children[0].intrinsicSizes(); } async layout(children, edges, constraints, styleMap, breakToken) { // 在这里执行布局代码&算法 // 在intrinsicSizes与layout函数中均会传入children列表 const child = children[0]; // 获取childInputProperties中列举的属性 const fooValue = child.styleMap.get('--foo'); // layoutNextFragment函数用来生成frament,执行布局 const fragment = await child.layoutNextFragment({}); } });
Layout Worklet
简要介绍Layout Worklet中的主要配置选项与函数
-
inputProperties
与childrenInputProperties
每个Layout API容器均有一个
styleMap
对象,在inputProperties
中可以设置从styleMap
中可读取的属性,该属性可以是css属性也可以是自定义属性,如margin-top
,padding
,--foo
等。子元素的styleMap可访问属性需要在childrenInputProperties
中定义 -
layoutOptions
设置布局选项,包含
childDisplay
和sizing
属性。其中sizing的取值有两种:block-like(默认)
与manual
,前者表示应用盒容器的尺寸特性,后者表示尺寸由开发者指定 -
layout()
该函数包含五个参数:
children
,edges
,constraints
,styleMap
和breakToken
children
([LayoutChild]) - 表示Parent中的LayoutChild列表edges
(LayoutEdges) - 包含盒模型的所有边界距内容的距离值,如inlineStart
,blockEnd
等,这里的inline和block指的是根据writing-mode
值所确定的排列方向。constraints
(LayoutConstraints) - 包含父级元素布局区域的各种尺寸,如fixedInlineSize
,availableBlockSize
等styleMap
(StylePropertyMapReadOnly) - 一个包含自定义属性与样式属性的只读Map,属性在inputProperties
中导入breakToken
- Spec中的说明(实际测试中未发现该参数,也许是笔者姿势不对..)
-
intrinsicSizes()
该函数中会传入三个参数:
children
,edges
,styleMap
。可以通过children
中LayoutChild的intrinsicSizes()
方法获取所有子元素的固有尺寸,在此基础上可以结合其他两种参数计算并返回该current layout中的固有尺寸。intrinsicSizes
对象包含minContentSize
和maxContentSize
两种属性
其他
Demo
GoogleChromeLabs的Houdini API相关示例:
https://googlechromelabs.github.io/houdini-samples/
一些Paint API示例:
https://github.com/gagyibenedek/paint-api-examples
支持情况检测
以Paint API
为例
JS中检测
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('paintWorklet.js');
}
CSS中检测
@supports (background: paint(id)) {
/* ... */
}
总结
介绍完了这些CSS Houdini API后,相信已经对它们有了一个比较系统的了解。期待它们在主流浏览器上的实现,并看看它带来的这些可能性怎么供广大开发者施展拳脚,拭目以待~
参考
- 原文作者:yrq110
- 原文链接:http://yrq110.me/post/front-end/try-houdini-api/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。