前言
今天想繼續(xù) CSS 的議題,常常會(huì)覺得學(xué) CSS 的新技術(shù)不太劃算,因?yàn)槊看慰吹叫碌?Feature 出現(xiàn),都只能當(dāng)下興奮幾分鐘,然后就會(huì)認(rèn)命接受可能還要再等個(gè)五年才能真正使用的可能性…如果你有跟我一樣的感受,那今天這篇文章或許可以帶給你一絲絲希望。
在現(xiàn)今的 Web 開發(fā)中,JavaScript 幾乎占據(jù)所有版面,除了控制頁面邏輯與操作 DOM 對(duì)象以外,連 CSS 都直接寫在 JavaScript 里面了,就算瀏覽器都還沒有實(shí)現(xiàn)的特性,總會(huì)有人做出對(duì)應(yīng)的 Polyfills,讓你快速的將新 Feature 應(yīng)用到 Production 環(huán)境中,更別提我們還有 Babel 等工具幫忙轉(zhuǎn)譯。
而 CSS 就不同了,除了制定 CSS 標(biāo)準(zhǔn)規(guī)范所需的時(shí)間外,各家瀏覽器的版本、實(shí)戰(zhàn)進(jìn)度差異更是曠日持久,再加上 CSS 并非 Javascript 這樣的動(dòng)態(tài)語言,我們無法簡(jiǎn)單的提供 Polyfills,頂多利用 PostCSS、Sass 等工具來幫我們轉(zhuǎn)譯出瀏覽器能接受的 CSS,而剩下的就是瀏覽器的事了。
這邊讓我們回想一下,瀏覽器在網(wǎng)頁的渲染過程中,做了哪些事情?
瀏覽器的 Render Pipeline 中,JavaScript 與 Style 兩個(gè)階段會(huì)解析 HTML 并為加載的 JS 與 CSS 建立 Render Tree,也就是所謂的 DOM 與 CSSOM:(對(duì)于 Render Pipeline 與 Render Tree 若不了解,可以先看看我先前的文章 Front-end kata 60fps的快感)
而就現(xiàn)階段的 Web 技術(shù)來看,開發(fā)者們能操作的就是通過 JS 去控制 DOM 與 CSSOM,來來影響頁面的變化,但是對(duì)于接下來的 Layout、Paint 與 Composite 就幾乎沒有控制權(quán)了。
既無法讓各家瀏覽器快速并統(tǒng)一實(shí)戰(zhàn)規(guī)格,亦不能輕易產(chǎn)生 Polyfills,所以到現(xiàn)在我們依然無法大膽使用 Flexbox,即便它早在 2009 年就被提出了…
但 CSS 并非就此駐足不前。
為了解決上述問題,為了讓 CSS 的魔力不再瀏覽器把持,Houdini 就誕生了!( Houdini 是美國(guó)的偉大魔術(shù)師,擅長(zhǎng)逃脫術(shù),很適合想將 CSS 從瀏覽器中解放的概念)
CSS Houdini
CSS Houdini 是由一群來自 Mozilla, Apple, Opera, Microsoft, HP, Intel, IBM, Adobe 與 Google 的工程師所組成的工作小組,志在建立一系列的 API,讓開發(fā)者能夠介入瀏覽器的 CSS engine 操作,帶給開發(fā)者更多的解決方案,用來解決 CSS 長(zhǎng)久以來的問題:
Cross-Browser isse
CSS Polyfill 的製作困難
Houdini task force 目前起草了一些 API 規(guī)范,并逐步努力讓其通過 W3C,成為真正的 Web standards。由于都是草稿階段,有些甚至只有規(guī)畫,還未被真正寫入規(guī)范,所以變動(dòng)很大,有些我也不是很了解,所以就大致介紹一下,若有錯(cuò)誤拜托務(wù)必告知!
另外,有興趣的讀者可以直接從這里 CSS Houdini Drafts 看詳細(xì)內(nèi)容( Drafts 的更新時(shí)間都非常近期,活躍中的草稿?。?。
下面這張圖我將 Google 提供的 Render pipeline 與 Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of 中提到的 pipeline 做個(gè)結(jié)合對(duì)比,顯示出 Houdini 試圖在瀏覽器的 Render pipeline 中提供哪些 API 給開發(fā)者使用:
其中灰色部分就是只在規(guī)劃階段,而黃色部份就是已經(jīng)寫入規(guī)范正在推行中。
Houdini API 介紹
CSS Properties and Values API
先介紹一個(gè)最能夠使用的 API,除了 IE family 以外,Chrome、Firefox 與 Safari 都已經(jīng)能夠直接使用了! caniuse
相信很多人都使用過 CSS Preprocessors,他給予開發(fā)者在 CSS 中使用變量的能力:
$font-size: 10px;$brightBlue: blue;.mark{ font-size: 1.5 * $font-size; color: $brightBlue}
但其實(shí)使用 Preprocessors 還是有其缺點(diǎn),像是不同的 Preprocessors 就有不同的 Syntax,需要額外 setup 與 compile,而現(xiàn)在 CSS 已經(jīng)有原生的變量可以使用了!就是 CSS Properties and Values API!
SCSS 與 Native CSS Custom Properties 的一個(gè)主要差別可以看下圖:
原生的 CSS variable syntax:
/* declaration */--VAR_NAME: declaration-value>;/* usage */var(--VAR_NAME)
變量可以定義在 root element selector 內(nèi),也能在一般 selector 內(nèi),甚至是給別的變量 reuse:
/* root element selector (全域) */:root { --main-color: #ff00ff; --main-bg: rgb(200, 255, 255); --block-font-size: 1rem;}.btn__active::after{ --btn-text: 'This is btn'; /* 相等於 --box-highlight-text:'This is btn been actived'; */ --btn-highlight-text: var(--btn-text)' been actived'; content: var(--btn-highlight-text); /* 也能使用 calc 來做運(yùn)算 */ font-size: calc(var(--block-font-size)*1.5);}body { /* variable usage */ color: var(--main-color);}
而有了變量以后,會(huì)為 CSS 帶來什么好處應(yīng)該很明顯,他的 Use case 可以多寫一篇文章來介紹了,或是可以直接看這篇的詳細(xì)介紹,我這邊介紹幾個(gè)我覺得比較有趣的:
模擬一個(gè)特殊的 CSS rule:
單純透過更改變量來達(dá)到改變 box-shadow 顏色
.textBox { --box-shadow-color: yellow; box-shadow: 0 0 30px var(--box-shadow-color);}.textBox:hover { /* box-shadow: 0 0 30px green; */ --box-shadow-color: green;}
動(dòng)態(tài)調(diào)整某個(gè) CSS rule 內(nèi)的各別屬性:
此外,我們也可以用 JavaScript 來控制:
const textBox = document.querySelector('.textBox');// GETconst Bxshc = getComputedStyle(textBox).getPropertyValue('--box-shadow-color');// SETtextBox.style.setProperty('--box-shadow-color', 'new color');
非常好用的特性,幾乎所有主流瀏覽器都已經(jīng)支持了,大家快來使用吧!
Box Tree API
Box tree API 并沒有出現(xiàn)在上圖中,但在 Paintin API 中會(huì)用到其概念。
大家都知道在 DOM tree 中的每個(gè)元素都有一個(gè) Box Modal,而在瀏覽器解析過程中,還會(huì)將其拆分成 fragments,至于什么是 fragments?以 drafts 中的例子來解釋:
上面的 HTML 總共就會(huì)拆出七個(gè) fragments:
最外層的 div
第一行的 box (包含 foo bar)
第二行的 box (包含 baz)
吃到 ::first-line 與 ::first-letter 的 f 也會(huì)被拆出來成獨(dú)立的 fragments
只吃到 ::first-line 的 oo 只好也獨(dú)立出來
吃到 ::first-line 與 包在 內(nèi)的 bar 當(dāng)然也是
在第二行底下且為 italic 的 baz
而 Box tree API 目的就是希望讓開發(fā)者能夠取得這些 fragments 的信息,至于取得后要如何使用,基本上應(yīng)該會(huì)跟后面介紹的 Parser API、Layout API 與 Paint API 有關(guān)聯(lián),當(dāng)我們能取得詳細(xì)的 Box Modal 信息時(shí),要客制化 Layout Module 才更為方便。
CSS Layout API
Layout API 顧名思義就是提供開發(fā)者撰寫自己的 Layout module,Layout module 也就是用來 assign 給 display 屬性的值,像是 display: grid 或 display: flex。你只要透過 registerLayout 的 function,傳入 Layout 名稱與 JS class 來定義 Layout 的邏輯即可,例如我們實(shí)戰(zhàn)一個(gè) block-like 的 Layout:
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 應(yīng)該就是前述的 Box Tree API 內(nèi)提到的 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, }; }});
上面這段代碼是來自 Houdini Draft 的示例,完整放上來是想給大家看一下實(shí)戰(zhàn)一個(gè) Layout 需要注意的細(xì)節(jié)有多少,其實(shí)并不是如想像中的輕松,相信未來會(huì)出現(xiàn)更多方便的 API 輔助開發(fā)。(放心接下來不會(huì)再有這么多 code 了 XD)
有了 Layout API,不管是自己實(shí)戰(zhàn)或是拿別人寫好的 Layout,你都可以直接如下方式使用:
.wrapper { display: layout('block-like');}
CSS Painting API
Painting API 與 Layout 類似,提供一個(gè)叫做 registerPaint 的方法:
定義 Paint Method,這邊偷偷用到了待會(huì)要介紹的 CSS Properties:
registerPaint('simpleRect', class { static get inputProperties() { return ['--rect-color']; } paint(ctx, size, properties) { // 依據(jù) properties 改變顏色 const color = properties.get('--rect-color'); ctx.fillStyle = color.cssText; ctx.fillRect(0, 0, size.width, size.height); }});
宣告使用:
.div-1 { --rect-color: red; width: 50px; height: 50px; background-image: paint(simpleRect);}.div-2 { --rect-color: yellow; width: 100px; height: 100px; background-size: 50% 50%; background-image: paint(simpleRect);}
.div-1 與 .div-2 就可以擁有各自定義寬高顏色的方形 background-image。
Worklets
在上述的 Layout API 與 Paint API 中,我們都有撰寫一個(gè) JS文件,用來定義新的屬性,然后在 CSS 文件中呼叫取用,你可能會(huì)覺得那個(gè) JS 文件就直接像一般 Web 嵌入 JS 的方式一樣即可,但實(shí)際上并非如此,我們需要通過 Worklets 來幫我們載入。以上面的 Paint API 為例:
// add a WorkletpaintWorklet.addModule('simpleRect.js');// WORKLET 'simpleRect.js'registerPaint('simpleRect', class { static get inputProperties() { return ['--rect-color']; } paint(ctx, size, properties) { // 依據(jù) properties 改變顏色 const color = properties.get('--rect-color'); ctx.fillStyle = color.cssText; ctx.fillRect(0, 0, size.width, size.height); }});
同理,Layout API 則是 layoutWorklet.addModule('blockLike.js')。
Worklets 光名字就有點(diǎn)像 Web Worker 了,都是獨(dú)立于主要執(zhí)行者之外,并且不直接與 DOM 互動(dòng)。你可能會(huì)想那為何還需要有一個(gè) Worklets?
因?yàn)?Houdini 是希望將開發(fā)者的程式碼 hook 到 CSS engine 中運(yùn)作,而根據(jù)規(guī)范內(nèi)的敘述,web worker 相對(duì)笨重,不適合用來處理 CSS engine 這種可能會(huì)牽扯到數(shù)百萬像素圖片的工作。
所以可以推斷,Worklets 的特點(diǎn)就是輕量以及生命周期較短。
共實(shí)除了 Layout Worklets 與 Paint Worklets 外,還有所謂的 Animation Worklet,雖然還沒有放入規(guī)范,但已經(jīng)有在著手進(jìn)行中,也有 Polyfills 了,Chrome 的 Sticky Header 就是采用 Houdini 的 Animation Worklet。Twitter 的 Header Effect 也是采用 Animation WorkletAnimation Worklet 是想介入 Render Pipeline 中的 Composite 步驟,也就是原本利用 JS 與 CSS 控制動(dòng)畫時(shí),瀏覽器會(huì)重新執(zhí)行的部分。
關(guān)于 Animation Worklet 的詳細(xì)操作介紹可以看這份PPT: houdini-codemotion
CSS Parser API
Parser API 目前還是處在 Unofficial draft,但我相信如果這個(gè) API 確認(rèn)的話,對(duì)前端開發(fā)有絕對(duì)的幫助,她的概念是想讓開發(fā)者能擴(kuò)充瀏覽器解析 HTML、CSS 的功能,也就是說,你可以想辦法讓他看得懂最新定義的 pseudo-classes 或甚至是 element-queries 等等,這樣就能正確解析出 CSSOM,從此不用再等瀏覽器更新。
CSS Typed OM
CSS Typed OM 就是 CSSOM 的強(qiáng)化版,最主要的功能在于將 CSSOM 所使用的字串值轉(zhuǎn)換成具有型別意義的 JavaScript 表示形態(tài),像是所有的 CSS Values 都有一個(gè) base class interface:
interface CSSStyleValue { stringifier; static CSSStyleValue? parse(DOMString property, DOMString cssText); static sequence? parseAll(DOMString property, DOMString cssText);};
你可以如下操作 CSS style: (source from CSS Houdini- the bridge between CSS, JavaScript and the browser)
// CSS -> JSconst map = document.querySelector('.example').styleMap;console.log( map.get('font-size') );// CSSSimpleLength {value: 12, type: 'px', cssText: '12px'}// JS -> JSconsole.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 )]));
根據(jù) Drafts 的內(nèi)容,有了型別定義,在 JavaScript 的操作上據(jù)說會(huì)有性能上的顯著提升。此外,CSS Typed OM 也應(yīng)用在 Parser API 與 CSS Properties API 上。
Font Metrics API
Font Metrics 也沒有出現(xiàn)在上方的 Houdini API on render pipeline 中,但它其實(shí)已經(jīng)被寫入 Draft 規(guī)范 中了。
老實(shí)說看不是很懂他的 spec 寫的內(nèi)容,但我猜測(cè)這東西的用途應(yīng)該跟這篇文章 Deep dive CSS: font metrics, line-height and vertical-align 其中提到一個(gè)問題有關(guān),(里面非常詳細(xì)的介紹了 font metrics、line-height 與 vertical-align 在網(wǎng)頁上如何互相影響,推薦大家有空的話耐心閱讀一番。):
不同 font-family 在相同 font-size 下,所產(chǎn)生的 span 高度會(huì)不同。
要想控制 Font metrics,也就是控制字所占的寬高的話,目前可以先用 CSS Properties 來處理,根據(jù)已知字體的 font-metrics 動(dòng)態(tài)算出我們要 apply 多少的 font-size:
p { /* 定義好我們已知字型的 Font metrics */ /* font metrics */ --font: Catamaran; --fm-capitalHeight: 0.68; --fm-descender: 0.54; --fm-ascender: 1.1; --fm-linegap: 0; /* 定義想要的高度 */ --capital-height: 100; /* 設(shè)定 font-family */ font-family: var(--font); /* 利用 Font metrics 的資訊與想定義的高度來計(jì)算出 font-size */ --computedFontSize: (var(--capital-height) / var(--fm-capitalHeight)); font-size: calc(var(--computedFontSize) * 1px);}
而想必 Font Metrics API 就是希望能 expose 出更方便的 API 來達(dá)成上述的事情。
總結(jié)
Web 開發(fā)基本上就是由 HTML、JS、CSS 三大要素構(gòu)成,然而 JS 與 CSS 的發(fā)展差異卻極其龐大,一個(gè)速度快到讓人跟不上,一個(gè)則是等半天還是無法放心使用新規(guī)則,實(shí)在非常有趣…
但透過這次了解 Houdini API 的過程,理解到了 CSS 算是朝向好的方向前進(jìn),雖然很多離實(shí)際采用還有段距離,但至少我們已經(jīng)能夠在最新的瀏覽器上使用 Custom Properties 了!CSS 的未來還是充滿希望的!
聯(lián)系客服