关于css:CSS-Houdini用浏览器引擎实现高级CSS效果

8次阅读

共计 11939 个字符,预计需要花费 30 分钟才能阅读完成。

vivo 互联网前端团队 -Wei Xing

Houdini 被称之为 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

这些 APIs 的反对状况在不断更新中,能够看到以后最新的一次更新工夫是在 2021 年 5 月份,还是比拟沉闷的。(注:图片来源于 Is Houdini ready yet?)

比照下图 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 ModelTypeScript 的命名相似,都减少了 Type 这个前缀,如果你应用过 TypeScript 就会理解到,TypeScript 加强了类型查看,让代码更稳固也更易保护,Typed Object Model也是如此。

相比于下面艰涩的传统办法,Typed Object Mode l 将 CSS 属性值包装为Typed JavaScript Object,让每个属性值都有本人的类型,简化了 CSS 属性的操作,并且带来了性能上的晋升。通过 JavaScript 对象来形容 CSS 值比字符串具备更好的可读性和可维护性,通常也更快,因为能够间接操作值,而后廉价地将其转换回底层值,而无需构建和解析 CSS 字符串。

Typed Object Model 中 CSSStyleValue 是所有 CSS 属性值的基类,在它之下的子类用于形容各种 CSS 属性值,例如:

  • CSSUnitValue
  • CSSImageValue
  • CSSKeywordValue
  • CSSMathValue
  • CSSNumericValue
  • CSSPositionValue
  • CSSTransformValue
  • CSSUnparsedValue
  • 其它

通过它们的命名就能够看出这些不同的子类别离用于示意哪种类型的 CSS 属性值,以 CSSUnitValue 为例,它能够用于示意带有单位的 CSS 属性值,例如 font-size、width、height,它的构造很简略,由 value 和 unit 组成。

{
  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
<div id="container">container</div>
 
// 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
<div id="container">container</div>
 
// JavaScript
CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>',
  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。

在将来,开发者不仅能够在 JavaScript 中显式申明 CSS 变量,也能够间接在 CSS 中间接申明:

@property --my-color{
  syntax: '<color>',
  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

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(处于草案阶段,暂不介绍)

Worklets 提供了惟一的办法 Worklet.addModule(),这个办法用于向 Worklet 增加执行模块,具体的应用办法,咱们在后续的 Paint API、Layout API、Animation API 中介绍。

九、Paint API

Paint API 容许开发者通过 Canvas 2d 的办法来绘制元素的背景、边框、内容等图形,这在原始的 CSS 规定中是无奈做到的。

Paint API 须要联合上述提到的 PaintWorklet 一起应用,简略来说就是开发者构建一个 PaintWorklet,再将它传入 Paint API 就能够绘制相应的 Canvas 图形。如果你相熟 Canvas,那 Paint API 对你来说也不会生疏。

应用 Paint API 的过程简述如下:

  1. 应用 registerPaint()办法创立一个 PaintWorklet。
  2. 将它增加到 Worklet 模块中,CSS.paintWorklet.addModule()。
  3. 在 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 */
<script>
    CSS.paintWorklet.addModule('path/to/checkboardWorklet.js')  // 增加 checkboardWorklet 到 paintWorklet
</script>
/* index.html */
<!doctype html>
<textarea></textarea>
<style>
  textarea {background-image: paint(checkerboard);  // 应用 paint()办法调用 checkboard 绘制背景}
</style>

通过上述三个步骤,最终生成的 textarea 背景成果如图所示:

感兴趣的同学能够拜访 houdini-samples 查看更多官网样例。

十、Animation API

在过来,当咱们想要对 DOM 元素执行动画时,通常只有两个抉择:CSS Transitions 和 CSS Animations。这两者在应用上尽管简略,也能满足大部分的动画需要,然而它们有两个独特的 毛病

  • 仅仅依赖工夫来执行动画(time-driven):动画的执行仅和工夫无关。
  • 无状态(stateless):开发者无奈干涉动画的执行过程,获取不到动画执行的中间状态。

然而在一些场景下,咱们想要开发一个非工夫驱动的动画或者想要管制动画的执行状态,就很难做到。比方 视差滚动(Parallax Scrolling),它是依据滚动的状况来执行动画的,并且每个元素依据滚动状况作出不统一的动画成果,上面是个简略的视差滚动成果示例,在通常状况下要实现更加简单的视差滚动成果(例如 beckett 页面的成果)是比拟艰难的。

Animation API却能够帮忙咱们轻松做到。

在性能方面,它是 CSS Transitions 和 CSS Animations 的扩大,它容许用户干涉动画执行的过程,例如联合用户的 scroll、hover、click 事件来管制动画执行,像是为动画减少了进度条,通过进度条管制动画过程,从而实现一些更加简单的动画场景。

在性能方面,它依赖于 AnimationWorklet,运行在独自的 Worklet 线程,因而具备更高的动画帧率和晦涩度,这在低端机型中尤为显著(当然,通常低端机型中的浏览器内核还不反对该个性,这里只是阐明 Animation API 对动画的视觉体验优化是很敌对的)。

Animation API 的应用和 Paint API 一样,也同样遵循 Worklet 的创立和应用流程,分为三个步骤,简述如下:

  1. 应用 registerAnimator()办法创立一个 AnimationWorklet。
  2. 将它增加到 Worklet 模块中,CSS.animationWorklet.addModule()。
  3. 应用 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:

用于管制动画执行的数值,对应于步骤 3 例子中传入的 document.timeline 参数,通常依据它的数值来动静批改另一个参数 effect,从而影响动画执行。例如咱们能够传入 document.timeline 或者传入 element.scrollTop 作为这个动静数值,传入前者表明咱们只是想用工夫变动来管制动画的执行,传入后者表明咱们想通过滚动间隔来管制动画执行。

document.timeline 是每个页面被关上后从 0 开始递增的工夫数值,能够简略了解为页面被关上的时长,初始时 document.timeline === 0,随着工夫一直递增。

  • effect:

对应于步骤 3 中传入的 new KeyframeEffect(),可通过批改它来影响动画执行。一个很常见的做法是,通过批改 effect.localTime 管制动画的执行,effect.localTime 的作用相当于管制动画播放的进度条,批改它的数值就相当于拖动动画播放的进度。

如果不批改 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 肯定会缓缓走进开发者的世界,大家能够期待并做好筹备迎接它。

参考文献:

  1. W3C Houdini Specification Drafts
  2. State of Houdini (Chrome Dev Summit 2018)
  3. Houdini’s Animation Worklet – Google Developers
  4. Interactive Introduction to CSS Houdini
  5. CSS Houdini Experiments
  6. Interactive Introduction to CSS Houdini
  7. Houdini Samples by Google Chrome Labs
正文完
 0