尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Next.js从入门到实战:文件路由、预渲染与全栈API开发

Next.js从入门到实战:文件路由、预渲染与全栈API开发 1. Next.js到底是什么为什么值得你从头学一遍先聊点实在的。React生态发展了这么多年组件化、虚拟DOM这些概念早就深入人心但真正做项目的时候你会发现React本身只是一个UI库它不告诉你路由怎么配、数据怎么拉、SEO怎么做、代码怎么分包。这些问题每个团队都有自己的解法于是出现了各种五花八门的脚手架和最佳实践新手上手时面对一堆工具链往往一头雾水。Next.js就是为了解决这一揽子问题而生的。它不是一个简单的脚手架而是一个基于React的全栈框架把路由、预渲染、服务端逻辑、静态导出、API接口这些能力全部收进框架内部并且提供了官方推荐的约定式方案。换句话说你用Next.js开发不是在“搭积木”而是在一个已经设计好的完整体系中写业务代码。这对新手来说意味着更少的选择焦虑对老手来说意味着更少的重复劳动。我第一次用Next.js的时候最大的感受是“原来React应用还可以这样写”。一个页面文件对应一个路由不需要手动配置React Router一个getServerSideProps函数就能在服务端拉数据一个export const revalidate就能实现增量静态再生。这种感觉就像是从手动挡换到了自动挡虽然一开始有点不太适应但开久了就再也回不去了。这篇内容适合谁如果你是React新手想跳过繁琐的工具链配置、直接用一个生产级框架做真实项目如果你是有React经验但一直听说Next.js却没用过的开发者如果你是想做个人博客、电商站点、内容型应用或者需要SEO优化的任何Web应用的开发者这套内容都能帮你建立起完整的知识框架。我尽量用实操的方式来讲所有代码都是能直接跑的。2. 从核心设计思路看懂Next.js的骨架2.1 文件即路由为什么说这是最友好的约定Next.js最让人舒服的一点就是路由基于文件系统。你在pages目录下新建一个about.js文件访问/about就自动对应这个页面新建一个pages/posts/[id].js文件就可以通过动态路由访问/posts/1、/posts/abc等等。这种约定式路由的设计理念本质上是在告诉开发者“你不用再写路由配置文件了你的文件结构本身就是路由地图。”这种设计有几个实实在在的好处。第一心智负担低。你不需要学习一套独立的配置语法只要会组织文件夹和文件名就会创建路由。第二文件结构即代码结构。一个有经验的同事扫一眼你的pages目录就能大概知道这个项目有哪些页面根本不用打开路由配置文件对照着看。第三避免了字符串路由容易拼写错误的问题。动态路由是其中比较关键的一个概念。Pages/post/[id].js中的[]语法表示这是一个动态参数占位符。你在useRouter()中拿到router.query.id就能根据这个id去请求对应文章的数据。嵌套动态路由比如pages/post/[id]/[comment].js对应的其实是/post/1/99这种两层动态路径更复杂的场景还能用catch-all路由[...slug].js来匹配任意层级的路径。Pages Router和App Router之分需要注意一下。Next.js 13.4之后主推App Router也就是app目录下的路由体系同时保留pages目录的兼容。App Router支持更灵活的布局嵌套、服务端组件和客户端组件并行使用API设计上更现代化。但Pages Router的模型更简单直接所以非常适合初学者理解Next.js的核心思想。在实际项目选型时我个人的建议是如果是新项目直接用App Router如果是学原理或维护老项目Pages Router也完全值得掌握两者在数据获取和渲染策略底层上是相通的。2.2 预渲染策略SSG、SSR、ISR到底该怎么选Next.js区别于纯客户端React应用的最大核心就是预渲染。纯React应用在浏览器里加载JS之后才能渲染内容搜索引擎爬虫未必能等到那一刻首屏性能也受限。Next.js允许你在构建时或请求时直接把HTML生成好用户访问到的直接是可交互的内容。这里有几个关键概念需要理清楚:静态生成(SSG)在构建时一次性生成HTML部署后所有用户访问到的是同一个静态文件。适合博客文章、产品介绍页、文档站这类内容不频繁变化、但需要快速加载的页面。生成速度极快也能通过CDN做缓存。服务端渲染(SSR)每次请求都动态生成HTML适合需要根据用户身份返回不同内容的场景比如个人中心、购物车页面。代价是每次请求都要执行服务端逻辑TBFB(Time To First Byte)会比SSG慢但比纯客户端加载快很多。增量静态再生(ISR)这是Next.js的独门绝技。构建时先生成静态页面但在配置的revalidate时间过后第一个用户访问时会触发后台重新生成页面然后更新静态缓存。相当于把静态页变成“会自己更新”的页面。适合电商价格、新闻列表这种内容需要周期性更新的场景。选型逻辑也不复杂。如果你能预先知道页面内容且不常变优先SSG若内容个性化要求很高且每次都需要新数据用SSR如果内容可以容忍偶尔的延迟更新但希望大部分时间享受静态缓存ISR就是最优解。实际项目里一个站点通常会在不同页面组合使用这三种策略这恰恰是Next.js框架层就支持多策略并存的优势。2.3 API Routes一个框架搞定前后端Next.js内置了API Routes能力你能在pages/api目录或App Router的route.ts文件下直接写服务端接口不需要单独搭一个Express服务器。这个设计非常适合全栈开发特别是个人项目和小型团队项目——你能把数据库查询、鉴权逻辑、外部API代理全部放在Next.js应用内部前后端共享类型定义部署时也只要部署一个应用。有人可能会问“那我为什么不用一个独立的Node.js后端”。答案在于开发效率和部署成本。独立后端意味着你需要维护两个服务、两套部署流水线、处理跨域问题用API Routes这些成本全部省掉。而且当你使用Vercel这类平台部署时API Routes会自动变成Serverless函数按调用量付费大部分个人项目基本免费。API Routes有几个需要注意的坑。第一默认情况下它运行在Node.js环境但如果你开启了runtime为edge就得注意不能使用Node.js内置模块。第二API Routes函数应该保持轻量不适合做长时间运行的任务否则Serverless环境会超时。第三写API时不要忘了设置响应状态码和错误处理否则前端排错会非常痛苦。3. 环境搭建和第一个实践项目3.1 快速初始化一个Next.js项目在开始之前确保你已经安装了Node.js 18.17.0或更高版本。这个版本要求是Next.js 14之后的硬性门槛低版本Node会导致依赖安装失败或者运行报错。如果提示Node版本太老建议直接用nvm安装对应版本不要用系统自带的旧版Node手工踩坑。初始化项目很简单打开终端执行npx create-next-applatest my-next-app执行过程中会有一系列交互式选项包括TypeScript是否启用、ESLint是否启用、Tailwind CSS是否集成、App Router还是Pages Router等。编程多年后我现在的建议是新项目直接选TypeScript、ESLint、Tailwind CSS路由用App Router。TypeScript能帮你提前发现类型错误Tailwind能极大提升样式开发速度App Router代表未来发展方向。虽然第一次接触会增加一点学习成本但这些都是值得的长期投资。项目创建好后进入目录启动开发服务器cd my-next-app npm run dev打开浏览器访问http://localhost:3000你就能看到默认的欢迎页面。到这里你的第一个Next.js应用就跑起来了。整个过程不超过两分钟比起你手动搭建一套Webpack React Router SEO的方案效率提升是明显的。3.2 使用过程中最常见的坑开发模式跑起来后你会发现Next.js的报错信息比普通React应用要详细很多而且它会有overlay式的错误弹窗直接展示在页面上这在调试时非常方便。但有几个坑是不太容易一眼看懂的。第一个坑是修改next.config.js之后必须重启服务。next.config.js里的配置项在开发模式下不会热更新你改了配置后发现没生效十有八九是因为没有重启npm run dev。第二个坑是环境变量。Next.js里只有以NEXT_PUBLIC_开头的环境变量才会暴露给浏览器端其他变量只能在服务端代码(如getServerSideProps、API Routes)中访问。新手常犯的错误是写了一个非NEXT_PUBLIC的变量然后在客户端组件里使用结果发现永远拿不到值。第三个坑是ESLint检查和构建冲突。Next.js默认启用ESLint校验如果你的代码有未使用的变量或者hook依赖项缺失npm run build阶段会报错。虽然可以在配置文件里关闭但我建议你保留这些检查它们能帮你养成好习惯。第四个坑是图片优化。Next.js内置了next/image组件默认开启图片优化直接使用本地图片需要把图片放在public目录下并且指定width和height属性否则会报错。很多教程里用img标签虽然也能跑但会失去自动webp转换、响应式尺寸、懒加载等优化能力既然用了Next.js就尽量用它的图片方案。3.3 手写一个真实页面从需求到实现光看不练没什么用我们直接做一个小东西。假设需求是做一篇文章展示页面文章数据用本地文件模拟标题存放在lib/posts.js中。首先创建pages目录下的pages/index.js如果是App Router则创建app/page.tsx在文件顶部引入组件在getStaticProps中读取数据并返回export default function Home({ posts }) { return ( div h1我的文章列表/h1 ul {posts.map(post ( li key{post.id} a href{/posts/${post.id}}{post.title}/a /li ))} /ul /div ); } export async function getStaticProps() { const posts [ { id: 1, title: Next.js入门指南 }, { id: 2, title: 服务端渲染实战 }, ]; return { props: { posts } }; }这段代码里getStaticProps会在构建时执行把数据传递给页面组件最终生成静态HTML。你用浏览器打开页面查看源代码就能直接看到文章列表的HTML内容而不是空的div壳子——这就是SSG的直观效果。接下来再建一个动态详情页pages/posts/[id].jsimport { useRouter } from next/router; export default function Post() { const router useRouter(); const { id } router.query; return div当前文章ID{id}/div; }访问/posts/1时页面会显示当前文章ID:1。这个示例清晰地展示了动态路由的匹配和参数传递过程。4. 数据获取模块的深度技术拆解4.1 getStaticProps和getServerSideProps的原理差异数据获取函数是Next.js数据层的心脏。getStaticProps做的事情是在构建阶段执行获取数据并构建一个静态页面。这个函数只运行在服务端不会打包到客户端JS中所以你可以直接在函数里写数据库连接、读写文件系统、调用外部API等逻辑不用担心密钥泄露。getServerSideProps则是每次请求时都执行一次。它的触发机制是用户访问页面 - Next.js服务端运行getServerSideProps - 把返回值传给页面组件渲染 - 生成HTML返回给用户。由于每次请求都执行因此数据总是最新的但性能开销较大且不能使用静态导出(static export)策略。两者在写法上看起来很相似但执行时机完全不同。举个实际例子一个新闻详情页如果使用getStaticProps那么构建时生成的HTML里就包含新闻内容即使你后台修改了新闻用户访问到的仍然是构建时生成的旧内容除非重新构建或使用ISR。而如果用getServerSideProps每次访问都会向数据库查一次最新内容自然就总是新的。从性能角度看getStaticProps生成的页面因为走CDN缓存性能极好getServerSideProps则每次都要经历服务端计算承载压力远高于SSG。所以设计页面时先判断内容是否真的需要实时获取如果不需要优先用SSG或ISR。4.2 App Router下的server component与data fetchingApp Router和Pages Router的最大区别之一在App Router下组件默认是Server Component。这意味着你写的组件默认只在服务端渲染不会向浏览器发送任何JavaScript。组件里可以完全自由地使用async/await获取数据不需要额外的getStaticProps或getServerSideProps包装。举个例子创建一个app/posts/page.tsxasync function getPosts() { const res await fetch(https://api.example.com/posts, { cache: force-cache }); return res.json(); } export default async function PostsPage() { const posts await getPosts(); return ( ul {posts.map(post li key{post.id}{post.title}/li)} /ul ); }这个写法直观多了你把数据获取直接写在组件内部没有多余的函数。App Router下默认做静态渲染但你可以在fetch里配置cache选项cache: force-cache表示缓存数据等同于SSG。cache: no-store表示不缓存每次请求都重新拉取等同于SSR。next: { revalidate: 60 }表示60秒后后台重新验证本质上等同于ISR的配置。这个是Next.js数据获取一个划时代的改进。你不再需要在页面顶层使用数据获取函数而是可以在组件树的任意层级异步获取数据React会负责协调渲染顺序和悬浮态。4.3 常见页面类型的数据获取方案速查不同页面类型适合不同数据获取策略用久了之后我心里有一个速查逻辑整理成表格方便大家对照页面类型推荐策略原因个人博客SSG内容基本不变缓存CDN性能极好电商商品详情ISR(revalidate: 300)价格偶有变动静态缓存定时更新折中用户购物车/订单SSR数据依赖每个用户身份不能静态化仪表盘大屏图表客户端数据获取需要实时刷新且是登录后页面营销落地页SSG内容固定追求首屏速度5. 实战构建一个带API和数据库的完整应用5.1 前置准备创建Todo应用的数据基础和API为了让这些知识点串起来我们做一个Todo List应用功能包含加载任务列表、添加任务、标记完成。通过它把API Routes、数据请求、状态管理全部串联起来。先做一个简易内存数据库lib/todos.jslet todos [ { id: 1, title: 学习Next.js, completed: false }, { id: 2, title: 写一篇技术博客, completed: false }, ]; export function getTodos() { return todos; } export function addTodo(title) { const newTodo { id: todos.length 1, title, completed: false }; todos.push(newTodo); return newTodo; } export function toggleTodo(id) { const todo todos.find(t t.id id); if (todo) todo.completed !todo.completed; return todo; }5.2 编写API路由在pages/api/todos.js中创建两个API列表接口和添加接口。因为REST风格更清爽我们用动态路由来处理不同请求。最简单的方式是用一个文件处理多个方法import { getTodos, addTodo } from ../../lib/todos; export default function handler(req, res) { if (req.method GET) { res.status(200).json(getTodos()); } else if (req.method POST) { const { title } req.body; if (!title) { res.status(400).json({ error: 标题不能为空 }); return; } const newTodo addTodo(title); res.status(201).json(newTodo); } else { res.setHeader(Allow, [GET, POST]); res.status(405).end(Method ${req.method} Not Allowed); } }代码里我们添加了参数校验和错误状态码区分这是一个很好的习惯前端可以根据不同的状态码做出对应的提示。5.3 前端页面集成前端页面pages/index.js通过getServerSideProps获取初始列表。这里我用SSR原因是每次打开都需要最新的任务状态用SSR更合理import { useState } from react; export default function Home({ initialTodos }) { const [todos, setTodos] useState(initialTodos); const [input, setInput] useState(); const handleAdd async () { if (!input.trim()) return; const res await fetch(/api/todos, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title: input }), }); if (res.ok) { const newTodo await res.json(); setTodos(prev [...prev, newTodo]); setInput(); } }; return ( div h1Next.js Todo/h1 input value{input} onChange{e setInput(e.target.value)} placeholder输入任务 / button onClick{handleAdd}添加/button ul {todos.map(todo ( li key{todo.id} style{{ textDecoration: todo.completed ? line-through : none }} {todo.title} /li ))} /ul /div ); } export async function getServerSideProps() { const res await fetch(http://localhost:3000/api/todos); const initialTodos await res.json(); return { props: { initialTodos } }; }一个完整的前后端交互闭环就做完了。注意getServerSideProps里fetch的时候URL要写完整地址。这个很多人会忽略写成/api/todos会请求不到因为服务端环境没有相对路径的概念必须指定完整URL。如果嫌硬编码麻烦可以把API地址配置到环境变量里。5.4 部署上线从本地到生产环境开发完这个应用后部署上线是另一个重要环节。最简单的部署方式是使用Vercel平台。Vercel和Next.js是同一家公司维护的对Next.js的支持是最完善的连接你的Git仓库推送代码到main分支它就会自动构建部署。如果你需要部署到自有服务器可以用以下命令构建静态导出或者标准Node.js部署npm run build npm run start这种情况下你需要在服务器上安装Node.js环境然后用pm2等进程管理器来守护Next.js服务进程。也可以用Docker把Next.js应用打包成镜像在任意容器环境中运行。本质上Next.js服务就是一个Node.js服务部署方式和你部署普通Node.js后端没有太大区别。6. 常见问题与排查技巧实录6.1 页面404了可能不是路由写错遇到404时第一反应通常是检查路由路径是否匹配。但实际上在Next.js里404还有一个非常隐蔽的原因你在组件里使用了客户端路由跳转Link但Link的href拼写有误或者动态路由参数没补齐。另一个常见原因是构建时有些页面因为数据获取错误被跳过了你可以检查构建日志或者直接看.next/server/pages目录下有没有生成对应的HTML文件。如果使用API Routes404也可能来自API路径拼写错误。很多人会在文件名带空格或者把文件放在了pages目录外面那自然访问不到。6.2 hydration mismatch警告如何排查这是服务端渲染最经典的一个坑。hydration mismatch指的是服务端渲染的HTML和客户端React渲染的DOM不一致。表现就是浏览器控制台报错“Hydration failed because the initial UI does not match what was rendered on the server”。最常见的诱因有几种在组件中直接使用了new Date()或者Date.now()导致服务端生成一个时间、客户端生成另一个时间。使用了window.localStorage或navigator对象服务端没有这些对象默认渲染成A内容客户端有就渲染成B内容。第三方库在客户端执行时修改了组件内容如随机数、加密组件等。排查的办法先在组件里临时写死一个静态值看警告是否消失。如果能消失说明问题出在动态数据上你就可以锁定是哪一行代码造成了差异。然后通过useEffect useState的方式把动态部分延迟到客户端渲染或使用suppressHydrationWarning属性如果不是关键内容。6.3 performance: TTFB太慢怎么优化如果你的页面使用SSR且TTFB很慢大概率不是Next.js本身的问题而是你的服务端逻辑太慢。检查以下几个方向getServerSideProps里的数据库查询是否缺少索引外部API调用是否有超时控制可以用Promise.race设置最大等待时间。是否在服务端做了高CPU的计算如模板编译、图片处理页面组件本身是否包含大量同步阻塞操作另外如果你用ISR需要注意revalidate时间设置是否合理。如果设置太短比如1秒等于每个用户都可能触发重新渲染静态缓存会形同虚设如果设置太长内容更新不够及时需要自己权衡。还有一个小技巧Next.js提供了Streaming SSR。在App Router里你可以把页面拆成多个异步组件并且在loading.tsx或Suspense中做细粒度的流式渲染让页面更快的首屏呈现。这样用户不用等最慢的数据请求完成才能看到页面骨架。6.4 构建产物过大如何优化如果你的page.js打包后体积过大可以从以下几个方面入手使用next/dynamic做动态导入把非首屏组件分包加载。检查是否引入了全局的大量第三方库例如把lodash导入为import _ from lodash会打包整个库应该改成import debounce from lodash/debounce这样按需引入。多使用Server Components把交互性不强的组件留在服务端渲染减少客户端JS体积。图片资源尽量使用next/image自动做尺寸优化和格式转换不要直接放原图。6.5 我在实际项目中积累的几个经验踩过不少坑之后我总结出以下几个实战建议开发时保持Server Component优先。在App Router下能不在客户端渲染的组件就尽量不放‘use client’这样做的收益是明显的JS体积变小首屏变快代码更直观。注意route handler的缓存行为。在App Router的route.ts里默认GET请求是静态化的如果你希望每个请求都动态执行需要显式设置export const dynamic force-dynamic或者在函数内部读取headers()、cookies()来触发动态行为。处理大型列表时不要一次性渲染所有项。可以用虚拟列表库或者在数据层做分页。SSR渲染一个几万条数据的列表构建时间和运行开销会非常恐怖。使用好Middleware。例如根据域名、用户登录状态做重定向或鉴权Middleware能统一处理且运行速度极快适合做轻量的请求前置逻辑。把环境变量按环境隔离。开发、测试、生产三套环境对应不同的.env文件Next.js支持.env.development和.env.production自动加载不要在代码里硬编码任何连接地址。还有一点特别想提一下在部署时如果你使用了Serverless平台要把数据库连接池的创建放在模块顶层一次建立不要在每次请求时都新建连接否则连接数会迅速打满。如果发现API Latency不稳定可以看看是不是这个原因。我现在在做一个新项目时已经习惯了这种开发方式先把页面结构设计好再确定每个部分的数据获取策略然后逐层实现组件最后封装API。整个流程走下来你会发现Next.js不只是“一个框架”更像是一套Web开发的完整方法论。写到这里如果你正打算开始一个React全栈项目不妨直接用它作为起点踩过我开始踩的那些坑之后你会越来越喜欢这种简单高效的开发节奏。
返回列表