- 原文地址:Font-size: An Unexpectedly Complex CSS Property
- 原文做者:Manish Goregaokar
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:zephyrJS
- 校對者:bambooom,Colafornia
font-size
是糟糕的 CSS 屬性css
這多是每個寫過 CSS 的人都知道的屬性。它隨處可見。前端
但它也十分的複雜。android
「它不過是個數值」,你說,「它能有多複雜呢?」ios
我曾經也這麼認爲,直到我開始致力於實現 stylo。git
Stylo 是一個將 Servo 的樣式系統集成到 Firefox 中的項目。這個樣式系統負責解析 CSS,肯定哪些規則適用於哪些元素,經過級聯運行這些規則,最終計算並將樣式分配給樹中的各個元素。這不只發生在頁面加載上,也發生在各類事件(包括 DOM 操做)觸發時,而且是頁面加載和交互時間的一個重要部分。github
Servo 使用 Rust,並在許多地方用到了 Rust 的並行安全特性,樣式即是其中之一。Stylo 有潛力將這些加速技術帶入 Firefox,以及更安全的系統語言帶來的代碼的安全性。web
不管如何,就樣式系統而言,我認爲字體大小是它必須處理的最複雜的屬性。當涉及到佈局或渲染時,有些屬性可能會更復雜,但 font-size 多是樣式中最複雜的屬性。後端
我但願這篇文章能給出一個關於 web 會變得多麼複雜的想法,同時也能夠做爲一些複雜問題的文檔。在這篇文章中,我也將嘗試解釋一個樣式系統是如何工做的。瀏覽器
好的。讓咱們看看 font-size 是有多麼的複雜。緩存
該屬性的語法很是簡單。你能夠將其指定爲:
12px
, 15pt
, 13em
, 4in
, 8rem
)50%
)calc(12px + 4em + 20%)
)medium
, small
, large
, x-large
, 等等)larger
, smaller
)前三種用法在長度相關的 CSS 屬性中十分常見。語法沒有異常。
接下來兩個頗有趣。本質上,絕對關鍵字映射到各類像素值,並匹配 <font size=foo>
的結果(例如 size=3
就至關於 font-size: medium
)。他們映射到的實際值並不簡單,我將在後面的文章中討論。
相對關鍵字基本上是向上或向下縮放。縮放的機制也是複雜的,可是這已經改變了。接下來我也會談到這個。
首先:em
單位。在任何基於長度的 CSS 屬性中均可以指定爲一個單位爲 em 或 rem 的值。
5em
是指 「應用於元素的 font-size 的 5 倍」。5rem
是指 「根元素的 font-size 的 5 倍」
這意味着字體大小須要在全部其餘屬性以前計算(好吧,不徹底是,可是咱們將討論這個!)以便在這段時間內它是可用的。
你也能夠在 font-size
中使用 em
單位。在本例中,它是相對於父元素的字體大小計算的,而不是根據自身的字體大小來計算。
瀏覽器容許您在它們的首選項中設置 「最小」 字體大小,文本不會比這個字體大小更小。這對於難以閱讀小字的人來講是頗有幫助的。
可是,這並不影響以 em 爲單位的 font-size 屬性。因此若是你使用最小字體大小,<div style="font-size: 1px; height: 1em; background-color: red">
將會有一個很小的高度(你能夠經過顏色注意到),但文本的大小卻會被限制在最小的尺寸上。
實際上這意味着你須要跟蹤兩個單獨計算的字體大小值。其中一個值用於肯定實際文本的字體大小(例如,用於計算 em
單位。),而當樣式系統須要知道字體大小時使用另外一個值。
但涉及到 ruby(旁註標記)時,這會變得更加複雜。在表意文字中(一般指漢字及基於漢字的日本漢字和朝鮮漢字),爲了幫助那些不熟悉漢字的讀者,用拼音字來表達每一個字符的發音有時是頗有用的,這就是所謂的 「ruby」(在日語中被叫作 「振り仮名」)。由於這些文字是表意的,因此學習者知道一個單詞的發音殊不知道如何書寫它的狀況並很多見。例如想要顯示 日本,則須要在日語的日本(日語中讀做 「nihon」)上用 ruby 添加上平假名 にほん 。
如你所見,拼音部分的 ruby 文本字體更小(一般是主文本字體大小的 50%1)。最小字體大小遵照這一點,並確保若是 ruby 應用 50%
的字體大小,則 ruby 的最小字體大小是原始最小字體大小的 50%
。這就避免了 日本(上下兩段字設置成相同大小時)的狀況,這樣看起來將會很醜。
Firefox 容許你在僅縮放的時候縮放文本。若是你在閱讀一些小字時遇到了困難,那麼在不須要整頁放大的狀況下就能把頁面上的文本放大(這意味着你須要大量滾動),這是很好的體驗。
在這個例子中,其餘設置了 em
單位的屬性也要被放大。畢竟,它們應該相對於文本的字體大小(而且可能與文本有某種關係),因此若是這個大小已經改變,那它們也應隨之改變。
(固然,這個論點也適用於最小字體大小。但我不知道爲何最小字體沒有應用。)
實際上這很容易實現。在計算絕對字體大小(包括關鍵字)時,若是文字縮放功能開啓則它們會相應的縮放。而其餘則一切照舊。
<svg:text>
元素禁止了文字縮放功能,這也引發了一些至關棘手的問題。
再繼續接下來的內容以前,我有必要概述下樣式系統是如何工做的。
樣式系統的職責是接受 CSS 代碼和 DOM 樹,併爲每一個元素分配計算樣式。
這裏的 「specified」 和 「computed」 是不同的。「specified」 樣式是在 CSS 中指定的樣式,而計算樣式是指那些附加到元素、發送到佈局並繼承自元素的那些樣式。當應用於不一樣的元素時,指定的樣式能夠計算出不一樣的值。
因此當你指定了 width: 5em
,它可能計算得出 width: 80px
。計算值一般是指定值清理後的結果。
樣式系統將首先解析 CSS,一般會生成一組包含聲明的規則(聲明相似於 width: 20%;
;即屬性名和指定值)
而後,它按照自頂向下的順序遍歷樹(在 Stylo 中這是並行的),找出每一個元素所適用的聲明以及其執行順序 - 有些聲明優先於其餘聲明。而後,它將根據元素的樣式(父樣式和其餘信息)計算每一個相關聲明,並將該值存儲在元素的 「計算樣式」 中。
爲了不重複的工做,Gecko 和 Servo 在這裏作了不少優化2。 有一個 bloom 過濾器用於快速檢查深層後代選擇器是否應用於子樹。有一個 「規則樹」 用於緩存已肯定的聲明。計算樣式常常被引用、計數和共享(由於默認狀態是從父樣式或默認樣式繼承的)。
總的來講,這就是樣式系統運做的基本原理。
好吧,這就是事情變得複雜的地方。
還記得我說的 font-size: medium
會映射到某個值嗎?
那麼它映射到什麼呢?
嗯,結果是,這取決於字體。對於如下 HTML:
<span style="font: medium monospace">text</span>
<span style="font: medium sans-serif">text</span>
複製代碼
你能從(codepen)看到運行結果。
其中第一個計算字體大小爲 13px,第二個字體大小爲 16px。你能從 devtools 的計算樣式窗口獲得答案,或者使用 getComputedStyle()
也行。
我認爲這背後的緣由是等寬字體每每更寬,而默認字體大小(medium)被縮小,使得它們看起來有類似的寬度,以及全部其餘關鍵字字體大小也被改變。最終的結果就變成這樣:
Firefox 和 Servo 有一個 矩陣 用在計算基於「基本大小」(也就是 font-size: medium 的計算值)的全部絕對字體大小的關鍵字的值。實際上,Firefox 有 三個表格 來支持一些遺留用例,例如怪異模式(Servo 還沒有添加對這三個表的支持)。咱們在瀏覽器的其餘部分查詢「基本大小」時是基於語言和字體的。
等等,這和語言又有什麼關係呢?語言是如何影響字體大小的?
實際上,基本大小取決於字體家族和語言,你能夠對它進行配置。
Firefox 和 Chrome(使用擴展)實際上都容許你爲每種語言設置使用哪些字體,以及默認(基本)的字體大小。
這並不像人們想象的那樣晦澀難懂。對於非拉丁語系的文字,默認字體一般很難看。我單獨安裝了一個字體, 能夠顯示好看的天城文連字
一樣的,有些文字也比拉丁文複雜得多。我爲天城文設置的默認字體爲 18 而不是 16。我已經開始學習普通話了,我也把字號設置爲 18。漢字字形可能會變得至關複雜,我仍然很難學會(以及認識)它們。更大的字體對學習它們更有幫助。
總之,這不會讓事情變得太複雜。這確實意味着 font family 須要在 font-size 以前計算,而 font-size 須要在大多數其餘屬性以前計算。語言能夠經過 HTML 的 lang
屬性來設置,因爲它是可繼承的,Firefox 內部將其視爲一個 CSS 屬性,必須儘早計算。
到此爲止,還不算太糟。
如今,難以預料的事情出現了。這種對 language 和 family 的依賴是能夠繼承的。
快看,div
裏面的字體大小是多少呢?
<div style="font-size: medium; font-family: sans-serif;"> <!-- base size 16 -->
font size is 16px
<div style="font-family: monospace"> <!-- base size 13 -->
font size is ??
</div>
</div>
複製代碼
對於可繼承的 CSS 屬性3,若是父級的計算值是 16px
,且子元素沒有被指定其餘值,那麼子元素將繼承這個 16px
的值。子元素不須要關心父元素是從哪裏獲得這個計算值的。
如今,font-size
「繼承」了一個 13px
的值。你能從這裏(codepen)看到結果:
基本上,若是計算的值來自關鍵字,那麼不管 font family 或 language 如何變化,font-size 都會用關鍵字裏的 font family 和 language 來從新計算。
這麼作的緣由是若是不這麼作,不一樣的字體大小將沒法工做。默認字體大小爲 medium
,所以根元素基本上會獲得一個 font-size: medium
而其餘元素將繼承這個聲明。若是在文檔中將其改成等寬字體或使用其餘語言,則須要從新計算字體大小。
不只如此。它甚至經過相對單位繼承(IE 除外)。
<div style="font-size: medium; font-family: sans-serif;"> <!-- base size 16 -->
font size is 16px
<div style="font-size: 0.9em"> <!-- could also be font-size: 50%-->
font size is 14.4px (16 * 0.9)
<div style="font-family: monospace"> <!-- base size 13 -->
font size is 11.7px! (13 * 0.9)
</div>
</div>
</div>
複製代碼
(codepen)
<div style="border: 1px solid black; display: inline-block; padding: 15px;">
<div style="font-size: medium; font-family: sans-serif;">font size is 16px
<div style="font-size: 0.9em">font size is 14.4px (16 * 0.9)
<div style="font-family: monospace">font size is 11.7px! (13 * 0.9)</div>
</div>
</div>
</div>
複製代碼
所以,當咱們從第二個 div 繼承時,實際繼承的是 0.9*medium
,而不是 14.4px
。
另外一種看待這個問題的方法是,每當 font family 或 language 怎麼變化,你都應該從新計算字體大小, 就好像 language 和 family 沒有變化同樣。
Firefox 同時使用了這兩種策略。最初的 Gecko 樣式系統經過實際返回樹的頂部並從新計算字體大小來處理這個問題,就好像 language 和 family 是不一樣的同樣。我懷疑這是低效的,可是規則樹彷佛使其略微高效了一些。
另外一方面,在計算的同時,Servo 會存儲一些額外的數據,這些數據會被複制到子元素中。基本上來講, 存儲的內容至關於:「是的,這個字體是從關鍵字計算出來的。關鍵字是 medium
,而後咱們對它應用了 0.9 因子。」4
在這兩種狀況下,這都會致使全部其餘字體大小複雜性加重,由於它們須要經過這種方式獲得謹慎的保護。
在 Servo 裏,多數狀況都是經過 font-size 自定義級聯函數 來處理的。
前面我提到了 font-size: larger
/smaller
的是按比例縮放的,但尚未提到對應的比例值。
根據 規範,若是當前字體大小與絕對關鍵字大小的值匹配(medium,large 等),則應該選擇上一個或下一個關鍵字大小的值。
若是是在兩個絕對關鍵字值之間,則在前兩個或後兩個尺寸中間尋找相同比例的點。
固然,這必須很好地處理以前提到的關鍵字字體大小的奇怪繼承問題。在 gecko 模型中這並不太難,由於 Gecko 不管如何都會從新計算。在 Servo 的模塊中,咱們存儲一系列 larger
/smaller
的應用和相對單位,而不是隻存儲一個相對單位。
此外,在文本縮放過程當中計算此值時,必須先取消縮放,而後再在表中查找,而後從新縮放。
總的來講,一堆複雜的東西並無帶來多大的收益 —— 原來只有 Gecko 真正遵循了規範!其餘瀏覽器引擎只是使用了簡單的比例縮放。
因此個人解決方案 就是把這種行爲從 Gecko 上移除。簡化了這個處理過程。
Firefox 和 Safari 支持數學標記語言 MathML。現在,它在網絡上使用很少,但它確實存在。
當談到字體大小時,MathML 也有它的複雜性。特別是 scriptminsize
,scriptlevel
和 scriptsizemultiplier
。
例如,在 MathML 中,分子、分母或是文字上標是其外部文本字體大小的 0.71 倍。這是由於 MathML 元素默認的 scriptsizemultiplier
爲 0.71, 而這些特定元素的 scriptlevel 默認爲 +1
。
基本上,scriptlevel=+1
的意思是 「字體大小乘以 scriptsizemultiplier
」,而 scriptlevel=-1
則用於消除這種影響。這能夠經過在 mstyle
元素上設置 scriptlevel
屬性指定。一樣你也能夠經過 scriptsizemultiplier
來調整(繼承的)乘數因子,經過 scriptminsize
來調整最小值。
例如:
<math><msup>
<mi>text</mi>
<mn>small superscript</mn>
</msup></math><br>
<math>
text
<mstyle scriptlevel=+1>
small
<mstyle scriptlevel=+1>
smaller
<mstyle scriptlevel=-1>
small again
</mstyle>
</mstyle>
</mstyle>
</math>
複製代碼
顯示以下(須要用 Firefox 來查看呈現版本,Safari 也支持 MathML,但支持不太好):
(codepen)
因此這沒那麼糟。就好像 scriptlevel
是一個奇怪的 em
單位。沒什麼大不了的,咱們已經知道如何處理這些問題了。
還有 scriptminsize
。這使你能夠爲 scriptlevel
所引發的更改設置最小字體大小。
這意味着,scriptminsize
將確保 scriptlevel
不會致使出現比最小尺寸更小的字體,但它會忽略特地指定的 em
單位和像素值。
這裏已經引入了一點微妙的複雜性,如今 scriptlevel
成了影響到 font-size
如何繼承的另外一個因素了。幸運的是,在 Firefox/Servo 的內部,scriptlevel
(以及 scriptminsize
和 scriptsizemultiplier
)也是做爲 CSS 屬性處理,這意味着咱們可使用與 font-family 和 language 同樣的框架來處理 —— 在字體大小設置以前計算腳本屬性,若是設置了 scriptlevel
,則強制從新計算字體大小,即便沒有設置字體大小自己。
在 Servo 中,咱們處理屬性依賴關係的方式是擁有一組 「早期」 屬性和一組 「後期」 屬性(容許依賴於早期屬性)。咱們對聲明進行了兩次查找,一次是查找早期屬性,另外一次是後期屬性。然而,如今咱們有了一組至關複雜的依賴關係,其中 font-size 必須在 language、font-family 和腳本屬性以後計算,但在其餘全部涉及長度的東西以前計算。另外,因爲另外一個我沒有談到的字體複雜性,font-family 必須在全部其餘早期屬性以後進行計算。
咱們處理這個問題的方法是在早期計算時 抽離 font-size 和 font-family ,直到早期計算完成後再處理它。
在這個階段,咱們首先處理文本縮放的禁用,而後處理 font-family 的複雜性。
而後計算 font family。若是指定了字體大小,則進行計算。若是沒有指定,但指定了 font family,lang 或 scriptlevel,則強制將計算做爲繼承,來處理全部的約束。
與其餘 「最小字體大小」 不一樣,在字體大小被 scriptminsize 限制時,在任何屬性中使用 em
單位都將用一個鉗位值來計算長度,而不是 「若是沒有被鉗位」 的值, 若是字體大小被 scriptminsize 限制。所以,乍一看,處理這一點彷佛很簡單;當由於 scriptlevel 而須要縮放時, 只考慮最小字體大小 scriptminsize。
和往常同樣,事情並無這麼簡單 😀:
<math>
<mstyle scriptminsize="10px" scriptsizemultiplier="0.75" style="font-size:20px">
20px
<mstyle scriptlevel="+1">
15px
<mstyle scriptlevel="+1">
11.25px
<mstyle scriptlevel="+1">
would be 8.4375, but is clamped at 10px
<mstyle scriptlevel="+1">
would be 6.328125, but is clamped at 10px
<mstyle scriptlevel="-1">
This is not 10px/0.75=13.3, rather it is still clamped at 10px
<mstyle scriptlevel="-1">
This is not 10px/0.75=13.3, rather it is still clamped at 10px
<mstyle scriptlevel="-1">
This is 11.25px again
<mstyle scriptlevel="-1">
This is 15px again
</mstyle>
</mstyle>
</mstyle>
</mstyle>
</mstyle>
</mstyle>
</mstyle>
</mstyle>
</mstyle>
</math>
複製代碼
(codepen)
基本上來講, 若是你在達到最小字體大小後繼續屢次增長層級, 而後減掉一個層級, 是無法當即計算出 min size / multiplier
的值的。這使之變得不對稱了, 若是乘數因子沒有變化, 一個淨層級爲 +5
應該與一個淨層級爲 +6 -1
的元素具備相同字體大小。
所以,所發生的狀況是,script level 是根據字體大小計算的就好像 scriptminsize 從未應用過同樣,並且只有當腳本大小大於最小大小時,咱們才使用該大小。
這不只僅是跟蹤 script level 還須要跟蹤 multiplier 的變化。所以,這最終將建立另外一個要繼承的字體大小值。
歸納地說,咱們如今有四種不一樣的繼承字體大小的概念:
另外一個複雜性在於下面這種狀況應該仍然能正常工做:
<math>
<mstyle scriptminsize="10px" scriptsizemultiplier="0.75" style="font-size: 5px">
5px
<mstyle scriptlevel="-1">
6.666px
</mstyle>
</mstyle>
</math>
複製代碼
(codepen)
若是已經比 scriptminsize 還小,減小 script level(以增大字體大小)不該該被鉗制,由於以後這會讓它看起來過於巨大。
這基本上意味着, 只能在 script level 對應的值大於腳本最小字體大小時, 使用 scriptminsize。
在 Servo 中,全部 MathML 的處理都被這個奇妙的註釋比代碼多的函數以及它附近函數的一些代碼完美解決。
這就是你要了解的。font-size
其實是至關複雜的。不少網絡平臺都隱藏着這樣的複雜狀況,但遇到了卻會以爲十分有趣。
(當我必須實現它們時,可能就沒那麼有趣了。 😂)
感謝 mystor,mgattozzi,bstrie 和 projektir 審閱了這篇文章的草稿。
font-family
繼承的 —— 除非另外設置。可是 transform
卻不是,若是你在元素上應用了 transform 但它的子元素卻不會繼承這個屬性。↩calc
s,這是我須要解決的問題。除了比率以外,還存儲一個絕對偏移量。↩掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。