踐指南)
中英文混排的字體問題幾乎每個(gè)前端在認(rèn)真調(diào)樣式時(shí)都會(huì)撞上你在font-family里寫了Microsoft YaHei, sans-serif想著反正中文能顯示就行結(jié)果頁面里的英文和數(shù)字也被微軟雅黑“接管”了標(biāo)題里的2025和整段英文看起來又粗又笨和旁邊專門設(shè)計(jì)的西文字體放在一起時(shí)視覺重量完全不在一個(gè)層級(jí)。更隱蔽的問題是很多人以為font-family就是“從上到下選第一個(gè)能用的字體”所以不知道瀏覽器其實(shí)是在按字符逐個(gè)匹配字體。如果能利用好這個(gè)機(jī)制完全可以讓漢字走中文字體、字母和數(shù)字走西文字體兩者在同一個(gè)元素里共存并且各司其職這也是 CSS 字體棧設(shè)計(jì)里最實(shí)用、最值得吃透的技巧之一。這篇文章我會(huì)把三種主流做法都講透最基礎(chǔ)的font-family列表順序回退、進(jìn)階的font-face配合unicode-range精確分派、以及工程上怎么用 CSS 變量把整套字體體系管起來。每個(gè)方案都會(huì)附上完整代碼和適用場(chǎng)景最后再分享一份我自己實(shí)際排查“字體不生效”問題的完整鏈路。不管你是剛?cè)胄械那岸诵氯诉€是已經(jīng)在做組件庫(kù)、后臺(tái)系統(tǒng)、內(nèi)容站的開發(fā)者這套方法論都能直接套用。1. 為什么中英文混排時(shí)字體總顯得“不對(duì)”瀏覽器按字符匹配字體先把概念糾正過來。font-family不是給整段文字“選一個(gè)字體”而是在瀏覽器渲染每一個(gè)字符的時(shí)候按照字體列表依次去找“第一個(gè)包含該字符的字體”。也就是說同一行文字里英文、數(shù)字、中文完全可以落在不同的字體上。這個(gè)機(jī)制既是很多字體問題的根源也是我們實(shí)現(xiàn)“漢字和字母分別用不同字體”的基礎(chǔ)。1.1 字體匹配的真實(shí)流程一個(gè)字符一個(gè)字符找當(dāng)瀏覽器遇到一個(gè)字符比如A、中、3它做的事可以簡(jiǎn)單概括為取當(dāng)前元素的font-family列表按書寫順序拿到第一項(xiàng)字體。問這個(gè)字體你包含A這個(gè)字形嗎如果包含就用你渲染如果不包含繼續(xù)往下問下一項(xiàng)。如果整個(gè)列表問完都沒有字體包含該字符交給瀏覽器默認(rèn)的 fallback 字體。關(guān)鍵在于“包含”兩個(gè)字。很多中文字體微軟雅黑、蘋方、思源黑體為了滿足中英文混排的基本需求內(nèi)部自帶了拉丁字母、數(shù)字、標(biāo)點(diǎn)的字形。這意味著你如果第一個(gè)寫中文字體英文字符會(huì)直接命中這個(gè)中文字體里的西文字形而不是繼續(xù)去找你后面列出的Helvetica或Arial。這就是為什么“把微軟雅黑放最前面”的寫法會(huì)讓英文和數(shù)字也變成微軟雅黑風(fēng)格。不是瀏覽器偷懶而是規(guī)則如此匹配到就立即停止不會(huì)跳過已經(jīng)命中的字體繼續(xù)尋找“更合適”的字體。1.2 中文字體里的西文字形為什么通常不適合直接顯示英文中文字體在設(shè)計(jì)時(shí)拉丁字形的主要目標(biāo)是“在中文為主的文本里不違和”所以它們的字重、x-height、數(shù)字高度往往跟專門設(shè)計(jì)的西文字體有明顯差異。舉幾個(gè)我實(shí)際對(duì)比過的例子微軟雅黑里的英文字母偏寬小字號(hào)下筆畫偏均勻但缺少節(jié)奏感大字號(hào)標(biāo)題里尤其顯得“肉”。蘋方里的數(shù)字不是等寬的放在表格或者價(jià)格列表里會(huì)對(duì)不齊。思源黑體的西文部分相對(duì)優(yōu)秀但跟 Inter、Roboto 這種專門為屏幕設(shè)計(jì)的字體放在一起對(duì)比還是能明顯看出字偶距和曲線處理的差距。這個(gè)差異在中文正文里可能感知不強(qiáng)但一旦頁面里有大量英文縮寫、URL、價(jià)格、編號(hào)或者你用了比較大的標(biāo)題字重西文字體的氣質(zhì)就直接影響整個(gè)頁面的精致度。所以“漢字用中文字體、字母和數(shù)字用西文字體”不是一個(gè)炫技需求是排版質(zhì)量的剛需。2. 最簡(jiǎn)單有效的方案用 font-family 列表順序做自動(dòng)分配如果不引入任何網(wǎng)絡(luò)字體只靠系統(tǒng)自帶的字體我們就能用一行font-family同時(shí)處理好中英文問題。這個(gè)方法零成本、零加載、兼容所有瀏覽器適合 90% 的內(nèi)容站和管理系統(tǒng)。2.1 字體列表順序背后的回退機(jī)制要利用回退機(jī)制實(shí)現(xiàn)“中西文分家”原則只有一條把希望西文使用的字體放在前面把希望中文使用的字體放在后面。反過來思考就明白了瀏覽器匹配字符時(shí)第一個(gè)包含該字形的字體立即生效。英文字體如Arial通常不包含漢字字形所以漢字會(huì)自動(dòng)跳過它落到后面的中文字體上而拉丁字母、數(shù)字在前面就命中Arial了不會(huì)再走到中文字體去。這樣一前一后就天然完成了按字符分派。反過來如果先寫中文字體字母和數(shù)字就會(huì)全部命中中文字體自帶的拉丁字形再也到不了后面的西文字體。所以順序是這套方案的靈魂比字體本身的選擇更重要。我的推薦寫法大致是這樣一套覆蓋 macOS、Windows、Linux 三大平臺(tái)body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; }這段字體棧里前幾個(gè)是各平臺(tái)的西文默認(rèn)字體macOS/iOS 上-apple-system會(huì)映射到 SF Pro蘋果生態(tài)里的顯示效果最協(xié)調(diào)。Windows 上Segoe UI是系統(tǒng)默認(rèn)西文字體中文會(huì)落到后面的微軟雅黑。Android 和 Chrome OS 上Roboto負(fù)責(zé)西文中文會(huì)落到系統(tǒng)默認(rèn)黑體。最后用PingFang SC、Hiragino Sans GB、Microsoft YaHei依次兜住簡(jiǎn)體中文最后sans-serif作為終極兜底。在這套列表下同樣一段我是 Typecho 用戶已經(jīng)用了 8 年。英文和數(shù)字會(huì)自動(dòng)用 SF Pro/Segoe UI/Roboto 渲染漢字自動(dòng)用蘋方/雅黑渲染不需要任何額外 JS 或類名。2.2 推薦的各場(chǎng)景字體組合參考不同場(chǎng)景下“前置西文字體”的選擇可以不一樣。我整理了幾個(gè)常用組合可以直接抄使用場(chǎng)景推薦字體棧適配平臺(tái)通用后臺(tái)、管理系統(tǒng)正文偏多-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, PingFang SC, Microsoft YaHei, sans-serifmacOS / Windows / Linux商務(wù)、媒體類站點(diǎn)正文偏西文風(fēng)格Georgia, Times New Roman, PingFang SC, Hiragino Sans GB, Microsoft YaHei, serif通用代碼塊、技術(shù)文檔JetBrains Mono, SFMono-Regular, Consolas, Liberation Mono, PingFang SC, monospace通用數(shù)據(jù)大屏、營(yíng)銷頁強(qiáng)調(diào)現(xiàn)代感Inter, Helvetica Neue, Arial, PingFang SC, Microsoft YaHei, sans-serif通用Inter 可為本地或 web font第二行的Georgia是有襯線西文字體搭配思源宋體或者宋體時(shí)整塊文字會(huì)呈現(xiàn)一種“報(bào)刊感”。這個(gè)組合很適合正文偏長(zhǎng)、內(nèi)容偏深度閱讀的頁面跟滿屏黑體一對(duì)比氣質(zhì)完全不一樣。2.3 數(shù)字和標(biāo)點(diǎn)會(huì)怎么走一個(gè)容易忽略的細(xì)節(jié)很多人只關(guān)注字母其實(shí)數(shù)字和標(biāo)點(diǎn)同樣受這套規(guī)則影響。數(shù)字在字體分類里屬于“西文”所以只要前置字體包含數(shù)字它們就會(huì)用西文字體渲染這對(duì)表格、價(jià)格、日期對(duì)齊非常有意義。因?yàn)楹芏辔魑淖煮w尤其等寬字體的數(shù)字寬度統(tǒng)一而中文字體里的數(shù)字寬度經(jīng)常不一致。標(biāo)點(diǎn)情況稍微復(fù)雜一點(diǎn)英文逗號(hào)、句號(hào)走前置西文字體沒問題但中文全角標(biāo)點(diǎn)?!啊币话阍谖魑淖煮w里不存在只能落到中文字體上所以用unicode-range做精細(xì)化控制時(shí)必須額外考慮 CJK 標(biāo)點(diǎn)的碼段否則中文內(nèi)容里的標(biāo)點(diǎn)可能被替換成奇奇怪怪的 fallback 字體。這一點(diǎn)到第 3 節(jié)再展開。3. 精確控制方案font-face unicode-range 把字符分給指定字體font-family列表回退方案的優(yōu)點(diǎn)是簡(jiǎn)單但它有一個(gè)局限你只能依賴“字體內(nèi)部是否包含某字符”這一個(gè)條件來分流。如果你從 Google Fonts 自己加載了一套很精致的英文字體比如 Inter希望頁面里的英文全部用 Inter漢字繼續(xù)保持系統(tǒng)蘋方或雅黑那光靠順序其實(shí)也能做到把 Inter 放最前面漢字自動(dòng) skip 過去。但如果這套英文字體意外包含了漢字字形有些西文字體會(huì)帶上一點(diǎn) CJK 兼容字符或者你想讓漢字不是走系統(tǒng)字體而是走另一套 web font就該unicode-range登場(chǎng)了。3.1 unicode-range 到底做了什么unicode-range是用在font-face規(guī)則里的一組字符碼段聲明。它的作用是告訴瀏覽器這個(gè)字體文件只負(fù)責(zé)渲染這些碼段的字符。注意它的工作方式不是“過濾”而是“聲明職責(zé)范圍”。當(dāng)頁面里的字符落在某個(gè)font-face聲明的unicode-range內(nèi)時(shí)瀏覽器才會(huì)優(yōu)先用這個(gè)字體去渲染它不在范圍內(nèi)的字符這個(gè) font-face 會(huì)被忽略。這種機(jī)制最開始是為了把大體積中文字體按字庫(kù)切分成多個(gè)子集文件、按需加載而設(shè)計(jì)的。后來人們發(fā)現(xiàn)它也可以用來做字體分派先聲明一個(gè)西文字體只覆蓋拉丁、希臘、西里爾區(qū)段再聲明一個(gè)中文字體只覆蓋 CJK 區(qū)段然后讓font-family同時(shí)引用兩個(gè)字體族名瀏覽器就會(huì)自動(dòng)按字符去向兩個(gè)字體。3.2 一個(gè)完整示例西文走 Inter漢字走思源宋體假設(shè)我從本地加載了兩套 web font目標(biāo)是正文里英文、數(shù)字用 Inter中文用思源宋體。完整的 CSS 寫法如下font-face { font-family: Inter; src: url(/fonts/Inter.woff2) format(woff2); font-weight: 400 700; font-display: swap; unicode-range: U0000-00FF, U0131, U0152-0153, U02BB-02BC, U02C6, U02DA, U02DC, U2000-206F, U2074, U20AC, U2122, U2212, U2215, UFEFF, UFFFD; } font-face { font-family: Source Han Serif SC; src: url(/fonts/SourceHanSerifSC-Regular.woff2) format(woff2); font-weight: 400; font-display: swap; unicode-range: U4E00-9FFF, U3000-303F, UFF00-FFEF; } body { font-family: Inter, Source Han Serif SC, PingFang SC, sans-serif; }這段代碼里Inter 的unicode-range覆蓋了基礎(chǔ)拉丁字母、數(shù)字、常用符號(hào)、貨幣符號(hào)等思源宋體只覆蓋U4E00-9FFF的漢字區(qū)和U3000-303F、UFF00-FFEF的 CJK 標(biāo)點(diǎn)與全角符號(hào)區(qū)。瀏覽器渲染Hello 世界時(shí)Hello命中 Inter世界命中思源宋體互不干擾。這里有個(gè)很容易踩的坑如果漏掉U3000-303F和UFF00-FFEF這兩個(gè)標(biāo)點(diǎn)區(qū)段中文逗號(hào)、句號(hào)、書名號(hào)就找不到“負(fù)責(zé)”它的字體最后會(huì)掉到瀏覽器默認(rèn)字體或系統(tǒng)字體上在一段正常中文里顯得特別突兀。我自己早期做中英混排 web font 時(shí)就因?yàn)橹粚懥藵h字區(qū)段導(dǎo)致全角逗號(hào)的樣式跟正文其他字符不一樣排查了好久。3.3 什么時(shí)候值得用 unicode-range什么時(shí)候不值得unicode-range不是銀彈它有自己的適用邊界。我建議按下面這張表來判斷情形是否推薦 unicode-range原因本地系統(tǒng)字體做中英混排不推薦直接用 font-family 順序即可瀏覽器原生支持代碼更簡(jiǎn)單引用了不含漢字的外文 web font推薦避免外文字體意外覆蓋中文兼容字符同時(shí)減少字體文件體積中文也使用了 web font強(qiáng)烈推薦中文字體體積巨大按區(qū)段拆子集會(huì)顯著提升加載速度只需要讓英文變成指定字體中文無所謂不推薦直接 dictating 字體順序就夠沒必要多加載字體另一個(gè)要注意的是unicode-range與字重font-weight的配合。給同一套字體注冊(cè)多個(gè)字重時(shí)每個(gè)font-face都說font-weight: 400、700這種unicode-range要保持一致否則可能出現(xiàn)“加粗文字反而跑去系統(tǒng)字體”的詭異情況。4. 工程化與性能用 CSS 變量管理多語言字體并優(yōu)化加載方案一和方案二講的是“怎么寫對(duì)”工程化講的是“怎么在大型項(xiàng)目里寫得不散、不亂、好維護(hù)”。一個(gè)真實(shí)項(xiàng)目里頁面往往有正文、標(biāo)題、輔助文字、代碼塊、數(shù)字表格等多種字體場(chǎng)景如果每個(gè)組件都單獨(dú)寫一遍字體棧后期換字體就是一場(chǎng)災(zāi)難。更建議的方法是把字體體系定義為 CSS 變量統(tǒng)一管理。4.1 字體變量統(tǒng)一管理我的做法是在:root里定義三組變量一組西文變量、一組中文變量、一組組合變量。組合變量負(fù)責(zé)把前面兩個(gè)拼起來:root { /* 西文字體族 */ --font-en: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial; /* 中文字體族 */ --font-cn: PingFang SC, Hiragino Sans GB, Microsoft YaHei; /* 通用正文字體西文優(yōu)先中文兜底 */ --font-base: var(--font-en), var(--font-cn), sans-serif; /* 強(qiáng)調(diào)數(shù)字的字體適合價(jià)格、表格、編號(hào) */ --font-num: Inter, SFMono-Regular, Consolas, var(--font-cn), monospace; /* 等寬字體代碼塊專用 */ --font-mono: JetBrains Mono, SFMono-Regular, Consolas, Liberation Mono, var(--font-cn), monospace; } body { font-family: var(--font-base); } .price, .stat, .table-number { font-family: var(--font-num); font-variant-numeric: tabular-nums; } code, pre, kbd { font-family: var(--font-mono); }這樣定義之后頁面里要換字體只需要改:root里的幾行。比如把西文字體從 Inter 換成 Manrope只改動(dòng)--font-en一處全站生效。中文字體同理想從系統(tǒng)蘋方切到思源黑體 web font只改--font-cn。另外font-variant-numeric: tabular-nums是很多數(shù)字展示場(chǎng)景必加的一行它要求表格數(shù)字采用等寬形式價(jià)格、序號(hào)、統(tǒng)計(jì)數(shù)字在縱向?qū)R時(shí)不會(huì)跳動(dòng)。配合前置等寬字體效果比純靠字體選擇更穩(wěn)定。4.2 代碼塊、表單、數(shù)字等特殊場(chǎng)景的字體策略代碼塊是我見過字體混亂最嚴(yán)重的地方。很多人只在body上設(shè)了字體沒管pre和code結(jié)果代碼里的中文注釋和字符串里的中文用瀏覽器默認(rèn)字體渲染在整個(gè)深色代碼塊里顯得特別突兀。給代碼塊設(shè)置字體棧時(shí)一個(gè)關(guān)鍵點(diǎn)是讓它同時(shí)覆蓋中文場(chǎng)景pre, code, kbd, samp { font-family: JetBrains Mono, SFMono-Regular, Consolas, Liberation Mono, PingFang SC, Microsoft YaHei, monospace; }這樣英文、代碼符號(hào)走等寬字體中文注釋回退到系統(tǒng)中文字體。要注意Consolas本身不含中文字形漢字會(huì)自動(dòng)落到后面的PingFang SC或Microsoft YaHei所以這個(gè)順序是安全的。表格和統(tǒng)計(jì)數(shù)字的場(chǎng)景我一般會(huì)單獨(dú)加一個(gè)--font-num變量并在代碼里配合font-variant-numeric使用。這樣不管數(shù)字出現(xiàn)在標(biāo)題、按鈕還是表格里都能統(tǒng)一用同一套數(shù)字字體視覺節(jié)奏更整齊。4.3 字體加載性能與子集化建議如果你決定使用 web font性能是第一優(yōu)先級(jí)。中文字體文件動(dòng)輒幾 MB不做任何處理直接引用首屏體驗(yàn)會(huì)非常差。經(jīng)驗(yàn)上有幾條鐵律務(wù)必開啟font-display: swap。默認(rèn)行為是字體加載完成前顯示 fallback加載完成后突然切換會(huì)造成 FOITFlash of Invisible Text。swap雖然會(huì)有 FOUTFlash of Unstyled Text但至少用戶能立刻看到內(nèi)容比白屏好得多。中文字體一定要按子集拆分。用unicode-range把常用漢字拆成若干子集再讓瀏覽器只請(qǐng)求需要的子集文件。目前 woff2 格式壓縮后的中文字體按 1000 字左右一個(gè)子集拆分每個(gè)文件可以控制在幾十 KB 級(jí)別。西文字體如果只想用其中幾檔字重只注冊(cè)那幾檔。不要圖省事把 400、500、600、700 全注冊(cè)上每個(gè)文件都是流量成本。很多字體提供 variable font 版本一個(gè)文件覆蓋所有字重也是不錯(cuò)的選擇。5. 踩坑實(shí)錄font-family 設(shè)置無效的排查鏈路字體這個(gè)領(lǐng)域坑很多很多問題看起來是“瀏覽器不聽話”實(shí)際是寫法有誤或者環(huán)境限制。我把踩過的坑按排查順序整理成一套鏈路遇到問題按這個(gè)順序走基本能定位九成的情況。5.1 DevTools 查看元素實(shí)際使用的字體遇到字體不生效我第一個(gè)動(dòng)作不是改代碼而是打開 DevTools 的 Rendering 面板勾選 “Highlight all text with fonts” 或者直接在 Elements 里選中元素查看 Computed 面板。高亮模式下頁面里的文字會(huì)被套上不同顏色的半透明背景每種顏色代表一種實(shí)際字體。這時(shí)一眼就能看出來這一段英文是不是真的用了前置字體中文是不是落在了預(yù)期中的中文字體上。如果顏色不對(duì)再去點(diǎn)開 Computed 里的 font-family看瀏覽器把哪些字體當(dāng)成候選、最終選中了哪個(gè)。這一步幾乎能解決 80% 的“為什么沒生效”。5.2 引號(hào)、字體族名與 font-face 加載失敗最常見的翻車點(diǎn)是沒有給多單詞字體名加引號(hào)/* 錯(cuò)誤寫法PingFang SC 會(huì)被拆成兩個(gè)關(guān)鍵字 */ font-family: PingFang SC, sans-serif; /* 正確寫法 */ font-family: PingFang SC, sans-serif;中文字體名有兩個(gè)體系比如 Windows 上微軟雅黑和Microsoft YaHei是同一個(gè)字體的不同名稱蘋方的英文名在系統(tǒng)里通常是PingFang SC。寫錯(cuò)一個(gè)瀏覽器就 fallback 到系統(tǒng)默認(rèn)表現(xiàn)就是“中文還是顯示但不是你要的字體”。font-face加載失敗的問題也常見。用 web font 時(shí)我在排查時(shí)會(huì)直接打開 Network 面板過濾font類型文件看字體文件是不是 200、404 還是 206。如果路徑?jīng)]問題但字體就是沒生效再檢查font-face是否聲明了font-display、font-weight是否和調(diào)用處的字重匹配、unicode-range是否覆蓋到了目標(biāo)字符。5.3 unicode-range 的邊界坑unicode-range有兩個(gè)高頻坑。第一個(gè)是碼段邊界。漢字常用區(qū)段U4E00-9FFF覆蓋 20992 個(gè)碼位但 Ext-A 區(qū)U3400-4DBF的生僻字不在其內(nèi)如果頁面內(nèi)容里有生僻字就會(huì)被 fallback 到其他字體看起來和普通漢字不一致。處理方式是看項(xiàng)目需求需要的話把U3400-4DBF也加進(jìn) range。第二個(gè)坑是中文標(biāo)點(diǎn)歸屬。上面提過U3000-303FCJK 標(biāo)點(diǎn)和UFF00-FFEF全角形式如果不單獨(dú)聲明中文引號(hào)、彎引號(hào)、破折號(hào)很容易落到系統(tǒng)默認(rèn)字體導(dǎo)致同一個(gè)中文字體內(nèi)部出現(xiàn)兩種樣式。所以用unicode-range控中文永遠(yuǎn)要把這兩個(gè)標(biāo)點(diǎn)區(qū)段帶上。5.4 消除常規(guī)誤解不是所有平臺(tái)都渲染一致字體是典型的“跟環(huán)境走”的屬性。Windows 上顯示良好的一套字體棧到 macOS 上可能因?yàn)槿鄙賹?duì)應(yīng)字體而 fallbackLinux 更是幾乎只有 DejaVu 系系統(tǒng)字體。所以工程上建議每個(gè)平臺(tái)的系統(tǒng)字體都要在棧里安排到位讓瀏覽器永遠(yuǎn)能找到“下一個(gè)”。不需要追求“所有平臺(tái)一模一樣”那不可能。追求的是每個(gè)平臺(tái)都用自己平臺(tái)最合適的那套字體。如果對(duì)字體有強(qiáng)品牌要求用 web font 并做好子集化比依賴系統(tǒng)字體更可控。我自己的習(xí)慣是在項(xiàng)目起步階段就把字體棧寫成文章里那套跨平臺(tái)標(biāo)準(zhǔn)體后續(xù)文章里提到的坑都在那里先規(guī)避掉。等團(tuán)隊(duì)穩(wěn)定之后再考慮要不要引入 web font 做品牌差異化而不是反過來先上 web font最后發(fā)現(xiàn)各個(gè)平臺(tái)崩潰再回頭補(bǔ)系統(tǒng)字體棧。最后再分享一個(gè)小技巧如果你只是想統(tǒng)一數(shù)字樣式又不想引入任何字體文件可以把font-family寫成只包含系統(tǒng)字體和一份中文兜底的組合再疊加font-variant-numeric: tabular-nums效果通常已經(jīng)足夠。很多細(xì)節(jié)問題都是在實(shí)際頁面反復(fù)橫跳之后逐漸摸清的把瀏覽器開發(fā)者工具里的字體高亮當(dāng)成第一診斷手段這套流程會(huì)幫你省下大量和樣式較勁的時(shí)間。