乐闻世界logo
搜索文章和话题

面试题手册

Next.js 中如何处理错误?

Next.js 提供了多种错误处理机制,帮助开发者构建健壮的应用。了解这些机制对于构建可靠的用户体验至关重要。错误处理层次Next.js 的错误处理分为多个层次:组件级错误:使用 Error Boundary路由级错误:使用 error.js 文件服务器级错误:使用全局错误处理API 错误:在 API Routes 中处理路由级错误处理(App Router)error.js 文件在 App Router 中,error.js 文件用于捕获路由段及其子组件中的错误。// app/error.js'use client';export default function Error({ error, reset,}) { return ( <div> <h2>Something went wrong!</h2> <p>{error.message}</p> <button onClick={() => reset()}>Try again</button> </div> );}export default function GlobalError({ error, reset }) { return ( <html> <body> <h2>Something went wrong!</h2> <button onClick={reset}>Try again</button> </body> </html> );}嵌套错误边界// app/dashboard/error.js'use client';export default function DashboardError({ error, reset }) { return ( <div> <h2>Dashboard Error</h2> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> );}// app/dashboard/settings/error.js'use client';export default function SettingsError({ error, reset }) { return ( <div> <h2>Settings Error</h2> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> );}Pages Router 错误处理自定义错误页面// pages/404.jsexport default function Custom404() { return ( <div> <h1>404 - Page Not Found</h1> <p>The page you're looking for doesn't exist.</p> <Link href="/">Go Home</Link> </div> );}// pages/500.jsexport default function Custom500() { return ( <div> <h1>500 - Server Error</h1> <p>Something went wrong on our end.</p> <Link href="/">Go Home</Link> </div> );}// pages/_error.jsexport default function Error({ statusCode, err }) { return ( <div> <p> {statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'} </p> <p>{err?.message}</p> </div> );}Error.getInitialProps = ({ res, err }) => { const statusCode = res ? res.statusCode : err ? err.statusCode : 404; return { statusCode };};getServerSideProps 错误处理export async function getServerSideProps(context) { try { const data = await fetchData(); if (!data) { return { notFound: true, // 返回 404 页面 }; } return { props: { data }, }; } catch (error) { return { props: { error: error.message }, }; }}服务器组件错误处理使用 try-catchasync function ProductPage({ params }) { try { const product = await fetchProduct(params.id); if (!product) { notFound(); // 返回 404 页面 } return <ProductDetails product={product} />; } catch (error) { console.error('Failed to fetch product:', error); throw error; // 触发最近的错误边界 }}使用 notFound()import { notFound } from 'next/navigation';async function BlogPost({ params }) { const post = await getPostBySlug(params.slug); if (!post) { notFound(); // 返回 404 页面 } return <PostContent post={post} />;}自定义 not-found 页面// app/not-found.jsexport default function NotFound() { return ( <div> <h2>Page Not Found</h2> <p>The page you're looking for doesn't exist.</p> <Link href="/">Go Home</Link> </div> );}// app/blog/not-found.jsexport default function BlogNotFound() { return ( <div> <h2>Blog Post Not Found</h2> <p>The blog post you're looking for doesn't exist.</p> <Link href="/blog">Back to Blog</Link> </div> );}API Routes 错误处理错误响应格式// app/api/hello/route.jsexport async function GET(request) { try { const data = await fetchData(); return Response.json(data); } catch (error) { console.error('API Error:', error); return Response.json( { error: 'Internal Server Error', message: error.message, timestamp: new Date().toISOString(), }, { status: 500 } ); }}验证错误import { z } from 'zod';const schema = z.object({ email: z.string().email(), password: z.string().min(8),});export async function POST(request) { try { const body = await request.json(); // 验证输入 const validated = schema.parse(body); // 处理请求 const result = await createUser(validated); return Response.json(result, { status: 201 }); } catch (error) { if (error instanceof z.ZodError) { return Response.json( { error: 'Validation Error', details: error.errors, }, { status: 400 } ); } return Response.json( { error: 'Internal Server Error' }, { status: 500 } ); }}认证错误export async function GET(request) { const token = request.headers.get('authorization'); if (!token) { return Response.json( { error: 'Unauthorized' }, { status: 401 } ); } try { const user = await verifyToken(token); return Response.json({ user }); } catch (error) { return Response.json( { error: 'Invalid token' }, { status: 401 } ); }}客户端错误处理Error Boundary 组件'use client';import { Component, ReactNode } from 'react';interface Props { children: ReactNode; fallback?: ReactNode;}interface State { hasError: boolean; error?: Error;}export class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: any) { console.error('Error caught by boundary:', error, errorInfo); // 发送错误到日志服务 logErrorToService(error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback || ( <div> <h2>Something went wrong</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> Try again </button> </div> ); } return this.props.children; }}使用 Error Boundary'use client';import { ErrorBoundary } from '@/components/ErrorBoundary';export default function Page() { return ( <ErrorBoundary fallback={ <div> <h2>Component Error</h2> <button onClick={() => window.location.reload()}> Reload Page </button> </div> } > <RiskyComponent /> </ErrorBoundary> );}异步错误处理'use client';import { useEffect } from 'react';export default function AsyncComponent() { useEffect(() => { const fetchData = async () => { try { const data = await fetch('/api/data').then(r => r.json()); console.log(data); } catch (error) { console.error('Fetch error:', error); // 显示错误消息 showErrorToast('Failed to fetch data'); } }; fetchData(); }, []); return <div>Loading...</div>;}全局错误处理使用 window.onerror// app/layout.js'use client';import { useEffect } from 'react';export default function RootLayout({ children }) { useEffect(() => { const handleError = (event: ErrorEvent) => { console.error('Global error:', event.error); // 发送到错误跟踪服务 logErrorToService({ message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error, }); }; window.addEventListener('error', handleError); return () => { window.removeEventListener('error', handleError); }; }, []); return ( <html lang="en"> <body>{children}</body> </html> );}使用 window.onunhandledrejectionuseEffect(() => { const handleUnhandledRejection = (event: PromiseRejectionEvent) => { console.error('Unhandled promise rejection:', event.reason); // 发送到错误跟踪服务 logErrorToService({ type: 'unhandledRejection', reason: event.reason, }); // 防止默认的控制台错误 event.preventDefault(); }; window.addEventListener('unhandledrejection', handleUnhandledRejection); return () => { window.removeEventListener('unhandledrejection', handleUnhandledRejection); };}, []);错误日志和监控使用 Sentry// sentry.client.config.jsimport * as Sentry from '@sentry/nextjs';Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0,});// sentry.server.config.jsimport * as Sentry from '@sentry/nextjs';Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0,});自定义错误日志// lib/errorLogger.jsexport function logError(error: Error, context?: any) { const errorData = { message: error.message, stack: error.stack, timestamp: new Date().toISOString(), context, }; // 发送到日志服务 if (typeof window !== 'undefined') { // 客户端 fetch('/api/log-error', { method: 'POST', body: JSON.stringify(errorData), }); } else { // 服务器端 console.error('Server error:', errorData); }}export function logApiError(error: any, request: Request) { const errorData = { url: request.url, method: request.method, error: error.message, stack: error.stack, timestamp: new Date().toISOString(), }; console.error('API error:', errorData);}最佳实践1. 提供有用的错误信息// ✅ 好的做法export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong</h2> <p>We couldn't load the page. Please try again.</p> <button onClick={reset}>Try again</button> <Link href="/">Go Home</Link> </div> );}// ❌ 不好的做法export default function Error({ error }) { return ( <div> <p>{error.message}</p> </div> );}2. 记录错误上下文// ✅ 好的做法try { const user = await getUser(userId); const posts = await getPosts(user.id); return { user, posts };} catch (error) { logError(error, { userId, action: 'fetchUserPosts' }); throw error;}// ❌ 不好的做法try { const user = await getUser(userId); const posts = await getPosts(user.id); return { user, posts };} catch (error) { console.error(error); throw error;}3. 提供恢复选项// ✅ 好的做法export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong</h2> <button onClick={reset}>Try again</button> <button onClick={() => window.location.href = '/'}> Go Home </button> </div> );}// ❌ 不好的做法export default function Error({ error }) { return ( <div> <h2>Something went wrong</h2> <p>Please refresh the page</p> </div> );}4. 区分开发和生产环境export default function Error({ error, reset }) { const isDev = process.env.NODE_ENV === 'development'; return ( <div> <h2>Something went wrong</h2> {isDev ? ( <details> <summary>Error Details</summary> <pre>{error.message}</pre> <pre>{error.stack}</pre> </details> ) : ( <p>We're sorry for the inconvenience. Please try again.</p> )} <button onClick={reset}>Try again</button> </div> );}5. 使用类型安全的错误处理// types/errors.tsexport class AppError extends Error { constructor( message: string, public statusCode: number = 500, public code?: string ) { super(message); this.name = 'AppError'; }}export class NotFoundError extends AppError { constructor(message: string = 'Resource not found') { super(message, 404, 'NOT_FOUND'); }}export class ValidationError extends AppError { constructor(message: string = 'Validation failed') { super(message, 400, 'VALIDATION_ERROR'); }}// 使用export async function getProduct(id: string) { const product = await db.product.findUnique({ where: { id } }); if (!product) { throw new NotFoundError(`Product ${id} not found`); } return product;}通过合理使用这些错误处理机制,可以构建出健壮、可靠的 Next.js 应用,提供良好的用户体验。
阅读 0·2月17日 23:31

Next.js 的 API Routes 是如何工作的?

Next.js 提供了强大的 API Routes 功能,允许开发者创建 API 端点来处理服务器端逻辑。API Routes 可以处理数据库查询、身份验证、表单提交等服务器端操作。API Routes 基础创建 API Route在 pages/api 目录下创建文件,每个文件都会成为一个 API 端点。pages/ api/ hello.js → /api/hello users/ index.js → /api/users [id].js → /api/users/123基本示例// pages/api/hello.jsexport default function handler(req, res) { res.status(200).json({ message: 'Hello from Next.js API!' });}请求和响应对象请求对象 (req)req 对象包含以下属性:req.method:HTTP 方法(GET、POST、PUT、DELETE 等)req.query:查询参数req.body:请求体(POST、PUT 等)req.headers:请求头req.cookies:Cookie响应对象 (res)res 对象提供以下方法:res.status(code):设置状态码res.json(data):发送 JSON 响应res.send(data):发送响应res.redirect(url):重定向res.setHeader(name, value):设置响应头处理不同的 HTTP 方法// pages/api/users/[id].jsexport default async function handler(req, res) { const { id } = req.query; switch (req.method) { case 'GET': const user = await getUserById(id); res.status(200).json(user); break; case 'PUT': const updatedUser = await updateUser(id, req.body); res.status(200).json(updatedUser); break; case 'DELETE': await deleteUser(id); res.status(204).end(); break; default: res.setHeader('Allow', ['GET', 'PUT', 'DELETE']); res.status(405).end(`Method ${req.method} Not Allowed`); }}中间件自定义中间件// lib/middleware.jsexport function authMiddleware(req, res, next) { const token = req.headers.authorization; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } // 验证 token const user = verifyToken(token); if (!user) { return res.status(401).json({ error: 'Invalid token' }); } req.user = user; next();}使用中间件// pages/api/protected.jsimport { authMiddleware } from '@/lib/middleware';export default function handler(req, res) { // 受保护的路由 res.status(200).json({ user: req.user });}// 应用中间件export const config = { api: { bodyParser: false, externalResolver: true, },};// 在实际使用中,需要手动调用中间件数据库集成使用 Prisma// pages/api/posts/index.jsimport { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export default async function handler(req, res) { if (req.method === 'GET') { const posts = await prisma.post.findMany(); res.status(200).json(posts); } else if (req.method === 'POST') { const post = await prisma.post.create({ data: req.body, }); res.status(201).json(post); } else { res.status(405).end(); }}使用 MongoDB// pages/api/users/index.jsimport clientPromise from '@/lib/mongodb';export default async function handler(req, res) { const client = await clientPromise; const db = client.db(); if (req.method === 'GET') { const users = await db.collection('users').find({}).toArray(); res.status(200).json(users); } else if (req.method === 'POST') { const result = await db.collection('users').insertOne(req.body); res.status(201).json(result); } else { res.status(405).end(); }}身份验证使用 NextAuth.js// pages/api/auth/[...nextauth].jsimport NextAuth from 'next-auth';import Providers from 'next-auth/providers';export default NextAuth({ providers: [ Providers.Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), Providers.Credentials({ name: 'Credentials', credentials: { username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" } }, authorize: async (credentials) => { // 验证用户 const user = await authenticate(credentials); if (user) { return user; } return null; } }), ], database: process.env.DATABASE_URL,});JWT 验证// lib/auth.jsimport jwt from 'jsonwebtoken';export function verifyToken(token) { try { return jwt.verify(token, process.env.JWT_SECRET); } catch (error) { return null; }}export function createToken(payload) { return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1d' });}文件上传// pages/api/upload.jsimport formidable from 'formidable';import fs from 'fs';import path from 'path';export const config = { api: { bodyParser: false, },};export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).end(); } const form = formidable({ uploadDir: path.join(process.cwd(), '/public/uploads'), keepExtensions: true, }); form.parse(req, (err, fields, files) => { if (err) { return res.status(500).json({ error: 'File upload failed' }); } const file = files.file[0]; res.status(200).json({ url: `/uploads/${path.basename(file.filepath)}` }); });}错误处理// pages/api/error.jsexport default function handler(req, res) { try { // 业务逻辑 const data = processData(req.body); res.status(200).json(data); } catch (error) { console.error('API Error:', error); if (error.name === 'ValidationError') { res.status(400).json({ error: error.message }); } else if (error.name === 'UnauthorizedError') { res.status(401).json({ error: 'Unauthorized' }); } else { res.status(500).json({ error: 'Internal server error' }); } }}CORS 配置// pages/api/cors.jsexport default function handler(req, res) { res.setHeader('Access-Control-Allow-Credentials', true); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT'); res.setHeader( 'Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' ); if (req.method === 'OPTIONS') { res.status(200).end(); return; } res.status(200).json({ message: 'CORS enabled' });}最佳实践使用环境变量:敏感信息存储在 .env 文件中验证输入:使用验证库如 Zod 或 Yup错误处理:统一错误处理格式日志记录:记录请求和错误信息速率限制:防止 API 滥用缓存:对频繁访问的数据进行缓存文档化:使用 Swagger 或 OpenAPI 文档化 APINext.js API Routes 提供了简单而强大的方式来构建服务器端 API,无需单独的后端服务器,使全栈开发变得更加便捷。
阅读 0·2月17日 23:31

Next.js 如何进行 SEO 优化?

Next.js 提供了强大的 SEO 优化功能,帮助开发者构建搜索引擎友好的 Web 应用。以下是 Next.js 中主要的 SEO 优化技术:1. 元数据管理Pages Router - 使用 Head 组件import Head from 'next/head';export default function BlogPost({ post }) { return ( <> <Head> <title>{post.title} - My Blog</title> <meta name="description" content={post.excerpt} /> <meta name="keywords" content={post.tags.join(', ')} /> <meta name="author" content={post.author} /> {/* Open Graph */} <meta property="og:title" content={post.title} /> <meta property="og:description" content={post.excerpt} /> <meta property="og:image" content={post.image} /> <meta property="og:type" content="article" /> <meta property="og:url" content={`https://example.com/blog/${post.slug}`} /> {/* Twitter Card */} <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content={post.title} /> <meta name="twitter:description" content={post.excerpt} /> <meta name="twitter:image" content={post.image} /> {/* Canonical URL */} <link rel="canonical" href={`https://example.com/blog/${post.slug}`} /> {/* Favicon */} <link rel="icon" href="/favicon.ico" /> </Head> <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> </> );}App Router - 使用 Metadata API// app/layout.jsexport const metadata = { title: { default: 'My Website', template: '%s | My Website' }, description: 'A website built with Next.js', keywords: ['nextjs', 'react', 'web development'], authors: [{ name: 'John Doe' }], creator: 'John Doe', openGraph: { type: 'website', locale: 'en_US', url: 'https://example.com', siteName: 'My Website', images: [ { url: 'https://example.com/og.jpg', width: 1200, height: 630, alt: 'My Website' } ] }, twitter: { card: 'summary_large_image', title: 'My Website', description: 'A website built with Next.js', images: ['https://example.com/twitter.jpg'] }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1, }, }, verification: { google: 'google-site-verification-code', yandex: 'yandex-verification-code', },};export default function RootLayout({ children }) { return ( <html lang="en"> <body>{children}</body> </html> );}动态元数据// app/blog/[slug]/page.jsexport async function generateMetadata({ params }) { const post = await getPostBySlug(params.slug); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.image], type: 'article', publishedTime: post.publishedAt, authors: [post.author], }, };}export default function BlogPost({ params }) { return <div>Post content</div>;}2. 结构化数据使用 JSON-LD 添加结构化数据,帮助搜索引擎理解内容。import Head from 'next/head';export default function BlogPost({ post }) { const structuredData = { '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title, description: post.excerpt, image: post.image, author: { '@type': 'Person', name: post.author, }, datePublished: post.publishedAt, dateModified: post.updatedAt, }; return ( <> <Head> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} /> </Head> <article>{/* Post content */}</article> </> );}3. 服务器端渲染(SSR)SSR 确保搜索引擎爬虫能够抓取到完整的 HTML 内容。export async function getServerSideProps() { const data = await fetch('https://api.example.com/data').then(r => r.json()); return { props: { data }, };}export default function Page({ data }) { return ( <div> <h1>{data.title}</h1> <p>{data.content}</p> </div> );}4. 静态生成(SSG)对于内容不经常变化的页面,使用 SSG 可以获得最佳性能。export async function getStaticProps() { const data = await fetch('https://api.example.com/data').then(r => r.json()); return { props: { data }, revalidate: 3600, // ISR:每小时重新生成 };}export default function Page({ data }) { return <div>{data.content}</div>;}5. 动态路由的 SEO使用 getStaticPaths 为动态路由生成静态页面。export async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: 'blocking', // 确保所有页面都可被索引 };}export async function getStaticProps({ params }) { const post = await getPostBySlug(params.slug); return { props: { post }, };}6. 语义化 HTML使用语义化 HTML 标签提升 SEO。export default function BlogPost({ post }) { return ( <article> <header> <h1>{post.title}</h1> <time dateTime={post.publishedAt}> {new Date(post.publishedAt).toLocaleDateString()} </time> </header> <main> {post.content} </main> <aside> <h2>Related Posts</h2> <ul> {post.related.map(related => ( <li key={related.id}> <Link href={`/blog/${related.slug}`}> {related.title} </Link> </li> ))} </ul> </aside> <footer> <p>Written by {post.author}</p> </footer> </article> );}7. 图片优化使用 next/image 优化图片,提升页面加载速度。import Image from 'next/image';export default function Page() { return ( <Image src="/hero.jpg" alt="Hero image" width={1200} height={630} priority placeholder="blur" /> );}8. Sitemap 和 Robots.txt生成 Sitemap// app/sitemap.jsimport { getPosts } from '@/lib/posts';export default async function sitemap() { const posts = await getPosts(); const baseUrl = 'https://example.com'; const postUrls = posts.map(post => ({ url: `${baseUrl}/blog/${post.slug}`, lastModified: new Date(post.updatedAt), changeFrequency: 'weekly', priority: 0.8, })); return [ { url: baseUrl, lastModified: new Date(), changeFrequency: 'daily', priority: 1, }, { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5, }, ...postUrls, ];}生成 Robots.txt// app/robots.jsexport default function robots() { return { rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/'], }, sitemap: 'https://example.com/sitemap.xml', };}9. 性能优化性能是 SEO 的重要因素,Next.js 提供了多种性能优化技术。使用 Web Vitals 监控// pages/_app.jsimport { useReportWebVitals } from 'next/web-vitals';export function reportWebVitals(metric) { // 发送到分析服务 console.log(metric);}export default function App({ Component, pageProps }) { useReportWebVitals(reportWebVitals); return <Component {...pageProps} />;}优化 Core Web Vitals// 使用 next/image 优化 LCPimport Image from 'next/image';// 使用 Suspense 优化 TTFBimport { Suspense } from 'react';// 优化 CLSexport default function Page() { return ( <div style={{ minHeight: '100vh' }}> {/* 内容 */} </div> );}10. 国际化(i18n)Next.js 内置支持国际化,帮助构建多语言网站。// next.config.jsmodule.exports = { i18n: { locales: ['en', 'zh', 'es'], defaultLocale: 'en', },};// 为每个语言设置不同的元数据export async function generateMetadata({ params }) { const { locale } = params; const translations = await getTranslations(locale); return { title: translations.title, description: translations.description, alternates: { canonical: `https://example.com/${locale}`, languages: { 'en': 'https://example.com/en', 'zh': 'https://example.com/zh', 'es': 'https://example.com/es', }, }, };}SEO 最佳实践使用 SSR 或 SSG:确保内容可被搜索引擎抓取优化元数据:为每个页面设置适当的标题、描述和关键词使用结构化数据:帮助搜索引擎理解内容优化页面性能:提升 Core Web Vitals创建 Sitemap:帮助搜索引擎发现所有页面使用语义化 HTML:提升内容可读性优化图片:使用 next/image 优化图片加载设置 Canonical URL:避免重复内容问题实现面包屑导航:提升用户体验和 SEO监控 SEO 指标:使用工具监控 SEO 表现通过合理使用这些 SEO 优化技术,可以显著提升 Next.js 应用在搜索引擎中的排名和可见性。
阅读 0·2月17日 23:31

什么是 Next.js 及其主要特性是什么?

Next.js 是一个基于 React 的开源 JavaScript 框架,用于构建服务器端渲染(SSR)和静态生成(SSG)的 Web 应用程序。它提供了许多开箱即用的功能,使开发者能够快速构建高性能的 React 应用。Next.js 的主要特性包括:服务器端渲染(SSR):Next.js 支持服务器端渲染,这意味着 HTML 在服务器上生成并发送到客户端,有助于 SEO 和首屏加载性能。静态生成(SSG):可以在构建时生成静态 HTML 文件,非常适合内容不经常变化的页面,如博客文章、产品页面等。自动代码分割:Next.js 自动将代码分割成小块,只加载当前页面所需的代码,提高应用性能。文件系统路由:基于 pages 目录的文件结构自动创建路由,无需手动配置路由。API 路由:可以创建 API 端点,处理服务器端逻辑,如数据库查询、身份验证等。CSS 支持:内置支持 CSS Modules、Sass、styled-jsx 等 CSS 方案。TypeScript 支持:开箱即用的 TypeScript 支持,提供类型安全。图像优化:提供 next/image 组件,自动优化图像大小和格式。快速刷新:开发时提供快速的热重载体验,保持组件状态。环境变量:支持 .env 文件来管理环境变量。国际化支持:内置 i18n 路由支持,轻松构建多语言应用。增量静态生成(ISR):允许在构建后更新静态页面,结合了 SSG 和 SSR 的优势。Next.js 通过这些特性,为 React 开发者提供了一个功能强大且易于使用的框架,特别适合构建需要良好 SEO、高性能和良好用户体验的 Web 应用。
阅读 0·2月17日 23:30

Next.js 的中间件是如何工作的?

Next.js 的中间件(Middleware)是一个强大的功能,允许你在请求到达页面之前执行代码。这对于身份验证、重定向、A/B 测试等场景非常有用。什么是中间件?中间件是在 Next.js 应用中运行的函数,它在请求完成之前拦截传入的请求。中间件可以:重写路径:将一个路径重写到另一个路径重定向:将用户重定向到不同的 URL修改请求/响应:添加或修改请求头、响应头身份验证:检查用户是否已登录地理位置路由:根据用户位置重定向基本用法创建中间件文件在项目根目录创建 middleware.js 或 middleware.ts 文件:// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { return NextResponse.next();}export const config = { matcher: '/about/:path*',};中间件配置// middleware.jsexport const config = { matcher: [ // 匹配所有路径 '/((?!api|_next/static|_next/image|favicon.ico).*)', // 匹配特定路径 '/dashboard/:path*', // 使用正则表达式 '/((?!api|_next/static|_next/image|favicon.ico).*)', // 匹配多个路径 ['/about/:path*', '/contact/:path*'], ],};中间件 APINextResponse 方法import { NextResponse } from 'next/server';export function middleware(request) { // 继续到下一个中间件或页面 return NextResponse.next(); // 重定向 return NextResponse.redirect(new URL('/login', request.url)); // 重写路径 return NextResponse.rewrite(new URL('/about', request.url)); // 返回自定义响应 return NextResponse.json({ message: 'Hello' });}修改请求和响应import { NextResponse } from 'next/server';export function middleware(request) { const response = NextResponse.next(); // 添加响应头 response.headers.set('x-custom-header', 'custom-value'); // 设置 Cookie response.cookies.set('theme', 'dark', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', }); return response;}实际应用场景1. 身份验证保护// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 公开路径 const publicPaths = ['/login', '/register', '/api/auth']; const isPublicPath = publicPaths.some(path => pathname.startsWith(path) ); if (isPublicPath) { return NextResponse.next(); } // 检查认证令牌 const token = request.cookies.get('auth-token')?.value; if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } // 验证令牌 try { const user = verifyToken(token); // 将用户信息添加到请求头 const response = NextResponse.next(); response.headers.set('x-user-id', user.id); response.headers.set('x-user-role', user.role); return response; } catch (error) { return NextResponse.redirect(new URL('/login', request.url)); }}export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ],};2. 国际化路由// middleware.jsimport { NextResponse } from 'next/server';const locales = ['en', 'zh', 'es'];const defaultLocale = 'en';export function middleware(request) { const { pathname } = request.nextUrl; // 检查路径中是否包含语言代码 const pathnameIsMissingLocale = locales.every( locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` ); if (pathnameIsMissingLocale) { // 检查 Accept-Language 头 const acceptLanguage = request.headers.get('accept-language') || ''; const preferredLocale = locales.find(locale => acceptLanguage.includes(locale) ) || defaultLocale; return NextResponse.redirect( new URL(`/${preferredLocale}${pathname}`, request.url) ); } return NextResponse.next();}export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ],};3. A/B 测试// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 只对特定页面进行 A/B 测试 if (pathname === '/landing') { // 获取或创建用户 ID const userId = request.cookies.get('user-id')?.value || generateUserId(); // 根据 ID 决定显示哪个版本 const variant = hash(userId) % 2 === 0 ? 'a' : 'b'; const response = NextResponse.rewrite( new URL(`/landing-${variant}`, request.url) ); // 设置用户 ID Cookie if (!request.cookies.get('user-id')) { response.cookies.set('user-id', userId); } // 添加 A/B 测试头 response.headers.set('x-ab-variant', variant); return response; } return NextResponse.next();}function generateUserId() { return Math.random().toString(36).substring(2, 15);}function hash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash);}4. 地理位置路由// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 只对首页进行地理位置路由 if (pathname === '/') { const country = request.geo?.country || 'US'; // 根据国家重定向到不同版本 const countryRoutes = { 'US': '/us', 'CN': '/cn', 'JP': '/jp', 'DE': '/de', }; const targetPath = countryRoutes[country] || '/us'; return NextResponse.redirect( new URL(targetPath, request.url) ); } return NextResponse.next();}5. 维护模式// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 检查是否在维护模式 const isMaintenanceMode = process.env.MAINTENANCE_MODE === 'true'; if (isMaintenanceMode) { // 允许管理员访问 const isAdmin = request.cookies.get('admin-token')?.value === process.env.ADMIN_TOKEN; if (isAdmin) { return NextResponse.next(); } // 重定向到维护页面 return NextResponse.rewrite( new URL('/maintenance', request.url) ); } return NextResponse.next();}export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico|maintenance).*)', ],};6. 速率限制// middleware.jsimport { NextResponse } from 'next/server';const rateLimit = new Map();export function middleware(request) { const { pathname } = request.nextUrl; // 只对 API 路由进行速率限制 if (!pathname.startsWith('/api/')) { return NextResponse.next(); } const ip = request.ip || 'unknown'; const now = Date.now(); const windowMs = 60 * 1000; // 1 分钟 const maxRequests = 100; // 获取或创建速率限制记录 const record = rateLimit.get(ip) || { count: 0, resetTime: now + windowMs }; // 重置过期的记录 if (now > record.resetTime) { record.count = 0; record.resetTime = now + windowMs; } // 增加请求计数 record.count++; rateLimit.set(ip, record); // 检查是否超过限制 if (record.count > maxRequests) { return NextResponse.json( { error: 'Too many requests' }, { status: 429, headers: { 'Retry-After': Math.ceil((record.resetTime - now) / 1000).toString(), }, } ); } return NextResponse.next();}7. CORS 配置// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 只对 API 路由应用 CORS if (!pathname.startsWith('/api/')) { return NextResponse.next(); } const response = NextResponse.next(); // 设置 CORS 头 response.headers.set('Access-Control-Allow-Credentials', 'true'); response.headers.set('Access-Control-Allow-Origin', '*'); response.headers.set('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT'); response.headers.set( 'Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' ); return response;}8. 日志记录// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const start = Date.now(); const response = NextResponse.next(); // 记录请求信息 const logData = { method: request.method, url: request.url, userAgent: request.headers.get('user-agent'), ip: request.ip, timestamp: new Date().toISOString(), }; console.log('Request:', JSON.stringify(logData)); // 记录响应时间 response.headers.set('x-response-time', `${Date.now() - start}ms`); return response;}高级用法1. 条件中间件// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 根据路径应用不同的逻辑 if (pathname.startsWith('/api/')) { return apiMiddleware(request); } else if (pathname.startsWith('/dashboard/')) { return dashboardMiddleware(request); } else if (pathname.startsWith('/admin/')) { return adminMiddleware(request); } return NextResponse.next();}function apiMiddleware(request) { // API 特定逻辑 return NextResponse.next();}function dashboardMiddleware(request) { // 仪表板特定逻辑 return NextResponse.next();}function adminMiddleware(request) { // 管理员特定逻辑 return NextResponse.next();}2. 链式中间件// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const middlewares = [ authMiddleware, rateLimitMiddleware, loggingMiddleware, ]; for (const mw of middlewares) { const result = mw(request); if (result) { return result; } } return NextResponse.next();}function authMiddleware(request) { const token = request.cookies.get('auth-token')?.value; if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } return null;}function rateLimitMiddleware(request) { // 速率限制逻辑 return null;}function loggingMiddleware(request) { // 日志记录逻辑 return null;}3. 动态重写// middleware.jsimport { NextResponse } from 'next/server';export function middleware(request) { const { pathname } = request.nextUrl; // 动态路由重写 if (pathname.startsWith('/blog/')) { const slug = pathname.split('/')[2]; return NextResponse.rewrite( new URL(`/api/blog?slug=${slug}`, request.url) ); } // 用户资料重写 if (pathname.startsWith('/u/')) { const username = pathname.split('/')[2]; return NextResponse.rewrite( new URL(`/profile?username=${username}`, request.url) ); } return NextResponse.next();}最佳实践1. 避免在中间件中进行繁重操作// ❌ 不好的做法:在中间件中进行数据库查询export function middleware(request) { const user = await db.user.findUnique({ where: { id: request.cookies.get('user-id')?.value } }); if (!user) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next();}// ✅ 好的做法:只验证令牌export function middleware(request) { const token = request.cookies.get('auth-token')?.value; if (!token || !verifyToken(token)) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next();}2. 使用 matcher 精确匹配// ❌ 不好的做法:匹配所有路径export const config = { matcher: '/(.*)',};// ✅ 好的做法:排除不需要的路径export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ],};3. 处理边缘情况export function middleware(request) { // 处理 OPTIONS 请求 if (request.method === 'OPTIONS') { return new NextResponse(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }); } return NextResponse.next();}4. 使用环境变量export function middleware(request) { // 根据环境变量应用不同逻辑 if (process.env.NODE_ENV === 'production') { // 生产环境特定逻辑 return productionMiddleware(request); } else { // 开发环境特定逻辑 return developmentMiddleware(request); }}Next.js 中间件是一个强大的工具,通过合理使用,可以实现复杂的路由逻辑、身份验证、重定向等功能,同时保持应用的性能和安全性。
阅读 0·2月17日 23:30

如何在 Next.js 中实现完整的国际化(i18n)支持?

Next.js 的国际化(i18n)支持是构建多语言网站的关键功能。Next.js 13+ App Router 提供了更灵活的国际化解决方案。App Router 国际化实现1. 基本路由结构// app/[lang]/layout.jsimport { notFound } from 'next/navigation';import { translations } from '../translations';const locales = ['zh-CN', 'en', 'ja'];export function generateStaticParams() { return locales.map((locale) => ({ lang: locale }));}export default async function LocaleLayout({ children, params: { lang } }) { if (!locales.includes(lang)) { notFound(); } return ( <html lang={lang}> <body> <header> <nav> <Link href={`/${lang}`}>首页</Link> <Link href={`/${lang}/about`}>关于</Link> <LanguageSwitcher currentLang={lang} /> </nav> </header> {children} </body> </html> );}// app/[lang]/components/LanguageSwitcher.js'use client';import { usePathname } from 'next/navigation';export default function LanguageSwitcher({ currentLang }) { const pathname = usePathname(); const locales = [ { code: 'zh-CN', name: '中文' }, { code: 'en', name: 'English' }, { code: 'ja', name: '日本語' } ]; return ( <select value={currentLang} onChange={(e) => { const newLang = e.target.value; const newPath = pathname.replace(`/${currentLang}`, `/${newLang}`); window.location.href = newPath; }} > {locales.map((locale) => ( <option key={locale.code} value={locale.code}> {locale.name} </option> ))} </select> );}2. 翻译系统// lib/i18n.jsconst translations = { 'zh-CN': { common: { home: '首页', about: '关于我们', contact: '联系我们', search: '搜索', loading: '加载中...', error: '出错了', submit: '提交', cancel: '取消' }, home: { title: '欢迎来到我们的网站', subtitle: '提供最好的服务', description: '我们致力于为您提供最优质的产品和服务' }, about: { title: '关于我们', mission: '我们的使命', vision: '我们的愿景', team: '我们的团队' } }, 'en': { common: { home: 'Home', about: 'About', contact: 'Contact', search: 'Search', loading: 'Loading...', error: 'Error', submit: 'Submit', cancel: 'Cancel' }, home: { title: 'Welcome to Our Website', subtitle: 'Providing the Best Services', description: 'We are committed to providing you with the highest quality products and services' }, about: { title: 'About Us', mission: 'Our Mission', vision: 'Our Vision', team: 'Our Team' } }, 'ja': { common: { home: 'ホーム', about: '私たちについて', contact: 'お問い合わせ', search: '検索', loading: '読み込み中...', error: 'エラー', submit: '送信', cancel: 'キャンセル' }, home: { title: '私たちのウェブサイトへようこそ', subtitle: '最高のサービスを提供', description: '最高品質の製品とサービスを提供することに尽力しています' }, about: { title: '私たちについて', mission: '私たちの使命', vision: '私たちのビジョン', team: '私たちのチーム' } }};export function getTranslations(lang) { return translations[lang] || translations['en'];}export function t(lang, key) { const keys = key.split('.'); let value = translations[lang] || translations['en']; for (const k of keys) { value = value?.[k]; } return value || key;}// app/[lang]/page.jsimport { getTranslations } from '@/lib/i18n';export default async function HomePage({ params: { lang } }) { const t = getTranslations(lang); return ( <div> <h1>{t.home.title}</h1> <p>{t.home.subtitle}</p> <p>{t.home.description}</p> </div> );}3. 动态翻译 Hook// hooks/useTranslation.js'use client';import { usePathname } from 'next/navigation';import { getTranslations } from '@/lib/i18n';export function useTranslation() { const pathname = usePathname(); const lang = pathname.split('/')[1] || 'en'; const t = getTranslations(lang); return { lang, t };}// app/[lang]/components/Navigation.js'use client';import { useTranslation } from '@/hooks/useTranslation';import Link from 'next/link';export default function Navigation() { const { t } = useTranslation(); return ( <nav> <Link href={`/${t.lang}`}>{t.common.home}</Link> <Link href={`/${t.lang}/about`}>{t.common.about}</Link> <Link href={`/${t.lang}/contact`}>{t.common.contact}</Link> </nav> );}4. 日期和数字格式化// lib/formatters.jsexport function formatDate(date, locale) { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(date));}export function formatNumber(number, locale) { return new Intl.NumberFormat(locale).format(number);}export function formatCurrency(amount, locale, currency = 'USD') { return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);}export function formatRelativeTime(date, locale) { const now = new Date(); const diff = now - new Date(date); const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); if (days > 0) return rtf.format(-days, 'day'); if (hours > 0) return rtf.format(-hours, 'hour'); if (minutes > 0) return rtf.format(-minutes, 'minute'); return rtf.format(-seconds, 'second');}// 使用示例// app/[lang]/blog/[slug]/page.jsimport { formatDate, formatRelativeTime } from '@/lib/formatters';export default async function BlogPost({ params: { lang, slug } }) { const post = await getPost(slug); return ( <article> <h1>{post.title}</h1> <time dateTime={post.date}> {formatDate(post.date, lang)} </time> <p> {formatRelativeTime(post.date, lang)} </p> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> );}5. SEO 优化// app/[lang]/page.jsimport { Metadata } from 'next';import { getTranslations } from '@/lib/i18n';export async function generateMetadata({ params: { lang } }): Promise<Metadata> { const t = getTranslations(lang); return { title: t.home.title, description: t.home.description, alternates: { canonical: `/${lang}`, languages: { 'zh-CN': '/zh-CN', 'en': '/en', 'ja': '/ja' } }, openGraph: { title: t.home.title, description: t.home.description, locale: lang, alternateLocale: ['zh-CN', 'en', 'ja'].filter(l => l !== lang) } };}6. 服务端组件中的翻译// app/[lang]/products/page.jsimport { getTranslations } from '@/lib/i18n';export default async function ProductsPage({ params: { lang } }) { const t = getTranslations(lang); const products = await getProducts(); return ( <div> <h1>{t.products.title}</h1> <div className="grid"> {products.map(product => ( <ProductCard key={product.id} product={product} lang={lang} /> ))} </div> </div> );}// app/[lang]/components/ProductCard.js'use client';import { useTranslation } from '@/hooks/useTranslation';export default function ProductCard({ product, lang }) { const { t } = useTranslation(); return ( <div className="card"> <img src={product.image} alt={product.name[lang]} /> <h3>{product.name[lang]}</h3> <p>{product.description[lang]}</p> <button>{t.common.buy}</button> </div> );}7. 翻译文件管理// translations/zh-CN/home.json{ "title": "欢迎来到我们的网站", "subtitle": "提供最好的服务", "description": "我们致力于为您提供最优质的产品和服务"}// translations/en/home.json{ "title": "Welcome to Our Website", "subtitle": "Providing the Best Services", "description": "We are committed to providing you with the highest quality products and services"}// lib/loadTranslations.jsimport fs from 'fs';import path from 'path';export async function loadTranslations(lang, namespace) { const filePath = path.join(process.cwd(), 'translations', lang, `${namespace}.json`); try { const fileContent = await fs.promises.readFile(filePath, 'utf-8'); return JSON.parse(fileContent); } catch (error) { console.error(`Failed to load translations for ${lang}/${namespace}`); return {}; }}// 使用// app/[lang]/page.jsimport { loadTranslations } from '@/lib/loadTranslations';export default async function HomePage({ params: { lang } }) { const homeTranslations = await loadTranslations(lang, 'home'); return ( <div> <h1>{homeTranslations.title}</h1> <p>{homeTranslations.subtitle}</p> </div> );}最佳实践默认语言: 设置一个默认语言作为后备翻译键命名: 使用一致的命名约定(如 namespace.key)缺失翻译: 提供优雅的降级处理性能优化: 使用静态生成和缓存SEO: 为每种语言设置正确的 hreflang 标签内容管理: 考虑使用 CMS 管理多语言内容测试: 确保所有语言版本都经过测试Next.js 的国际化支持使得构建多语言网站变得简单高效,同时保持了优秀的性能和 SEO 表现。
阅读 0·2月17日 23:30

Next.js 的图片优化是如何工作的?

Next.js 的图片优化功能通过 next/image 组件提供,这是一个强大的工具,可以自动优化图片以提升性能和用户体验。next/image 组件基础基本用法import Image from 'next/image';export default function Page() { return ( <Image src="/hero.jpg" alt="Hero image" width={800} height={600} /> );}必需属性src: 图片路径(本地或远程)width: 图片宽度(整数)height: 图片高度(整数)alt: 替代文本(用于可访问性)图片优化特性1. 自动格式转换Next.js 自动将图片转换为现代格式(WebP、AVIF),以减少文件大小。<Image src="/photo.jpg" alt="Photo" width={800} height={600} // 自动转换为 WebP 或 AVIF/>2. 响应式图片自动生成不同尺寸的图片,根据设备加载合适的尺寸。<Image src="/responsive.jpg" alt="Responsive image" width={1200} height={800} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" // 生成多个尺寸:640w, 750w, 828w, 1080w, 1200w, 1920w, 2048w, 3840w/>3. 懒加载默认情况下,图片会懒加载,只有当它们进入视口时才会加载。<Image src="/lazy.jpg" alt="Lazy loaded image" width={800} height={600} // 默认启用懒加载/>// 禁用懒加载(首屏图片)<Image src="/hero.jpg" alt="Hero image" width={800} height={600} priority // 禁用懒加载,优先加载/>4. 模糊占位符在图片加载前显示模糊的占位符,提升用户体验。<Image src="/photo.jpg" alt="Photo" width={800} height={600} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD..."/>5. 避免布局偏移通过设置明确的宽高,避免图片加载时的布局偏移。<Image src="/photo.jpg" alt="Photo" width={800} height={600} // 预留空间,避免布局偏移/>高级用法1. 远程图片配置允许的远程图片域名。// next.config.jsmodule.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', port: '', pathname: '/images/**', }, { protocol: 'https', hostname: 'cdn.example.com', }, ], },};使用远程图片:<Image src="https://example.com/images/photo.jpg" alt="Remote image" width={800} height={600}/>2. 动态图片import Image from 'next/image';export default function ProductGallery({ images }) { return ( <div> {images.map((image) => ( <Image key={image.id} src={image.url} alt={image.alt} width={image.width} height={image.height} placeholder="blur" blurDataURL={image.blurDataURL} /> ))} </div> );}3. 图片填充模式<Image src="/photo.jpg" alt="Photo" width={800} height={600} fill // 填充父容器 style={{ objectFit: 'cover' }}/>4. 图片质量控制<Image src="/photo.jpg" alt="Photo" width={800} height={600} quality={80} // 默认 75,范围 1-100/>5. 自定义加载器import Image from 'next/image';const imageLoader = ({ src, width, quality }) => { return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`;};export default function Page() { return ( <Image loader={imageLoader} src="photo.jpg" alt="Photo" width={800} height={600} /> );}配置选项next.config.js 配置// next.config.jsmodule.exports = { images: { // 设备尺寸 deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // 图片尺寸 imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // 图片格式 formats: ['image/avif', 'image/webp'], // 最小化缓存时间(秒) minimumCacheTTL: 60, // 禁用静态导入 disableStaticImages: false, // 禁用优化 unoptimized: false, // 危险地允许所有域名(不推荐) dangerouslyAllowSVG: false, // 内容安全策略 contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", // 内容处置类型 contentDispositionType: 'attachment', // 远程图片模式 remotePatterns: [ { protocol: 'https', hostname: 'example.com', port: '', pathname: '/images/**', }, ], // 域名列表(已弃用,使用 remotePatterns) domains: ['example.com'], },};实际应用场景1. 电商产品图片import Image from 'next/image';export default function ProductCard({ product }) { return ( <div className="product-card"> <Image src={product.image} alt={product.name} width={400} height={400} priority // 首屏产品优先加载 placeholder="blur" blurDataURL={product.blurDataURL} sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" /> <h3>{product.name}</h3> <p>${product.price}</p> </div> );}2. 博客文章封面图import Image from 'next/image';export default function BlogPost({ post }) { return ( <article> <Image src={post.coverImage} alt={post.title} width={1200} height={630} priority // 文章封面优先加载 placeholder="blur" blurDataURL={post.blurDataURL} sizes="100vw" /> <h1>{post.title}</h1> <p>{post.excerpt}</p> </article> );}3. 用户头像import Image from 'next/image';export default function UserAvatar({ user, size = 40 }) { return ( <div className="avatar"> <Image src={user.avatar || '/default-avatar.png'} alt={user.name} width={size} height={size} className="rounded-full" placeholder="blur" blurDataURL={user.blurDataURL} /> </div> );}4. 图片画廊'use client';import { useState } from 'react';import Image from 'next/image';export default function ImageGallery({ images }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="gallery"> <div className="main-image"> <Image src={images[selectedIndex].url} alt={images[selectedIndex].alt} width={1200} height={800} priority placeholder="blur" blurDataURL={images[selectedIndex].blurDataURL} /> </div> <div className="thumbnails"> {images.map((image, index) => ( <button key={image.id} onClick={() => setSelectedIndex(index)} className={index === selectedIndex ? 'active' : ''} > <Image src={image.url} alt={image.alt} width={150} height={100} placeholder="blur" blurDataURL={image.blurDataURL} /> </button> ))} </div> </div> );}5. 响应式英雄图片import Image from 'next/image';export default function HeroSection() { return ( <section className="hero"> <Image src="/hero.jpg" alt="Hero image" width={1920} height={1080} priority placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD..." sizes="100vw" style={{ width: '100%', height: 'auto', objectFit: 'cover', }} /> <div className="hero-content"> <h1>Welcome to Our Site</h1> <p>Discover amazing content</p> </div> </section> );}性能优化技巧1. 使用 blurDataURL// 生成模糊占位符import { getPlaiceholder } from 'plaiceholder';export async function getBlurDataURL(src) { const buffer = await fetch(src).then(res => res.arrayBuffer()); const { base64 } = await getPlaiceholder(buffer); return base64;}// 使用const blurDataURL = await getBlurDataURL('/photo.jpg');<Image src="/photo.jpg" alt="Photo" width={800} height={600} placeholder="blur" blurDataURL={blurDataURL}/>2. 优化图片尺寸// 为不同场景使用合适的尺寸// 小缩略图<Image src="/thumb.jpg" alt="Thumbnail" width={150} height={150}/>// 中等图片<Image src="/medium.jpg" alt="Medium" width={400} height={300}/>// 大图<Image src="/large.jpg" alt="Large" width={1200} height={800}/>3. 使用合适的图片格式// next.config.jsmodule.exports = { images: { formats: ['image/avif', 'image/webp'], },};4. 延迟加载非关键图片// 首屏图片使用 priority<Image src="/hero.jpg" alt="Hero" width={800} height={600} priority/>// 非首屏图片使用默认懒加载<Image src="/below-fold.jpg" alt="Below fold" width={800} height={600}/>5. 使用 CDN 加速// next.config.jsmodule.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com', }, ], },};最佳实践1. 始终提供 alt 文本// ✅ 好的做法<Image src="/photo.jpg" alt="A beautiful sunset over the ocean" width={800} height={600}/>// ❌ 不好的做法<Image src="/photo.jpg" alt="" width={800} height={600}/>2. 为首屏图片设置 priority// ✅ 好的做法:首屏图片优先加载<Image src="/hero.jpg" alt="Hero" width={800} height={600} priority/>// ❌ 不好的做法:所有图片都设置 priority<Image src="/below-fold.jpg" alt="Below fold" width={800} height={600} priority // 不必要的优先加载/>3. 使用合适的 sizes 属性// ✅ 好的做法:根据布局设置 sizes<Image src="/photo.jpg" alt="Photo" width={800} height={600} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"/>// ❌ 不好的做法:不设置 sizes<Image src="/photo.jpg" alt="Photo" width={800} height={600} // 浏览器会假设图片占满视口/>4. 优化图片质量// ✅ 好的做法:根据图片类型调整质量<Image src="/photo.jpg" alt="Photo" width={800} height={600} quality={80} // 照片使用较高质量/><Image src="/icon.png" alt="Icon" width={64} height={64} quality={90} // 图标使用更高质量/>// ❌ 不好的做法:所有图片使用相同质量<Image src="/photo.jpg" alt="Photo" width={800} height={600} quality={100} // 不必要的高质量/>5. 使用占位符提升用户体验// ✅ 好的做法:使用模糊占位符<Image src="/photo.jpg" alt="Photo" width={800} height={600} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD..."/>// ❌ 不好的做法:不使用占位符<Image src="/photo.jpg" alt="Photo" width={800} height={600} // 图片加载前会显示空白/>常见问题Q: 如何处理动态图片?A: 使用动态导入或从 API 获取图片信息。import Image from 'next/image';export default async function DynamicImagePage() { const images = await fetch('/api/images').then(res => res.json()); return ( <div> {images.map(image => ( <Image key={image.id} src={image.url} alt={image.alt} width={image.width} height={image.height} /> ))} </div> );}Q: 如何禁用图片优化?A: 在 next.config.js 中设置 unoptimized: true。module.exports = { images: { unoptimized: true, },};Q: 如何处理 SVG 图片?A: SVG 图片需要特殊配置。module.exports = { images: { dangerouslyAllowSVG: true, contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", },};Next.js 的图片优化功能可以显著提升应用性能和用户体验。通过合理使用 next/image 组件和配置选项,可以构建出高性能的图片展示方案。
阅读 0·2月17日 23:30

Next.js 的缓存机制是如何工作的?

Next.js 的缓存机制是其性能优化的核心,理解这些缓存策略对于构建高性能应用至关重要。缓存层次Next.js 提供多个层次的缓存:构建时缓存:静态生成和 ISR请求时缓存:fetch API 缓存CDN 缓存:边缘网络缓存浏览器缓存:客户端缓存数据缓存:React Query、SWR 等Fetch API 缓存缓存选项Next.js 扩展了 fetch API,提供了多种缓存选项。// 默认缓存(force-cache)async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', // 默认 }).then(r => r.json()); return <div>{data.content}</div>;}// 不缓存(no-store)async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>;}// 验证缓存(no-cache)async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-cache', // 每次验证缓存 }).then(r => r.json()); return <div>{data.content}</div>;}// 仅当有缓存时使用(only-if-cached)async function Page() { const response = await fetch('https://api.example.com/data', { cache: 'only-if-cached', }); if (!response.ok) { return <div>Loading...</div>; } const data = await response.json(); return <div>{data.content}</div>;}Next.js 扩展选项// revalidate:设置重新验证时间(秒)async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60, // 每 60 秒重新验证 }, }).then(r => r.json()); return <div>{data.content}</div>;}// tags:用于按需重新验证async function Page() { const data = await fetch('https://api.example.com/data', { next: { tags: ['posts', 'latest'], }, }).then(r => r.json()); return <div>{data.content}</div>;}// 组合使用async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', next: { revalidate: 3600, tags: ['data'], }, }).then(r => r.json()); return <div>{data.content}</div>;}按需重新验证revalidatePathimport { revalidatePath } from 'next/cache';'use server';export async function updatePost(postId: string, formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.update({ where: { id: postId }, data: { title, content }, }); // 重新验证特定路径 revalidatePath('/posts'); revalidatePath(`/posts/${postId}`); return { success: true };}revalidateTagimport { revalidateTag } from 'next/cache';'use server';export async function createPost(formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.create({ data: { title, content }, }); // 重新验证所有带有 'posts' 标签的缓存 revalidateTag('posts'); return { success: true };}// 使用标签的数据获取async function Page() { const posts = await fetch('https://api.example.com/posts', { next: { tags: ['posts'], revalidate: 3600, }, }).then(r => r.json()); return <PostList posts={posts} />;}静态生成缓存getStaticProps 缓存export async function getStaticProps() { const data = await fetchData(); return { props: { data }, revalidate: 3600, // ISR:每小时重新生成 };}getStaticPaths 缓存export async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: 'blocking', // 或 false 或 true };}export async function getStaticProps({ params }) { const post = await getPostBySlug(params.slug); return { props: { post }, revalidate: 86400, // 每天重新生成 };}服务器组件缓存默认缓存行为// 默认情况下,fetch 请求会被缓存async function Page() { const data = await fetch('https://api.example.com/data') .then(r => r.json()); return <div>{data.content}</div>;}禁用缓存// 禁用缓存async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>;}动态数据获取// 对于动态数据,禁用缓存async function UserDashboard({ params }) { const user = await fetch(`https://api.example.com/user/${params.id}`, { cache: 'no-store', }).then(r => r.json()); return <Dashboard user={user} />;}客户端缓存React Query 缓存'use client';import { useQuery, useQueryClient } from '@tanstack/react-query';export default function DataComponent() { const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['data'], queryFn: () => fetch('/api/data').then(r => r.json()), staleTime: 60000, // 数据在 60 秒内被认为是新鲜的 cacheTime: 300000, // 缓存在 5 分钟内有效 }); const handleRefresh = () => { // 手动刷新数据 queryClient.invalidateQueries({ queryKey: ['data'] }); }; if (isLoading) return <div>Loading...</div>; return ( <div> <div>{data.content}</div> <button onClick={handleRefresh}>Refresh</button> </div> );}SWR 缓存'use client';import useSWR from 'swr';const fetcher = (url) => fetch(url).then(r => r.json());export default function DataComponent() { const { data, error, isLoading, mutate } = useSWR( '/api/data', fetcher, { revalidateOnFocus: false, // 窗口聚焦时不重新验证 revalidateOnReconnect: false, // 重新连接时不重新验证 dedupingInterval: 60000, // 60 秒内去重请求 refreshInterval: 0, // 不自动刷新 } ); const handleRefresh = () => { // 手动刷新数据 mutate(); }; if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return ( <div> <div>{data.content}</div> <button onClick={handleRefresh}>Refresh</button> </div> );}CDN 缓存Vercel CDN部署到 Vercel 时,静态资源会自动缓存到 CDN。// next.config.jsmodule.exports = { // 静态资源会被缓存到 Vercel CDN output: 'standalone',};自定义 CDN 头// app/api/data/route.jsexport async function GET() { const data = await fetchData(); return Response.json(data, { headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', 'CDN-Cache-Control': 'public, s-maxage=3600', }, });}浏览器缓存设置缓存头// app/api/data/route.jsexport async function GET() { const data = await fetchData(); return Response.json(data, { headers: { // 强缓存:1 小时 'Cache-Control': 'public, max-age=3600, immutable', // 协商缓存:ETag 'ETag': generateETag(data), // 最后修改时间 'Last-Modified': new Date().toUTCString(), }, });}function generateETag(data) { const crypto = require('crypto'); return crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');}条件请求// app/api/data/route.jsexport async function GET(request) { const data = await fetchData(); const etag = generateETag(data); // 检查 If-None-Match 头 const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch === etag) { return new Response(null, { status: 304 }); } return Response.json(data, { headers: { 'ETag': etag, 'Cache-Control': 'public, max-age=3600', }, });}高级缓存策略分层缓存// 第一层:浏览器缓存// 第二层:CDN 缓存// 第三层:Next.js 缓存// 第四层:数据源async function Page() { const data = await fetch('https://api.example.com/data', { // Next.js 缓存 cache: 'force-cache', next: { revalidate: 3600, tags: ['data'], }, }).then(r => r.json()); return <div>{data.content}</div>;}// API 路由设置 CDN 缓存export async function GET() { const data = await fetchData(); return Response.json(data, { headers: { // CDN 缓存 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', }, });}智能缓存失效'use server';import { revalidateTag, revalidatePath } from 'next/cache';export async function updateData(id: string, formData: FormData) { const data = await updateDataInDB(id, formData); // 根据数据类型选择失效策略 if (data.type === 'post') { revalidateTag('posts'); revalidatePath('/posts'); } else if (data.type === 'user') { revalidateTag('users'); revalidatePath('/users'); } return { success: true };}缓存预热// app/api/warmup/route.jsexport async function GET() { const urls = [ 'https://example.com/api/data', 'https://example.com/api/posts', 'https://example.com/api/users', ]; // 并行预热缓存 await Promise.all( urls.map(url => fetch(url)) ); return Response.json({ message: 'Cache warmed up' });}实际应用场景1. 博客文章缓存// app/blog/[slug]/page.jsasync function BlogPost({ params }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { revalidate: 86400, // 每天重新验证 tags: [`post-${params.slug}`, 'posts'], }, }).then(r => r.json()); return <PostContent post={post} />;}// 更新文章时重新验证'use server';import { revalidateTag } from 'next/cache';export async function updatePost(postId: string, formData: FormData) { const post = await updatePostInDB(postId, formData); revalidateTag(`post-${post.slug}`); revalidateTag('posts'); return { success: true };}2. 电商产品缓存// app/products/[id]/page.jsasync function ProductPage({ params }) { const product = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 3600, // 每小时重新验证 tags: [`product-${params.id}`, 'products'], }, }).then(r => r.json()); return <ProductDetails product={product} />;}// 产品列表页面async function ProductListPage() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 600, // 每 10 分钟重新验证 tags: ['products'], }, }).then(r => r.json()); return <ProductList products={products} />;}3. 用户数据缓存// app/dashboard/page.jsasync function DashboardPage() { const session = await auth(); // 用户数据不缓存(动态数据) const user = await fetch(`https://api.example.com/user/${session.user.id}`, { cache: 'no-store', }).then(r => r.json()); // 统计数据可以缓存 const stats = await fetch(`https://api.example.com/stats/${session.user.id}`, { next: { revalidate: 300, // 每 5 分钟重新验证 tags: [`stats-${session.user.id}`], }, }).then(r => r.json()); return <Dashboard user={user} stats={stats} />;}4. 实时数据缓存// app/live/page.jsasync function LivePage() { // 实时数据不缓存 const liveData = await fetch('https://api.example.com/live', { cache: 'no-store', }).then(r => r.json()); return <LiveData data={liveData} />;}// 使用 SWR 进行客户端轮询'use client';import useSWR from 'swr';export default function LiveDataComponent() { const { data } = useSWR( '/api/live', fetcher, { refreshInterval: 5000, // 每 5 秒刷新 revalidateOnFocus: true, } ); return <div>{data?.content}</div>;}最佳实践1. 根据数据类型选择缓存策略// ✅ 好的做法:静态内容使用长时间缓存async function Page() { const data = await fetch('https://api.example.com/static-data', { next: { revalidate: 86400, // 1 天 }, }).then(r => r.json()); return <div>{data.content}</div>;}// ✅ 好的做法:动态内容禁用缓存async function Page() { const data = await fetch('https://api.example.com/dynamic-data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>;}// ❌ 不好的做法:所有数据使用相同缓存策略async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600, }, }).then(r => r.json()); return <div>{data.content}</div>;}2. 使用标签进行精确的缓存失效// ✅ 好的做法:使用标签async function Page() { const data = await fetch('https://api.example.com/data', { next: { tags: ['posts', 'latest'], }, }).then(r => r.json()); return <div>{data.content}</div>;}// 更新时只失效相关标签revalidateTag('posts');// ❌ 不好的做法:失效整个路径revalidatePath('/');3. 合理设置重新验证时间// ✅ 好的做法:根据更新频率设置async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600, // 1 小时 }, }).then(r => r.json()); return <div>{data.content}</div>;}// ❌ 不好的做法:设置过短的重新验证时间async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 10, // 10 秒(太短) }, }).then(r => r.json()); return <div>{data.content}</div>;}4. 监控缓存命中率// lib/cacheMonitor.jsexport function logCacheHit(key: string, hit: boolean) { console.log(`Cache ${hit ? 'hit' : 'miss'}: ${key}`); // 发送到监控服务 if (typeof window !== 'undefined') { fetch('/api/cache-log', { method: 'POST', body: JSON.stringify({ key, hit }), }); }}// 使用async function Page() { const key = 'data'; const data = await fetch('https://api.example.com/data', { next: { tags: [key], }, }).then(r => r.json()); logCacheHit(key, true); return <div>{data.content}</div>;}通过合理使用 Next.js 的缓存机制,可以显著提升应用性能,减少服务器负载,提供更好的用户体验。
阅读 0·2月17日 23:30

Next.js 的路由系统是如何工作的?

Next.js 的路由系统基于文件系统,提供了简单而强大的路由管理方式。以下是 Next.js 路由系统的核心概念:1. 基础路由在 pages 目录下,每个文件都会自动成为一个路由。pages/ index.js → / (首页) about.js → /about blog/ index.js → /blog post.js → /blog/post2. 动态路由使用方括号 [] 创建动态路由段,可以匹配任何值。pages/ blog/ [slug].js → /blog/hello-world, /blog/nextjs-tutorial users/ [id]/ profile.js → /users/123/profile获取动态路由参数:export default function BlogPost({ params }) { // Next.js 13+ App Router return <div>Post: {params.slug}</div>;}// 或者使用 getStaticPaths 和 getStaticProps (Pages Router)export async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: false };}export async function getStaticProps({ params }) { const post = await getPostBySlug(params.slug); return { props: { post } };}3. 捕获所有路由使用 [...param] 语法可以捕获多层路径。pages/ docs/ [...path].js → /docs/a, /docs/a/b, /docs/a/b/cexport default function DocsPage({ params }) { // params.path 是一个数组,如 ['a', 'b', 'c'] return <div>Path: {params.path.join('/')}</div>;}4. 可选的捕获所有路由使用 [[...param]] 语法可以创建可选的捕获所有路由,即使没有参数也能匹配。pages/ shop/ [[...category]].js → /shop, /shop/clothes, /shop/clothes/men5. 嵌套路由Next.js 支持嵌套路由,可以通过布局组件实现。// pages/blog/index.jsexport default function Blog() { return ( <div> <h1>Blog</h1> <PostList /> </div> );}// pages/blog/[slug].jsexport default function BlogPost() { return ( <div> <h1>Blog Post</h1> <PostContent /> </div> );}6. 路由跳转Next.js 提供了多种路由跳转方式:使用 Link 组件(推荐)import Link from 'next/link';export default function Navigation() { return ( <nav> <Link href="/">Home</Link> <Link href="/about">About</Link> <Link href="/blog/nextjs">Blog Post</Link> </nav> );}使用 useRouter Hookimport { useRouter } from 'next/router';export default function Button() { const router = useRouter(); const handleClick = () => { router.push('/about'); // 或者 router.replace('/about') 替换当前历史记录 }; return <button onClick={handleClick}>Go to About</button>;}7. 路由中间件Next.js 12+ 支持路由中间件,可以在请求到达页面之前执行逻辑。// middleware.jsexport function middleware(request) { const url = request.nextUrl; if (url.pathname.startsWith('/admin') && !request.cookies.get('token')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next();}8. 路由组(App Router)Next.js 13+ 的 App Router 引入了路由组概念,使用括号 () 创建不会影响 URL 的路由组。app/ (marketing)/ about/ page.js → /about contact/ page.js → /contact (dashboard)/ layout.js → 共享布局 settings/ page.js → /settings9. 并行路由和拦截路由(App Router)App Router 还支持高级路由模式:并行路由:同时渲染多个页面拦截路由:在不改变 URL 的情况下显示其他内容最佳实践保持路由简洁:使用有意义的 URL 结构使用动态路由:对于相似内容的页面使用动态路由合理使用捕获所有路由:只在必要时使用利用 Link 组件:提供更好的用户体验和性能使用路由中间件:处理认证、重定向等逻辑Next.js 的路由系统设计简洁而强大,通过文件系统自动管理路由,大大简化了路由配置工作,同时提供了足够的灵活性来处理复杂的路由需求。
阅读 0·2月17日 23:30

什么是 npm,它作为 JavaScript 项目的包管理器是如何工作的?

npm (Node Package Manager) 是 Node.js 的默认包管理器,用于安装、管理和发布 JavaScript 包。核心功能npm 主要提供以下功能:包管理:从 npm registry 下载和安装第三方包依赖管理:自动处理包之间的依赖关系版本控制:使用语义化版本管理包版本脚本执行:通过 package.json 定义和运行项目脚本包发布:将自定义包发布到 npm registry 供他人使用工作原理npm 的工作流程如下:初始化项目:运行 npm init 创建 package.json 文件安装依赖:运行 npm install 安装 package.json 中定义的依赖依赖解析:npm 解析依赖树,确定需要安装的所有包及其版本下载安装:从 npm registry 下载包到 node_modules 目录锁定版本:生成 package-lock.json 记录精确的依赖版本package.json 结构{ "name": "my-project", "version": "1.0.0", "description": "项目描述", "main": "index.js", "scripts": { "start": "node index.js", "test": "jest" }, "dependencies": { "express": "^4.18.0" }, "devDependencies": { "jest": "^29.0.0" }}常用命令npm install <package>:安装包npm install -g <package>:全局安装包npm install --save-dev <package>:安装为开发依赖npm update:更新依赖包npm uninstall <package>:卸载包npm run <script>:运行 package.json 中定义的脚本依赖类型dependencies:生产环境依赖,应用运行时必需devDependencies:开发环境依赖,仅在开发时需要peerDependencies:同伴依赖,需要宿主项目提供optionalDependencies:可选依赖,安装失败不会中断npm 通过 package.json 和 package-lock.json 文件确保项目依赖的一致性和可重现性,是现代 JavaScript 开发生态系统的核心工具。
阅读 0·2月17日 23:29