CSS Houdini,JSinCSS的魔术师

Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。 Houdini是一组API,它们使开发人员可以直接访问CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。

背景

前端开发者需要更多的CSS控制“权限”

webkitflow.png

浏览器在网页渲染的过程中,通常都是html/css parse -> style calculate -> layout > paint > composite这样一个过程。javascript 与 style 两个阶段会解析 HTML 并为加载的 JS 与 CSS 建立 Render Tree,也就是所谓的 DOM 与 CSSOM。

目前前端开发者们能操作的就是通过js操作DOM与CSSDOM影响页面的变化,但是对于接下來的 Layout、Paint 与 Composite 就几乎沒有控制权了。由此导致的一个显著的问题就是js改变样式(即操作DOM),整个网页渲染流程将重新走一遍,性能差。

由此前端开发者抛出了问题:浏览器有没有能直接操作前面这些环节的方法呢?有没有方法不用js操作DOM改变style或者切换class来改变样式呢?

CSS新特性的痛点

js的新特性,往往能通过babel或其他途径实现polyfill,从而快速将新特性应用到生产环境中。不同于js,而CSS并非js这样的动态语言,我们无法简单的提供polyfills,顶多利用postCSS、Less/Sass/Stylus 等工具转译出浏览器能接受的 CSS,是否能兼容就是浏览器的事了,然而现实是各家浏览器的版本、更新进度很是缓慢。

由此前端开发者又抛出了问题:能否让开发人员有能力创建他们自己自定义的CSS特性,并让这些特性在浏览器的原生渲染管道中高效运行。这样就可以实现CSS的polyfill,让这些新特性在浏览器的原生渲染管道中高效运行。

这些痛点就是CSS Houdini诞生的原因。

介绍

Houdini 是美国的伟大魔术师,擅长逃脱术。

CSS Houdini 是由一群来自W3C, Mozilla, Apple, Opera, Microsoft, HP, Intel, IBM, Adobe 与 Google 的工程师所组成的工作小组,志在建立一系列的 API,让开发者能够介入浏览器的 CSS engine 操作,带给开发者更多的解決方案,用来解決 CSS 长久以来的问题:

  • Cross-Browser isse
  • CSS Polyfill

特性和优势

与使用JavaScript.style进行DOM样式更改相比,Houdini有着更快的解析时间。浏览器在应用在脚本中找到的任何样式更新之前解析CSSOM(包括布局、绘制和组合进程)。此外,布局、绘制和合成过程会重复进行JavaScript样式的更新。Houdini代码不会等待第一个渲染周期完成。相反,它包含在第一个循环中——创建可渲染、可理解的样式。Houdini提供了一个基于对象的API,用于在JavaScript中处理CSS值。

上手

Houdini的CSS-Typed OM是一个CSS对象模型,包含类型和方法,将值作为JavaScript对象公开,使得CSS操作比以前基于字符串的HTMLElement.style操作更加直观。每个元素和样式表规则都有一个可通过其样式属性映射访问的样式映射。

CSS Houdini的一个特性是工作集。使用工作集,您可以创建模块化CSS,需要一行JavaScript来导入可配置的组件:不需要前处理器、后处理器或JavaScript框架。

这个添加的模块包含registerPaint()函数,这些函数注册完全可配置的工作集。

CSS paint()函数参数包括工作集的名称以及可选参数。工作集还可以访问元素的自定义属性:它们不需要作为函数参数传递。

如:

1
2
3
4
5
6
7
8
9
10
<script> 
CSS.paintWorklet.addModule('csscomponent.js');
</script>
<style>
li {
background-image: paint(myComponent, stroke, 10px);
--hilights: blue;
--lowlights: green;
}
</style>

API

可访问文档>>

CSS Parser API

一种更直接地公开CSS解析器的API,用于将任意类CSS语言解析为一种轻微类型化的表示。

(目前尚未为此API编写指南或引用,可以看https://drafts.css-houdini.org/css-parser-api/。)

CSS Properties and Values API

定义用于注册新CSS属性的API。使用此API注册的属性提供了定义类型、继承行为和初始值的解析语法。关于CSS3变量的详细情况可见另一篇笔记《【笔记】使用CSS变量》

使用如:

1
2
3
4
5
6
7
$font-size: 10px;
$brightBlue: blue;

.m-mark {
font-size: 1.5 * $font-size;
color: $brightBlue
}

相比于预处理工具的变量,CSS变量有着优势。比如动态调整:

1
2
3
4
5
6
7
8
.u-txt { 
--box-shadow-color: yellow;
box-shadow: 0 0 30px var(--box-shadow-color);
}
.u-txt:hover {
/* 实现了box-shadow: 0 0 30px red; */
--box-shadow-color: red;
}

或者是js控制:

1
2
3
const textBox = document.querySelector('.u-txt'); // GET 
const Bxshc = getComputedStyle(textBox).getPropertyValue('--box-shadow-color'); // SET
textBox.style.setProperty('--box-shadow-color', 'new color');

兼容情况

变量移动兼容情况还可以,PC除IE外兼容很好。

p-value.png

*注册属性

目前没有完全稳定的兼容使用

使用如:

1
2
3
4
5
6
CSS.registerProperty({
name: '--myprop', //属性名字
syntax: '<length>', // 什么类型的单位,这里是长度
initialValue: '1px', // 默认值
inherits: true // 会不会继承,true为继承父元素
});

如果inherits: true那就是我们看见的作用域的效果,如果不是true则不会被父作用域影响,而且取默认值。

这个自定义属性,精辟在于,可以用永久循环的animation驱动一次性的transform。换句话说,我们如果用了css变量+transform,可以靠js改变这个变量达到花俏的效果。但是,现在不需要js,只要css内部消化,transform成为永动机。

1
2
3
4
5
6
7
8
9
// 我们先注册几种属性
['x1','y1','z1','x2','y2','z2'].forEach(p => {
CSS.registerProperty({
name: `--${p}`,
syntax: '<angle>',
inherits: false,
initialValue: '0deg'
});
});

样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<style>
#myprop, #myprop1 {
width: 200px;
border: 2px dashed #000;
border-bottom: 10px solid #000;
animation:myprop 3000ms alternate infinite ease-in-out;
transform:
rotateX(var(--x2))
rotateY(var(--y2))
rotateZ(var(--z2))
}
@keyframes myprop {
25% {
--x1: 20deg;
--y1: 30deg;
--z1: 40deg;
}
50% {
--x1: -20deg;
--z1: -40deg;
--y1: -30deg;
}
75% {
--x2: -200deg;
--y2: 130deg;
--z2: -350deg;
}
100% {
--x1: -200deg;
--y1: 130deg;
--z1: -350deg;
}
}

@keyframes myprop1 {
25% {
--x1: 20deg;
--y1: 30deg;
--z1: 40deg;
}
50% {
--x2: -200deg;
--y2: 130deg;
--z2: -350deg;
}
75% {
--x1: -20deg;
--z1: -40deg;
--y1: -30deg;
}
100% {
--x1: -200deg;
--y1: 130deg;
--z1: -350deg;
}
}
</style>

<div id="myprop"></div>
<div id="myprop1"></div>

CSS Typed OM

type om是一种把css的属性和值存在attributeStyleMap对象中,我们只要直接操作这个对象就可以做到之前的js改变css的操作,会导致显著的性能开销。CSS类型的OM将CSS值公开为类型化的JavaScript对象,以允许对其进行性能操作。另外一个很重要的点,attributeStyleMap存的是css的数值而不是字符串,而且支持各种算数以及单位换算,比起操作字符串,性能明显更优。

像是所有的 CSS Values 都有一个 base class interface:

最主要的功能在于将 CSSOM 所使用的字串值转换成具有型別意义的 JavaScript 表示形态,像是所有的 CSS Values 都有一个 base class interface:

1
2
3
4
5
interface CSSStyleValue { 
stringifier;
static CSSStyleValue? parse(DOMString property, DOMString cssText);
static sequence<CSSStyleValue>? parseAll(DOMString property, DOMString cssText);
};

你可以如下操作 CSS style:

1
2
3
4
5
6
7
8
9
10
11
// CSS -> JS 
const map = document.querySelector('.example').styleMap;
console.log( map.get('font-size') ); // CSSSimpleLength {value: 12, type: "px", cssText: "12px"}

// JS -> JS
console.log( new CSSUnitValue(5, "px") ); // CSSUnitValue{value:5,unit:"px",type:"length",cssText:"5px"}

// JS -> CSS
// set style "transform: translate3d(0px, -72.0588%, 0px);"

elem.outputStyleMap.set('transform', new CSSTransformValue([ new CSSTranslation( 0, new CSSSimpleLength(100 - currentPercent, '%'), 0 )]));

计算单位:

1
2
3
4
5
6
if ('CSS' in window) {
CSS.px(1); // 1px 返回的结果是:CSSUnitValue {value: 1, unit: "px"}
CSS.number(0); // 0 比如top:0,也经常用到
CSS.rem(2); //2rem
new CSSUnitValue(2, 'percent'); // 还可以用构造函数,这里的结果就是2%
}

数学运算:

1
2
new CSSMathSum(CSS.rem(10), CSS.px(-1)) // calc(10rem - 1px),要用new不然报错
new CSSMathMax(CSS.px(1), CSS.px(2)) // 就是较大值,单位不同也可以进行比较

CSS Layout API

这个API旨在提高CSS的可扩展性,它使开发人员能够编写自己的布局算法,比如masonry或line snapping。它还不是本地可用的。顾名思义就是提供开发者撰写自己的 Layout module,Layout module 也就是用来 assign 给 display 属性的值,像是 display: grid 或 display: flex。 你只要透过 registerLayout 的 function,传入 Layout 名称与 JS class 来定义 Layout 的逻辑即可,例如我们实战一个 block-like 的 Layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
registerLayout('block-like', class extends Layout { 
static blockifyChildren = true;
static inputProperties = super.inputProperties;

*layout(space, children, styleMap) {
const inlineSize = resolveInlineSize(space, styleMap);
const bordersAndPadding = resolveBordersAndPadding(constraintSpace, styleMap);
const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap);
const availableInlineSize = inlineSize - bordersAndPadding.inlineStart - bordersAndPadding.inlineEnd - scrollbarSize.inline;
const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) - bordersAndPadding.blockStart - bordersAndPadding.blockEnd - scrollbarSize.block;
const childFragments = [];
const childConstraintSpace = new ConstraintSpace({ inlineSize: availableInlineSize, blockSize: availableBlockSize, });
let maxChildInlineSize = 0;
let blockOffset = bordersAndPadding.blockStart;
for (let child of children) {
const fragment = yield child.layoutNextFragment(childConstraintSpace); // 这段控制 Layout 下的 children 要 inline 排列 // fragment 就是前述的 Box Tree API 內提到的 fragment
fragment.blockOffset = blockOffset;
fragment.inlineOffset = Math.max( bordersAndPadding.inlineStart, (availableInlineSize - fragment.inlineSize) / 2);
maxChildInlineSize = Math.max(maxChildInlineSize, childFragments.inlineSize);
blockOffset += fragment.blockSize;
}
const inlineOverflowSize = maxChildInlineSize + bordersAndPadding.inlineEnd;
const blockOverflowSize = blockOffset + bordersAndPadding.blockEnd;
const blockSize = resolveBlockSize( constraintSpace, styleMap, blockOverflowSize);
return { inlineSize: inlineSize, blockSize: blockSize, inlineOverflowSize: inlineOverflowSize, blockOverflowSize: blockOverflowSize, childFragments: childFragments, };
}
});

使用如:

1
2
3
.wrapper { 
display: layout('block-like');
}

(目前尚未为此API编写指南或引用。)

CSS Painting API

开发用于提高CSS的可扩展性-允许开发人员编写JavaScript函数,这些函数可以通过paint() CSS函数直接绘制到元素的背景、边框或内容中。

比如定义 Paint Method:

1
2
3
4
5
6
7
8
9
10
registerPaint('simpleRect', class { 
static get inputProperties() {
return ['--rect-color'];
}
paint(ctx, size, properties) { // 根据 properties 改变颜色
const color = properties.get('--rect-color');
ctx.fillStyle = color.cssText;
ctx.fillRect(0, 0, size.width, size.height);
}
});

使用如:

1
2
3
4
5
6
7
8
9
10
11
12
13
.demo-1 { 
--rect-color: red;
width: 50px;
height: 50px;
background-image: paint(simpleRect);
}
.demo-2 {
--rect-color: yellow;
width: 100px;
height: 100px;
background-size: 50% 50%;
background-image: paint(simpleRect);
}

如此.demo-1 与 .demo-2 就可以拥有各自定义宽高颜色的方形 background-image。

兼容情况

除了新版Chrome和较新的Android,其他没啥兼容的。

p-paint.png

Worklets

用于在呈现管道的不同阶段运行脚本的API,独立于主JavaScript执行环境。工作集在概念上类似于Web工作者,由呈现引擎调用并扩展。在上述的 Layout API 与 Paint API 中,我们都有写一个 JS文件,用来定义新的属性,然后在 CSS 文件中取用,你可能会觉得那个 JS 文件就直接像一般 Web 嵌入 JS 的方式一样即可, 但实际上并非如此,我们需要通过 Worklets 來帮我们载入。以上面的 Painting API 为例:

1
2
3
4
5
6
7
8
9
10
11
12
// add a Worklet 
paintWorklet.addModule('simpleRect.js'); // WORKLET "simpleRect.js"
registerPaint('simpleRect', class {
static get inputProperties() {
return ['--rect-color'];
}
paint(ctx, size, properties) { // 根据 properties 改变顏色
const color = properties.get('--rect-color');
ctx.fillStyle = color.cssText;
ctx.fillRect(0, 0, size.width, size.height);
}
});

Worklets 的特点就是轻量以及生命周期较短

总体兼容

p-total.png

使用

封装库

三方封装库extra.css

使用

使用它有两个步骤:

  • HTML包括工作集和自定义属性文件<script src='<packageName></script>
  • CSS使用后台访问paint工作集:paint(<workletName>)

URL的格式如下:https://unpkg.com/extra.css/.js单击上面的链接可获取任何正确的URL以获取CDN链接。如果您转到这个链接您的URL栏,您将自动链接到JS包的最新版本号,其中包括所有注册的自定义属性和工作集。这是指向最新版本(即https://unpkg.com/extra.css@1.1.0/.js)的链接,但如果您始终想要evergreen最新版本,则可以跳过版本号。“演示”选项卡将打开一个解释性的代码笔项目,所有内容都已正确连接。

下面是一个示例,说明跨路径的HTML和CSS的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<h1>Hello<br/> World</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit <span>asperiores</span> cupiditate dolor quo iste animi, eveniet in inventore <span>obcaecati</span> quisquam architecto pariatur et perspiciatis, deleniti voluptatum consequuntur <span>voluptas</span> voluptatibus repellat!</p>

<!-- This is where we include the worklet -->
<script src='https://unpkg.com/extra.css/crossOut.js'</script>

<style>
// This is where we use the custom properties

@supports (background: paint(something)) {
h1 {
/*
Optionally set property values.
This can be done at root or local scope
*/
--extra-crossColor: #fc0; /* default: black */
--extra-crossWidth: 3; /* default: 1 */

background: paint(extra-crossOut);
line-height: 1.5;
}

h2 {
margin: 10rem auto;
display: block;
width: 80vw;
max-width: 900px;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
}

span {
--extra-crossColor: #d4f;/* default: black */

background: paint(extra-crossOut);
}
}

// Additional Styling

body {
font-family: monospace;
text-align: center;
padding: 1rem;
margin: 0;
}

p {
max-width: 660px;
margin: 0 auto;
font-weight: 300;
font-size: 1.2rem;
line-height: 1.5;
text-align: left;
}

h1 {
font-size: 4rem;
display: inline-block;
font-weight: 100;
margin: 1rem auto;
width: 100%;
max-width: 760px;
position: relative;
}

h2 {
font-size: 2.5rem;
font-weight: 100;
display: inline-block;
position: relative;
}
</style>

Demo

有个很好的库:https://github.com/GoogleChromeLabs/houdini-samples


相关链接