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

Next.js

next.js是一个基于React的通用JavaScript框架,next.js为React组件模型提供了扩展,支持基于服务器的组件渲染,同时也支持在客户端继续进行渲染 next.js是一个基于React的通用JavaScript框架,next.js为React组件模型提供了扩展,支持基于服务器的组件渲染,同时也支持在客户端继续进行渲染 next.js是一个基于React的通用JavaScript框架,next.js为React组件模型提供了扩展,支持基于服务器的组件渲染,同时也支持在客户端继续进行渲染
Next.js
查看更多相关内容
Vercel 与 Next.js 的集成优势是什么?## Vercel 与 Next.js 的集成优势是什么? Vercel 与 Next.js 的集成可以说是天作之合,因为 Vercel 的创始团队也是 Next.js 的创建者。这种深度集成带来了许多独特的优势,使得在 Vercel 上部署 Next.js 应用成为最佳选择。 ### 深度集成的技术优势 #### 1. 零配置部署 **自动检测和优化**: - Vercel 自动识别 Next.js 项目 - 自动配置构建设置和路由 - 无需手动配置 `vercel.json` - 自动应用 Next.js 特定的优化 **智能构建**: - 自动识别页面类型(静态、动态、ISR) - 优化构建流程 - 自动处理图片优化 - 智能缓存策略 #### 2. Serverless Functions 无缝支持 **API Routes 部署**: ```javascript // pages/api/hello.js export default function handler(req, res) { res.status(200).json({ message: 'Hello from Vercel' }); } ``` **自动部署为 Serverless 函数**: - 每个 API Route 自动成为独立的 Serverless 函数 - 自动处理函数的冷启动 - 优化的函数内存和超时配置 - 自动扩展以应对流量 #### 3. 增量静态再生成(ISR)优化 **原生支持**: ```javascript export async function getStaticProps() { return { props: { data: await fetchData() }, revalidate: 60 // 每 60 秒重新生成 }; } ``` **Vercel 特定优化**: - 智能的缓存失效策略 - 后台重新生成,不影响用户体验 - 分布式缓存确保一致性 - 自动处理 CDN 缓存 #### 4. 边缘运行时支持 **Edge Runtime**: ```javascript export const runtime = 'edge'; export default function handler() { return new Response('Hello from Edge!'); } ``` **优势**: - 在全球边缘节点执行代码 - 极低的延迟 - 自动地理位置路由 - 优化的冷启动时间 ### 性能优化 #### 1. 图片优化 **自动图片优化**: ```jsx import Image from 'next/image'; <Image src="/hero.jpg" alt="Hero" width={800} height={600} priority /> ``` **Vercel 优化**: - 自动生成多种尺寸和格式 - WebP、AVIF 等现代格式支持 - 智能的懒加载 - CDN 缓存优化后的图片 #### 2. 字体优化 **next/font 集成**: ```jsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'] }); ``` **优势**: - 自动优化字体加载 - 零布局偏移 - 自动托管字体文件 - 智能的字体子集化 #### 3. 代码分割和懒加载 **自动优化**: - 路由级别的代码分割 - 组件级别的懒加载 - 自动预加载关键资源 - 优化的包大小 ### 开发体验提升 #### 1. 预览部署 **Pull Request 预览**: - 每个 PR 自动生成预览 URL - 实时更新预览 - 独立的环境变量 - 便于代码审查 #### 2. 实时日志 **详细的日志信息**: - 构建日志 - 运行时日志 - 错误堆栈跟踪 - 性能指标 #### 3. Analytics 集成 **Vercel Analytics**: ```jsx import { Analytics } from '@vercel/analytics/react'; export default function RootLayout({ children }) { return ( <html> <body> {children} <Analytics /> </body> </html> ); } ``` **功能**: - Web Vitals 监控 - 用户行为分析 - 性能洞察 - 无需额外配置 ### 高级功能支持 #### 1. 中间件支持 **Next.js Middleware**: ```javascript import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { return NextResponse.rewrite(new URL('/dashboard', request.url)); } export const config = { matcher: '/home/:path*', }; ``` **Vercel 优势**: - 在边缘运行中间件 - 极快的响应时间 - 自动扩展 - 支持复杂的路由逻辑 #### 2. Server Components 支持 **React Server Components**: ```jsx // Server Component async function UserProfile({ userId }) { const user = await fetchUser(userId); return <div>{user.name}</div>; } ``` **优势**: - 自动在服务器端渲染 - 减少客户端 JavaScript - 更快的首屏加载 - 更好的 SEO #### 3. Streaming 支持 **渐进式渲染**: ```jsx import { Suspense } from 'react'; export default function Page() { return ( <div> <Header /> <Suspense fallback={<Loading />}> <SlowComponent /> </Suspense> </div> ); } ``` **Vercel 优化**: - 自动支持 Streaming - 优化的传输协议 - 更快的 Time to First Byte (TTFB) - 更好的用户体验 ### 部署和运维优势 #### 1. 自动扩展 **按需扩展**: - 自动处理流量峰值 - 无需手动配置服务器 - 全球边缘网络 - 高可用性保证 #### 2. 回滚功能 **一键回滚**: - 保留所有历史部署 - 快速回滚到任何版本 - 零停机时间 - 简单的版本管理 #### 3. 环境管理 **多环境支持**: - Production、Preview、Development 环境 - 独立的环境变量 - 环境特定的配置 - 简化的环境切换 ### 成本效益 #### 1. 免费额度 **免费计划包含**: - 无限带宽 - 100GB 带宽 - 6,000 分钟构建时间 - 100GB-Hours Serverless Functions - 无限预览部署 #### 2. 按需付费 **付费计划优势**: - 更高的配额 - 优先支持 - 团队协作功能 - 高级分析 ### 与其他平台的对比 #### 1. vs Netlify **Vercel 优势**: - 更好的 Next.js 支持 - 更快的边缘函数 - 更详细的日志 - 更好的开发体验 #### 2. vs AWS Amplify **Vercel 优势**: - 更简单的配置 - 更快的部署 - 更好的预览部署 - 更直观的界面 #### 3. vs 自托管 **Vercel 优势**: - 零运维成本 - 自动扩展 - 全球 CDN - 自动 SSL ### 最佳实践 #### 1. 利用 ISR - 对动态内容使用 ISR - 设置合理的 revalidate 时间 - 使用 on-demand revalidation - 监控缓存命中率 #### 2. 优化图片 - 使用 next/image 组件 - 提供正确的尺寸 - 使用 priority 属性 - 启用自动格式转换 #### 3. 使用 Edge Runtime - 对需要低延迟的功能使用 Edge Runtime - 注意 Edge Runtime 的限制 - 合理划分 Serverless 和 Edge 函数 #### 4. 监控性能 - 使用 Vercel Analytics - 监控 Web Vitals - 跟踪错误率 - 优化关键路径 ### 实际应用案例 #### 1. 电商网站 **优势**: - ISR 实现产品页面缓存 - Edge Functions 处理购物车 - 图片优化提升加载速度 - 全球 CDN 确保快速访问 #### 2. 内容平台 **优势**: - SSG 生成静态页面 - ISR 更新内容 - 预览部署便于内容审核 - Analytics 了解用户行为 #### 3. SaaS 应用 **优势**: - Serverless Functions 处理 API - Middleware 处理认证 - Edge Runtime 提升响应速度 - 自动扩展应对用户增长 ### 总结 Vercel 与 Next.js 的深度集成提供了: 1. **零配置体验**:自动识别和优化 2. **卓越性能**:边缘网络、CDN、优化 3. **开发效率**:预览部署、实时日志 4. **可扩展性**:自动扩展、高可用 5. **成本效益**:免费额度、按需付费 这种集成使得开发者能够专注于构建功能,而不必担心基础设施和部署细节,是 Next.js 应用的理想部署平台。
服务端 · 2月21日 16:50
Next.js 13+ 的 React Server Components 是什么?Next.js 13 引入了 React Server Components(RSC),这是一个重大的架构变革,彻底改变了我们在 Next.js 中构建应用的方式。 ## 什么是 React Server Components? React Server Components 是一种新的组件类型,它们在服务器上渲染,而不是在客户端。这意味着: 1. **服务器端渲染**:组件在服务器上执行,生成 HTML 2. **零客户端 JavaScript**:服务器组件不会发送任何 JavaScript 到客户端 3. **直接访问后端资源**:可以直接访问数据库、文件系统等 4. **保持代码私密**:服务器代码不会暴露给客户端 ## 服务器组件 vs 客户端组件 ### 服务器组件(默认) ```javascript // 默认情况下,所有组件都是服务器组件 async function BlogList() { // 可以直接访问数据库 const posts = await db.post.findMany(); // 可以使用文件系统 const content = await fs.readFile('./content.md', 'utf-8'); return ( <div> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); } ``` **特点:** - 在服务器上渲染 - 不能使用 React Hooks(useState, useEffect 等) - 不能使用浏览器 API(window, document 等) - 不能使用事件处理器(onClick, onChange 等) - 可以直接访问数据库和文件系统 - 不会发送 JavaScript 到客户端 ### 客户端组件 ```javascript 'use client'; import { useState, useEffect } from 'react'; export default function InteractiveCounter() { const [count, setCount] = useState(0); useEffect(() => { // 可以使用浏览器 API document.title = `Count: ${count}`; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); } ``` **特点:** - 在客户端渲染 - 可以使用所有 React Hooks - 可以使用浏览器 API - 可以使用事件处理器 - 不能直接访问数据库 - 会发送 JavaScript 到客户端 ## 混合使用服务器和客户端组件 ### 在服务器组件中使用客户端组件 ```javascript // 服务器组件 async function BlogPage() { const posts = await fetchPosts(); return ( <div> <h1>Blog Posts</h1> <PostList posts={posts} /> <LikeButton postId={posts[0].id} /> </div> ); } // 客户端组件 'use client'; function LikeButton({ postId }) { const [liked, setLiked] = useState(false); return ( <button onClick={() => setLiked(!liked)}> {liked ? '❤️' : '🤍'} </button> ); } ``` ### 在客户端组件中使用服务器组件 ```javascript // 客户端组件 'use client'; import dynamic from 'next/dynamic'; // 动态导入服务器组件 const ServerComponent = dynamic(() => import('./ServerComponent'), { ssr: true }); export default function ClientComponent() { return ( <div> <h1>Client Component</h1> <ServerComponent /> </div> ); } ``` ## 服务器组件的优势 ### 1. 减少客户端 JavaScript ```javascript // 传统方式(客户端组件) 'use client'; import { useState, useEffect } from 'react'; function BlogList() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/posts') .then(res => res.json()) .then(setPosts); }, []); return ( <div> {posts.map(post => <Post key={post.id} {...post} />)} </div> ); } // 服务器组件方式 async function BlogList() { const posts = await db.post.findMany(); return ( <div> {posts.map(post => <Post key={post.id} {...post} />)} </div> ); } ``` ### 2. 直接访问数据库 ```javascript import { prisma } from '@/lib/prisma'; async function UserDashboard({ userId }) { // 直接访问数据库,无需 API 路由 const user = await prisma.user.findUnique({ where: { id: userId }, include: { posts: true, comments: true } }); return ( <div> <h1>Welcome, {user.name}</h1> <p>You have {user.posts.length} posts</p> </div> ); } ``` ### 3. 保持代码私密 ```javascript // 服务器组件中的敏感代码不会暴露给客户端 async function AdminPanel() { const apiKey = process.env.SECRET_API_KEY; // 这个 API 调用不会暴露给客户端 const data = await fetch(`https://api.example.com?key=${apiKey}`) .then(res => res.json()); return <div>{data.content}</div>; } ``` ### 4. 更好的性能 ```javascript // 服务器组件可以并行获取数据 async function Dashboard() { const [user, posts, notifications] = await Promise.all([ fetchUser(), fetchPosts(), fetchNotifications() ]); return ( <div> <UserProfile user={user} /> <PostList posts={posts} /> <NotificationList notifications={notifications} /> </div> ); } ``` ## 实际应用场景 ### 1. 博客文章列表 ```javascript // app/blog/page.js async function BlogPage() { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' }, take: 10 }); return ( <div> <h1>Latest Posts</h1> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); } // components/PostCard.js export default function PostCard({ post }) { return ( <article> <h2>{post.title}</h2> <p>{post.excerpt}</p> <Link href={`/blog/${post.slug}`}>Read more</Link> </article> ); } ``` ### 2. 电商产品页面 ```javascript // app/products/[id]/page.js async function ProductPage({ params }) { const product = await db.product.findUnique({ where: { id: params.id }, include: { reviews: true, relatedProducts: true } }); return ( <div> <ProductDetails product={product} /> <ProductReviews reviews={product.reviews} /> <RelatedProducts products={product.relatedProducts} /> <AddToCartButton productId={product.id} /> </div> ); } 'use client'; function AddToCartButton({ productId }) { const [loading, setLoading] = useState(false); const handleAddToCart = async () => { setLoading(true); await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ productId }) }); setLoading(false); }; return ( <button onClick={handleAddToCart} disabled={loading}> {loading ? 'Adding...' : 'Add to Cart'} </button> ); } ``` ### 3. 仪表板 ```javascript // app/dashboard/page.js import { auth } from '@/auth'; async function Dashboard() { const session = await auth(); const [stats, recentActivity, notifications] = await Promise.all([ getUserStats(session.user.id), getRecentActivity(session.user.id), getNotifications(session.user.id) ]); return ( <div> <DashboardStats stats={stats} /> <RecentActivity activities={recentActivity} /> <NotificationPanel notifications={notifications} /> </div> ); } ``` ## 最佳实践 ### 1. 默认使用服务器组件 ```javascript // ✅ 好的做法 async function Page() { const data = await fetchData(); return <div>{data.content}</div>; } // ❌ 不好的做法 'use client'; function Page() { const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []); return <div>{data?.content}</div>; } ``` ### 2. 只在需要交互的地方使用客户端组件 ```javascript // 服务器组件 async function ProductList() { const products = await fetchProducts(); return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ); } // 只在需要交互的子组件中使用 'use client' 'use client'; function ProductCard({ product }) { const [liked, setLiked] = useState(false); return ( <div> <h3>{product.name}</h3> <button onClick={() => setLiked(!liked)}> {liked ? '❤️' : '🤍'} </button> </div> ); } ``` ### 3. 将客户端组件移到组件树的底部 ```javascript // ✅ 好的做法:客户端组件在底部 async function Page() { const data = await fetchData(); return ( <div> <Header /> <Content data={data} /> <InteractiveWidget /> </div> ); } 'use client'; function InteractiveWidget() { // 交互逻辑 } // ❌ 不好的做法:客户端组件在顶部 'use client'; function Page() { const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []); return ( <div> <Header /> <Content data={data} /> </div> ); } ``` ### 4. 使用动态导入减少客户端 JavaScript ```javascript import dynamic from 'next/dynamic'; // 动态导入重型组件 const HeavyComponent = dynamic(() => import('./HeavyComponent'), { loading: () => <div>Loading...</div>, ssr: false // 禁用服务器端渲染 }); async function Page() { const data = await fetchData(); return ( <div> <LightContent data={data} /> <HeavyComponent /> </div> ); } ``` ## 常见问题 ### Q: 如何在服务器组件中使用状态? A: 服务器组件不能使用 useState,但可以通过以下方式处理: ```javascript // 使用 URL 参数管理状态 async function Page({ searchParams }) { const page = parseInt(searchParams.page || '1'); const posts = await getPosts(page); return ( <div> <PostList posts={posts} /> <Pagination currentPage={page} /> </div> ); } 'use client'; function Pagination({ currentPage }) { const router = useRouter(); return ( <div> <button onClick={() => router.push(`?page=${currentPage - 1}`)}> Previous </button> <button onClick={() => router.push(`?page=${currentPage + 1}`)}> Next </button> </div> ); } ``` ### Q: 如何在服务器组件中处理表单提交? A: 使用 Server Actions: ```javascript 'use server'; import { revalidatePath } from 'next/cache'; export async function createPost(formData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.create({ data: { title, content } }); revalidatePath('/blog'); } // 在组件中使用 import { createPost } from './actions'; export default function CreatePostForm() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">Create</button> </form> ); } ``` React Server Components 是 Next.js 的未来,通过合理使用服务器组件和客户端组件,可以构建出性能更好、用户体验更佳的应用。
服务端 · 2月17日 23:33
Next.js App Router 与 Pages Router 的核心区别及迁移策略是什么?Next.js 13+ 引入了全新的 App Router,与传统的 Pages Router 相比有显著差异。App Router 基于 React Server Components 构建,提供了更强大的功能和更好的性能。 ## 主要区别 ### 1. 文件结构 **Pages Router:** ``` pages/ index.js about.js api/ users.js ``` **App Router:** ``` app/ page.js about/ page.js api/ users/ route.js ``` ### 2. 布局系统 **App Router 的布局系统更强大:** ```javascript // app/layout.js import './globals.css'; export default function RootLayout({ children }) { return ( <html lang="zh-CN"> <body> <header>全局头部</header> {children} <footer>全局页脚</footer> </body> </html> ); } // app/about/layout.js export default function AboutLayout({ children }) { return ( <div className="about-layout"> <aside>关于我们侧边栏</aside> <main>{children}</main> </div> ); } ``` ### 3. 数据获取方式 **Pages Router:** ```javascript // pages/index.js export async function getServerSideProps() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return { props: { data } }; } export default function Home({ data }) { return <div>{data.title}</div>; } ``` **App Router:** ```javascript // app/page.js async function getData() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } }); return res.json(); } export default async function Page() { const data = await getData(); return <div>{data.title}</div>; } ``` ### 4. Server Components vs Client Components **App Router 默认使用 Server Components:** ```javascript // app/page.js (Server Component - 默认) async function Page() { const data = await fetch('https://api.example.com/data').then(r => r.json()); return <div>{data.title}</div>; } export default Page; // app/components/Interactive.js (Client Component) 'use client'; import { useState } from 'react'; export default function Interactive() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } ``` ### 5. 路由参数 **Pages Router:** ```javascript // pages/posts/[id].js export async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { id: post.id } })), fallback: 'blocking' }; } export async function getStaticProps({ params }) { const post = await getPost(params.id); return { props: { post } }; } ``` **App Router:** ```javascript // app/posts/[id]/page.js export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map(post => ({ id: post.id })); } export default async function PostPage({ params }) { const post = await getPost(params.id); return <article>{post.content}</article>; } ``` ### 6. 加载状态和错误处理 **App Router 内置支持:** ```javascript // app/posts/loading.js export default function Loading() { return <div>加载中...</div>; } // app/posts/error.js 'use client'; export default function Error({ error, reset }) { return ( <div> <h2>出错了!</h2> <button onClick={() => reset()}>重试</button> </div> ); } // app/posts/not-found.js export default function NotFound() { return <div>文章未找到</div>; } ``` ### 7. Server Actions **App Router 独有功能:** ```javascript // app/actions.js 'use server'; import { revalidatePath } from 'next/cache'; export async function createPost(formData) { const title = formData.get('title'); const content = formData.get('content'); await savePost({ title, content }); revalidatePath('/posts'); } // app/posts/new/page.js import { createPost } from '../actions'; export default function NewPostPage() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">创建文章</button> </form> ); } ``` ## 迁移策略 ### 渐进式迁移 ```javascript // 可以在同一个项目中同时使用两个 Router // pages/ - 旧代码继续使用 Pages Router // app/ - 新功能使用 App Router // 在 App Router 中链接到 Pages Router <Link href="/old-page">旧页面</Link> ``` ### 关键迁移步骤 1. 安装 Next.js 13+ 并更新配置 2. 创建 `app/` 目录结构 3. 迁移布局到 `app/layout.js` 4. 将页面逐步迁移到 `app/` 目录 5. 更新数据获取逻辑 6. 添加 `'use client'` 指令到交互式组件 7. 更新 API 路由为 Route Handlers 8. 测试并优化性能 ## 性能对比 **App Router 优势:** - 更小的客户端包(Server Components 不发送到客户端) - 更好的 SEO(默认服务端渲染) - 更灵活的缓存策略 - 内置加载和错误状态 - 更简洁的代码结构 **Pages Router 优势:** - 更成熟稳定 - 更丰富的生态系统和文档 - 更简单的学习曲线 - 更多的第三方库支持 ## 最佳实践 1. **新项目:** 优先使用 App Router 2. **现有项目:** 渐进式迁移,先迁移简单页面 3. **组件选择:** 默认使用 Server Components,只在需要交互时使用 Client Components 4. **数据获取:** 利用 fetch API 的缓存选项 5. **状态管理:** Server Actions 替代部分 API 路由 App Router 代表了 Next.js 的未来方向,提供了更现代的开发体验和更好的性能表现。
服务端 · 2月17日 23:33
Next.js 中的 SSR、SSG 和 ISR 有什么区别?Next.js 中有三种主要的渲染方式,每种方式都有其特定的使用场景和优势: ## 1. 客户端渲染(CSR) 客户端渲染是传统的 React 应用渲染方式。页面初始加载时返回一个空的 HTML 文件,然后 JavaScript 在浏览器中执行,动态生成页面内容。 **特点:** - 首屏加载较慢,需要等待 JavaScript 下载和执行 - SEO 不友好,搜索引擎爬虫可能无法抓取动态内容 - 交互性强,适合高度交互的应用 - 适合不需要 SEO 的后台管理系统、仪表盘等 **实现方式:** ```javascript export default function CSRPage() { const [data, setData] = useState(null); useEffect(() => { fetch('/api/data').then(res => res.json()).then(setData); }, []); return <div>{data ? data.content : 'Loading...'}</div>; } ``` ## 2. 服务器端渲染(SSR) 服务器端渲染在每次请求时,服务器会生成完整的 HTML 并发送给客户端。这样可以确保搜索引擎爬虫能够抓取到完整的内容。 **特点:** - 首屏加载快,HTML 已经在服务器上生成 - SEO 友好,搜索引擎可以抓取完整内容 - 每次请求都需要服务器处理,服务器负载较高 - 适合内容频繁变化、需要 SEO 的页面 **实现方式:** ```javascript export async function getServerSideProps(context) { const data = await fetch('https://api.example.com/data').then(res => res.json()); return { props: { data } }; } export default function SSRPage({ data }) { return <div>{data.content}</div>; } ``` ## 3. 静态生成(SSG) 静态生成在构建时生成 HTML 文件,这些文件可以被 CDN 缓存,提供最快的加载速度。 **特点:** - 性能最佳,HTML 文件可以被 CDN 缓存 - SEO 友好,静态内容易于搜索引擎抓取 - 构建时生成,内容更新需要重新构建 - 适合内容不经常变化的页面,如博客、产品页面等 **实现方式:** ```javascript export async function getStaticProps() { const data = await fetch('https://api.example.com/data').then(res => res.json()); return { props: { data }, revalidate: 60 // 可选:启用 ISR,每 60 秒重新生成 }; } export default function SSGPage({ data }) { return <div>{data.content}</div>; } ``` ## 4. 增量静态生成(ISR) ISR 是 SSG 的增强版本,允许在构建后更新静态页面。它结合了 SSG 的性能和 SSR 的动态性。 **特点:** - 保持静态页面的性能优势 - 可以在后台更新页面内容 - 用户总是看到最新的内容 - 适合需要定期更新但不需要实时更新的内容 **实现方式:** ```javascript export async function getStaticProps() { const data = await fetch('https://api.example.com/data').then(res => res.json()); return { props: { data }, revalidate: 3600 // 每小时重新生成一次 }; } ``` ## 选择建议 - **CSR**:后台管理系统、仪表盘、不需要 SEO 的应用 - **SSR**:需要 SEO 且内容频繁变化的页面,如新闻网站、电商产品页 - **SSG**:内容不经常变化的页面,如博客、文档、关于我们页面 - **ISR**:需要定期更新但不需要实时更新的内容,如博客文章、产品列表 在实际项目中,可以根据不同页面的需求混合使用这些渲染方式,以达到最佳的性能和 SEO 效果。
服务端 · 2月17日 23:32
Next.js 的 Pages Router 和 App Router 有什么区别?Next.js 提供了两种主要的路由架构:Pages Router 和 App Router。它们在设计理念、功能和性能方面有显著差异。 ## Pages Router Pages Router 是 Next.js 的传统路由系统,基于 `pages` 目录。 ### 特点 1. **文件结构简单** ``` pages/ index.js about.js blog/ [slug].js ``` 2. **数据获取方法** - `getStaticProps`:静态生成时获取数据 - `getServerSideProps`:每次请求时获取数据 - `getStaticPaths`:定义动态路由的路径 3. **生命周期** - 支持 React 的完整生命周期 - 使用 `useEffect` 进行客户端数据获取 4. **路由钩子** ```javascript import { useRouter } from 'next/router'; const router = useRouter(); console.log(router.pathname, router.query); ``` ### 优点 - 成熟稳定,文档和社区支持完善 - 学习曲线较平缓 - 适合中小型项目 ### 缺点 - 不支持嵌套路由 - 不支持服务器组件 - 性能优化有限 ## App Router App Router 是 Next.js 13+ 引入的新路由系统,基于 `app` 目录。 ### 特点 1. **文件结构** ``` app/ page.js layout.js about/ page.js blog/ [slug]/ page.js ``` 2. **服务器组件和客户端组件** ```javascript // 服务器组件(默认) async function BlogList() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return <div>{posts.map(post => <Post key={post.id} {...post} />)}</div>; } // 客户端组件 'use client'; import { useState } from 'react'; function InteractiveComponent() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } ``` 3. **数据获取** ```javascript // 直接在组件中获取数据 async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } // ISR }).then(r => r.json()); return <div>{data.content}</div>; } ``` 4. **布局系统** ```javascript // app/layout.js export default function RootLayout({ children }) { return ( <html> <body> <Header /> {children} <Footer /> </body> </html> ); } ``` 5. **路由钩子** ```javascript import { useParams, usePathname } from 'next/navigation'; const params = useParams(); const pathname = usePathname(); ``` ### 优点 - 支持服务器组件,减少客户端 JavaScript - 支持嵌套路由和布局 - 更好的性能和用户体验 - 更现代的 API 设计 ### 缺点 - 学习曲线较陡峭 - 相对较新,生态系统还在发展 - 需要理解服务器组件和客户端组件的区别 ## 主要区别对比 | 特性 | Pages Router | App Router | |------|--------------|------------| | 目录 | `pages/` | `app/` | | 服务器组件 | 不支持 | 支持 | | 嵌套路由 | 不支持 | 支持 | | 布局系统 | 有限 | 强大 | | 数据获取 | getStaticProps/getServerSideProps | 直接在组件中 | | 路由钩子 | next/router | next/navigation | | 文件约定 | index.js | page.js | | 加载状态 | 需要手动实现 | 自动支持 loading.js | | 错误处理 | 需要手动实现 | 自动支持 error.js | | 流式渲染 | 不支持 | 支持 | ## 迁移建议 ### 何时使用 Pages Router - 现有项目已经在使用 Pages Router - 项目规模较小,不需要复杂的功能 - 团队对 Pages Router 更熟悉 ### 何时使用 App Router - 新项目 - 需要服务器组件来优化性能 - 需要嵌套路由和复杂布局 - 需要更好的 SEO 和性能 ### 混合使用 Next.js 允许同时使用两种路由器: - `pages/` 目录使用 Pages Router - `app/` 目录使用 App Router ```javascript // 两个目录可以共存 pages/ api/ hello.js // API 路由 app/ page.js // 主页面 ``` ## 最佳实践 1. **新项目优先使用 App Router**:获得更好的性能和开发体验 2. **逐步迁移**:现有项目可以逐步将页面迁移到 App Router 3. **合理使用服务器组件**:将不需要交互的组件设为服务器组件 4. **利用布局系统**:使用嵌套布局来共享 UI 5. **保持一致性**:在一个项目中尽量统一使用一种路由器 选择哪种路由器取决于项目需求、团队经验和性能要求。App Router 代表了 Next.js 的未来方向,但 Pages Router 仍然是可靠的选择。
服务端 · 2月17日 23:32
Next.js 有哪些性能优化技术?Next.js 提供了多种性能优化技术,帮助开发者构建高性能的 Web 应用。以下是 Next.js 的主要性能优化策略: ## 1. 自动代码分割 Next.js 自动将代码分割成小块,只加载当前页面所需的代码。 ```javascript // pages/index.js import dynamic from 'next/dynamic'; // 动态导入组件 const DynamicComponent = dynamic(() => import('../components/HeavyComponent'), { loading: () => <p>Loading...</p>, ssr: false // 禁用服务器端渲染 }); export default function Home() { return ( <div> <h1>Home Page</h1> <DynamicComponent /> </div> ); } ``` ## 2. 图片优化 使用 `next/image` 组件自动优化图片。 ```javascript import Image from 'next/image'; export default function ImageExample() { return ( <Image src="/hero.jpg" alt="Hero image" width={800} height={600} priority // 首屏图片使用优先加载 placeholder="blur" // 模糊占位符 blurDataURL="data:image/jpeg;base64,..." /> ); } ``` **图片优化特性:** - 自动选择最佳格式(WebP、AVIF) - 响应式图片 - 懒加载 - 避免布局偏移 ## 3. 字体优化 使用 `next/font` 优化字体加载。 ```javascript import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', }); export default function RootLayout({ children }) { return ( <html lang="en" className={inter.variable}> <body>{children}</body> </html> ); } ``` ## 4. 数据获取优化 ### 使用缓存和 ISR ```javascript // 使用 fetch 的缓存选项 async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60, // ISR:每 60 秒重新验证 tags: ['data'] // 标签用于按需重新验证 } }).then(r => r.json()); return <div>{data.content}</div>; } ``` ### 使用 React Query 或 SWR ```javascript 'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then(r => r.json()); export default function DataComponent() { const { data, error, isLoading } = useSWR('/api/data', fetcher, { revalidateOnFocus: false, dedupingInterval: 60000, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return <div>{data.content}</div>; } ``` ## 5. 预加载和预取 ```javascript import Link from 'next/link'; export default function Navigation() { return ( <nav> <Link href="/about" prefetch={true}> About </Link> <Link href="/contact" prefetch={false}> Contact </Link> </nav> ); } ``` ## 6. 脚本优化 使用 `next/script` 优化第三方脚本加载。 ```javascript import Script from 'next/script'; export default function Page() { return ( <> <Script src="https://www.googletagmanager.com/gtag/js" strategy="afterInteractive" /> <Script id="google-analytics" strategy="afterInteractive"> {` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'GA_MEASUREMENT_ID'); `} </Script> </> ); } ``` **脚本加载策略:** - `beforeInteractive`:在页面交互前加载 - `afterInteractive`:在页面可交互后立即加载 - `lazyOnload`:在浏览器空闲时加载 ## 7. 使用 React.memo 和 useMemo ```javascript 'use client'; import { memo, useMemo } from 'react'; const ExpensiveComponent = memo(function ExpensiveComponent({ data }) { const processedData = useMemo(() => { return data.map(item => ({ ...item, computed: expensiveCalculation(item) })); }, [data]); return <div>{/* 渲染处理后的数据 */}</div>; }); ``` ## 8. 虚拟化长列表 ```javascript 'use client'; import { useVirtualizer } from '@tanstack/react-virtual'; export default function VirtualList({ items }) { const parentRef = useRef(); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, }); return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px` }}> {virtualizer.getVirtualItems().map(virtualItem => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, }} > {items[virtualItem.index]} </div> ))} </div> </div> ); } ``` ## 9. 服务器组件优化 ```javascript // 服务器组件默认不发送 JavaScript 到客户端 async function ServerComponent() { const data = await fetchData(); return ( <div> <h1>{data.title}</h1> <p>{data.content}</p> </div> ); } // 只在需要交互的地方使用客户端组件 'use client'; function InteractiveComponent() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } ``` ## 10. 使用 Streaming ```javascript import { Suspense } from 'react'; async function SlowComponent() { const data = await slowFetch(); return <div>{data}</div>; } export default function Page() { return ( <div> <h1>Page Title</h1> <Suspense fallback={<div>Loading...</div>}> <SlowComponent /> </Suspense> </div> ); } ``` ## 11. 缓存策略 ### 使用 Next.js 缓存 ```javascript // 缓存 API 响应 export async function getStaticProps() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', // 或 'no-store', 'no-cache' }).then(r => r.json()); return { props: { data }, revalidate: 3600, // 1 小时 }; } ``` ### 使用 Redis 缓存 ```javascript import { Redis } from '@upstash/redis'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, }); export async function getCachedData(key) { const cached = await redis.get(key); if (cached) return JSON.parse(cached); const data = await fetchData(); await redis.set(key, JSON.stringify(data), { ex: 3600 }); return data; } ``` ## 12. 构建优化 ### 分析构建输出 ```javascript // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({ // 其他配置 }); ``` ### 压缩和优化 ```javascript // next.config.js module.exports = { compress: true, swcMinify: true, productionBrowserSourceMaps: false, // 优化图片 images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, }; ``` ## 性能监控 ### 使用 Web Vitals ```javascript // pages/_app.js import { useReportWebVitals } from 'next/web-vitals'; export function reportWebVitals(metric) { // 发送到分析服务 console.log(metric); // 或发送到 Google Analytics // gtag('event', metric.name, { value: metric.value }); } export default function App({ Component, pageProps }) { useReportWebVitals(reportWebVitals); return <Component {...pageProps} />; } ``` ## 最佳实践 1. **使用服务器组件**:减少客户端 JavaScript 2. **优化图片**:使用 next/image 组件 3. **懒加载**:延迟加载非关键资源 4. **缓存数据**:使用 ISR 和缓存策略 5. **监控性能**:使用 Web Vitals 监控 6. **分析构建**:定期分析 bundle 大小 7. **使用 CDN**:部署到 Vercel 或其他 CDN 8. **优化字体**:使用 next/font 优化字体加载 通过合理使用这些优化技术,可以显著提升 Next.js 应用的性能和用户体验。
服务端 · 2月17日 23:32
Next.js 中有哪些数据获取方法?Next.js 提供了多种数据获取方法,开发者可以根据不同的渲染策略和需求选择合适的方式。以下是 Next.js 中主要的数据获取方法: ## Pages Router 数据获取方法 ### 1. getStaticProps 在构建时获取数据,用于静态生成(SSG)。 ```javascript export async function getStaticProps(context) { const data = await fetch('https://api.example.com/data').then(r => r.json()); return { props: { data }, revalidate: 60, // 可选:ISR,每 60 秒重新生成 notFound: false, // 可选:返回 404 页面 redirect: { destination: '/login', permanent: false }, // 可选:重定向 }; } export default function Page({ data }) { return <div>{data.content}</div>; } ``` **适用场景:** - 数据在构建时可用 - 页面内容不经常变化 - 需要预渲染以提升 SEO ### 2. getServerSideProps 在每次请求时获取数据,用于服务器端渲染(SSR)。 ```javascript export async function getServerSideProps(context) { const { req, res, query, params } = context; // 可以访问请求和响应对象 const token = req.cookies.token; const data = await fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${token}` } }).then(r => r.json()); return { props: { data }, // 不支持 revalidate }; } export default function Page({ data }) { return <div>{data.content}</div>; } ``` **适用场景:** - 数据在请求时才能获取 - 需要访问请求/响应对象 - 内容频繁变化 ### 3. getStaticPaths 用于动态路由的静态生成,定义所有可能的路径。 ```javascript export async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: false, // 或 'blocking' 或 true }; } export async function getStaticProps({ params }) { const post = await getPostBySlug(params.slug); return { props: { post }, }; } export default function BlogPost({ post }) { return <div>{post.title}</div>; } ``` **fallback 选项:** - `false`:只返回预生成的路径,其他路径返回 404 - `'blocking'`:服务器渲染新路径,等待完成后返回 - `true`:立即返回静态页面,后台生成新路径 ## App Router 数据获取方法 ### 1. 服务器组件中的 fetch 在服务器组件中直接使用 fetch 获取数据。 ```javascript async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', // 或 'no-store', 'no-cache', 'default' next: { revalidate: 60, // ISR tags: ['data'] // 用于按需重新验证 } }).then(r => r.json()); return <div>{data.content}</div>; } ``` **cache 选项:** - `force-cache`:强制使用缓存(默认) - `no-store`:不使用缓存 - `no-cache`:每次验证缓存 - `default`:使用默认缓存策略 ### 2. 使用 React Server Components ```javascript async function BlogList() { const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } }).then(r => r.json()); return ( <div> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); } ``` ### 3. 使用 Suspense 和 Streaming ```javascript import { Suspense } from 'react'; async function SlowComponent() { const data = await fetch('https://api.example.com/slow', { next: { revalidate: 60 } }).then(r => r.json()); return <div>{data.content}</div>; } export default function Page() { return ( <div> <h1>Page Title</h1> <Suspense fallback={<div>Loading...</div>}> <SlowComponent /> </Suspense> </div> ); } ``` ## 客户端数据获取 ### 1. 使用 useEffect ```javascript 'use client'; import { useState, useEffect } from 'react'; export default function ClientDataComponent() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch('/api/data') .then(res => res.json()) .then(data => { setData(data); setLoading(false); }); }, []); if (loading) return <div>Loading...</div>; return <div>{data.content}</div>; } ``` ### 2. 使用 SWR ```javascript 'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then(res => res.json()); export default function SWRComponent() { const { data, error, isLoading } = useSWR('/api/data', fetcher, { revalidateOnFocus: false, revalidateOnReconnect: false, dedupingInterval: 60000, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return <div>{data.content}</div>; } ``` ### 3. 使用 React Query ```javascript 'use client'; import { useQuery } from '@tanstack/react-query'; async function fetchData() { const res = await fetch('/api/data'); return res.json(); } export default function ReactQueryComponent() { const { data, error, isLoading } = useQuery({ queryKey: ['data'], queryFn: fetchData, staleTime: 60000, cacheTime: 300000, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return <div>{data.content}</div>; } ``` ## 数据获取最佳实践 ### 1. 选择合适的方法 | 场景 | 推荐方法 | |------|----------| | 静态内容,构建时可用 | getStaticProps / SSG | | 动态内容,需要实时数据 | getServerSideProps / SSR | | 需要用户交互 | 客户端数据获取 | | SEO 重要,内容变化不频繁 | SSG + ISR | | 需要访问请求/响应对象 | getServerSideProps | ### 2. 缓存策略 ```javascript // 长期缓存 fetch('/api/data', { cache: 'force-cache', next: { revalidate: 3600 } }); // 短期缓存 fetch('/api/data', { cache: 'no-store' }); // 按需重新验证 fetch('/api/data', { next: { tags: ['data'] } }); // 在 API 路由中重新验证 import { revalidateTag } from 'next/cache'; export async function POST() { revalidateTag('data'); return Response.json({ revalidated: true }); } ``` ### 3. 错误处理 ```javascript export async function getStaticProps() { try { const data = await fetchData(); return { props: { data } }; } catch (error) { return { notFound: true, }; } } ``` ### 4. 加载状态 ```javascript // App Router - 使用 loading.js // app/loading.js export default function Loading() { return <div>Loading...</div>; } // Pages Router - 使用自定义加载组件 export default function LoadingPage() { return <div>Loading...</div>; } ``` ### 5. 并行数据获取 ```javascript // 并行获取多个数据 export async function getStaticProps() { const [posts, users, comments] = await Promise.all([ fetch('/api/posts').then(r => r.json()), fetch('/api/users').then(r => r.json()), fetch('/api/comments').then(r => r.json()), ]); return { props: { posts, users, comments }, }; } ``` ## 性能优化建议 1. **使用 ISR**:对于需要定期更新的内容,使用 ISR 而不是 SSR 2. **缓存数据**:合理设置缓存时间,减少不必要的请求 3. **并行获取**:使用 Promise.all 并行获取多个数据源 4. **流式渲染**:使用 Suspense 实现流式渲染,提升用户体验 5. **客户端缓存**:使用 SWR 或 React Query 缓存客户端数据 6. **按需重新验证**:使用标签系统按需重新验证数据 通过合理选择和使用这些数据获取方法,可以构建高性能、用户体验良好的 Next.js 应用。
服务端 · 2月17日 23:32
Next.js 中如何进行状态管理?Next.js 提供了多种状态管理解决方案,开发者可以根据项目需求选择合适的方式。以下是 Next.js 中常用的状态管理方法: ## 1. React 内置状态管理 ### useState Hook 用于管理组件的本地状态。 ```javascript 'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); } ``` ### useReducer Hook 用于管理复杂的状态逻辑。 ```javascript 'use client'; import { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } export default function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </div> ); } ``` ### useContext Hook 用于跨组件共享状态。 ```javascript 'use client'; import { createContext, useContext, useState } from 'react'; const ThemeContext = createContext(); export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within ThemeProvider'); } return context; } // 使用 export default function App() { return ( <ThemeProvider> <Header /> <Content /> </ThemeProvider> ); } function Header() { const { theme, setTheme } = useTheme(); return ( <header> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> </header> ); } ``` ## 2. 全局状态管理库 ### Zustand 轻量级、简单的状态管理库。 ```javascript // store/useStore.js import { create } from 'zustand'; export const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); // 使用 'use client'; import { useStore } from '@/store/useStore'; export default function Counter() { const { count, increment, decrement, reset } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> ); } ``` ### Redux Toolkit 功能强大的状态管理库,适合大型应用。 ```javascript // store/slices/counterSlice.js import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; // store/index.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './slices/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); // store/hooks.js import { useDispatch, useSelector } from 'react-redux'; import type { TypedUseSelectorHook } from 'react-redux'; import type { RootState, AppDispatch } from './index'; export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; // 使用 'use client'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { increment, decrement, incrementByAmount } from '@/store/slices/counterSlice'; export default function Counter() { const count = useAppSelector((state) => state.counter.value); const dispatch = useAppDispatch(); return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch(increment())}>+</button> <button onClick={() => dispatch(decrement())}>-</button> <button onClick={() => dispatch(incrementByAmount(10))}>+10</button> </div> ); } ``` ### Jotai 原子化状态管理,类似于 Recoil。 ```javascript // store/atoms.js import { atom } from 'jotai'; export const countAtom = atom(0); export const doubledCountAtom = atom((get) => get(countAtom) * 2); // 使用 'use client'; import { useAtom } from 'jotai'; import { countAtom, doubledCountAtom } from '@/store/atoms'; export default function Counter() { const [count, setCount] = useAtom(countAtom); const [doubledCount] = useAtom(doubledCountAtom); return ( <div> <p>Count: {count}</p> <p>Doubled: {doubledCount}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); } ``` ## 3. 服务器状态管理 ### SWR 用于数据获取和缓存。 ```javascript 'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then((res) => res.json()); export default function UserProfile() { const { data, error, isLoading } = useSWR('/api/user', fetcher); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); } ``` ### React Query (TanStack Query) 强大的数据获取和状态管理库。 ```javascript 'use client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; async function fetchUser() { const res = await fetch('/api/user'); return res.json(); } async function updateUser(data) { const res = await fetch('/api/user', { method: 'PUT', body: JSON.stringify(data), }); return res.json(); } export default function UserProfile() { const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ queryKey: ['user'], queryFn: fetchUser, }); const mutation = useMutation({ mutationFn: updateUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user'] }); }, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> <button onClick={() => mutation.mutate({ name: 'New Name' })}> Update Name </button> </div> ); } ``` ## 4. 表单状态管理 ### React Hook Form 高性能的表单状态管理。 ```javascript 'use client'; import { useForm } from 'react-hook-form'; export default function ContactForm() { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = (data) => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label>Name</label> <input {...register('name', { required: true })} /> {errors.name && <span>This field is required</span>} </div> <div> <label>Email</label> <input {...register('email', { required: true, pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i })} /> {errors.email && <span>Invalid email</span>} </div> <button type="submit">Submit</button> </form> ); } ``` ## 5. URL 状态管理 ### 使用 URL 查询参数 ```javascript 'use client'; import { useRouter, useSearchParams } from 'next/navigation'; export default function ProductList() { const router = useRouter(); const searchParams = useSearchParams(); const page = parseInt(searchParams.get('page') || '1'); const category = searchParams.get('category') || 'all'; const handlePageChange = (newPage) => { const params = new URLSearchParams(searchParams.toString()); params.set('page', newPage.toString()); router.push(`?${params.toString()}`); }; const handleCategoryChange = (newCategory) => { const params = new URLSearchParams(searchParams.toString()); params.set('category', newCategory); params.set('page', '1'); router.push(`?${params.toString()}`); }; return ( <div> <select value={category} onChange={(e) => handleCategoryChange(e.target.value)} > <option value="all">All</option> <option value="electronics">Electronics</option> <option value="clothing">Clothing</option> </select> <div>Current page: {page}</div> <button onClick={() => handlePageChange(page - 1)}>Previous</button> <button onClick={() => handlePageChange(page + 1)}>Next</button> </div> ); } ``` ## 6. 服务器组件状态 ### 使用服务器组件 ```javascript // 服务器组件不需要客户端状态管理 async function ProductList() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } }).then(r => r.json()); return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ); } ``` ## 状态管理最佳实践 1. **选择合适的工具**: - 简单状态:useState, useReducer - 跨组件状态:useContext - 全局状态:Zustand, Redux Toolkit - 服务器状态:SWR, React Query - 表单状态:React Hook Form 2. **最小化状态**:只存储必要的状态,其他状态通过计算得出 3. **服务器优先**:尽可能使用服务器组件,减少客户端状态 4. **避免过度设计**:不要为简单的状态引入复杂的状态管理库 5. **类型安全**:使用 TypeScript 确保类型安全 6. **性能优化**:使用 React.memo, useMemo, useCallback 优化性能 7. **持久化**:使用 localStorage 或 IndexedDB 持久化重要状态 通过合理选择和使用这些状态管理方法,可以构建高效、可维护的 Next.js 应用。
服务端 · 2月17日 23:32
Next.js 中如何实现身份验证?Next.js 提供了多种身份验证解决方案,开发者可以根据项目需求选择合适的方式。以下是 Next.js 中常用的身份验证方法: ## 1. NextAuth.js NextAuth.js 是 Next.js 最流行的身份验证库,提供完整的身份验证解决方案。 ### 基本配置 ```javascript // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import GoogleProvider from 'next-auth/providers/google'; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: 'Credentials', credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" } }, async authorize(credentials) { const user = await authenticateUser(credentials); if (user) { return user; } return null; } }), ], pages: { signIn: '/auth/signin', error: '/auth/error', }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, async session({ session, token }) { session.user.id = token.id; session.user.role = token.role; return session; }, }, session: { strategy: 'jwt', }, secret: process.env.NEXTAUTH_SECRET, }); export { handlers as GET, handlers as POST }; ``` ### 使用 Session ```javascript 'use client'; import { useSession, signIn, signOut } from 'next-auth/react'; export default function Navbar() { const { data: session, status } = useSession(); if (status === 'loading') { return <div>Loading...</div>; } return ( <nav> {session ? ( <> <p>Welcome, {session.user.name}</p> <button onClick={() => signOut()}>Sign Out</button> </> ) : ( <button onClick={() => signIn()}>Sign In</button> )} </nav> ); } ``` ### 服务器端获取 Session ```javascript import { auth } from '@/auth'; export default async function Dashboard() { const session = await auth(); if (!session) { redirect('/auth/signin'); } return ( <div> <h1>Welcome, {session.user.name}</h1> <p>Role: {session.user.role}</p> </div> ); } ``` ### 保护路由 ```javascript import { auth } from '@/auth'; export default async function ProtectedPage() { const session = await auth(); if (!session) { redirect('/auth/signin'); } return <div>Protected content</div>; } ``` ## 2. 自定义 JWT 实现 使用 Next.js API Routes 和 JWT 实现自定义身份验证。 ### JWT 工具函数 ```javascript // lib/auth.js import jwt from 'jsonwebtoken'; const SECRET = process.env.JWT_SECRET; export function signToken(payload) { return jwt.sign(payload, SECRET, { expiresIn: '7d' }); } export function verifyToken(token) { try { return jwt.verify(token, SECRET); } catch (error) { return null; } } export function setAuthCookie(res, token) { res.setHeader('Set-Cookie', `token=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=604800`); } export function clearAuthCookie(res) { res.setHeader('Set-Cookie', 'token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0'); } ``` ### 登录 API ```javascript // app/api/auth/login/route.js import { signToken, setAuthCookie } from '@/lib/auth'; import { compare } from 'bcryptjs'; import { db } from '@/lib/db'; export async function POST(request) { try { const { email, password } = await request.json(); const user = await db.user.findUnique({ where: { email } }); if (!user || !await compare(password, user.password)) { return Response.json( { error: 'Invalid credentials' }, { status: 401 } ); } const token = signToken({ userId: user.id, email: user.email }); const response = Response.json({ user: { id: user.id, email: user.email, name: user.name } }); setAuthCookie(response, token); return response; } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); } } ``` ### 注册 API ```javascript // app/api/auth/register/route.js import { hash } from 'bcryptjs'; import { db } from '@/lib/db'; export async function POST(request) { try { const { email, password, name } = await request.json(); const existingUser = await db.user.findUnique({ where: { email } }); if (existingUser) { return Response.json( { error: 'User already exists' }, { status: 400 } ); } const hashedPassword = await hash(password, 10); const user = await db.user.create({ data: { email, password: hashedPassword, name, }, }); return Response.json( { user: { id: user.id, email: user.email, name: user.name } }, { status: 201 } ); } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); } } ``` ### 获取当前用户 ```javascript // lib/getCurrentUser.js import { cookies } from 'next/headers'; import { verifyToken } from '@/lib/auth'; import { db } from '@/lib/db'; export async function getCurrentUser() { const cookieStore = await cookies(); const token = cookieStore.get('token')?.value; if (!token) { return null; } const decoded = verifyToken(token); if (!decoded) { return null; } const user = await db.user.findUnique({ where: { id: decoded.userId }, select: { id: true, email: true, name: true, role: true } }); return user; } ``` ## 3. 中间件保护路由 ```javascript // middleware.js import { NextResponse } from 'next/server'; import { verifyToken } from '@/lib/auth'; export function middleware(request) { const token = request.cookies.get('token')?.value; const { pathname } = request.nextUrl; // 公开路由 const publicPaths = ['/auth/signin', '/auth/register', '/api/auth/login', '/api/auth/register']; if (publicPaths.some(path => pathname.startsWith(path))) { return NextResponse.next(); } // API 路由 if (pathname.startsWith('/api')) { if (!token || !verifyToken(token)) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } return NextResponse.next(); } // 页面路由 if (!token || !verifyToken(token)) { return NextResponse.redirect(new URL('/auth/signin', request.url)); } return NextResponse.next(); } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico).*)', ], }; ``` ## 4. OAuth 集成 ### Google OAuth ```javascript // app/api/auth/google/route.js import { OAuth2Client } from 'google-auth-library'; import { signToken, setAuthCookie } from '@/lib/auth'; import { db } from '@/lib/db'; const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); export async function POST(request) { try { const { idToken } = await request.json(); const ticket = await client.verifyIdToken({ idToken, audience: process.env.GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); let user = await db.user.findUnique({ where: { email: payload.email } }); if (!user) { user = await db.user.create({ data: { email: payload.email, name: payload.name, image: payload.picture, provider: 'google', }, }); } const token = signToken({ userId: user.id, email: user.email }); const response = Response.json({ user: { id: user.id, email: user.email, name: user.name } }); setAuthCookie(response, token); return response; } catch (error) { return Response.json( { error: 'Authentication failed' }, { status: 401 } ); } } ``` ## 5. 权限控制 ### 基于角色的访问控制(RBAC) ```javascript // lib/permissions.js export const ROLES = { ADMIN: 'admin', USER: 'user', GUEST: 'guest', }; export function hasPermission(userRole, requiredRole) { const roleHierarchy = { [ROLES.ADMIN]: 3, [ROLES.USER]: 2, [ROLES.GUEST]: 1, }; return roleHierarchy[userRole] >= roleHierarchy[requiredRole]; } // 使用示例 export default function AdminPanel({ user }) { if (!hasPermission(user.role, ROLES.ADMIN)) { return <div>Access denied</div>; } return <div>Admin content</div>; } ``` ### 服务器端权限检查 ```javascript import { getCurrentUser } from '@/lib/getCurrentUser'; import { ROLES, hasPermission } from '@/lib/permissions'; export default async function AdminPage() { const user = await getCurrentUser(); if (!user || !hasPermission(user.role, ROLES.ADMIN)) { redirect('/'); } return <div>Admin content</div>; } ``` ## 6. 密码重置 ### 发送重置邮件 ```javascript // app/api/auth/forgot-password/route.js import { db } from '@/lib/db'; import { signToken } from '@/lib/auth'; import { sendEmail } from '@/lib/email'; export async function POST(request) { try { const { email } = await request.json(); const user = await db.user.findUnique({ where: { email } }); if (!user) { // 不透露用户是否存在 return Response.json({ message: 'If user exists, email sent' }); } const resetToken = signToken( { userId: user.id, type: 'password_reset' }, { expiresIn: '1h' } ); await sendEmail({ to: email, subject: 'Password Reset', html: `<a href="${process.env.APP_URL}/auth/reset-password?token=${resetToken}">Reset Password</a>`, }); return Response.json({ message: 'Email sent' }); } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); } } ``` ### 重置密码 ```javascript // app/api/auth/reset-password/route.js import { verifyToken } from '@/lib/auth'; import { hash } from 'bcryptjs'; import { db } from '@/lib/db'; export async function POST(request) { try { const { token, password } = await request.json(); const decoded = verifyToken(token); if (!decoded || decoded.type !== 'password_reset') { return Response.json( { error: 'Invalid or expired token' }, { status: 400 } ); } const hashedPassword = await hash(password, 10); await db.user.update({ where: { id: decoded.userId }, data: { password: hashedPassword }, }); return Response.json({ message: 'Password reset successful' }); } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); } } ``` ## 身份验证最佳实践 1. **使用 HTTPS**:始终使用 HTTPS 传输敏感数据 2. **HttpOnly Cookies**:使用 HttpOnly 防止 XSS 攻击 3. **CSRF 保护**:实施 CSRF 令牌保护 4. **密码哈希**:使用 bcrypt 或 argon2 哈希密码 5. **速率限制**:防止暴力破解攻击 6. **安全头部**:设置适当的安全头部 7. **日志记录**:记录身份验证事件用于审计 8. **会话管理**:设置合理的会话过期时间 9. **多因素认证**:为敏感操作实施 MFA 10. **定期更新**:保持依赖项和安全补丁更新 通过合理实施这些身份验证方法,可以构建安全可靠的 Next.js 应用。
服务端 · 2月17日 23:32
Next.js 如何部署到生产环境?Next.js 提供了多种部署选项,开发者可以根据项目需求选择最适合的部署方式。以下是 Next.js 的主要部署选项和最佳实践: ## 1. Vercel(推荐) Vercel 是 Next.js 的创建者提供的托管平台,提供最佳的 Next.js 部署体验。 ### 优点 - 零配置部署 - 自动 HTTPS - 全球 CDN - 边缘函数支持 - 预览部署 - 自动优化 ### 部署步骤 ```bash # 1. 安装 Vercel CLI npm i -g vercel # 2. 登录 Vercel vercel login # 3. 部署 vercel # 4. 生产环境部署 vercel --prod ``` ### 配置文件 ```javascript // vercel.json { "buildCommand": "npm run build", "outputDirectory": ".next", "framework": "nextjs", "regions": ["iad1"], "functions": { "app/api/**/*.js": { "maxDuration": 30 } }, "headers": [ { "source": "/(.*)", "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, { "key": "X-Frame-Options", "value": "DENY" } ] } ] } ``` ## 2. 自托管(Docker) 使用 Docker 容器化 Next.js 应用,部署到任何支持 Docker 的平台。 ### Dockerfile ```dockerfile # 多阶段构建 FROM node:18-alpine AS base # 依赖阶段 FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci # 构建阶段 FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # 运行阶段 FROM base AS runner WORKDIR /app ENV NODE_ENV production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 ENV HOSTNAME "0.0.0.0" CMD ["node", "server.js"] ``` ### docker-compose.yml ```yaml version: '3.8' services: nextjs: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=${DATABASE_URL} restart: unless-stopped ``` ### 构建和运行 ```bash # 构建镜像 docker build -t nextjs-app . # 运行容器 docker run -p 3000:3000 nextjs-app # 使用 docker-compose docker-compose up -d ``` ## 3. Node.js 服务器 将 Next.js 应用部署到传统的 Node.js 服务器。 ### 使用 PM2 ```javascript // ecosystem.config.js module.exports = { apps: [{ name: 'nextjs-app', script: 'node_modules/next/dist/bin/next', args: 'start -p 3000', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' }] }; ``` ```bash # 安装 PM2 npm install -g pm2 # 启动应用 pm2 start ecosystem.config.js # 查看状态 pm2 status # 查看日志 pm2 logs # 重启应用 pm2 restart nextjs-app ``` ### 使用 Nginx 反向代理 ```nginx server { listen 80; server_name example.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## 4. 静态导出 对于不需要服务器端功能的简单应用,可以导出为静态 HTML。 ### 配置 ```javascript // next.config.js module.exports = { output: 'export', images: { unoptimized: true } }; ``` ### 构建和部署 ```bash # 构建 npm run build # 输出在 out/ 目录 # 可以部署到任何静态托管服务,如: # - GitHub Pages # - Netlify # - AWS S3 + CloudFront # - Firebase Hosting ``` ## 5. 云平台部署 ### AWS #### 使用 AWS Amplify ```bash # 安装 Amplify CLI npm install -g @aws-amplify/cli # 初始化 amplify init # 添加托管 amplify add hosting # 发布 amplify publish ``` #### 使用 AWS Lambda ```javascript // app/api/hello/route.js export const runtime = 'edge'; export async function GET() { return Response.json({ message: 'Hello from Edge!' }); } ``` ### Google Cloud #### 使用 Cloud Run ```bash # 构建镜像 gcloud builds submit --tag gcr.io/PROJECT_ID/nextjs-app # 部署到 Cloud Run gcloud run deploy nextjs-app \ --image gcr.io/PROJECT_ID/nextjs-app \ --platform managed \ --region us-central1 \ --allow-unauthenticated ``` ### Azure #### 使用 Azure Static Web Apps ```bash # 安装 Azure CLI npm install -g @azure/static-web-apps-cli # 部署 swa deploy ./out --env production ``` ## 6. CI/CD 集成 ### GitHub Actions ```yaml name: Deploy to Vercel on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Build run: npm run build - name: Deploy to Vercel uses: amondnet/vercel-action@v20 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PROJECT_ID }} vercel-args: '--prod' ``` ### GitLab CI ```yaml image: node:18 stages: - test - build - deploy test: stage: test script: - npm ci - npm test build: stage: build script: - npm ci - npm run build artifacts: paths: - .next/ deploy: stage: deploy script: - npm install -g vercel - vercel --prod --token=$VERCEL_TOKEN only: - main ``` ## 环境变量管理 ### .env 文件 ```bash # .env.local(本地开发) DATABASE_URL=postgresql://localhost/mydb API_KEY=your_api_key # .env.production(生产环境) DATABASE_URL=postgresql://prod-db-host/mydb API_KEY=prod_api_key ``` ### Vercel 环境变量 ```bash # 使用 Vercel CLI vercel env add DATABASE_URL production # 或在 Vercel Dashboard 中设置 ``` ## 性能优化 ### 1. 启用压缩 ```javascript // next.config.js module.exports = { compress: true, }; ``` ### 2. 优化图片 ```javascript // next.config.js module.exports = { images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920], }, }; ``` ### 3. 启用 SWC 压缩 ```javascript // next.config.js module.exports = { swcMinify: true, }; ``` ## 监控和日志 ### 使用 Vercel Analytics ```javascript // pages/_app.js import { Analytics } from '@vercel/analytics/react'; export default function App({ Component, pageProps }) { return ( <> <Component {...pageProps} /> <Analytics /> </> ); } ``` ### 使用 Sentry ```javascript // sentry.client.config.js import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 1.0, }); ``` ## 部署最佳实践 1. **使用 Vercel**:获得最佳的 Next.js 部署体验 2. **环境变量**:使用 .env 文件管理环境变量 3. **CI/CD**:设置自动部署流程 4. **监控**:使用监控工具跟踪应用性能 5. **备份**:定期备份数据库和重要文件 6. **测试**:部署前运行完整的测试套件 7. **渐进式部署**:使用蓝绿部署或金丝雀发布 8. **文档化**:记录部署流程和配置 通过合理选择部署方式和遵循最佳实践,可以确保 Next.js 应用稳定、高效地运行在生产环境中。
服务端 · 2月17日 23:32