ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Next.js 从入门到实践:掌握渲染模式、数据获取与部署

Next.js 从入门到实践:掌握渲染模式、数据获取与部署 说实话Next.js 这套东西一开始并不好上手。我最早接触 React 是从纯 SPA 项目开始的那时候客户端渲染、路由懒加载、SEO 无力、首屏白屏这些问题都要自己一个个去解。后来在团队里做内容型站点才开始认真评估 Next.js然后一步步把项目迁移过来。如果你也在学 Next.js或者学过一点但总卡在 Server Component、渲染策略、API Routes 这些概念上这篇内容应该能帮你把思路理顺。它没有想象中那么神秘但确实需要跳出一层“组件思维”去理解请求、渲染、缓存这整条链路。1. 先搞清楚为什么是 Next.js1.1 React 只解决视图层剩下的事都得自己搭React 的本质是一个视图层库它负责把状态映射成 DOM但路由怎么办、数据请求放在哪里、SSR 怎么做、SEO 怎么优化React 本身并不关心。很多刚上手 Next.js 的人会把它当成“React 的又一个脚手架”其实这是低估了它。Next.js 是一个完整的 React 框架它把约定式路由、服务端渲染、静态生成、增量静态再生成、API 后端能力全都内置进来让你不再需要从零组装一套复杂的前端工程。我见过不少团队用 React React Router Redux Toolkit Axios 各种自定义封装搭了一套“伪框架”项目初期跑得飞快到了后期每次加页面都要考虑路由配置、数据预取、权限守卫、SEO 降级方案维护成本非常高。Next.js 这些年在社区里能站稳脚跟靠的不是某一个炫技功能而是把“全栈 React 应用”这件事做成了一套默认的、有规范的技术路径。1.2 渲染模式的组合拳CSR、SSR、SSG、ISRNext.js 最核心的价值在于支持多种渲染模式而且能在同一个项目里混合使用。很多初学者搞不清这些术语我习惯用一个类比把网页想象成一盘菜。纯客户端渲染CSR是“先给个空盘子等顾客坐下后厨再炒菜”白屏时间长SEO 基本看运气服务端渲染SSR是“顾客点单后厨马上炒炒完端上来”每次点单都现炒适合实时性要求高的页面静态生成SSG是“早就把菜做好放在保温柜里”谁来了都能秒取适合内容基本不变的页面增量静态再生成ISR则是“大部分菜预置好每隔一段时间把几道菜重新炒一次”兼顾速度与新鲜度。模式生成时机适用场景优点缺点CSR浏览器端运行后台系统、私密页面交互强后端压力小首屏慢、SEO 差SSR每次请求实时数据、个性化页面SEO 好、首屏快服务器压力大SSG构建时博客、文档、营销页极快、成本低内容一更新需重建ISR构建时后台定时内容较稳定的站点兼顾速度与更新有缓存窗口期选哪种模式没有绝对对错关键看你的用户怎么访问页面。我在实际项目里一般优先考虑 SSG 和 ISR只有涉及用户维度的数据购物车、个人中心、站内通知才强制走 SSR 或客户端请求。1.3 版本演进与生态定位Pages Router 到 App Router如果说你之前用过的 Next.js 是 12 或 13 之前的版本那你熟悉的应该是 Pages Router在pages目录下创建页面文件用getStaticProps、getServerSideProps做数据获取。到了 Next.js 13官方正式推出 App Router把pages目录换成了app目录引入 Server Component、Nested Layout、Server Actions 这些新概念。很多人看到这里容易慌觉得以前学的都白费了。其实不用这么想。App Router 是下一阶段的主要方向但 Pages Router 也还在维护至少在很长一段时间内都能正常使用。更关键的是两种模式的思想是互通的服务端渲染、静态生成、路由动态参数这些底层能力没有变只是组织方式和语法更现代化了。所以入门时我建议直接学 App Router项目里老代码可以逐步迁移不必推倒重来。2. 初始化项目与两种路由体系的关键切换2.1 创建项目从零跑通最小可运行版本创建 Next.js 项目非常简单最难的一步反而是选安装选项。打开终端执行npx create-next-applatest my-app交互式命令行会问你 TypeScript、ESLint、Tailwind CSS、App Router 等配置。对新手来说建议 TypeScript 选 YesTailwind 可以先选 NoApp Router 选 Yessrc目录根据自己偏好选择即可。我是建议开src目录的后面代码多了能把配置文件和源码分开看着清爽很多。初始化完成后项目目录结构大致是src/app/ layout.tsx page.tsx globals.css public/ next.config.mjs package.json其中src/app/layout.tsx是根布局包住所有页面src/app/page.tsx对应根路径/。你只要运行npm run dev打开http://localhost:3000就能看到默认首页。这个最小项目其实已经包含了很多关键概念布局文件、页面文件、全局样式、路由约定。把这里搞懂了后面加页面就是在app目录下新建文件夹和page.tsx的事。2.2 App Router 的特殊文件page、layout、loading、errorApp Router 的一个显著特点是“约定式特殊文件”也就是说你不需要手动配置路由表文件系统本身就代表路由结构。以app目录为例app/blog/page.tsx就是/blog页面app/blog/[slug]/page.tsx就是/blog/hello-world这种动态路由。每个路由下还可以放额外文件layout.tsx定义布局loading.tsx定义加载状态error.tsx定义错误边界not-found.tsx定义 404 页面。举个例子想给博客这种内容型模块加一个统一的侧边栏布局就新建app/blog/layout.tsxexport default function BlogLayout({ children, }: { children: React.ReactNode; }) { return ( div classNameflex aside侧边栏分类/aside main{children}/main /div ); }这个布局只作用于/blog及它下面的子路由不会影响根路径。在 Pages Router 时代做布局往往要用高阶组件或者_app.tsx里手动套壳现在这种文件约定直接把复杂度降下来了也让每个路由的代码更容易被“看见”。2.3 Server Component 与 Client Component 的分界线App Router 里最需要适应的概念是“默认服务端组件”。在app目录下创建的组件默认是 Server Component可以在里面直接访问数据库、读取环境变量而不需要暴露 API 接口。但这引出了一个问题onClick这些交互逻辑在 Server Component 里是不能用的一旦写了浏览器的 API 或者事件监听控制台就会报错。这时就需要给组件顶部加一行use client把它变成 Client Component。我通常会这样划分页面主体尽量留在服务端组件里只有那些需要用户交互的局部组件按钮、表单、弹窗、播放器才用客户端组件。很多新手图省事直接在page.tsx顶部加use client等于把整个页面变成了客户端渲染前面那些 SSG、ISR 的优势全丢了。这是 App Router 项目里最常见的滥用场景。3. 数据获取的四种姿势与选型逻辑3.1 Pages Router 的经典写法与现代 App Router 写法对比如果你在网上搜 Next.js 教程会看到大量getStaticProps和getServerSideProps的代码它们是 Pages Router 时代的官方答案。在 App Router 里这些 API 被更简洁的方式取代服务端组件里直接async function加fetch按需选择缓存策略。先看 Pages Router 的写法export async function getStaticProps() { const res await fetch(https://api.example.com/posts); const posts await res.json(); return { props: { posts } }; } export default function Blog({ posts }: { posts: any[] }) { return ( ul {posts.map((post) ( li key{post.id}{post.title}/li ))} /ul ); }再看 App Router 的写法export const revalidate 3600; export default async function Blog() { const res await fetch(https://api.example.com/posts, { next: { revalidate: 3600 }, }); const posts await res.json(); return ( ul {posts.map((post) ( li key{post.id}{post.title}/li ))} /ul ); }从代码量上看App Router 明显更短。你不需要分开写数据获取函数和组件直接在组件里await fetch而且默认情况下这个组件只在服务端运行返回的 HTML 已经包含数据不会把数据请求逻辑暴露给浏览器。理解这套写法之后可以再看那些关键词revalidate是更新频率next: { revalidate }控制 ISR 的间隔。3.2 动态路由、generateStaticParams 与 fallback 行为内容型站点经常有/posts/xxx这种动态路径每个路径对应一篇文章。在 SSG 模式下你需要让 Next.js 知道哪些路径是构建时生成的。Pages Router 用的是getStaticPathsApp Router 用的是generateStaticParams。export async function generateStaticParams() { const posts await getAllPosts(); return posts.map((post) ({ slug: post.slug })); } export default async function PostPage({ params, }: { params: { slug: string }; }) { const post await getPost(params.slug); return article{post.title}/article; }这里的重点是dynamicParams的默认行为如果有一个新文章发布但它的路径不在generateStaticParams返回列表里Next.js 会先把请求交给服务端渲染生成一次 HTML同时缓存下来。这个行为对很多内容站非常合适不需要每次发布文章都触达全量构建但我踩过坑的是如果站点希望后台发布后立刻生效就必须提前把新文章的路径拼进去否则第一次访问会经历一次或长或短的等待。3.3 ISR 的缓存窗口与刷新时机ISR 是最容易让新手产生认知偏差的功能。看起来就是revalidate写个秒数实际上它的缓存窗口不是“绝对定时更新”而是“在最后一次请求后开始倒计时”。举个例子设revalidate 300用户在 12:00 访问了页面那么到 12:05 前的所有请求都会命中缓存12:05 之后有人访问才会触发后台重新生成页面之后的用户看到更新版本。所以在内容发布密集的场景下光靠 ISR 可能不够及时。我常用两种补救一是管理后台发布文章后调用 On-Demand Revalidation也就是 Next.js 的revalidatePath(/posts)接口主动告诉框架“这个路径该清理缓存了”二是用增量轮询的页面内容接口做降级让管理员能手工触发刷新。下面这段代码在 route handler 里可以这样写import { revalidatePath } from next/cache; export async function POST(request: Request) { const body await request.json(); if (body.secret process.env.REVALIDATE_SECRET) { revalidatePath(/blog); return new Response(ok, { status: 200 }); } return new Response(forbidden, { status: 403 }); }这套方案在生产里很实用重点在于给接口加一个自定义 secret否则任何人都能触发全站缓存刷新非常容易被滥用。4. 中间件、认证与 Server Actions 的实战组合4.1 中间件的运行时机与边界Next.js 的middleware.ts默认在项目根目录或者src目录下它不运行在 Node.js 环境而是运行在 Edge Runtime也就是一个更轻量、更靠近 CDN 的运行时。因为运行环境受限你不能在中间件里直接使用fs、node:crypto这类 Node 原生模块但可以做重定向、路径匹配、读取 Cookie、简单鉴权。我常把中间件用在这些地方未登录用户访问后台统一跳转到登录页根据用户语言设置做地区路由在用 A/B 测试时按条件重写到不同页面。一个最小示例import { NextResponse, type NextRequest } from next/server; export function middleware(request: NextRequest) { const token request.cookies.get(token); const url request.nextUrl; if (!token url.pathname.startsWith(/dashboard)) { return NextResponse.redirect(new URL(/login, request.url)); } return NextResponse.next(); } export const config { matcher: [/dashboard/:path*], };这个写法很直观但我也提醒一句中间件里的鉴权只是“门卫”真正的数据保护还得靠服务端组件或 Route Handler 里再校验一遍。因为中间件本身很容易被绕过而且 Cookie 信息不一定可靠防君子不防小人。4.2 认证方案JWT 与 Session 的落地思路Next.js 本身不提供认证系统你可以选择 JWT 也可以选择服务端 Session。我的习惯是简单项目用 JWT 存在 HttpOnly Cookie 里减少跨站脚本攻击风险复杂项目用数据库 Session 表方便后台主动踢人和统计登录设备。一个非常朴素的 JWT 流程是这样的登录接口校验用户名密码生成 token写入 HttpOnly Cookie用户访问受保护接口时服务端代码读取 Cookie验签并解析用户身份如果需要更新登录态就续期 token 并重新写回 Cookie。整个过程没有把 token 暴露给 JavaScript所以即使页面里被人恶意注入脚本也拿不到敏感凭据。在 App Router 中读取用户信息可以放在layout.tsx或page.tsx的服务端代码里比如写一个getCurrentUser()函数内部使用cookies()读取 cookie再用jwt.verify()去校验。这个函数只能在服务端调用千万不要加上use client否则会因为在浏览器里没有密钥环境而直接报错。4.3 Server Actions从表单提交到渐进增强Server Actions 是 App Router 在 Next.js 13.4 之后逐步补齐的能力。它让你在 React 组件里直接调用一个服务端函数而不需要自己写 API route、再手写 fetch 调用。用起来最直观的是表单场景use server; export async function createPost(formData: FormData) { const title formData.get(title); // 这里可以安全地操作数据库或文件系统 return { success: true }; }然后在页面组件里使用form action{createPost}浏览器会把这个动作提交给服务端。整个过程默认支持渐进增强即使 JavaScript 暂时加载失败表单也能通过原生请求提交。这是我在做内容管理功能时最喜欢的能力比传统“前端调接口再处理响应”少了一大堆模板代码。不过 Server Actions 有几点要叮嘱第一表单提交之后要动态更新页面你需要配合revalidatePath()或者redirect()第二不是所有逻辑都适合丢进 Server Action比如依赖复杂客户端状态的交互会让代码变得不好追踪第三权限校验务必写在服务端函数内部不能只看客户端传过来的数据就信任用户。5. 生产环境的性能与部署清单5.1 图片、字体、核心指标优化一个 Next.js 项目上线前我建议至少做三件事。第一是开启next/image的图片优化它能根据设备尺寸自动生成合适大小的图片还支持 lazy load。用的时候一般这样写import Image from next/image; Image src/images/banner.jpg width{1200} height{630} altbanner sizes(max-width: 768px) 100vw, 1200px priority /第二是使用next/font来自动加载字体避免font-face手动配置带来的 CLS 问题。第三是检查 bundle 体积尤其是 Client Component 里是否引入了过重的库。我第一次把一个 Markdown 渲染库放在客户端组件里结果首屏 bundle 多出来 300KB后来移到服务端组件里直接变成 HTML 输出指标立刻好看了很多。这些优化不用一上来就全做完但上线前最好用 Lighthouse 跑一次把首屏内容LCP和布局偏移CLS检查一遍。Next.js 默认的 app 目录下还能用next dev --turbo体验 Turbopack 加速开发不过生产构建目前我还是用默认模式多稳定第一。5.2 部署到 Vercel vs 自托管standalone 输出部署方式直接决定你的运维成本。最简单的方案是直接推到 GitHub用 Vercel 做自动部署环境变量在后台配置分支和域名都帮你处理好了。大部分内容站和个人项目用 Vercel 就够了特别是 ISR 和 On-Demand Revalidation 在 Vercel 上体验最完整。如果项目需要部署到自己的服务器或内网环境就需要在next.config.mjs里开启const nextConfig { output: standalone, }; export default nextConfig;开启后执行npm run build会生成.next/standalone目录。这个目录是整个服务端的精简版本拷贝到服务器上安装好 Node.js然后执行node server.js就能跑起来。再把.next/static同步复制到standalone/.next/static才能确保静态资源正常加载。这个坑我踩过自托管时只复制了standalone忘记同步静态目录结果页面 HTML 能访问但 JS、CSS 全部 404。5.3 环境变量、日志与缓存策略环境变量在 Next.js 中分成两类以NEXT_PUBLIC_开头的会暴露给浏览器其余的只在服务端可用。这个规则很容易让人踩坑比如把数据库地址写进NEXT_PUBLIC_DATABASE_URL等于把底层连接信息发给了所有访客。我通常只把站点公网地址、第三方前端统计脚本 ID 这类信息用NEXT_PUBLIC_前缀其余密钥一律只在服务端代码里使用。日志方面本地调试靠终端输出就够了生产环境就需要接第三方日志服务或云厂商自带的日志查询。Next.js 服务端组件里的console.log会输出到服务端日志客户端组件里加了use client的话日志输出到浏览器 console排查时要分清楚位置。缓存这块除了 ISR 之外还可以在 Route Handler 返回时设置Cache-Control响应头控制 CDN 的缓存行为。6. 实战案例用 Payload CMS 搭建一个真实内容站6.1 为什么是 Payload CMS你可能最近搜过“next.js payload 教程”这个热搜词这里的 payload 一般指的是 Payload CMS一个开源的 Headless CMS。Headless CMS 的意思是它只管内容管理和 API不管最终展示效果。团队里运营人员可以通过后台编辑文章前端用 Next.js 从 API 拉取内容并渲染页面。我之前用过不少内容管理工具最后在几个项目里长期使用 Payload CMS主要是因为它和 Next.js 的集成相当自然。Payload 本身是 TypeScript 优先的数据模型就用 TS 和 JSON 定义没有后台界面死板的那一套也不需要额外写一套 GraphQL schema。如果你已经有 Next.js 项目后面接一个内容管理后台非常快。6.2 创建 Payload 项目并接入 Next.js目前最直接的方式是用它自带的脚手架创建项目。假设你想在一个已有 Next.js 项目里逐步加入 Payload可以参考下面的流程。先安装依赖npm install payload payloadcms/next payloadcms/richtext-lexical之后在项目里创建一个payload.config.ts定义内容模型和数据库连接。Payload 支持很多数据库本地开发用 SQLite 最省事生产环境可以用 PostgreSQL。然后按官方模板配置app/(payload)/admin这些路由让/admin变成 CMS 管理后台。我没有办法在这里把所有配置步骤逐条列完因为版本迭代很快但核心思路是固定的Payload 用 Collection 定义内容类型比如文章、分类、作者用 Field 定义字段比如标题、正文、封面图后台生成 REST API而 Next.js 通过 fetch 调用/api/posts获取内容。搭一次之后你会发现这比写一个完整的管理后台要快得多。6.3 数据模型、API 调用与渲染注意事项假设我要做一个博客站先定义一个 Post Collection包含标题、slug、封面、正文。这个配置会直接生成 REST API 路由。在 Next.js 的 App Router 中我在服务端组件里这样获取文章列表const response await fetch( ${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/posts?limit20, { next: { revalidate: 300 } }, ); const data await response.json();这里要注意两点。第一所有请求都在服务端发起所以不需要处理 CORS但生产环境一定要把PAYLOAD_PUBLIC_SERVER_URL配成部署后的完整 URL。第二Payload 的正文是富文本结构不是一段 HTML 字符串你需要根据内容版块渲染。如果你用的是 Lexical 富文本可以安装官方提供的 React 渲染组件把内容数组转成可渲染的组件如果用默认的 Slate 结构就需要自己去匹配textNode、block这些类型。第一次接触的人可能会在这里卡住其实说白了就是把 JSON 结构翻译成 React 组件。最后强烈建议把 CMS 的访问控制配置好。Payload 默认的管理后台只对已登录用户开放但 API 需要决定是否对所有人公开。我习惯把文章列表、详情设为公开读取但评论、投稿这样的业务模块必须带着鉴权字段才能读写。这套权限逻辑直接在 Collection 的 access 配置里写清楚比在前端一遍遍判断有没有 token 靠谱得多。7. 避坑速查表与调试实录7.1 常见问题的现象、原因与处理方案这里整理了我在 Next.js 项目里高频遇到的问题用表格列出来方便速查。问题现象常见原因处理方式热更新慢、内存占用高项目过大且大量依赖在客户端组件中加载拆分依赖、使用动态导入升级 Node.js 和 Turbopack控制台报错“createContext from Server Component”在服务端组件里使用了上下文或客户端状态库把相关逻辑移到独立客户端组件在服务端组件中引入该组件图片突然变模糊或布局跳动next/image未设置宽高或使用了固定尺寸设置width、height或sizes属性环境变量为空导致请求 500在组件中直接访问了非NEXT_PUBLIC_环境变量检查是否在服务端代码中引用及部署平台是否配置ISR 页面更新不及时对revalidate的理解有偏差或页面在 CDN 层缓存改用 On-Demand Revalidation并检查 CDN 响应头生产部署后静态资源 404standalone 模式漏同步.next/static复制静态目录到 standalone 输出目录Middleware 中无法使用 Node 包Edge Runtime 环境限制改用服务端组件或 Route Handler 处理重活这些坑几乎是每个 Next.js 项目从开发到上线都会撞上的尤其早期不熟悉 App Router 的时候最容易在服务端和客户端边界上栽跟头。7.2 调试 Next.js 项目的一些个人习惯我在本地调试时一般会做几件事。一是打开浏览器开发者工具的 Network 面板查看 HTML 文档的回传大小如果服务端组件返回的 HTML 里已经有完整内容说明渲染正常如果返回的 HTML 是空壳再通过网络请求看到一堆接口调用说明这个页面实际已经退化成纯客户端渲染了。二是用next build next start来模拟生产模式因为很多问题只在生产构建里出现开发模式热更新会掩盖缓存和并发问题。第三是给项目配一套 log 约定。服务端组件里我会用带前缀的日志比如[DB]、[payload]这样在日志平台里一眼能定位到底是数据库查询慢、CMS 接口慢还是页面渲染耗时长。如果日志显示 API 响应已经很快但页面生成仍然慢再去看是不是有重型组件阻塞了服务端渲染。我之前接手过一个页面首屏加载非常慢排查下来发现是富文本渲染组件被放在了客户端组件里而且正文数据是几千行的大文章浏览器每访问一次就要把整段富文本 JSON 加载下来并转换。后来我把渲染逻辑移到 Server Component 内部直接服务端输出 HTML首屏加载体积掉了一大截。这个案例给我最大的启发是Next.js 里性能问题的核心往往不是框架本身而是你把它当成了 SPA 在用。7.3 最后几个值得记住的实践建议写 Next.js 项目的时候大多数坑其实都源于没有理解“代码到底在哪里运行”。文件系统路由虽然直观但每个文件在构建时被切成服务端和客户端两套环境写代码前先问一句“这个文件会被谁执行”很重要。我会在项目启动时把app目录下所有加过use client的文件盘点一遍能减则减因为客户端代码越多首屏 bundle 越重SSG/ISR 的优势就越淡。另外一个建议是尽量早地把部署方式确定下来。如果你计划部署到 Vercel那么环境变量、缓存刷新、Preview 部署这套流程都是开箱即用的如果你要自己租服务器就要先把 standalone 输出、日志收集、进程守护这些基础设施准备好。项目一旦上了生产环境再去迁移部署方式会痛苦得多。我自己比较喜欢的做法是先跑通最小可用的部署链路再慢慢加功能这样项目从一开始就有稳定的底座。
返回列表