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

面试题手册

Next.js 13+ 的 React Server Components 是什么?

Next.js 13 引入了 React Server Components(RSC),这是一个重大的架构变革,彻底改变了我们在 Next.js 中构建应用的方式。什么是 React Server Components?React Server Components 是一种新的组件类型,它们在服务器上渲染,而不是在客户端。这意味着:服务器端渲染:组件在服务器上执行,生成 HTML零客户端 JavaScript:服务器组件不会发送任何 JavaScript 到客户端直接访问后端资源:可以直接访问数据库、文件系统等保持代码私密:服务器代码不会暴露给客户端服务器组件 vs 客户端组件服务器组件(默认)// 默认情况下,所有组件都是服务器组件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 到客户端客户端组件'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 到客户端混合使用服务器和客户端组件在服务器组件中使用客户端组件// 服务器组件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> );}在客户端组件中使用服务器组件// 客户端组件'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// 传统方式(客户端组件)'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. 直接访问数据库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. 保持代码私密// 服务器组件中的敏感代码不会暴露给客户端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. 更好的性能// 服务器组件可以并行获取数据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. 博客文章列表// app/blog/page.jsasync 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.jsexport default function PostCard({ post }) { return ( <article> <h2>{post.title}</h2> <p>{post.excerpt}</p> <Link href={`/blog/${post.slug}`}>Read more</Link> </article> );}2. 电商产品页面// app/products/[id]/page.jsasync 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. 仪表板// app/dashboard/page.jsimport { 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. 默认使用服务器组件// ✅ 好的做法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. 只在需要交互的地方使用客户端组件// 服务器组件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. 将客户端组件移到组件树的底部// ✅ 好的做法:客户端组件在底部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. 使用动态导入减少客户端 JavaScriptimport 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,但可以通过以下方式处理:// 使用 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:'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 的未来,通过合理使用服务器组件和客户端组件,可以构建出性能更好、用户体验更佳的应用。
阅读 0·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.jsApp Router:app/ page.js about/ page.js api/ users/ route.js2. 布局系统App Router 的布局系统更强大:// app/layout.jsimport './globals.css';export default function RootLayout({ children }) { return ( <html lang="zh-CN"> <body> <header>全局头部</header> {children} <footer>全局页脚</footer> </body> </html> );}// app/about/layout.jsexport default function AboutLayout({ children }) { return ( <div className="about-layout"> <aside>关于我们侧边栏</aside> <main>{children}</main> </div> );}3. 数据获取方式Pages Router:// pages/index.jsexport 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:// app/page.jsasync 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 ComponentsApp Router 默认使用 Server Components:// 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:// pages/posts/[id].jsexport 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:// app/posts/[id]/page.jsexport 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 内置支持:// app/posts/loading.jsexport 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.jsexport default function NotFound() { return <div>文章未找到</div>;}7. Server ActionsApp Router 独有功能:// 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.jsimport { createPost } from '../actions';export default function NewPostPage() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">创建文章</button> </form> );}迁移策略渐进式迁移// 可以在同一个项目中同时使用两个 Router// pages/ - 旧代码继续使用 Pages Router// app/ - 新功能使用 App Router// 在 App Router 中链接到 Pages Router<Link href="/old-page">旧页面</Link>关键迁移步骤安装 Next.js 13+ 并更新配置创建 app/ 目录结构迁移布局到 app/layout.js将页面逐步迁移到 app/ 目录更新数据获取逻辑添加 'use client' 指令到交互式组件更新 API 路由为 Route Handlers测试并优化性能性能对比App Router 优势:更小的客户端包(Server Components 不发送到客户端)更好的 SEO(默认服务端渲染)更灵活的缓存策略内置加载和错误状态更简洁的代码结构Pages Router 优势:更成熟稳定更丰富的生态系统和文档更简单的学习曲线更多的第三方库支持最佳实践新项目: 优先使用 App Router现有项目: 渐进式迁移,先迁移简单页面组件选择: 默认使用 Server Components,只在需要交互时使用 Client Components数据获取: 利用 fetch API 的缓存选项状态管理: Server Actions 替代部分 API 路由App Router 代表了 Next.js 的未来方向,提供了更现代的开发体验和更好的性能表现。
阅读 0·2月17日 23:33

Next.js 中的 SSR、SSG 和 ISR 有什么区别?

Next.js 中有三种主要的渲染方式,每种方式都有其特定的使用场景和优势:1. 客户端渲染(CSR)客户端渲染是传统的 React 应用渲染方式。页面初始加载时返回一个空的 HTML 文件,然后 JavaScript 在浏览器中执行,动态生成页面内容。特点:首屏加载较慢,需要等待 JavaScript 下载和执行SEO 不友好,搜索引擎爬虫可能无法抓取动态内容交互性强,适合高度交互的应用适合不需要 SEO 的后台管理系统、仪表盘等实现方式: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 的页面实现方式: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 友好,静态内容易于搜索引擎抓取构建时生成,内容更新需要重新构建适合内容不经常变化的页面,如博客、产品页面等实现方式: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 的动态性。特点:保持静态页面的性能优势可以在后台更新页面内容用户总是看到最新的内容适合需要定期更新但不需要实时更新的内容实现方式: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 效果。
阅读 0·2月17日 23:32

Next.js 的 Pages Router 和 App Router 有什么区别?

Next.js 提供了两种主要的路由架构:Pages Router 和 App Router。它们在设计理念、功能和性能方面有显著差异。Pages RouterPages Router 是 Next.js 的传统路由系统,基于 pages 目录。特点文件结构简单pages/ index.js about.js blog/ [slug].js数据获取方法getStaticProps:静态生成时获取数据getServerSideProps:每次请求时获取数据getStaticPaths:定义动态路由的路径生命周期支持 React 的完整生命周期使用 useEffect 进行客户端数据获取路由钩子import { useRouter } from 'next/router';const router = useRouter();console.log(router.pathname, router.query);优点成熟稳定,文档和社区支持完善学习曲线较平缓适合中小型项目缺点不支持嵌套路由不支持服务器组件性能优化有限App RouterApp Router 是 Next.js 13+ 引入的新路由系统,基于 app 目录。特点文件结构app/ page.js layout.js about/ page.js blog/ [slug]/ page.js服务器组件和客户端组件// 服务器组件(默认)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>;}数据获取// 直接在组件中获取数据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>;}布局系统// app/layout.jsexport default function RootLayout({ children }) { return ( <html> <body> <Header /> {children} <Footer /> </body> </html> );}路由钩子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 Routerapp/ 目录使用 App Router// 两个目录可以共存pages/ api/ hello.js // API 路由app/ page.js // 主页面最佳实践新项目优先使用 App Router:获得更好的性能和开发体验逐步迁移:现有项目可以逐步将页面迁移到 App Router合理使用服务器组件:将不需要交互的组件设为服务器组件利用布局系统:使用嵌套布局来共享 UI保持一致性:在一个项目中尽量统一使用一种路由器选择哪种路由器取决于项目需求、团队经验和性能要求。App Router 代表了 Next.js 的未来方向,但 Pages Router 仍然是可靠的选择。
阅读 0·2月17日 23:32

Next.js 有哪些性能优化技术?

Next.js 提供了多种性能优化技术,帮助开发者构建高性能的 Web 应用。以下是 Next.js 的主要性能优化策略:1. 自动代码分割Next.js 自动将代码分割成小块,只加载当前页面所需的代码。// pages/index.jsimport 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 组件自动优化图片。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 优化字体加载。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// 使用 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'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. 预加载和预取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 优化第三方脚本加载。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'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. 虚拟化长列表'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 到客户端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. 使用 Streamingimport { 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 缓存// 缓存 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 缓存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. 构建优化分析构建输出// next.config.jsconst withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true',});module.exports = withBundleAnalyzer({ // 其他配置});压缩和优化// next.config.jsmodule.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// pages/_app.jsimport { 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} />;}最佳实践使用服务器组件:减少客户端 JavaScript优化图片:使用 next/image 组件懒加载:延迟加载非关键资源缓存数据:使用 ISR 和缓存策略监控性能:使用 Web Vitals 监控分析构建:定期分析 bundle 大小使用 CDN:部署到 Vercel 或其他 CDN优化字体:使用 next/font 优化字体加载通过合理使用这些优化技术,可以显著提升 Next.js 应用的性能和用户体验。
阅读 0·2月17日 23:32

Next.js 中有哪些数据获取方法?

Next.js 提供了多种数据获取方法,开发者可以根据不同的渲染策略和需求选择合适的方式。以下是 Next.js 中主要的数据获取方法:Pages Router 数据获取方法1. getStaticProps在构建时获取数据,用于静态生成(SSG)。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>;}适用场景:数据在构建时可用页面内容不经常变化需要预渲染以提升 SEO2. getServerSideProps在每次请求时获取数据,用于服务器端渲染(SSR)。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用于动态路由的静态生成,定义所有可能的路径。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 获取数据。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 Componentsasync 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 和 Streamingimport { 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'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'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'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. 缓存策略// 长期缓存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. 错误处理export async function getStaticProps() { try { const data = await fetchData(); return { props: { data } }; } catch (error) { return { notFound: true, }; }}4. 加载状态// App Router - 使用 loading.js// app/loading.jsexport default function Loading() { return <div>Loading...</div>;}// Pages Router - 使用自定义加载组件export default function LoadingPage() { return <div>Loading...</div>;}5. 并行数据获取// 并行获取多个数据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 }, };}性能优化建议使用 ISR:对于需要定期更新的内容,使用 ISR 而不是 SSR缓存数据:合理设置缓存时间,减少不必要的请求并行获取:使用 Promise.all 并行获取多个数据源流式渲染:使用 Suspense 实现流式渲染,提升用户体验客户端缓存:使用 SWR 或 React Query 缓存客户端数据按需重新验证:使用标签系统按需重新验证数据通过合理选择和使用这些数据获取方法,可以构建高性能、用户体验良好的 Next.js 应用。
阅读 0·2月17日 23:32

Next.js 中如何进行状态管理?

Next.js 提供了多种状态管理解决方案,开发者可以根据项目需求选择合适的方式。以下是 Next.js 中常用的状态管理方法:1. React 内置状态管理useState Hook用于管理组件的本地状态。'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用于管理复杂的状态逻辑。'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用于跨组件共享状态。'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轻量级、简单的状态管理库。// store/useStore.jsimport { 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功能强大的状态管理库,适合大型应用。// store/slices/counterSlice.jsimport { 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.jsimport { configureStore } from '@reduxjs/toolkit';import counterReducer from './slices/counterSlice';export const store = configureStore({ reducer: { counter: counterReducer, },});// store/hooks.jsimport { 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。// store/atoms.jsimport { 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用于数据获取和缓存。'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)强大的数据获取和状态管理库。'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高性能的表单状态管理。'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 查询参数'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. 服务器组件状态使用服务器组件// 服务器组件不需要客户端状态管理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> );}状态管理最佳实践选择合适的工具:简单状态:useState, useReducer跨组件状态:useContext全局状态:Zustand, Redux Toolkit服务器状态:SWR, React Query表单状态:React Hook Form最小化状态:只存储必要的状态,其他状态通过计算得出服务器优先:尽可能使用服务器组件,减少客户端状态避免过度设计:不要为简单的状态引入复杂的状态管理库类型安全:使用 TypeScript 确保类型安全性能优化:使用 React.memo, useMemo, useCallback 优化性能持久化:使用 localStorage 或 IndexedDB 持久化重要状态通过合理选择和使用这些状态管理方法,可以构建高效、可维护的 Next.js 应用。
阅读 0·2月17日 23:32

Next.js 中如何实现身份验证?

Next.js 提供了多种身份验证解决方案,开发者可以根据项目需求选择合适的方式。以下是 Next.js 中常用的身份验证方法:1. NextAuth.jsNextAuth.js 是 Next.js 最流行的身份验证库,提供完整的身份验证解决方案。基本配置// app/api/auth/[...nextauth]/route.jsimport 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'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> );}服务器端获取 Sessionimport { 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> );}保护路由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 工具函数// lib/auth.jsimport 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// app/api/auth/login/route.jsimport { 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// app/api/auth/register/route.jsimport { 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 } ); }}获取当前用户// lib/getCurrentUser.jsimport { 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. 中间件保护路由// middleware.jsimport { 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// app/api/auth/google/route.jsimport { 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)// lib/permissions.jsexport 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>;}服务器端权限检查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. 密码重置发送重置邮件// app/api/auth/forgot-password/route.jsimport { 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 } ); }}重置密码// app/api/auth/reset-password/route.jsimport { 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 } ); }}身份验证最佳实践使用 HTTPS:始终使用 HTTPS 传输敏感数据HttpOnly Cookies:使用 HttpOnly 防止 XSS 攻击CSRF 保护:实施 CSRF 令牌保护密码哈希:使用 bcrypt 或 argon2 哈希密码速率限制:防止暴力破解攻击安全头部:设置适当的安全头部日志记录:记录身份验证事件用于审计会话管理:设置合理的会话过期时间多因素认证:为敏感操作实施 MFA定期更新:保持依赖项和安全补丁更新通过合理实施这些身份验证方法,可以构建安全可靠的 Next.js 应用。
阅读 0·2月17日 23:32

Next.js 如何部署到生产环境?

Next.js 提供了多种部署选项,开发者可以根据项目需求选择最适合的部署方式。以下是 Next.js 的主要部署选项和最佳实践:1. Vercel(推荐)Vercel 是 Next.js 的创建者提供的托管平台,提供最佳的 Next.js 部署体验。优点零配置部署自动 HTTPS全球 CDN边缘函数支持预览部署自动优化部署步骤# 1. 安装 Vercel CLInpm i -g vercel# 2. 登录 Vercelvercel login# 3. 部署vercel# 4. 生产环境部署vercel --prod配置文件// 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# 多阶段构建FROM node:18-alpine AS base# 依赖阶段FROM base AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci# 构建阶段FROM base AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .RUN npm run build# 运行阶段FROM base AS runnerWORKDIR /appENV NODE_ENV productionRUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjsCOPY --from=builder /app/public ./publicCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/staticUSER nextjsEXPOSE 3000ENV PORT 3000ENV HOSTNAME "0.0.0.0"CMD ["node", "server.js"]docker-compose.ymlversion: '3.8'services: nextjs: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=${DATABASE_URL} restart: unless-stopped构建和运行# 构建镜像docker build -t nextjs-app .# 运行容器docker run -p 3000:3000 nextjs-app# 使用 docker-composedocker-compose up -d3. Node.js 服务器将 Next.js 应用部署到传统的 Node.js 服务器。使用 PM2// ecosystem.config.jsmodule.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' }]};# 安装 PM2npm install -g pm2# 启动应用pm2 start ecosystem.config.js# 查看状态pm2 status# 查看日志pm2 logs# 重启应用pm2 restart nextjs-app使用 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。配置// next.config.jsmodule.exports = { output: 'export', images: { unoptimized: true }};构建和部署# 构建npm run build# 输出在 out/ 目录# 可以部署到任何静态托管服务,如:# - GitHub Pages# - Netlify# - AWS S3 + CloudFront# - Firebase Hosting5. 云平台部署AWS使用 AWS Amplify# 安装 Amplify CLInpm install -g @aws-amplify/cli# 初始化amplify init# 添加托管amplify add hosting# 发布amplify publish使用 AWS Lambda// app/api/hello/route.jsexport const runtime = 'edge';export async function GET() { return Response.json({ message: 'Hello from Edge!' });}Google Cloud使用 Cloud Run# 构建镜像gcloud builds submit --tag gcr.io/PROJECT_ID/nextjs-app# 部署到 Cloud Rungcloud run deploy nextjs-app \ --image gcr.io/PROJECT_ID/nextjs-app \ --platform managed \ --region us-central1 \ --allow-unauthenticatedAzure使用 Azure Static Web Apps# 安装 Azure CLInpm install -g @azure/static-web-apps-cli# 部署swa deploy ./out --env production6. CI/CD 集成GitHub Actionsname: Deploy to Vercelon: 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 CIimage: node:18stages: - test - build - deploytest: stage: test script: - npm ci - npm testbuild: 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 文件# .env.local(本地开发)DATABASE_URL=postgresql://localhost/mydbAPI_KEY=your_api_key# .env.production(生产环境)DATABASE_URL=postgresql://prod-db-host/mydbAPI_KEY=prod_api_keyVercel 环境变量# 使用 Vercel CLIvercel env add DATABASE_URL production# 或在 Vercel Dashboard 中设置性能优化1. 启用压缩// next.config.jsmodule.exports = { compress: true,};2. 优化图片// next.config.jsmodule.exports = { images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920], },};3. 启用 SWC 压缩// next.config.jsmodule.exports = { swcMinify: true,};监控和日志使用 Vercel Analytics// pages/_app.jsimport { Analytics } from '@vercel/analytics/react';export default function App({ Component, pageProps }) { return ( <> <Component {...pageProps} /> <Analytics /> </> );}使用 Sentry// sentry.client.config.jsimport * as Sentry from '@sentry/nextjs';Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 1.0,});部署最佳实践使用 Vercel:获得最佳的 Next.js 部署体验环境变量:使用 .env 文件管理环境变量CI/CD:设置自动部署流程监控:使用监控工具跟踪应用性能备份:定期备份数据库和重要文件测试:部署前运行完整的测试套件渐进式部署:使用蓝绿部署或金丝雀发布文档化:记录部署流程和配置通过合理选择部署方式和遵循最佳实践,可以确保 Next.js 应用稳定、高效地运行在生产环境中。
阅读 0·2月17日 23:32