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 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
- 其它
通過它們的命名就可以看出這些不同的子類分別用于表示哪種型別的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的程序簡述如下:
- 使用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 */
<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的創建和使用流程,分為三個步驟,簡述如下:
- 使用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:
用于控制影片執行的數值,對應于步驟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一定會慢慢走進開發者的世界,大家可以期待并做好準備迎接它,
參考文獻:
- 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/498884.html
標籤:JavaScript
