擁抱更底層技術——從CSS變數到Houdini

lhyt發表於2019-02-27

0. 前言

平時寫CSS,感覺有很多多餘的程式碼或者不好實現的方法,於是有了前處理器的解決方案,主旨是write less &do more。其實原生css中,用上css變數也不差,加上bem命名規則只要巢狀不深也能和less、sass的巢狀媲美。在一些動畫或者炫酷的特效中,不用js的話可能是用了css動畫、svg的animation、過渡,複雜動畫實現用了js的話可能用了canvas、直接修改style屬性。用js的,然後有沒有想過一個問題:“要是canvas那套放在dom上就爽了”。因為複雜的動畫頻繁操作了dom,違背了倒背如流的“效能優化之一:儘量少操作dom”的規矩,嘴上說著不要,手倒是很誠實地ele.style.prop = <newProp>,可是要實現效果這又是無可奈何或者大大減小工作量的方法。

我們都知道,瀏覽器渲染的流程:解析html和css(parse),樣式計算(style calculate),佈局(layout),繪製(paint),合併(composite),修改了樣式,改的環節越深代價越大。js改變樣式,首先是操作dom,整個渲染流程馬上重新走,可能走到樣式計算到合併環節之間,代價大,效能差。然後痛點就來了,瀏覽器有沒有能直接操作前面這些環節的方法呢而不是依靠js?有沒有方法不用js操作dom改變style或者切換class來改變樣式呢?

於是就有CSS Houdini了,它是W3C和那幾個頂級公司的工程師組成的小組,讓開發者可以通過新api操作CSS引擎,帶來更多的自由度,讓整個渲染流程都可以被開發者控制。上面的問題,不用js就可以實現曾經需要js的效果,而且只在渲染過程中,就已經按照開發者的程式碼渲染出結果,而不是渲染完成了再重新用js強行走一遍流程。

關於houdini最近動態可點選這裡 上次CSS大會知道了有Houdini的存在,那時候只有cssom,layout和paint api。前幾天突然發現,Animation api也有了,不得不說,以後很可能是Houdini遍地開花的時代,現在得進一步瞭解一下了。一句話:這是css in js到js in css的轉變

1. CSS變數

如果你用less、sass只為了人家有變數和巢狀,那用原生css也是差不多的,因為原生css也有變數:

比如定義一個全域性變數--color(css變數雙橫線開頭)

:root {
  --color: #f00;
}
複製程式碼

使用的時候只要var一下

.f{
  color: var(--color);
}
複製程式碼

我們的html:

<div class="f">123</div>
複製程式碼

於是,紅色的123就出來了。

css變數還和js變數一樣,有作用域的:

:root {
  --color: #f00;
}

.f {
  --color: #aaa
} 

.g{
  color: var(--color);
}

.ft {
  color: var(--color);
}
複製程式碼

html:

        <div className="f">
          <div className="ft">123</div>
        </div>
        <div className="">
          <div className="g">123</div>
        </div>
複製程式碼

於是,是什麼效果你應該也很容易就猜出來了:

image

css能搞變數的話,我們就可以做到修改一處牽動多處的變動。比如我們做一個像準星一樣的四個方向用準線鎖定滑鼠位置的效果:

image
用css變數的話,比傳統一個個元素設定style優雅多了:

<div id="shadow">
    <div class="x"></div>
    <div class="y"></div>
    <div class="x_"></div>
    <div class="y_"></div>
  </div>
複製程式碼
  :root{
    --x: 0px;
    --y: 0px;
  }
  body{
    margin: 0
  }
#shadow{
  width: 50%;
  height: 600px;
  border: #000 1px solid;
  position: relative;
  margin: 0;
}

.x, .y, .x_, .y_ {
  position: absolute;
  border: #f00 2px solid;
}

.x {
  top: 0;
  left: var(--x);
  height: 20px;
  width: 0;
}
.y {
  top: var(--y);
  left: 0;
  height: 0;
  width: 20px;
}
.x_ {
  top: 600px;
  left: var(--x);
  height: 20px;
  width: 0;
}
.y_ {
  top: var(--y);
  left: 100%;
  height: 0;
  width: 20px;
}
複製程式碼
const style = document.documentElement.style
shadow.addEventListener('mousemove', e => {
  style.setProperty(`--x`, e.clientX + 'px')
  style.setProperty(`--y`, e.clientY + 'px')
})
複製程式碼

那麼,對於github的404頁面這種內容和滑鼠位置有關的頁面,思路是不是一下子就出來了

2. CSS type OM

都有DOM了,那CSSOM也理所當然存在。我們平時改變css的時候,通常是直接修改style或者切換類,實際上就是操作DOM來間接操作CSSOM,而type om是一種把css的屬性和值存在attributeStyleMap物件中,我們只要直接操作這個物件就可以做到之前的js改變css的操作。另外一個很重要的點,attributeStyleMap存的是css的數值而不是字串,而且支援各種算數以及單位換算,比起操作字串,效能明顯更優。

接下來,基本脫離不了window下的CSS這個屬性。在使用的時候,首先,我們可以採取漸進式的做法: if('CSS' in window){...}

2.1 單位

CSS.px(1); // 1px  返回的結果是:CSSUnitValue {value: 1, unit: "px"}
CSS.number(0); // 0  比如top:0,也經常用到
CSS.rem(2); //2rem
new CSSUnitValue(2, 'percent'); // 還可以用建構函式,這裡的結果就是2%
// 其他單位同理
複製程式碼

2.2 數學運算

自己在控制檯輸入CSSMath,可以看見的提示,就是數學運算

new CSSMathSum(CSS.rem(10), CSS.px(-1)) // calc(10rem - 1px),要用new不然報錯
new CSSMathMax(CSS.px(1),CSS.px(2)) // 顧名思義,就是較大值,單位不同也可以進行比較
複製程式碼

2.3 怎麼用

既然是新的東西,那就有它的使用規則。

  • 獲取值element.attributeStyleMap.get(attributeName),返回一個CSSUnitValue物件
  • 設定值element.attributeStyleMap.set(attributeName, newValue),設定值,傳入的值可以是css值字串或者是CSSUnitValue物件

當然,第一次get是返回null的,因為你都沒有set過。“那我還是要用一下getComputedStyle再set咯,這還不是和之前的差不多嗎?”

實際上,有一個類似的方法:element.computedStyleMap,返回的是CSSUnitValue物件,這就ok了。我們拿前面的第一部分CSS變數的程式碼測試一波

document.querySelector('.x').computedStyleMap().get('height') // CSSUnitValue {value: 20, unit: "px"}
document.querySelector('.x').computedStyleMap().set('height', CSS.px(0)) // 不見了
複製程式碼

3. paint API

paint、animation、layout API都是以worker的形式工作,具體有幾個步驟:

  • 建立一個worker.js,比如我們想用paint API,先在這個js檔案註冊這個模組registerPaint('mypaint', class),這個class是一個類下面具體講到
  • 在html引入CSS.paintWorklet.addModule('worker.js')
  • 在css中使用,background: paint(mypaint) 主要的邏輯,全都寫在傳入registerPaint的class裡面。paint API很像canvas那套,實際上可以當作自己畫一個img。既然是img,那麼在所有的能用到圖片url的地方都適合用paint API,比如我們來自己畫一個有點炫酷的背景(滿屏隨機顏色方塊)。有空的話可以想一下js怎麼做,再對比一下paint API的方案。
    image
// worker.js
class RandomColorPainter {
    // 可以獲取的css屬性,先寫在這裡
    // 我這裡定義寬高和間隔,從css獲取
    static get inputProperties() {
        return ['--w', '--h', '--spacing'];
      }
      /**
       * 繪製函式paint,最主要部分
       * @param {PaintRenderingContext2D} ctx 類似canvas的ctx
       * @param {PaintSize} PaintSize 繪製範圍大小(px) { width, height }
       * @param {StylePropertyMapReadOnly} props 前面inputProperties列舉的屬性,用get獲取
       */
    paint(ctx, PaintSize, props) {
        const w = props.get('--w') && +props.get('--w')[0].trim() || 30;
        const h = props.get('--h') && +props.get('--h')[0].trim() || 30;
        const spacing = +props.get('--spacing')[0].trim() || 10;
        
        for (let x = 0; x < PaintSize.width / w; x++) {
            for (let y = 0; y < PaintSize.height / h; y++) {
                ctx.fillStyle = `#${Math.random().toString(16).slice(2, 8)}`
                ctx.beginPath();
                ctx.rect(x * (w + spacing), y * (h + spacing), w, h);
                ctx.fill();
            }
        }
    }
}

registerPaint('randomcolor', RandomColorPainter);
複製程式碼

接著我們需要引入該worker:

CSS && CSS.paintWorklet.addModule('worker.js');

最後我們在一個class為paint的div應用樣式:

.paint{
  background-image: paint(randomcolor);
  width: 100%;
  height: 600px;
  color: #000;
  --w: 50;
  --h: 50;
  --spacing: 10;
}
複製程式碼

再想想用js+div,是不是要先動態生成n個,然後各種計算各種操作dom,想想就可怕。如果是canvas,這可是canvas背景,你又要在上面放一個div,而且還要定位一波。

注意: worker是沒有window的,所以想搞動畫的就不能內部消化了。不過可以靠外面的css變數,我們用js操作css變數可以解決,也比傳統的方法優雅

可以來我的githubio看看效果

4. 自定義屬性

支援情況 點選這裡檢視 首先,看一下支援度,目前瀏覽器並沒有完全穩定使用,所以需要跟著它的提示:Experimental Web Platform features” on chrome://flags,在chrome位址列輸入chrome://flags再找到Experimental Web Platform features並開啟。

CSS.registerProperty({
    name: '--myprop', //屬性名字
    syntax: '<length>', // 什麼型別的單位,這裡是長度
    initialValue: '1px', // 預設值
    inherits: true // 會不會繼承,true為繼承父元素
  }); 
複製程式碼

說到繼承,我們回到前面的css變數,已經說了變數是區分作用域的,其實父作用域定義變數,子元素使用該變數實際上是繼承的作用。如果inherits: true那就是我們看見的作用域的效果,如果不是true則不會被父作用域影響,而且取預設值。

這個自定義屬性,精闢在於,可以用永久迴圈的animation驅動一次性的transform。換句話說,我們如果用了css變數+transform,可以靠js改變這個變數達到花俏的效果。但是,現在不需要js,只要css內部消化,transform成為永動機。

// 我們先註冊幾種屬性
  ['x1','y1','z1','x2','y2','z2'].forEach(p => {
      CSS.registerProperty({
        name: `--${p}`,
        syntax: '<angle>',
        inherits: false,
        initialValue: '0deg'
      });
    });
複製程式碼

然後寫個樣式

#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;
  }
}
複製程式碼

html就兩個div:

  <div id="myprop"></div>
  <div id="myprop1"></div>
複製程式碼

效果是什麼呢,自己可以跑一遍看看,反正功能很強大,但是想象力限制了發揮。

自己動手改的時候注意一下,動畫關鍵幀裡面,不能只存在1,那樣子就不能驅動transform了,做不到永動機的效果,因為我的rotate寫的是 rotateX(var(--x2))。接下來隨意發揮吧

最後

再囉嗦一次

ENJOY YOURSELF!!!

相關文章