样式与静态资源 — CodeRoadMap样式与静态资源
Tailwind、Image/Font 优化。
样式与静态资源
这一课你将能做到
- 在 Next.js 中正确使用全局样式、CSS Modules、Tailwind CSS 三种方案
- 用
next/font 加载 Google Fonts 和本地字体,避免布局偏移
- 用
next/image 实现图片的自动优化、懒加载、占位符
- 用
public/ 目录管理静态资源,并理解其缓存行为
- 处理 SVG、视频、PDF 等非图片静态资源
为什么要学
样式和资源处理是前端性能的隐形杀手。一张未压缩的 5MB PNG、一个阻塞渲染的 Google Fonts 链接、一次意外的布局偏移(CLS),都能让 Lighthouse 分数掉 20 分。Next.js 把这些优化做成了「默认就该用」的内置组件:<Image> 自动压缩裁剪转 WebP,next/font 自动下载字体文件并内联关键 CSS,CSS Modules 自动解决类名冲突。
人话:你自己手动优化图片字体,就像自己手磨咖啡——能喝但费事。Next.js 的内置优化是全自动咖啡机,按一下按钮,压缩、格式转换、缓存策略全给你配好。
先建立图像
样式方案选择:
├── 全局样式(globals.css) ← reset、变量、第三方库样式
├── CSS Modules(*.module.css) ← 组件级样式,类名自动哈希
├── Tailwind CSS ← 原子化 CSS,utility-first
└── 内联样式(style={{}}) ← 动态值、简单覆盖
静态资源路径:
public/images/logo.png → 访问 /images/logo.png
app/icon.png → 访问 /icon(favicon)
app/opengraph-image.png → 社交分享卡片图
跟着做
步骤 1:全局样式
app/globals.css:
:root {
--primary: #0070f3;
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
}
在 app/layout.tsx 中导入:
import './globals.css';
全局样式只能在这里导入一次,不能在组件里重复导入。
步骤 2:CSS Modules
app/components/Button.module.css:
.button {
padding: 8px 16px;
border: none;
border-radius: 6px;
background: var(--primary);
color: white;
cursor: pointer;
}
.button:hover {
opacity: 0.9;
}
.danger {
background: #e00;
}
app/components/Button.tsx:
import styles from './Button.module.css';
interface Props {
children: React.ReactNode;
danger?: boolean;
}
export default function Button({ children, danger }: Props) {
const className = danger ? `${styles.button} ${styles.danger}` : styles.button;
return <button className={className}>{children}</button>;
}
构建后类名变成 Button_button__abc123、Button_danger__def456,天然解决命名冲突。
步骤 3:Tailwind CSS
create-next-app 时选了 Tailwind 就自动配置好了。如果没选,手动装:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: '#0070f3',
},
},
},
plugins: [],
};
export default config;
@tailwind base;
@tailwind components;
@tailwind utilities;
export default function Card() {
return (
<div className="rounded-lg border border-gray-200 p-6 shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-xl font-semibold text-gray-900">卡片标题</h2>
<p className="mt-2 text-gray-600">卡片内容描述</p>
<button className="mt-4 rounded bg-primary px-4 py-2 text-white hover:bg-blue-700">
按钮
</button>
</div>
);
}
步骤 4:字体优化
import { Inter, Noto_Sans_SC } from 'next/font/google';
import './globals.css';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
});
const notoSansSC = Noto_Sans_SC({
subsets: ['latin'],
weight: ['400', '500', '700'],
variable: '--font-noto-sans-sc',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" className={`${inter.variable} ${notoSansSC.variable}`}>
<body>{children}</body>
</html>
);
}
body {
font-family: var(--font-noto-sans-sc), var(--font-inter), system-ui, sans-serif;
}
next/font 在构建时下载字体文件,自托管,避免外部请求阻塞渲染,同时自动处理 font-display: swap。
import localFont from 'next/font/local';
const myFont = localFont({
src: './fonts/my-font.woff2',
variable: '--font-my-font',
});
步骤 5:图片优化
import Image from 'next/image';
export default function GalleryPage() {
return (
<main style={{ padding: 40 }}>
<h1>图片画廊</h1>
{/* 本地图片,放在 public/images/ */}
<Image
src="/images/photo1.jpg"
alt="风景照"
width={800}
height={600}
priority // 首屏图片,预加载
/>
{/* 远程图片,需要配置域名 */}
<Image
src="https://picsum.photos/800/600"
alt="随机图"
width={800}
height={600}
placeholder="blur" // 模糊占位
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
</main>
);
}
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'picsum.photos',
},
],
},
};
export default nextConfig;
- 根据设备尺寸生成多分辨率 srcset
- 自动转 WebP/AVIF(浏览器支持时)
- 懒加载(
loading="lazy" 默认)
- 防止布局偏移(width/height 或 fill 模式)
步骤 6:public 目录与静态资源
public/
├── images/
│ └── logo.png → /images/logo.png
├── fonts/
│ └── custom.woff2 → /fonts/custom.woff2
├── favicon.ico → /favicon.ico
├── robots.txt → /robots.txt
└── sitemap.xml → /sitemap.xml
export default function Header() {
return (
<header>
<img src="/images/logo.png" alt="Logo" width={120} height={40} />
{/* 或直接用字符串路径 */}
<link rel="icon" href="/favicon.ico" />
</header>
);
}
public/ 的文件直接以根路径对外服务,不经过构建优化。适合:
- 不需要优化的静态文件(robots.txt、sitemap.xml)
- 已经优化过的第三方资源
- 需要保持原始路径的遗留资源
注意:public/ 的文件没有缓存优化,Next.js 不会自动加 hash。需要长期缓存的文件(如带版本号的 JS/CSS),应该放在 public/ 下但文件名自己带版本,如 app.v1.2.3.js。
讲清楚要点
- 全局样式只导入一次:在根 layout 导入,不能在组件里重复导入,否则热更新时样式会重复。
- CSS Modules 是默认推荐:类名自动哈希,无冲突;支持
:global() 写全局覆盖;支持 composes 组合。
- Tailwind 是运行时零成本的:构建时 purge 未使用的类,最终 CSS 只包含用到的 utility。
- next/font 的核心价值:自托管字体(无外部请求)、自动
font-display: swap、自动子集化(subsetting)、CSS 变量注入。
<Image> 必须给尺寸:width + height 或 fill + 父容器 position: relative。不给尺寸会导致布局偏移(CLS)。
- 远程图片必须白名单:
next.config.ts 里的 remotePatterns 是安全限制,防止任意域名的图片被代理。
- SVG 处理两种方式:
import Logo from './logo.svg'(作为 React 组件,可传 props)或 <img src="/logo.svg">(作为静态文件)。SVGR 需要额外配置。
public/ 的缓存策略:文件内容不变时浏览器缓存,但 Next.js 不自动加 hash。需要长期缓存的文件自己管理版本号。
你可能会懵的地方
-
CSS Modules 和 Tailwind 能混用吗?
能。Tailwind 处理布局和通用样式,CSS Modules 处理复杂组件状态(hover、active、动画)。但尽量统一,避免心智负担。
-
Tailwind 的 @apply 推荐用吗?
不推荐。@apply 会把 utility 组合成自定义类,但失去了 Tailwind「所见即所得」的优势,而且增加 CSS 体积。复杂样式用 CSS Modules。
-
next/font 的字体文件放哪?
Google Fonts 由 next/font 自动下载到 .next/static/media/;本地字体放 app/fonts/ 或 public/fonts/,用 localFont 或 @font-face 引入。
-
<Image> 的 fill 模式怎么用?
父容器设 position: relative 和固定宽高,<Image fill style={{ objectFit: 'cover' }} />。适合响应式图片、背景图。
-
placeholder="blur" 的 blurDataURL 怎么生成?
手动生成:用 plaiceholder 或在线工具把图片转 base64 模糊图。自动生成:服务端渲染时动态生成,或构建时脚本批量处理。
-
public/ 和 app/ 下的 icon.png 冲突吗?
不冲突。app/icon.png 是 Next.js 的元数据约定文件,自动生成 favicon;public/favicon.ico 是静态文件。两者同时存在时,app/icon.png 优先级更高。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 在组件里导入 globals.css | 样式重复、热更新异常 | 只在根 layout 导入一次 |
<Image> 不给 width/height | 布局偏移,CLS 飙升 | 给尺寸或用 fill 模式 |
| 远程图片不配置 remotePatterns | 运行时报错 | 在 next.config.ts 里配置白名单 |
用 <img> 替代 <Image> | 无优化、无懒加载、无 WebP | 一律用 <Image> |
Tailwind 里写大量 @apply | 失去 utility 优势,CSS 膨胀 | 复杂组件用 CSS Modules |
public/ 放未压缩的 5MB PNG | 无优化,直接服务原图 | 压缩后放,或放 app/ 下用 <Image> |
在 CSS Modules 里写 !important | 破坏样式层叠 | 提高选择器特异性,或用 :global 覆盖 |
卡点急救
- Tailwind 类不生效:检查
tailwind.config.ts 的 content 数组是否包含当前文件路径;检查 globals.css 是否导入了 @tailwind 指令;重启 dev server。
- 字体加载后布局跳动:
next/font 已经处理了大部分情况,如果还有跳动,检查是否有未使用 next/font 的自定义 @font-face。
<Image> 报 hostname not configured:在 next.config.ts 的 images.remotePatterns 里添加域名。
- CSS Modules 类名冲突:CSS Modules 天然防冲突,如果冲突说明用了
:global() 或全局样式。检查是否误用了 :global(.my-class)。
- 生产环境图片 404:
public/ 的文件在构建时复制到 .next/static,但路径不变。确认文件确实在 public/ 下,且路径以 / 开头。
面试常问
-
Next.js 的图片优化具体做了什么?
① 响应式 srcset:根据设备 DPR 和视口生成多尺寸;② 格式转换:自动转 WebP/AVIF;③ 懒加载:进入视口才加载;④ 模糊占位:placeholder="blur" 防止布局跳动;⑤ 缓存:优化后的图片带 hash,长期缓存。
-
next/font 比传统 <link> 加载字体好在哪里?
传统方式:外部请求阻塞渲染、字体文件未压缩、无子集化、可能布局偏移。next/font:构建时下载自托管、自动子集化、自动 font-display: swap、CSS 变量注入、无外部请求。
-
CSS Modules 和 Tailwind 的选型标准?
项目小、设计系统简单、团队熟悉 utility-first → Tailwind;组件复杂、状态多、需要精细控制 → CSS Modules;两者可混用,但要有规范。
-
public/ 的缓存问题怎么解决?
public/ 文件不经过构建,没有自动 hash。方案:① 文件名手动带版本号 logo.v2.png;② 用 <Image> 处理(会加 hash);③ 需要强缓存的文件放 CDN。
-
SVG 在 Next.js 里怎么用最方便?
小图标:内联 SVG 或 dangerouslySetInnerHTML;需要样式控制的图标:SVGR 转 React 组件(import Logo from './logo.svg');静态文件:放 public/ 用 <img> 或 <Image>。
自测三问
next/font 加载 Google Fonts 时,字体文件最终存在哪里?为什么比 <link> 方式快?
<Image> 的 fill 模式和 width/height 模式分别适用于什么场景?
- 为什么
public/ 下的文件没有自动缓存 hash?怎么解决这个问题?
小结一句话
Next.js 的样式和资源优化是「约定优于配置」的典范:全局样式进 layout、组件样式用 CSS Modules、utility 用 Tailwind、字体交给 next/font、图片交给 <Image>——按约定写,性能优化自动完成。
延伸