4、@font-face 规则:从声明到精准控制

好,咱们进入 @font-face 的世界。这玩意儿是自定义字体的基石,说白了就是告诉浏览器:「嘿,我这里有个字体文件,你按我给的规则去加载和使用。」

我在项目里见过太多人直接把 @font-face 当模板复制粘贴,结果出了问题一脸懵。今天咱们把它拆开揉碎了讲清楚。

4.1 @font-face 语法详解

先看一个最完整的声明结构:

@font-face {
  font-family: 'MyCustomFont';
  src: url('fonts/myfont.woff2') format('woff2'),
       url('fonts/myfont.woff') format('woff'),
       local('My Custom Font'),
       local('MyCustomFont-Regular');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0025-00FF;
}

每个属性都有它的使命。我习惯把 @font-face 想象成一份「字体简历」——告诉浏览器这个字体的名字、它在哪、什么情况下能用。

属性 作用 是否必填
font-family 定义字体的引用名称 是
src 指定字体文件来源 是
font-weight 声明该字体的字重 推荐
font-style 声明该字体的样式 推荐
font-display 控制字体加载时的渲染行为 推荐
unicode-range 限定字体覆盖的字符范围 可选
注意: font-family 的名字可以随便起,但必须和后续 CSS 中引用的名字完全一致。我曾经见过有人声明时叫 'PingFang SC',引用时写成 'PingFangSC',结果字体死活不生效。

4.2 src 属性与 local() 函数

src 是 @font-face 里最核心的属性。它告诉浏览器去哪里找字体文件。这里有个优先级问题——浏览器会按你写的顺序依次尝试。

4.2.1 url() 与 format()

url() 指向远程字体文件,format() 提示浏览器文件格式。我建议永远把 woff2 放在最前面:

src: url('fonts/roboto.woff2') format('woff2'),
     url('fonts/roboto.woff') format('woff');

为什么?因为 woff2 压缩率最高,体积最小。浏览器如果支持 woff2,就直接下载它,跳过后面的 woff。不支持?那就继续尝试 woff。这叫「渐进增强」。

4.2.2 local() 函数

local() 是个容易被忽略但很有用的东西。它检查用户本地是否已经安装了某个字体。如果装了,就直接用本地的,省去一次网络请求。

src: local('Roboto Regular'),
     local('Roboto-Regular'),
     url('fonts/roboto.woff2') format('woff2');

嗯,这里要注意:local() 里的名字必须和系统里注册的字体名完全一致。Windows 和 macOS 的字体命名规则还不一样。我踩过这个坑——在 Mac 上写 local('PingFang SC') 没问题,但 Windows 上可能得写成 local('Microsoft YaHei')。

我的习惯: 先写 local(),再写 url()。这样用户本地有字体就直接用,没有才去下载。对于中文字体这种动不动几 MB 的大家伙,这个优化特别明显。

4.3 font-display 属性

这个属性控制的是「字体还没加载完时,页面怎么显示文字」。说白了就是解决 FOIT(Flash of Invisible Text)和 FOUT(Flash of Unstyled Text)的问题。

值 行为 适用场景
auto 浏览器默认行为(通常等同于 block) 不推荐,交给浏览器赌运气
block 先隐藏文字,等字体加载完再显示 图标字体、品牌字体(必须精确展示)
swap 先用后备字体显示,字体加载完再替换 正文内容、博客文章(优先可读性)
fallback 短时间隐藏,加载不完就用后备字体 对字体有要求但可妥协的场景
optional 极短时间隐藏,加载不完就放弃 弱网环境、装饰性字体

我个人最常用 swap。你想想看,用户打开页面,文字先显示出来让他能读,等字体加载完了再优雅替换——这才是用户体验优先的做法。

避坑指南: 我曾经在一个电商项目里用了 block,结果用户网络差的时候,整个商品标题区域空白了 3 秒。转化率直接掉了 5%。从那以后,正文内容我坚决用 swap。

4.4 unicode-range 应用

unicode-range 是个高级玩法。它让你精确控制某个 @font-face 规则只对特定字符生效。

/* 只对英文和数字使用 Roboto */
@font-face {
  font-family: 'MyFont';
  src: url('roboto.woff2') format('woff2');
  unicode-range: U+0020-007E;
}

/* 只对中文使用 Noto Sans SC */
@font-face {
  font-family: 'MyFont';
  src: url('notosanssc.woff2') format('woff2');
  unicode-range: U+4E00-9FFF;
}

这样做的好处很明显:英文用西文字体,中文用中文字体,各自只加载需要的字符集。中文字体文件通常很大,如果只加载几千个常用汉字,体积能小很多。

常见的 Unicode 范围:

范围 含义
U+0020-007E 基本拉丁字母(英文、数字、标点)
U+4E00-9FFF CJK 统一表意文字(常用汉字)
U+FF00-FFEF 全角字符(中文标点、全角字母)
U+3000-303F CJK 符号和标点
注意: 使用 unicode-range 时,多个 @font-face 的 font-family 必须相同,否则浏览器会当成不同字体处理。我见过有人把两个规则写成不同名字,结果一个都没生效。

下面这张图帮你理清 @font-face 的核心逻辑:

@font-face 核心逻辑图 @font-face 核心逻辑 @font-face 声明 src 字体来源 font-display 策略 unicode-range 范围 url() + format() local() 本地检测 swap / block / fallback U+4E00-9FFF 等范围 核心原则 先 local() 检测本地字体 → 再 url() 下载远程字体 font-display 控制加载体验,unicode-range 精确限定字符范围

总结一下:@font-face 的核心就是「声明—来源—策略—范围」四个维度。声明字体名字,告诉浏览器去哪找,决定加载时的表现,限定覆盖的字符。把这四点吃透了,自定义字体这块你就拿捏住了。