JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第19章—组件篇Font

首次发表于 2024-03-22, 更新于 2024-03-22

前言

Next.js 内置了 next/font 组件,相比于传统使用字体的方式,使用 font 组件会更加灵活便捷。font 组件的使用主要分为两块,一块是 Google 字体,一块是本地字体,都是通过 font 组件实现,但具体配置上会略有不同。

本篇我们会先从传统使用字体的方式开始讲起,然后讲解 font 组件带来的便利和优化,最后深入细节,讲解 font 函数的具体参数,这些细节在学习的时候只用大致了解即可,在实际项目开发的时候可再具体了解。

1. 传统使用字体

我们先讲讲传统使用字体的方式。

最基本的方法是通过 @font-face指定一个自定义字体,字体文件可以来自远程文件,也可以来自本地文件。然后在 font-family 中使用该字体。

// global.css
@font-face {
  font-family: "Bitstream Vera Serif Bold";
  src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf");
}

body {
  font-family: "Bitstream Vera Serif Bold", serif;
}

借助 Google Fonts 这样的字体网站,我们可以快速生成样式文件,再通过 link 标签或者 @import 的方式直接使用。

使用 link 标签:

// layout.js
export default function Layout({ children }) {
  return (
    <html>
      <head>
        <link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap" rel="stylesheet" />
      </head>
      <body>
        {children}
      </body>
    </html>
  )
}
// globals.css
body {
  font-family: "Ma Shan Zheng", serif;
}

使用 @import

// globals.css
@import url('https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap');

body {
  font-family: "Ma Shan Zheng", serif;
}

字体效果如下:

image.png

2. next/font 与布局偏移

Next.js 内置了 next/font 组件,帮助你更好的管理和使用字体。next/font会自动优化字体(包括自定义字体),就比如借助 CSS 的 size-adjust 属性实现零布局偏移。

布局偏移我们在 Image 组件篇讲过,除了图片不设置宽高导致布局偏移,网页字体加载的时候也容易出现布局偏移,就比如:

image.png

三行文字的 font-size都是 64px,唯一区别就是字体不同,但观察图片左侧三行文字的高度,你会发现虽然 font-size 设置的都是 64px,但实际对应的高度并不一定是 64px。这种时候,就可以借助 CSS 的 size-adjust 调整大小,保证最终都是 64px 大小。Next.js 自动做了这个优化。

除了防止布局偏移,next/font 还可以帮助你快捷使用 Google 字体,而且 CSS 和字体文件会在构建的时候下载,和其他静态资源一样被保存,浏览器也不会向 Google 发送任何请求,保证了性能和隐私性。更多功能我们会在本篇详细讲解。

next/font 具体又分为 next/font/googlenext/font/local,分别对应使用 Google 字体和使用本地字体。我们逐一讲解。

3. next/font/google

3.1. 使用示例

借助 next/font/google,我们不需要像以前一样到 Google Fonts 复制样式文件的链接,并通过 link 或者 import 导入,而是可以直接导入想要使用的字体。使用示例如下:

// app/layout.js
// 1. 导入想要使用的字体
import { Inter } from 'next/font/google'

// 2. 实例化字体对象,设置使用子集等
const inter = Inter({
  subsets: ['latin']
})

//  3. 应用,inter.className 会返回一个只读的 CSS 类名用于加载字体
export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

最终实现的代码为:

image.png

Next.js 推荐使用可变字体来获得最佳的性能和灵活性。如果不能使用可变字体,你需要声明 weight(字重,是指字体的粗细程度):

// app/layout.js
import { Roboto } from 'next/font/google'
 
const roboto = Roboto({
  weight: '400',
  subsets: ['latin']
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="en" className={roboto.className}>
      <body>{children}</body>
    </html>
  )
}

3.2. 可变字体

那什么是可变字体呢?所谓可变字体,引用维基百科的介绍:

OpenType 可变字体(英语:OpenType variable fonts)是字体格式 OpenType 在 1.8 版规范中引入的扩展规范,由苹果、微软、谷歌和 Adobe 联合开发,于 2016 年 9 月 14日 正式发布。支持这一规范的计算机字体可以储存轮廓变化数据,在初始字形轮廓的基础上自动生成丰富的变化造型,使用户可以自由调整文字的外观。

简单的来说,可变字体可以自由调整字宽、字重、倾斜等,从而实现一款字体展示出多款字体的效果。Next.js 推荐使用可变字体。

你也可以使用数组指定多个 weight、样式:

// app/layout.js
const roboto = Roboto({
  weight: ['400', '700'],
  style: ['normal', 'italic'],
  subsets: ['latin'],
  display: 'swap',
})

如果字体是多单词,使用下划线(_)连接,比如 Roboto Mono,导入的时候写成 Roboto_Mono

// app/layout.js
import { Ma_Shan_Zheng } from 'next/font/google'

const font = Ma_Shan_Zheng({
  subsets: ['latin'],
  weight: '400'
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={font.className}>
      <body>{children}</body>
    </html>
  )
}

3.3. 指定子集

谷歌的字体是可以指定子集(subset)的,就比如 Roboto Mono 支持拉丁文、西里尔文和希腊文等,我们没有必要都用到,就可以使用 subsets 属性指定某个子集,还可以减少字体文件的大小并改善性能。

这些子集默认会被预加载(通过 preload 属性控制,本篇后续会讲到),如果 preloadtrue,但不指定子集会有警告。有些字体只有一个默认子集,比如 latin,也需要手动制定。

拉丁字母,又称罗马字母,指的是一套以古罗马字母为基础改造而来的成熟字母体系,最初在意大利半岛和西欧流通,在 19 世纪时扩散为全世界最通行的字母,亦是世界使用人数最多的字母,是现代绝大多数欧美国家的唯一标准字体。拉丁字母就是我们写的 26 个字母。所以很多字体的子集都有 latin。

// app/layout.js
const inter = Inter({ subsets: ['latin'] })

那怎么知道一个字体有哪些子集呢?你随便指定一个子集,如果不是有效的,Next.js 会提示你有哪些可用子集:

image.png

4. next/font/local

使用本地字体,通过 next/font/local并使用 src声明本地文件的地址。Next.js 依然推荐使用可变字体。使用示例如下:

// app/layout.js
import localFont from 'next/font/local'
 
const myFont = localFont({
  src: './my-font.woff2',
  display: 'swap',
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="en" className={myFont.className}>
      <body>{children}</body>
    </html>
  )
}

src 也可以是数组形式,比如一个字体使用多个本地文件:

onst roboto = localFont({
  src: [
    {
      path: './Roboto-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './Roboto-Italic.woff2',
      weight: '400',
      style: 'italic',
    },
    {
      path: './Roboto-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
    {
      path: './Roboto-BoldItalic.woff2',
      weight: '700',
      style: 'italic',
    },
  ],
})

5. Font 函数参数

了解了 next/font 的两种主要用法后,我们来详细的介绍下 Font 函数参数:

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  // 到底还有哪些参数呢?
})

next/font/googlenext/font/local略有不同,这是比较表:

Key font/google font/local 类型 必传
src String or Array of Objects
weight String or Array 看情况
style String or Array -
subsets Array of Strings -
axes Array of Strings -
display String -
preload Boolean -
fallback Array of Strings -
adjustFontFallback Boolean or String -
variable String -
declarations Array of Objects -

5.1. src 参数

next/font/local中必传,可以是字符串,也可以是对象数组(类型为:Array<{path: string, weight?: string, style?: string}>),路径地址相当于字体加载函数调用的位置。

比如 app/page.js中使用 src:'./fonts/my-font.woff2'调用字体加载函数,my-font.woff2就放置在 app/fonts/下。

5.2. weight

字重,概念参考 font-weight。如果是可变字体,则非必传,如果不是可变字体,则必传。

值可以是字符串,如 weight: '400'weight: '100 900'(可变字体,从 100 到 900 之间的范围),也可以是字符串数组,如 weight: ['100','400','900'](不可变字体的 3 个可能值)。

5.3. style

概念参考 font-style,默认值为 normal,其他值还有 italicoblique等,参考 font-style。如果使用 next/font/google 的非可变字体,也可以传入一组样式值,如 style: ['italic','normal']

5.4. subsets

子集的概念上节已经介绍过,就不多说了。

5.5. axes

axes(axis 的复数形式,中文翻译:轴),与 subsets一样,只用于 next/font/google中。前面我们讲到可变字体可以自由调整字宽、字重、倾斜等,从而实现一款字体展示出多款字体的效果。字宽、字重、倾斜等就是一种“变形轴",我们以 Inter 字体为例,可以看到到 Inter 字体里不止一个变型轴:

image.png

Axes 一共有两个,slntwghtslntslant 的意思,wghtweight 的意思。如果我们将这个字体下载下来然后上传到 https://wakamaifondue.com/ 这个网站解析,我们就可以在线看到不同轴数值的调整带来的不同效果:

image.png

wght 我懂了,是字重,对应的 CSS 属性是 font-weight,那 slnt 呢?”

我们通常写 font-style CSS 属性的时候,它的值默认是 normal,除了 normal,常用的就是 italicoblique了,italic 表示斜体,oblique 表示倾斜体。查看 CSS3 font-style 规范,可以得知 italic 和 oblique 都是字体的不同样式,italic 的设计初衷是斜体样式,oblique 是保持原本直立结构的斜体,我们看个例子:

image.png

我们以字体 f 为例的话,italic 的斜体更为“花哨”一点,这不是计算机能够模拟出来的,是需要作者单独设计的,而 oblique 则是在原本直立结构上让其倾斜,这个计算机可以模拟出来。

而在具体 font-style 使用的时候,如果选择 italic,没有对应的可用斜体版本,会选用倾斜体(oblique)替代。如果选择 oblique,如果没有对应的可用倾斜体版本,会选用斜体(italic)替代。如果都没有,计算机会模拟出一个倾斜体,你可以称之为仿 oblique。

回到 obliqueslnt 就是在 oblique 样式中控制倾斜程度的轴,所以你拖动 slnt 这个轴,字体会在不断的倾斜程度中变化。以后可能还会遇到其他轴,这是一个对应表:

名称 对应 CSS
wght Weight font-weight
wdth Width font-stretch
ital, slnt Italic, Slant font-style
opsz Optical Size font-optical-sizing

axes 是一个字符串数组形式,比如 axes: ['slnt'],你可以在Google 可变字体页面查询字体的 Axes 有哪些。之所以需要声明,是因为默认情况下,只有 weight 轴会被留下以减少字体文件大小,如果需要其他的轴就需要单独声明。

5.6 display

概念参考 font-display。默认我们加载字体的时候,使用该字体的地方会先显示空白,然后直到字体下载完成之后才会显示。CSS font-display 控制的就是这个过程。CSS font-display 的值有 'auto''block', 'swap''fallback''optional',与 next/font 组件的 display 的值一样。介绍下这些值加载效果的不同:

  1. auto:使用浏览器的预设值,一般是 block
  2. block:先显示空白(3s),然后切换为备用字体,等字体加载完毕后切换
  3. swap:先显示备用字体,等字体加载完成后切换
  4. fallback:先显示空白(大约 100ms),然后切换为备用字体,时间大概是 3s,3s 内能加载完字体,就使用字体,3s 内加载不完,后续接着使用备用字体
  5. optional:先显示空白(大约 100ms),100ms 内能加载完就用,加载不完就直接使用备用字体

CSS font-display 的默认值为 autonext/font 组件的默认值为 swap

5.7. preload

布尔值,制定字体是否应该被预加载,默认值为 true

5.8. fallback

字体无法被加载时的备用字体,没有默认值,字符串数组形式,如 fallback: ['system-ui', 'arial']

5.9. adjustFontFallback

对于 next/font/googleadjustFontFallback 是一个布尔值,设置是否应该使用自动备用字体以减少累积布局偏移。默认值为 true

对于 next/font/localadjustFontFallback 可以是字符串,也可以是 false。可能的值有 ArialTimes New Romanfalse。默认值是 Arial

Arail是经典的无衬线字体,Times New Roman是经典的衬线字体,其实就是让你选用衬线还是无衬线字体作为备用字体进行调整,当然你也可以选择不调整,那就是 false。谷歌字体之所以不用选,是因为 Next.js 自动帮你判断了。

5.10. variable

这个属性与 CSS 变量有关,我们先简单复习一下 CSS 变量的概念。

CSS 变量由开发者自己定义,先通过自定义属性标记设定值(比如: --main-color: black;),然后由 var() 函数来获取值(比如: color: var(--main-color);)。

好处有两个,一是方便重复使用,比如一个色值可能在多个地方用到,如果发生变化,就需要全局搜索替换,使用 CSS 变量,只用更改变量的值即可。二是语义化,比如,--main-text-color 会比 #00ff00 更易理解。自定义属性受级联的约束,并从其父级继承其值。

比如你在 html 元素上声明一个 CSS 变量:

html {
  --main-bg-color: brown;
}

需要用到该色值的元素可以直接使用:

p {
  background-color: var(--main-bg-color);
}

使用 next/font 如何声明一个 CSS 变量呢?便是借助 variable 属性:

// app/page.js
import { Inter } from 'next/font/google'
import styles from '../styles/component.module.css'
 
const inter = Inter({
  variable: '--font-inter',
})

此时我们就建立了一个 CSS 变量 --font-inter,它的具体值在添加到 HTML 上后可以查看到:

image.png

其中 __Inter_a64ecd__Inter_Fallback_a64ecd 对应的是 Next.js 自动生成的自定义字体名字:

/* latin */
@font-face {
  font-family: '__Inter_a64ecd';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2) format('woff2');
  //...
}

@font-face {
  font-family: '__Inter_Fallback_a64ecd';
  src: local("Arial");
  ascent-override: 90.20%;
  descent-override: 22.48%;
  line-gap-override: 0.00%;
  size-adjust: 107.40%
}

所以 --font-inter: '__Inter_a64ecd', '__Inter_Fallback_a64ecd'; 的意思就是声明了两个自定义字体。

PS:如果你不想要有 __Inter_Fallback_a64ecd,只有 __Inter_a64ecd,设置 adjustFontFallback为 false。

现在只是声明,我们还需要通过 var() 函数使用,将两个字体放到 font-family 属性中:

// styles/component.module.css
.text {
  font-family: var(--font-inter);
  font-weight: 200;
  font-style: italic;
}

最后一步,将声明添加到父元素,将自定义的 text 样式添加到子元素,这样子元素才可以获取到父元素中声明的变量:

// app/page.js
<main className={inter.variable}>
  <p className={styles.text}>Hello World</p>
</main>

有的时候,为了方便,会直接将声明添加到 HTML 元素上,这样所有的元素都可以使用该声明:

// layout.js
import './globals.css' 
import { Inter } from 'next/font/google'


const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable}`}>
      <body>{children}</body>
    </html>
  )
}

5.11. declarations

上一节我们看到 Next.js 自动生成的 @font-face 的内容:

@font-face {
  font-family: '__Inter_a64ecd';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2) format('woff2');
  //...
}

但其实@font-face下的属性还有很多,有很多我们并不熟悉的如 ascent-overridedescent-overridefont-feature-settingsfont-variation-settingsline-gap-overrideunicode-range等,具体查看 MDN @font-facedeclarations 就是为了让你进一步自定义 @font-face 的生成,使用示例如下:

declarations: [{ prop: 'ascent-override', value: '90%' }]

注意该属性只用于 next/font/local

6. 应用样式的三种方法

6.1. className

目前,我们讲到的添加样式的方法都是通过 className属性:

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin']
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.className}`}>
      <body>{children}</body>
    </html>
  )
}

当你读取字体对象的 className 属性时,会返回一个只读的 CSS className

image.png

在这个例子中,inter.className 返回的值为 __className_a64ecd,Next.js 对应在自动生成的 layout.css 中设置的样式为:

.__className_a64ecd {
    font-family: '__Inter_a64ecd', '__Inter_Fallback_a64ecd';
    font-style: normal;
}

@font-face {
  font-family: '__Inter_a64ecd';
  //...
}

@font-face {
  font-family: '__Inter_Fallback_a64ecd';
  //...
}

6.2. style

除了 className,还可以使用 style,它会返回一个只读的 style 对象,示例代码如下:

// layout.js
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin']
})

export default function RootLayout({ children }) {
  return (
    <html style={inter.style}>
      <body>{children}</body>
    </html>
  )
}

生成的 HTML 代码为:

image.png

6.3. CSS 变量

CSS 变量已经介绍过,就不多说了。

7. 常见问题

7.1. 使用多种字体

你可以导入并使用多种字体,有两种方法:

第一种方法是创建一个工具函数用于导出字体,然后在需要的时候导入字体,应用 className。这可以保证只有在使用它时候才预加载字体。

导出两个字体对象:

// app/fonts.js
import { Inter, Roboto_Mono } from 'next/font/google'
 
export const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})
 
export const roboto_mono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
})

在需要的时候导入并使用:

// app/layout.js
import { inter } from './fonts'
 
export default function Layout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>
        <div>{children}</div>
      </body>
    </html>
  )
}
// app/page.js
import { roboto_mono } from './fonts'
 
export default function Page() {
  return (
    <>
      <h1 className={roboto_mono.className}>My page</h1>
    </>
  )
}

第二种方法是,创建一个 CSS 变量,可以跟你喜欢的 CSS 方案一起使用,举个例子:

// app/layout.js
import { Inter, Roboto_Mono } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})
 
const roboto_mono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-roboto-mono',
  display: 'swap',
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
      <body>
        <h1>My App</h1>
        <div>{children}</div>
      </body>
    </html>
  )
}

最终实现的代码为:

image.png

你可以看到,声明了两个 CSS 变量,--font-roboto-mono--font-inter,当你需要为字体添加样式的时候,直接使用该变量即可:

// app/global.css
html {
  font-family: var(--font-inter);
}
 
h1 {
  font-family: var(--font-roboto-mono);
}

7.2. 搭配 Tailwind CSS

next/font 可以通过 CSS 变量的形式与 Tailwind CSS 搭配使用。

首先通过 variable声明 CSS 变量:

// app/layout.js 
import './globals.css' 
import { Ma_Shan_Zheng, Roboto_Mono } from 'next/font/google'


const ma_shan_zheng = Ma_Shan_Zheng({
  subsets: ['latin'],
  display: 'swap',
  weight: '400',
  variable: '--font-ma-shan-zheng',
})
 
const roboto_mono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${ma_shan_zheng.variable} ${roboto_mono.variable}`}>
      <body>{children}</body>
    </html>
  )
}
// globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

然后,将 CSS 变量添加到 Tailwind CSS 配置中:

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      fontFamily: {
        "ma": ['var(--font-ma-shan-zheng)'],
        "mono": ['var(--font-roboto-mono)'],
      },
    },
  },
  plugins: [],
}

最后,以 font- 作为前缀如(font-mafont-mono)为元素添加样式:

// page.js
export default function Page() {
  return <h1 className="font-ma underline">你好,世界!Hello World!</h1>
}

7.3. 使用字体定义文件

每次调用字体函数的时候,该字体都会作为一个实例被托管,所以如果多个地方使用同一个字体,还是应该在一个地方加载,然后按需导入。这就是字体定义文件的作用。

举个例子,在根目录下的 styles 文件夹下创建一个 fonts.ts文件,然后声明字体定义:

// styles/fonts.js
import { Inter, Lora, Source_Sans_3 } from 'next/font/google'
import localFont from 'next/font/local'
 

const inter = Inter()
const lora = Lora()

const sourceCodePro400 = Source_Sans_3({ weight: '400' })
const sourceCodePro700 = Source_Sans_3({ weight: '700' })

const greatVibes = localFont({ src: './GreatVibes-Regular.ttf' })
 
export { inter, lora, sourceCodePro400, sourceCodePro700, greatVibes }

现在你可以在代码中使用这些定义:

// app/page.js
import { inter, lora, sourceCodePro700, greatVibes } from '../styles/fonts'
 
export default function Page() {
  return (
    <div>
      <p className={inter.className}>Hello world using Inter font</p>
      <p style={lora.style}>Hello world using Lora font</p>
      <p className={sourceCodePro700.className}>
        Hello world using Source_Sans_3 font with weight 700
      </p>
      <p className={greatVibes.className}>My title in Great Vibes font</p>
    </div>
  )
}

为了更轻松的访问字体定义文件,你可以在 tsconfig.jsonjsconfig.json 中定义路径别名:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/fonts": ["./styles/fonts"]
    }
  }
}

现在你可以这样使用:

// app/about/page.js
import { greatVibes, sourceCodePro400 } from '@/fonts'

参考链接

  1. Components: Font
  2. Optimizing: Fonts
  3. https://web.dev/articles/css-size-adjust?hl=zh-cn
  4. https://developers.google.com/fonts/docs/getting_started?hl=zh-cn#specifying_script_subsets
  5. https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
  6. 可变字体(Variable Fonts)尝鲜
  7. https://fonts.google.com/knowledge/glossary/oblique
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru