- Published on
nextjs博客项目-进阶研读
- Authors

- Name
- Kto
前言
本篇博客基于 Next.js Starter Blog 项目进行二次开发的实践经验分享。这个博客模板采用了现代化的技术栈,包括 Next.js App Router、Tailwind CSS、Contentlayer(MDX 处理)、Framer Motion 等。本文将深入介绍如何基于这个模板进行个性化定制,涵盖组件开发、样式系统、性能优化等多个方面。
一、如何引入图片
方式一:使用本地图片
将图片放置在 public/ 目录下,然后通过 Image 组件引用:
import Image from '@/components/Image'
// 直接使用
;<Image src="/images/your-image.png" alt="描述文字" width={800} height={400} />
方式二:使用远程图片
首先需要在 next.config.js 中配置允许的图片域名:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com', // 替换为你的图片域名
pathname: '/images/**',
},
],
// 图片优化配置
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
},
}
然后在 MDX 文件中使用:

或使用组件:
<Image
src="https://example.com/image.png"
alt="描述"
width={800}
height={400}
priority // 首屏图片优先加载
/>
响应式图片处理
使用 sizes 属性实现响应式图片:
<Image
src="/images/hero.jpg"
alt="Hero Image"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: 'cover' }}
/>
图片占位符和模糊效果
实现图片加载时的模糊占位效果:
<Image
src="/images/large.jpg"
alt="大图"
width={1200}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
图片懒加载策略
import Image from 'next/image'
export default function Gallery() {
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{[...Array(9)].map((_, i) => (
<Image
key={i}
src={`/images/image-${i}.jpg`}
alt={`Gallery image ${i}`}
width={400}
height={300}
loading="lazy" // 懒加载
// 非首屏图片设置 loading="lazy"
/>
))}
</div>
)
}
二、如何修改样式
全局样式修改
项目使用 Tailwind CSS 作为样式框架。全局样式文件位于 css/tailwind.css。
/* css/tailwind.css */
/* 自定义滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-gray-100 dark:bg-gray-800;
}
::-webkit-scrollbar-thumb {
@apply rounded-full bg-gray-400 dark:bg-gray-600;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-gray-500 dark:bg-gray-500;
}
/* 自定义选中文字样式 */
::selection {
@apply bg-primary/20 text-primary;
}
/* 自定义动画 */
@keyframes custom-animation {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.animate-custom {
animation: custom-animation 2s ease-in-out infinite;
}
/* 呼吸灯效果 */
@keyframes breathe {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.animate-breathe {
animation: breathe 3s ease-in-out infinite;
}
Tailwind 配置扩展
在 tailwind.config.js 中可以扩展主题配置:
module.exports = {
darkMode: 'class', // 基于类的暗黑模式
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './layouts/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
// 添加自定义颜色系列
brand: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
},
fontFamily: {
// 添加自定义字体
custom: ['CustomFont', 'sans-serif'],
display: ['DisplayFont', 'sans-serif'],
},
animation: {
// 添加自定义动画
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.5s ease-out',
'bounce-slow': 'bounce 3s infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
spacing: {
// 添加自定义间距
128: '32rem',
144: '36rem',
},
borderRadius: {
// 添加自定义圆角
'4xl': '2rem',
},
boxShadow: {
// 添加自定义阴影
glow: '0 0 20px rgba(59, 130, 246, 0.5)',
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
}
CSS 变量系统
使用 CSS 变量实现主题动态切换:
/* css/tailwind.css */
:root {
/* 颜色变量 */
--color-primary: 59 130 246; /* RGB values */
--color-secondary: 139 92 246;
/* 间距变量 */
--spacing-unit: 0.25rem;
/* 字体大小 */
--font-size-base: 1rem;
/* 过渡时间 */
--transition-fast: 150ms;
--transition-normal: 300ms;
}
.dark {
--color-primary: 96 165 250;
--color-secondary: 167 139 250;
}
在 Tailwind 中使用:
<div className="bg-[rgb(var(--color-primary))] p-[calc(var(--spacing-unit)*4)]">动态样式内容</div>
组件级样式修改
对于特定组件,可以直接修改组件文件。例如修改导航栏样式:
// components/Header.tsx
import { useState, useEffect } from 'react'
const nav = `
sticky top-0 z-50
bg-white/80 dark:bg-gray-900/80
backdrop-blur-md
border-b border-gray-200 dark:border-gray-700
transition-all duration-300
`
const navLinkClass = `
relative
px-4 py-2
text-gray-700 dark:text-gray-300
hover:text-primary dark:hover:text-primary
transition-colors duration-200
after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-0 after:h-0.5
after:bg-primary after:transition-all after:duration-300
hover:after:w-full
`
export default function Header() {
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return (
<header className={`${nav} ${scrolled ? 'shadow-lg' : ''}`}>
<nav className="container mx-auto px-4">{/* 导航内容 */}</nav>
</header>
)
}
动态样式和条件样式
使用 clsx 或 classnames 实现动态样式:
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
// 合并 Tailwind 类的工具函数
function cn(...inputs) {
return twMerge(clsx(inputs))
}
export function Button({ variant = 'primary', size = 'md', children, className }) {
const baseStyles = 'rounded-lg font-medium transition-all duration-200'
const variants = {
primary: 'bg-primary text-white hover:bg-primary-dark',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
outline: 'border-2 border-primary text-primary hover:bg-primary hover:text-white',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button className={cn(baseStyles, variants[variant], sizes[size], className)}>
{children}
</button>
)
}
三、如何修改背景色
修改全局背景
在 app/layout.tsx 中修改 body 的背景:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="zh-cn" className="scroll-smooth">
<body className="bg-gray-50 dark:bg-gray-900">{/* 页面内容 */}</body>
</html>
)
}
渐变背景
项目当前使用了渐变背景效果:
// app/layout.tsx
<body className="
animate-gradient
bg-gradient-to-r
from-indigo-50 via-purple-50 to-pink-50
dark:from-stone-900 dark:via-zinc-900 dark:to-slate-900
pl-[calc(100vw-100%)]
text-black antialiased dark:text-white
">
关键类说明:
animate-gradient: 自定义动画类,让渐变缓慢移动bg-gradient-to-r: 水平渐变方向from-{color},via-{color},to-{color}: 渐变颜色点dark:前缀: 暗黑模式下的样式pl-[calc(100vw-100%)]: 防止滚动条出现时的布局偏移antialiased: 字体抗锯齿
修改渐变动画
在 css/tailwind.css 中定义动画:
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animate-gradient {
background-size: 200% 200%;
animation: gradient 10s ease infinite;
}
/* 更快的渐变动画 */
.animate-gradient-fast {
background-size: 200% 200%;
animation: gradient 5s ease infinite;
}
/* 垂直渐变动画 */
@keyframes gradient-vertical {
0% {
background-position: 50% 0%;
}
50% {
background-position: 50% 100%;
}
100% {
background-position: 50% 0%;
}
}
.animate-gradient-vertical {
background-size: 200% 200%;
animation: gradient-vertical 15s ease infinite;
}
网格背景效果
创建科技感的网格背景:
// components/GridBackground.tsx
export default function GridBackground() {
return (
<div className="fixed inset-0 -z-10">
<div
className="
bg-[linear-gradient(to_right,#80808012_1px,transparent_1px), [linear-gradient(to_bottom,#80808012_1px,transparent_1px)]
absolute
inset-0
bg-[size:24px_24px]
"
/>
<div
className="
absolute left-0 right-0 top-0 -z-10
h-[500px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(120,119,198,0.3),rgba(255,255,255,0))]
"
/>
</div>
)
}
动态粒子背景
使用 Canvas 创建动态粒子效果:
// components/ParticleBackground.tsx
'use client'
import { useEffect, useRef } from 'react'
export default function ParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const particles: Array<{ x: number; y: number; vx: number; vy: number }> = []
for (let i = 0; i < 50; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
})
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
particles.forEach((p, i) => {
p.x += p.vx
p.y += p.vy
if (p.x < 0 || p.x > canvas.width) p.vx *= -1
if (p.y < 0 || p.y > canvas.height) p.vy *= -1
ctx.beginPath()
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(59, 130, 246, 0.5)'
ctx.fill()
particles.slice(i + 1).forEach((p2) => {
const dx = p.x - p2.x
const dy = p.y - p2.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 100) {
ctx.beginPath()
ctx.moveTo(p.x, p.y)
ctx.lineTo(p2.x, p2.y)
ctx.strokeStyle = `rgba(59, 130, 246, ${0.2 * (1 - dist / 100)})`
ctx.stroke()
}
})
})
requestAnimationFrame(animate)
}
animate()
const handleResize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return <canvas ref={canvasRef} className="fixed inset-0 -z-10 opacity-30 dark:opacity-20" />
}
四、如何添加组件
创建新组件
在 components/ 目录下创建新的组件文件:
// components/MyComponent.tsx
import { ReactNode } from 'react'
interface MyComponentProps {
title: string
children?: ReactNode
variant?: 'default' | 'primary' | 'secondary'
}
export default function MyComponent({ title, children, variant = 'default' }: MyComponentProps) {
const variants = {
default: 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800',
primary: 'border-primary bg-primary/5 dark:bg-primary/10',
secondary: 'border-purple-300 bg-purple-50 dark:border-purple-700 dark:bg-purple-900/20',
}
return (
<div
className={`
rounded-lg border p-4 shadow-sm transition-all duration-200
hover:shadow-md
${variants[variant]}
`}
>
<h3 className="mb-2 text-lg font-semibold">{title}</h3>
{children}
</div>
)
}
在 MDX 中使用组件
首先在 components/MDXComponents.tsx 中注册组件:
// components/MDXComponents.tsx
import { MDXComponents } from 'mdx/types'
import Image from './Image'
import TOCInline from './TOCInline'
import MyComponent from './MyComponent'
import CustomLink from './Link'
export const components: MDXComponents = {
Image,
TOCInline,
MyComponent,
a: CustomLink,
}
然后在 MDX 文件中直接使用:
---
title: 使用自定义组件
date: 2026-01-02
tags: ['nextjs', 'mdx']
---
## 引言
这是一个演示如何使用自定义组件的文章。
<MyComponent title="提示信息" variant="primary">
这是组件的内容部分。你可以在这里放置任何内容。
</MyComponent>
<MyComponent title="警告信息" variant="secondary">
这是警告样式的组件。
</MyComponent>
全局组件注册
对于需要在整个应用中使用的组件,在布局文件中引入:
// app/layout.tsx
import ProgressBar from '@/components/ProgressBar'
import GridBackground from '@/components/GridBackground'
export default function RootLayout({ children }) {
return (
<html>
<body>
<GridBackground />
<ProgressBar />
{children}
</body>
</html>
)
}
组件通信和状态管理
使用 Context API 实现跨组件状态共享:
// components/Accordion.tsx
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
interface AccordionContextType {
openItems: Set<string>
toggleItem: (id: string) => void
}
const AccordionContext = createContext<AccordionContextType | null>(null)
function useAccordion() {
const context = useContext(AccordionContext)
if (!context) {
throw new Error('Accordion components must be used within <Accordion>')
}
return context
}
export function Accordion({
children,
defaultOpen = [],
}: {
children: ReactNode
defaultOpen?: string[]
}) {
const [openItems, setOpenItems] = useState(new Set(defaultOpen))
const toggleItem = (id: string) => {
const newOpen = new Set(openItems)
if (newOpen.has(id)) {
newOpen.delete(id)
} else {
newOpen.add(id)
}
setOpenItems(newOpen)
}
return (
<AccordionContext.Provider value={{ openItems, toggleItem }}>
<div className="space-y-2">{children}</div>
</AccordionContext.Provider>
)
}
export function AccordionItem({
id,
title,
children,
}: {
id: string
title: string
children: ReactNode
}) {
const { openItems, toggleItem } = useAccordion()
const isOpen = openItems.has(id)
return (
<div className="rounded-lg border border-gray-200 dark:border-gray-700">
<button
onClick={() => toggleItem(id)}
className="w-full px-4 py-3 text-left font-medium transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
>
<span className="flex items-center justify-between">
{title}
<span className={`transform transition-transform ${isOpen ? 'rotate-180' : ''}`}>▼</span>
</span>
</button>
{isOpen && (
<div className="border-t border-gray-200 px-4 py-3 dark:border-gray-700">{children}</div>
)}
</div>
)
}
使用示例:
<Accordion>
<AccordionItem id="1" title="第一章节">
这是第一章节的内容...
</AccordionItem>
<AccordionItem id="2" title="第二章节">
这是第二章节的内容...
</AccordionItem>
</Accordion>
组合组件模式
创建可组合的卡片组件:
// components/Card.tsx
import { ReactNode } from 'react'
interface CardProps {
children: ReactNode
className?: string
}
function Card({ children, className = '' }: CardProps) {
return (
<div
className={`
rounded-lg border border-gray-200 bg-white
shadow-sm transition-all duration-200
hover:shadow-md
dark:border-gray-700 dark:bg-gray-800
${className}
`}
>
{children}
</div>
)
}
function CardHeader({ children }: { children: ReactNode }) {
return <div className="border-b border-gray-200 p-4 dark:border-gray-700">{children}</div>
}
function CardTitle({ children }: { children: ReactNode }) {
return <h3 className="text-lg font-semibold">{children}</h3>
}
function CardContent({ children }: { children: ReactNode }) {
return <div className="p-4">{children}</div>
}
function CardFooter({ children }: { children: ReactNode }) {
return <div className="border-t border-gray-200 p-4 dark:border-gray-700">{children}</div>
}
export { Card, CardHeader, CardTitle, CardContent, CardFooter }
使用示例:
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/Card'
;<Card>
<CardHeader>
<CardTitle>卡片标题</CardTitle>
</CardHeader>
<CardContent>
<p>这是卡片的主要内容...</p>
</CardContent>
<CardFooter>
<button>操作按钮</button>
</CardFooter>
</Card>
五、如何添加进度条
项目包含一个精美的阅读进度条组件,位于 components/ProgressBar.tsx。
进度条实现原理
// components/ProgressBar.tsx
'use client'
import { useEffect, useState } from 'react'
import { motion, useSpring, useTransform } from 'framer-motion'
export function useReadingProgress() {
const [progress, setProgress] = useState(0)
useEffect(() => {
function updateScroll() {
const currentScrollY = window.scrollY
const scrollHeight = document.body.scrollHeight - window.innerHeight
if (scrollHeight > 0) {
const progress = Number((currentScrollY / scrollHeight).toFixed(2)) * 100
setProgress(progress)
}
}
// 使用 requestAnimationFrame 优化性能
let ticking = false
function onScroll() {
if (!ticking) {
window.requestAnimationFrame(() => {
updateScroll()
ticking = false
})
ticking = true
}
}
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
return progress
}
export default function ProgressBar() {
const progress = useReadingProgress()
// 使用弹性动画
const scaleX = useSpring(progress / 100, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
})
return (
<motion.div className="fixed left-0 right-0 top-0 z-50 h-1 origin-left" style={{ scaleX }}>
<div className="h-full bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500" />
</motion.div>
)
}
使用进度条
在 app/layout.tsx 中引入:
import ProgressBar from '@/components/ProgressBar'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ProgressBar />
{children}
</body>
</html>
)
}
自定义进度条样式
可以修改进度条的样式属性:
// 渐变进度条
<motion.div
className="fixed top-0 left-0 right-0 h-2 z-50 origin-left"
style={{ scaleX: progress / 100 }}
>
<div className="h-full bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500" />
</motion.div>
// 发光效果进度条
<motion.div
className="fixed top-0 left-0 right-0 h-1 z-50 origin-left"
style={{ scaleX: progress / 100 }}
>
<div className="h-full bg-primary shadow-[0_0_10px_rgba(59,130,246,0.8)]" />
</motion.div>
// 多色进度条
<motion.div
className="fixed top-0 left-0 right-0 h-1 z-50 origin-left bg-gray-200 dark:bg-gray-700"
style={{ scaleX: progress / 100 }}
>
<div className="h-full bg-gradient-to-r from-green-400 via-blue-500 to-purple-600" />
</motion.div>
底部进度条
创建底部固定位置的进度条:
// components/BottomProgressBar.tsx
export default function BottomProgressBar() {
const progress = useReadingProgress()
return (
<div className="fixed bottom-0 left-0 right-0 z-50">
<motion.div className="bg-primary h-1 origin-left" style={{ scaleX: progress / 100 }} />
<div className="bg-gray-900/80 px-4 py-2 text-center text-sm text-white backdrop-blur-sm">
阅读进度: {Math.round(progress)}%
</div>
</div>
)
}
圆形进度指示器
创建圆形进度指示器:
// components/CircularProgress.tsx
export default function CircularProgress() {
const progress = useReadingProgress()
return (
<div className="fixed bottom-8 right-8 z-50">
<div className="relative h-16 w-16">
<svg className="h-full w-full -rotate-90 transform">
<circle
cx="32"
cy="32"
r="28"
stroke="currentColor"
strokeWidth="4"
fill="none"
className="text-gray-200 dark:text-gray-700"
/>
<motion.circle
cx="32"
cy="32"
r="28"
stroke="currentColor"
strokeWidth="4"
fill="none"
strokeDasharray={175.93}
strokeDashoffset={175.93 * (1 - progress / 100)}
className="text-primary"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center text-xs font-medium">
{Math.round(progress)}%
</div>
</div>
</div>
)
}
六、如何添加文章 TOC 组件
文章目录(Table of Contents)组件位于 components/TOCInline.tsx,是一个功能完善的目录导航组件。
TOC 组件功能特性
- 自动提取文章标题生成目录
- 滚动时高亮当前章节
- 平滑滚动到指定章节
- 响应式设计(桌面端固定右侧)
- 毛玻璃背景效果
- 支持嵌套标题层级
- 性能优化(IntersectionObserver + 防抖)
在文章中使用 TOC
在 MDX 文件中直接使用:
---
title: 你的文章标题
date: 2026-01-02
tags: ['nextjs']
---
## 引言
文章引言内容,介绍文章的主要内容...
<TOCInline />
## 第一节:基础概念
这是第一节的详细内容...
### 1.1 子章节
这是子章节的内容...
## 第二节:进阶应用
这是第二节的详细内容...
TOC 配置选项
组件内部包含可配置的常量:
const CONFIG = {
SCROLL: {
OFFSET: 96, // 滚动偏移量,考虑固定头部高度
DEBOUNCE_DELAY: 100, // 防抖延迟(毫秒)
CLICK_THRESHOLD: 500, // 点击后的冷却时间(毫秒)
},
STYLE: {
INDENT_MULTIPLIER: 1.25, // 每级标题的缩进倍数
},
OBSERVER: {
rootMargin: '0px 0px -50% 0px', // IntersectionObserver 的根边距
threshold: [0.25, 0.75], // 触发回调的可见比例阈值
},
}
自定义 TOC 样式
可以通过修改组件的 className 来自定义样式:
<div
className="
no-scrollbar fixed right-4
top-24 hidden
max-h-[calc(100vh-8rem)]
w-64 overflow-y-auto
rounded-lg border border-gray-200
bg-white/80 p-4
shadow-lg
backdrop-blur-sm
dark:border-gray-700
dark:bg-gray-900/80
xl:block
"
>
{/* TOC 内容 */}
</div>
样式类说明:
fixed right-4 top-24: 固定定位在右侧hidden xl:block: 仅在超大屏幕上显示max-h-[calc(100vh-8rem)]: 最大高度为视口高度减去边距backdrop-blur-sm: 毛玻璃效果no-scrollbar: 隐藏滚动条(自定义类)
TOC 实现原理深度解析
1. 标题提取(Contentlayer)
标题在 Contentlayer 构建时自动提取:
// contentlayer.config.ts
import { extractTocHeadings } from '@pliny/mdx'
export const Blog = defineDocumentType(() => ({
name: 'Blog',
filePathPattern: 'blog/**/*.mdx',
contentType: 'mdx',
computedFields: {
toc: {
type: 'json',
resolve: (doc) => extractTocHeadings(doc.body.raw),
},
},
}))
提取的 TOC 结构:
interface TocItem {
value: string // 标题文字
url: string // 锚点链接
depth: number // 标题层级(1-6)
}
2. 可见性追踪(IntersectionObserver)
使用 IntersectionObserver API 高效追踪标题可见性:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
})
},
{
rootMargin: '0px 0px -50% 0px', // 当元素到达视口中线时触发
threshold: [0.25, 0.75], // 25% 和 75% 可见时触发
}
)
3. 滚动同步优化
使用防抖和 requestAnimationFrame 优化滚动性能:
useEffect(() => {
const updateActiveId = () => {
// 计算当前可见的标题
const headings = document.querySelectorAll('h2, h3, h4')
// ... 计算逻辑
}
let ticking = false
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateActiveId()
ticking = false
})
ticking = true
}
}
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
4. 平滑滚动
点击目录项时平滑滚动到对应标题:
const handleClick = (url: string) => {
const element = document.querySelector(url)
if (element) {
const yOffset = -CONFIG.SCROLL.OFFSET // 考虑固定头部
const y = element.getBoundingClientRect().top + window.scrollY + yOffset
window.scrollTo({ top: y, behavior: 'smooth' })
}
}
TOC 进阶定制
添加进度指示器
在 TOC 旁边添加当前阅读进度:
// components/TOCWithProgress.tsx
export default function TOCWithProgress({ toc }: { toc: TocItem[] }) {
const [activeId, setActiveId] = useState<string>('')
// 计算当前章节的阅读进度
const activeIndex = toc.findIndex((item) => item.url === `#${activeId}`)
const progress = toc.length > 0 ? ((activeIndex + 1) / toc.length) * 100 : 0
return (
<div className="relative">
{/* 环形进度条 */}
<svg className="h-8 w-8 -rotate-90 transform">
<circle
cx="16"
cy="16"
r="14"
stroke="currentColor"
strokeWidth="2"
fill="none"
className="text-gray-200 dark:text-gray-700"
/>
<circle
cx="16"
cy="16"
r="14"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeDasharray={87.96}
strokeDashoffset={87.96 * (1 - progress / 100)}
className="text-primary"
/>
</svg>
{/* TOC 内容 */}
<TOCInline toc={toc} />
</div>
)
}
折叠式 TOC
添加折叠展开功能:
// components/CollapsibleTOC.tsx
'use client'
import { useState } from 'react'
export default function CollapsibleTOC({ toc }: { toc: TocItem[] }) {
const [isCollapsed, setIsCollapsed] = useState(false)
return (
<div
className="
{isCollapsed ? 'w-12'
: 'w-64'}
fixed right-4 top-24
hidden rounded-lg
border border-gray-200
bg-white/80 backdrop-blur-sm
transition-all duration-300 dark:border-gray-700 dark:bg-gray-900/80 xl:block
"
>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="w-full p-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800"
>
{isCollapsed ? '→' : '←'}
</button>
{!isCollapsed && (
<div className="p-4">
<TOCInline toc={toc} />
</div>
)}
</div>
)
}
七、其他实用组件
加载进度条 (LoadingBar)
位于 components/LoadingBar.tsx,在路由切换时显示加载进度。
// 使用方法
import LoadingBar from '@/components/LoadingBar'
// 在 layout.tsx 中添加
;<LoadingBar />
特性:
- 监听页面加载和路由切换
- 自动完成机制(最多5秒)
- 渐变色动画效果
- 防止重复触发
主题切换 (ThemeSwitch)
位于 components/ThemeSwitch.tsx,支持亮色/暗色主题切换。
// 自定义主题切换按钮
import ThemeSwitch from '@/components/ThemeSwitch'
;<ThemeSwitch />
特性:
- 使用 next-themes 管理主题
- 优雅的切换动画
- 太阳/月亮图标
- 自动适配系统主题
返回顶部 (ScrollTopAndComment)
位于 components/ScrollTopAndComment.tsx,提供返回顶部和跳转评论区的按钮。
// 使用方法
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
;<ScrollTopAndComment />
特性:
- 滚动超过50px后显示
- 返回顶部功能
- 跳转到评论区
- 平滑滚动动画
代码块复制按钮
// components/CodeBlock.tsx
'use client'
import { useState } from 'react'
import { CheckIcon, ClipboardIcon } from '@heroicons/react/24/outline'
export default function CodeBlock({
children,
className,
}: {
children: React.ReactNode
className?: string
}) {
const [copied, setCopied] = useState(false)
const copyToClipboard = async () => {
const code = children?.toString()
await navigator.clipboard.writeText(code || '')
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="group relative">
<pre className={className}>
<code>{children}</code>
</pre>
<button
onClick={copyToClipboard}
className="absolute right-2 top-2 rounded-md bg-gray-700 p-2 opacity-0 transition-opacity group-hover:opacity-100"
>
{copied ? (
<CheckIcon className="h-4 w-4 text-green-400" />
) : (
<ClipboardIcon className="h-4 w-4 text-gray-300" />
)}
</button>
</div>
)
}
通知提示组件
// components/Toast.tsx
'use client'
import { useEffect, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
interface ToastProps {
message: string
type?: 'success' | 'error' | 'info'
duration?: number
}
export function useToast() {
const [toast, setToast] = useState<ToastProps | null>(null)
const showToast = (props: ToastProps) => {
setToast(props)
}
return { toast, showToast }
}
export default function Toast({ message, type = 'info', duration = 3000 }: ToastProps) {
const [visible, setVisible] = useState(true)
useEffect(() => {
const timer = setTimeout(() => setVisible(false), duration)
return () => clearTimeout(timer)
}, [duration])
const types = {
success: 'bg-green-500',
error: 'bg-red-500',
info: 'bg-blue-500',
}
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`fixed right-4 top-4 z-50 rounded-lg px-6 py-3 text-white shadow-lg ${types[type]}`}
>
{message}
</motion.div>
)}
</AnimatePresence>
)
}
八、性能优化技巧
React 性能优化
使用 React.memo 防止不必要的重渲染
import { memo } from 'react'
const ExpensiveComponent = memo(function ExpensiveComponent({ data }: { data: any }) {
// 组件逻辑
return <div>{/* 渲染内容 */}</div>
})
使用 useMemo 缓存计算结果
import { useMemo } from 'react'
function ProcessedList({ items }: { items: any[] }) {
const processed = useMemo(() => {
return items.map((item) => {
// 昂贵的计算
return transform(item)
})
}, [items])
return <div>{processed}</div>
}
使用 useCallback 稳定函数引用
import { useCallback } from 'react'
function ParentComponent() {
const handleClick = useCallback(() => {
// 处理点击
}, [])
return <ChildComponent onClick={handleClick} />
}
代码分割和懒加载
动态导入组件
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
loading: () => <div>Loading...</div>,
ssr: false, // 客户端渲染
})
路由级代码分割
// Next.js App Router 自动进行路由级代码分割
// app/blog/[slug]/page.tsx 会自动分割
图片优化
import Image from 'next/image'
// 正确的图片使用方式
;<Image
src="/images/photo.jpg"
alt="描述"
width={800}
height={600}
priority // 首屏图片使用
loading="lazy" // 非首屏图片使用
placeholder="blur" // 添加模糊占位符
blurDataURL="data:image/..." // 模糊数据
/>
字体优化
// app/layout.tsx
import { Space_Grotesk } from 'next/font/google'
const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
display: 'swap',
variable: '--font-space-grotesk',
})
export default function RootLayout({ children }) {
return (
<html className={spaceGrotesk.variable}>
<body>{children}</body>
</html>
)
}
九、SEO 优化
元数据配置
// app/layout.tsx
export const metadata = {
title: {
default: '网站标题',
template: '%s | 网站标题',
},
description: '网站描述',
keywords: ['关键词1', '关键词2'],
authors: [{ name: '作者名' }],
openGraph: {
title: '网站标题',
description: '网站描述',
url: 'https://example.com',
siteName: '网站名称',
images: [
{
url: 'https://example.com/og.jpg',
width: 1200,
height: 630,
},
],
locale: 'zh_CN',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: '网站标题',
description: '网站描述',
images: ['https://example.com/og.jpg'],
},
}
文章元数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return {
title: post.title,
description: post.summary,
openGraph: {
title: post.title,
description: post.summary,
type: 'article',
publishedTime: post.date,
authors: [post.author],
images: post.images,
},
}
}
结构化数据
// components/StructuredData.tsx
export function BlogStructuredData({ post }: { post: Blog }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.summary,
datePublished: post.date,
author: {
'@type': 'Person',
name: post.author,
},
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
Sitemap 生成
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://example.com'
return [
{
url: baseUrl,
lastModified: new Date(),
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
},
// 动态生成文章链接
...getAllPosts().map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.lastmod || post.date),
})),
]
}
十、开发建议
1. 组件设计原则
- 单一职责:每个组件只负责一个功能
- 可复用性:通过 props 实现灵活配置
- 组合优于继承:使用组件组合构建复杂 UI
- 类型安全:充分利用 TypeScript 类型系统
2. 样式一致性
- 使用 Tailwind 的设计令牌保持风格统一
- 定义统一的颜色、间距、字体规范
- 创建可复用的样式变体
3. 性能优化
- 使用 React DevTools Profiler 分析性能
- 实施代码分割减少初始加载体积
- 优化图片和字体加载
- 使用缓存策略减少重复请求
4. 响应式设计
- 移动优先的设计策略
- 测试不同设备和屏幕尺寸
- 使用 Tailwind 的响应式修饰符
- 考虑触摸交互和手势
5. 可访问性
- 使用语义化 HTML 标签
- 提供键盘导航支持
- 添加适当的 ARIA 属性
- 确保颜色对比度符合标准
- 为图片添加 alt 描述
6. 代码组织
- 按功能而非类型组织文件
- 保持文件结构清晰和一致
- 使用 barrel exports 简化导入
- 编写清晰的注释和文档
十一、进阶主题
MDX 自定义插件
// contentlayer.config.ts
import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math'
export default defineConfig({
mdx: {
remarkPlugins: [
remarkMath, // 数学公式支持
],
rehypePlugins: [
rehypeKatex, // KaTeX 渲染
],
},
})
API 路由集成
// app/api/views/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const { slug } = await request.json()
// 更新浏览计数
await updateViews(slug)
return NextResponse.json({ views: await getViews(slug) })
}
评论系统集成
// components/Comments.tsx
'use client'
import { useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
export default function Comments() {
const ref = useRef<HTMLDivElement>(null)
const pathname = usePathname()
useEffect(() => {
// 懒加载评论组件
const script = document.createElement('script')
script.src = 'https://giscus.app/client.js'
script.setAttribute('data-repo', 'your-repo')
script.setAttribute('data-repo-id', 'your-repo-id')
script.setAttribute('data-theme', 'preferred_color_scheme')
script.async = true
ref.current?.appendChild(script)
}, [pathname])
return <div ref={ref} className="mt-12" />
}
搜索功能定制
// components/Search.tsx
'use client'
import { KBarProvider, KBarPortal, KBarPositioner } from 'kbar'
import { useActions } from '@/lib/actions'
export default function Search({ children }: { children: React.ReactNode }) {
const actions = useActions()
return (
<KBarProvider actions={actions}>
<KBarPortal>
<KBarPositioner>{/* 搜索界面 */}</KBarPositioner>
</KBarPortal>
{children}
</KBarProvider>
)
}
十二、部署和构建优化
环境变量配置
# .env.local
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
DATABASE_URL=postgresql://...
构建优化
// next.config.js
module.exports = {
// 启用 SWC 压缩
swcMinify: true,
// 生产源映射
productionBrowserSourceMaps: false,
// 实验性功能
experimental: {
// Turbopack 支持
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
}
静态导出
// next.config.js
module.exports = {
output: 'export', // 静态 HTML 导出
images: {
unoptimized: true, // 静态导出时禁用图片优化
},
}
总结
这个 Next.js 博客模板提供了完善的组件体系和样式系统,通过合理使用和扩展这些功能,可以快速构建出功能完善、外观精美的个人博客。本文涵盖了从基础组件使用到高级性能优化的各个方面,建议在修改前先了解项目结构,按照最佳实践进行定制开发。
学习路径建议
- 基础阶段:熟悉项目结构,理解组件系统
- 进阶阶段:定制样式,添加自定义组件
- 优化阶段:性能优化,SEO 提升
- 高级阶段:扩展功能,集成第三方服务