問題:設計稿復現中的字體難題
開發 H5 項目時,設計同學給出的設計稿中全部使用 PingFang 字體。但當我們按照設計稿完成開發並進行驗收時,遇到了一個問題:在 iOS 上,一切按照期望進行,但在 Android 手機上,需要加粗的文本卻不如期望那樣顯示了。
原因分析:PingFang 字體在 Android 手機上的兼容問題
不加粗的文本,其對應的 CSS 如下:
.foo {
font-family: 'PingFangSC-Medium';
}
經驗豐富的開發很快就能看出原因,PingFang 字體在 Android 手機上並不受支持。
解決思路:利用 font-weight 替代 font-family
首次嘗試的解決方案是使用 font-weight 來替代 font-family。一個小提示:PingFang 字體支持6種 font-weight,如下表所示:
| font-weight | font-family |
|---|---|
| 100 | PingFangSC-Ultralight |
| 200 | PingFangSC-Thin |
| 300 | PingFangSC-Light |
| 400 | PingFangSC-Regular |
| 500 | PingFangSC-Medium |
| 600 | PingFangSC-Semibold |
同時設置全局的 font-family,如下:
body {
font-family: system-ui, -apple-system, "PingFang SC", sans-serif;
}
現在,元素的 CSS 更改為:
.foo {
font-weight: 500;
}
這個解決方案看似完美,實際上,在大多數 Android 手機上仍然存在問題——文本並沒有被成功地加粗。原因在於,大多數 Android 手機的自帶字體並不支持 500 的字重,瀏覽器將其視作 400 來進行呈現,導致文本顯示為普通粗細。
安卓默認字體對中英文的支持情況如下:
- 中文、日文、韓文字體 Noto Sans,只支持兩種字重 Regular 400 和 Bold 700。
- 英文字體 Roboto,支持完整字重 100-900。
瀏覽器的字體匹配算法如下:
當明確指定了 font-weight 數值,即所需的字重,並且字體中確實存在該對應字重時,瀏覽器將直接選擇對應的字重進行渲染。若無法找到對應的字重,瀏覽器將依照以下規則選擇最合適的字重進行展示:
- 若所需的字重小於400,則首先降序檢查小於所需字重的各個字重,如仍然沒有,則升序檢查大於所需字重的各字重,直到找到匹配的字重。
- 若所需的字重大於500,則首先升序檢查大於所需字重的各字重,之後降序檢查小於所需字重的各字重,直到找到匹配的字重。
- 若所需的字重是400,那麼會優先匹配500對應的字重,如仍沒有,那麼執行第一條所需字重小於400的規則。
- 若所需的字重精確為500,首選字重為400的選項;如無法匹配,將同樣啓用對應字重小於400的匹配規則。
引入外部字體?增大 font-weight?
當然,我們可以引入外部字體來解決 Android 不支持 500 字重的問題。然而,中文字體往往體積較大,會嚴重影響頁面的 LCP 指標。同時,大部分字體的商業使用需要相應的費用,因此這不是一個適合我們項目的解決方案。
中文字體體積大主要因素如下:
- 英文字體僅包含26個字母以及一些符號,而中文字體包含的字形則海量無比。
- 中文字形的線條複雜度遠勝於英文字形,因為控制中文字形線條的數據點比英文字形更多,導致數據量更大。
鑑於目前的情況,我們可以嘗試將 font-weight 提高到 700 來修復問題。然而,這會使 iOS 端的字體變得過於粗重,與設計稿有出入,這讓設計團隊難以接受。
那麼,是否存在這樣一種方法,讓我們可以在 iOS 端將 font-weight 設置為 500,而在 Android 端將 font-weight 設置為 700,這樣就可以完美地調和兩者了。
根據設備來設置 font-weight
我們當然可以使用 JavaScript 通過 navigator.userAgent 屬性來判斷設備是 Android 還是 iOS,從而為元素在不同設備上應用不同的 CSS。
但有一種更加簡單的方式,可以使用 @supports CSS 指令來分辨設備類型。現在,元素的 CSS 更改為:
.foo {
font-weight: 700;
}
@supports (-webkit-touch-callout: none) {
.foo {
font-family: PingFangSC-Medium;
}
}
現如今,Android 設備上的樣式為 font-weight: 700,而 iOS 設備上元素的樣式為 font-family: PingFangSC-Medium,從而優雅地解決了問題。
自動化處理:使用 PostCSS 插件
然而,每次要手動替換元素的樣式需要大量的工作量。為了簡化流程,我編寫了一個 PostCSS 插件,可以使這個過程完全自動化。
PostCSS 是什麼
PostCSS 是一個用於轉換 CSS 代碼的工具。它提供處理 CSS 的 API,開發者可以使用這些 API 編寫插件實現各種 CSS 處理任務,比如 autoprefixer 就是使用 PostCSS 編寫的。這些插件可以完成許多任務,包括:
- 自動添加供應商前綴以確保 CSS 在不同的瀏覽器中能夠兼容性地運行
- 使用未來的 CSS 語法
- 添加 CSS 變量和混合
- 轉換 px 為 em
- 還有許多其他任務......
如何開發 PostCSS 插件
PostCSS 插件就是默認導出是一個函數的 CommonJS 文件,如下:
module.exports = (opts = {}) => {
return {
postcssPlugin: 'PLUGIN NAME',
// 插件的監聽器
Once (root) {
// 每個文件調用一次,每個文件都對應一個 root 對象
},
Declaration (decl) {
// 全部聲明的節點
}
}
}
module.exports.postcss = true
大多數 PostCSS 插件做2件事:
- 在 CSS 中查找元素(例如,will-change 屬性)。
- 更改找到的元素(例如,在
will-change之前插入transform: translateZ(0)來兼容舊瀏覽器)。
PostCSS 將 CSS 解析為抽象語法書樹(AST)。這棵樹包含:
Root:樹的根節點,表示 CSS 文件。AtRule:@ 開頭的語句,如@charset "UTF-8"或@media (screen) {}。Rule:包含聲明的選擇器。如button {}。Declaration:鍵值對,如color: black;。Comment:獨立存在的註釋。選擇器、規則參數和值中的註釋存儲在節點的 raws 屬性中。
當你找到正確的節點時,將需要更改、插入或刪除它們周圍的其他節點。PostCSS 節點有一個類似 DOM 的 API 來轉換 AST。節點有方法可以四處移動(如 Node#next 或 Node#parent)、查看子節點(如 Container#some)、刪除節點或添加新節點。
Declaration (node, { Rule }) {
let newRule = new Rule({ selector: 'a', source: node.source })
node.root().append(newRule)
newRule.append(node)
}
我的插件流程比較簡單,掃描所有的 CSS,當發現 font-family 包含 PingFang 時,就將其類名和 font-family 值記錄下來,然後將 font-family 轉換為 font-weight。掃描結束後,將記錄下來的 CSS 類名和其 font-family 值放到 @supports (-webkit-touch-callout: none) 規則中。
你可以在以下地址看到詳細的代碼實現:https://github.com/SyMind/postcss-pingfang 。
現在,你不需要再對 CSS 進行任何更改——它可以維持最初的樣子:
.foo {
font-family: 'PingFangSC-Medium';
}
插件會自動將其轉換為:
.foo {
font-weight: 700;
}
@supports (-webkit-touch-callout: none) {
.foo {
font-family: PingFangSC-Medium;
}
}
於開發者而言,這個過程是完全無感知的,你不再需要操心任何關於 font-family 和 font-weight 的問題,僅需按需複製粘貼設計稿中的 CSS 代碼即可。
通過 npm 包安裝並使用
我已經將此工具作為 npm 包發佈。你可以通過以下方式進行安裝和使用:
安裝:
npm install postcss-pingfang
使用:
// 依賴
const fs = require('fs')
const postcss = require('postcss')
const pingfang = require('postcss-pingfang')
// 要處理的 css
const css = fs.readFileSync('input.css', 'utf8')
// 處理 css
const output = postcss()
.use(pingfang())
.process(css)
.css
總結
面對設計同學使用 PingFang SC 作為設計稿默認字體時,會帶來以下的問題:
- PingFang SC 並非安卓手機的內置字體,可以使用 font-weight 替換原設計稿中的字體,如 PingFangSC-Medium 替換為 500。
- 但大部分安卓手機內置的字體僅對中文支持 3 種字重,此時如 font-weight 為 500 的字體實際上並不會被加粗。
我們提出一種相對輕量級的解決方案相比於引入外部字體極其輕量級的方案,通過 @supports CSS 指令,在 iOS 手機上使用 PingFangSC-Medium,在安卓手機上使用 font-weight: 700。並開發 PostCSS 插件,從而使得整個過程完全自動化。