Next.js 内置了 next/font 组件,相比于传统使用字体的方式,使用 font 组件会更加灵活便捷。font 组件的使用主要分为两块,一块是 Google 字体,一块是本地字体,都是通过 font 组件实现,但具体配置上会略有不同。
本篇我们会先从传统使用字体的方式开始讲起,然后讲解 font 组件带来的便利和优化,最后深入细节,讲解 font 函数的具体参数,这些细节在学习的时候只用大致了解即可,在实际项目开发的时候可再具体了解。
我们先讲讲传统使用字体的方式。
最基本的方法是通过 @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;
}
字体效果如下:
Next.js 内置了 next/font 组件,帮助你更好的管理和使用字体。next/font会自动优化字体(包括自定义字体),就比如借助 CSS 的 size-adjust 属性实现零布局偏移。
布局偏移我们在 Image 组件篇讲过,除了图片不设置宽高导致布局偏移,网页字体加载的时候也容易出现布局偏移,就比如:
三行文字的 font-size都是 64px,唯一区别就是字体不同,但观察图片左侧三行文字的高度,你会发现虽然 font-size 设置的都是 64px,但实际对应的高度并不一定是 64px。这种时候,就可以借助 CSS 的 size-adjust 调整大小,保证最终都是 64px 大小。Next.js 自动做了这个优化。
除了防止布局偏移,next/font 还可以帮助你快捷使用 Google 字体,而且 CSS 和字体文件会在构建的时候下载,和其他静态资源一样被保存,浏览器也不会向 Google 发送任何请求,保证了性能和隐私性。更多功能我们会在本篇详细讲解。
next/font 具体又分为 next/font/google 和 next/font/local,分别对应使用 Google 字体和使用本地字体。我们逐一讲解。
借助 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>
)
}
最终实现的代码为:
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>
)
}
那什么是可变字体呢?所谓可变字体,引用维基百科的介绍:
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>
)
}
谷歌的字体是可以指定子集(subset)的,就比如 Roboto Mono 支持拉丁文、西里尔文和希腊文等,我们没有必要都用到,就可以使用 subsets 属性指定某个子集,还可以减少字体文件的大小并改善性能。
这些子集默认会被预加载(通过 preload 属性控制,本篇后续会讲到),如果 preload 为 true,但不指定子集会有警告。有些字体只有一个默认子集,比如 latin,也需要手动制定。
拉丁字母,又称罗马字母,指的是一套以古罗马字母为基础改造而来的成熟字母体系,最初在意大利半岛和西欧流通,在 19 世纪时扩散为全世界最通行的字母,亦是世界使用人数最多的字母,是现代绝大多数欧美国家的唯一标准字体。拉丁字母就是我们写的 26 个字母。所以很多字体的子集都有 latin。
// app/layout.js
const inter = Inter({ subsets: ['latin'] })
那怎么知道一个字体有哪些子集呢?你随便指定一个子集,如果不是有效的,Next.js 会提示你有哪些可用子集:
使用本地字体,通过 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',
},
],
})
了解了 next/font 的两种主要用法后,我们来详细的介绍下 Font 函数参数:
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
// 到底还有哪些参数呢?
})
next/font/google 和 next/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 | - |
在 next/font/local中必传,可以是字符串,也可以是对象数组(类型为:Array<{path: string, weight?: string, style?: string}>),路径地址相当于字体加载函数调用的位置。
比如 app/page.js中使用 src:'./fonts/my-font.woff2'调用字体加载函数,my-font.woff2就放置在 app/fonts/下。
字重,概念参考 font-weight。如果是可变字体,则非必传,如果不是可变字体,则必传。
值可以是字符串,如 weight: '400'、weight: '100 900'(可变字体,从 100 到 900 之间的范围),也可以是字符串数组,如 weight: ['100','400','900'](不可变字体的 3 个可能值)。
概念参考 font-style,默认值为 normal,其他值还有 italic、oblique等,参考 font-style。如果使用 next/font/google 的非可变字体,也可以传入一组样式值,如 style: ['italic','normal']。
子集的概念上节已经介绍过,就不多说了。
axes(axis 的复数形式,中文翻译:轴),与 subsets一样,只用于 next/font/google中。前面我们讲到可变字体可以自由调整字宽、字重、倾斜等,从而实现一款字体展示出多款字体的效果。字宽、字重、倾斜等就是一种“变形轴",我们以 Inter 字体为例,可以看到到 Inter 字体里不止一个变型轴:
Axes 一共有两个,slnt 和 wght 。slnt 是 slant 的意思,wght 是 weight 的意思。如果我们将这个字体下载下来然后上传到 https://wakamaifondue.com/ 这个网站解析,我们就可以在线看到不同轴数值的调整带来的不同效果:
“wght 我懂了,是字重,对应的 CSS 属性是 font-weight,那 slnt 呢?”
我们通常写 font-style CSS 属性的时候,它的值默认是 normal,除了 normal,常用的就是 italic 和 oblique了,italic 表示斜体,oblique 表示倾斜体。查看 CSS3 font-style 规范,可以得知 italic 和 oblique 都是字体的不同样式,italic 的设计初衷是斜体样式,oblique 是保持原本直立结构的斜体,我们看个例子:
我们以字体 f 为例的话,italic 的斜体更为“花哨”一点,这不是计算机能够模拟出来的,是需要作者单独设计的,而 oblique 则是在原本直立结构上让其倾斜,这个计算机可以模拟出来。
而在具体 font-style 使用的时候,如果选择 italic,没有对应的可用斜体版本,会选用倾斜体(oblique)替代。如果选择 oblique,如果没有对应的可用倾斜体版本,会选用斜体(italic)替代。如果都没有,计算机会模拟出一个倾斜体,你可以称之为仿 oblique。
回到 oblique,slnt 就是在 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 轴会被留下以减少字体文件大小,如果需要其他的轴就需要单独声明。
概念参考 font-display。默认我们加载字体的时候,使用该字体的地方会先显示空白,然后直到字体下载完成之后才会显示。CSS font-display 控制的就是这个过程。CSS font-display 的值有 'auto'、'block', 'swap'、'fallback'、 'optional',与 next/font 组件的 display 的值一样。介绍下这些值加载效果的不同:
CSS font-display 的默认值为 auto,next/font 组件的默认值为 swap。
布尔值,制定字体是否应该被预加载,默认值为 true。
字体无法被加载时的备用字体,没有默认值,字符串数组形式,如 fallback: ['system-ui', 'arial']。
对于 next/font/google,adjustFontFallback 是一个布尔值,设置是否应该使用自动备用字体以减少累积布局偏移。默认值为 true。
对于 next/font/local,adjustFontFallback 可以是字符串,也可以是 false。可能的值有 Arial、Times New Roman、false。默认值是 Arial。
Arail是经典的无衬线字体,Times New Roman是经典的衬线字体,其实就是让你选用衬线还是无衬线字体作为备用字体进行调整,当然你也可以选择不调整,那就是 false。谷歌字体之所以不用选,是因为 Next.js 自动帮你判断了。
这个属性与 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 上后可以查看到:
其中 __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>
)
}
上一节我们看到 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-override、descent-override、font-feature-settings、font-variation-settings、line-gap-override、unicode-range等,具体查看 MDN @font-face。declarations 就是为了让你进一步自定义 @font-face 的生成,使用示例如下:
declarations: [{ prop: 'ascent-override', value: '90%' }]
注意该属性只用于 next/font/local。
目前,我们讲到的添加样式的方法都是通过 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:
在这个例子中,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';
//...
}
除了 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 代码为:
CSS 变量已经介绍过,就不多说了。
你可以导入并使用多种字体,有两种方法:
第一种方法是创建一个工具函数用于导出字体,然后在需要的时候导入字体,应用 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>
)
}
最终实现的代码为:
你可以看到,声明了两个 CSS 变量,--font-roboto-mono 和 --font-inter,当你需要为字体添加样式的时候,直接使用该变量即可:
// app/global.css
html {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-roboto-mono);
}
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-ma、font-mono)为元素添加样式:
// page.js
export default function Page() {
return <h1 className="font-ma underline">你好,世界!Hello World!</h1>
}
每次调用字体函数的时候,该字体都会作为一个实例被托管,所以如果多个地方使用同一个字体,还是应该在一个地方加载,然后按需导入。这就是字体定义文件的作用。
举个例子,在根目录下的 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.json 或 jsconfig.json 中定义路径别名:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/fonts": ["./styles/fonts"]
}
}
}
现在你可以这样使用:
// app/about/page.js
import { greatVibes, sourceCodePro400 } from '@/fonts'