从零开始学前端 | 第四十七章:第五阶段综合实战:文章详情页、动态路由参数与正文渲染
本章定位上一章我们已经把第五阶段综合实战从“项目骨架搭建”推进到了“内容主线真正开始跑起来”的阶段。你已经完成了这些非常关键的事情博客项目已经有了首页、文章列表页、关于页、联系页和基础布局。本地文章数据已经开始接入项目。首页已经能展示推荐文章和最近文章。文章列表页已经能展示全部文章。slug、文章数据和getPostBySlug这条主线已经准备好了。也就是说到现在为止你的博客项目已经不是“空壳网站”了而是已经开始有内容入口、有阅读入口、有站点感。接下来我们要继续推进一条非常关键的主线让用户真的点进一篇文章并把这篇文章完整读下来。这一章非常重要。因为博客项目和很多工具型项目不一样它有一个天然非常核心的页面文章详情页如果没有详情页用户只能看到首页入口看到文章列表看到标题和摘要但还不能真正进入阅读所以这一篇我们要解决的核心问题是动态路由参数到底在解决什么问题详情页怎样通过slug找到对应文章如果这篇文章不存在页面应该怎么办文章标题、分类、日期、封面和正文应该怎样组织到详情页里为什么正文第一版先用string[]渲染就是一个很稳的方案你可以把这一章理解成第五阶段综合实战里第一次让博客项目真正具备“可点击进入阅读”的主线篇。本章学习目标学完这一章后你应该能做到理解为什么文章详情页是博客项目里最核心的阅读页面之一。理解动态路由参数在博客详情页里的作用。学会把路由地址、slug和文章数据之间的关系真正串起来。知道src/app/posts/[slug]/page.tsx在当前项目里扮演什么角色。学会在详情页中通过getPostBySlug查找对应文章。理解为什么文章不存在时更适合走 404 流程而不是默默显示空白页。学会使用notFound()处理不存在文章的场景。学会把标题、分类、日期、封面、正文段落组织成第一版详情页结构。理解为什么正文第一版先用string[]渲染是一个适合当前阶段的折中方案。知道generateStaticParams在本地内容型博客里可以先怎么理解。理解详情页和列表页的职责区别。为下一篇继续进入关于页、联系页与表单交互做好准备。一、这一篇到底要打通哪条阅读主线上一章我们已经把数据首页列表页这条主线接起来了。这一章要接的是博客项目里另一条同样关键的主线阅读主线它可以先被理解成这样一条路径用户在首页或列表页看到某篇文章。用户点击这篇文章的标题或“阅读全文”入口。浏览器进入这篇文章的详情地址。页面根据slug找到对应文章。页面把这篇文章的完整内容渲染出来。这条主线一旦成立博客项目就会从有内容入口的网站继续走向有完整阅读路径的网站二、为什么文章详情页在博客项目里这么关键很多内容型网站真正最核心的价值往往不在首页。首页更像入口文章列表页更像总览而文章详情页更像真正承载内容价值的地方1. 列表页负责“让你看见”它会告诉用户有哪些内容这些内容大概是什么哪篇文章值得点进去2. 详情页负责“让你读进去”它会真正承载标题摘要正文阅读节奏深入理解3. 为什么博客项目不能没有它因为如果没有详情页博客项目很容易停留在一组文章卡片展示页而不是一个真正的内容网站。三、动态路由参数到底在解决什么问题这一节非常重要。很多初学者第一次看到[slug]时会觉得它只是个“文件夹写法”。其实它解决的是一个很实在的问题多篇文章如何共用一套详情页结构但又显示不同内容1. 如果不用动态路由会怎样你可能就要为每篇文章都写一份页面文件。例如react-basic/page.tsxhtml-semantic/page.tsxtypescript-generics/page.tsx这显然既重复也很难维护。2. 动态路由在做什么它的核心作用是用一个“变量位置”去匹配不同文章地址。例如/posts/react-basic /posts/html-semantic-basics /posts/typescript-generics这些地址虽然最后一段不同但它们都可以共用同一套详情页模板。3. 当前阶段最值得先记住什么你可以先记住一句话动态路由参数解决的不是“怎么写目录更炫”而是“怎么让一类内容共用一套页面结构”。四、先把目录结构和地址关系再看一遍当前阶段博客详情页最关键的目录关系是这一段src/app/ └── posts/ ├── page.tsx └── [slug]/ └── page.tsx它可以先这样理解src/app/posts/page.tsx对应文章列表页/postssrc/app/posts/[slug]/page.tsx对应文章详情页/posts/某篇文章标识例如/posts/html-semantic-basics /posts/react-state-basics /posts/nextjs-routing-introduction1. 为什么这个结构很适合当前博客项目因为它正好对应了两类页面一组文章的总览页单篇文章的详情页2. 这和上一章的数据设计为什么刚好能接上因为我们上一章已经给每篇文章准备了slug所以现在路由层和数据层已经开始自然对上了。五、params.slug可以先怎么理解当用户访问/posts/html-semantic-basics时详情页可以拿到这样一份参数params.slughtml-semantic-basics你可以先把它理解成当前 URL 里属于文章标识的那一段内容。1. 为什么这个值非常关键因为它就是详情页去查文章的钥匙。详情页不是随便显示一篇文章而是要根据当前访问的是哪一个slug来决定显示哪一篇。2. 不同版本的 Next.js 看起来会不会略有差异会有可能。例如某些版本在类型提示上会让params的写法稍微不一样。但当前阶段最重要的核心理解并不会变详情页会拿到一个slug再用这个slug去找到对应文章。只要这条主线你抓住了后面版本差异就不会太难适应。六、详情页第一版至少应该展示哪些内容在开始写页面前先别急着立刻敲代码。先想清楚第一版详情页最值得先展示什么。当前阶段我建议至少先包含下面这些内容返回文章列表入口文章分类和发布日期文章标题文章摘要文章封面图文章正文段落1. 为什么这几个部分已经很够用了因为它们已经能撑起一条完整阅读主线用户知道自己在哪用户知道这篇文章是什么用户能看到正文2. 当前阶段先不必急着加什么例如评论目录锚点上一篇 / 下一篇阅读时长代码高亮这些后面都可以扩展。现在更重要的是阅读主线先成立。七、为什么getPostBySlug是这一篇的核心桥梁上一章我们已经在src/lib/posts.ts准备了这个函数exportfunctiongetPostBySlug(slug:string){returnblogPostList.find(function(postItem){returnpostItem.slugslug;});}这一章它会成为真正的关键桥梁。为什么因为它刚好把两层内容连起来了路由层拿到的slug数据层维护的文章数组1. 如果没有这个函数会怎样页面里就会开始自己写很多find判空字段处理这会让页面越来越像数据脚本。2. 现在把它单独抽出来的价值是什么它让详情页可以更专注地表达当前页面怎样展示当前文章而不是一边写页面一边到处散落数据查找逻辑。八、先从最小的详情页查找逻辑开始在真正写完整详情页之前我们可以先把最小逻辑跑通。当前页面文件是src/app/posts/[slug]/page.tsx它最小可以先写成这样import { getPostBySlug } from /lib/posts; interface PostDetailPageProps { params: { slug: string; }; } export default function PostDetailPage({ params }: PostDetailPageProps) { const post getPostBySlug(params.slug); return section{post?.title}/section; }1. 这一段代码在做什么它只做了两件事拿到当前访问地址里的slug用slug去文章数组里找对应内容2. 为什么要先跑最小逻辑因为这样你可以先确认路由参数到底有没有真的和文章数据接上。这一步跑通以后再补页面结构会稳很多。九、文章找不到时为什么更适合 404而不是普通空状态这一点非常值得现在就建立。列表页和详情页虽然都可能“没有内容”但它们表达的问题通常不同。1. 列表页为空更适合空状态因为它往往在表达当前还没有任何文章或者当前筛选结果为空。2. 详情页找不到文章更适合 404因为它往往在表达用户访问了一个本来应该存在、但当前并不存在的具体地址。3. 为什么这两者不要混为一谈因为它们背后的场景完全不同。当前阶段更稳的理解是列表页“没有一组内容”更像空状态详情页“没有这一个具体对象”更像 404十、先用notFound()处理不存在文章Next.js 提供了一个非常适合当前场景的能力notFound()你可以先把它理解成当当前详情页对应的文章不存在时直接交给 Next.js 走 404 页面流程。一个很典型的写法是import { notFound } from next/navigation; import { getPostBySlug } from /lib/posts; interface PostDetailPageProps { params: { slug: string; }; } export default function PostDetailPage({ params }: PostDetailPageProps) { const post getPostBySlug(params.slug); if (!post) { notFound(); } return section{post.title}/section; }1. 为什么这种处理方式很合适因为它更符合详情页的语义这不是“当前没有内容可以展示”而是“你访问的这篇文章不存在”。2. 后面还能不能做更友好的 404 页面当然可以。后面你完全可以继续补src/app/not-found.tsx让 404 页面更友好。但当前阶段先把找不到文章 - 正确走 404这条主线跑通就已经很有价值了。十一、第一版详情页完整结构可以怎么写当最小查找逻辑和 404 主线都成立以后就可以开始把详情页真正写完整了。一个适合当前阶段的第一版详情页可以先写成下面这样import Image from next/image; import Link from next/link; import { notFound } from next/navigation; import { getPostBySlug, getSortedPostList } from /lib/posts; interface PostDetailPageProps { params: { slug: string; }; } // 这里提前把所有本地文章的 slug 告诉 Next.js方便当前阶段的内容型路由更稳地工作 export function generateStaticParams() { return getSortedPostList().map(function (postItem) { return { slug: postItem.slug }; }); } export default function PostDetailPage({ params }: PostDetailPageProps) { const post getPostBySlug(params.slug); // 当前 slug 找不到文章时直接交给 Next.js 走 404 流程 if (!post) { notFound(); } return ( article classNamesite-shell post-detail-page Link href/posts classNameback-link 返回文章列表 /Link p classNamepost-detail-meta {post.category} · {post.publishedAt} /p h1{post.title}/h1 p classNamepost-detail-summary{post.summary}/p Image src{post.coverImage} alt{${post.title} 的封面图} width{1200} height{630} / div classNamepost-content {/* 这里把正文数组稳定渲染成段落列表先让阅读主线跑通 */} {post.content.map(function (paragraph, index) { return p key{${post.slug}-${index}}{paragraph}/p; })} /div /article ); }1. 为什么这份结构已经很够用了因为它已经把当前阶段最关键的阅读元素都接起来了返回入口元信息标题摘要封面图正文内容2. 这一整段代码最关键的主线是什么你可以把它压缩理解成路由参数拿到slug- 用slug找文章 - 找不到就 404 - 找到了就渲染整篇文章这就是本章最核心的主线。十二、为什么generateStaticParams值得现在就加上这一节对当前这种“本地内容型博客项目”来说很有价值。你可以先把generateStaticParams理解成先把所有已知文章的slug告诉 Next.js。1. 这在当前项目里为什么特别自然因为我们现在的文章数据是本地可知的。也就是说有哪些文章每篇文章的slug是什么在项目构建时就已经能知道。2. 它和详情页是什么关系它们的关系很简单generateStaticParams()先给出一组可用slug详情页再根据这些slug渲染不同页面3. 当前阶段如果你一时没完全吃透怎么办也完全正常。你先记住这句就够了对本地内容型页面来说generateStaticParams可以帮助 Next.js 提前知道会有哪些详情页地址。十三、为什么正文第一版先用string[]渲染就够用了这一点我们上一章其实已经埋过伏笔。但到了详情页这件事会真正落地。1. 为什么它对当前阶段特别合适因为它足够简单同时又足够有用。你可以很自然地做这件事一个字符串数组项渲染成一个正文段落。例如content:[HTML 标签不只是长得不一样它们本身就在表达结构。,当你理解了结构页面和内容的关系就会更清楚。]然后详情页里直接{post.content.map(function (paragraph) { return p{paragraph}/p; })}2. 这样做的核心价值是什么它能让你先把正文来源正文渲染正文结构这三件事跑顺。3. 现在为什么还不急着上 Markdown不是 Markdown 不重要而是当前阶段更重要的是先把阅读主线跑通。如果你一上来就把重点切到 Markdown 解析、插件配置、样式适配反而容易把详情页主线打散。十四、详情页里的元信息在解决什么问题元信息虽然看起来只是几行小字但它对阅读体验很重要。例如分类日期这类内容会让用户更快知道这篇文章大概属于哪个主题它是什么时候发布的1. 为什么这不是装饰因为内容型页面里读者经常会先判断主题是否相关时间是否新近这些信息能帮助用户快速建立阅读预期。2. 当前阶段元信息先做到什么程度就够了先做到分类日期这两个基础项就已经很有价值。后面可以再逐步扩展阅读时长标签作者十五、返回列表入口为什么现在就值得加上很多初学者写详情页时会先把返回入口忘掉。但从真实阅读路径看这个入口非常自然。1. 它解决什么问题它解决的是用户看完一篇文章后怎么方便地回到文章总览页。2. 为什么它很适合现在就加因为它既简单又很符合真实使用路径。你只要先在详情页顶部加一个Link href/posts返回文章列表/Link阅读路径就会顺很多。3. 当前阶段先不用把它做得太花不需要一开始就带图标带复杂悬停动画带面包屑导航先把阅读路径理顺最重要。十六、详情页和列表页的职责为什么不能混掉这一点在博客项目里非常关键。1. 列表页更像什么更像内容总览页它的重点是看有哪些内容快速浏览摘要决定下一步点哪篇2. 详情页更像什么更像内容沉浸页它的重点是读完整内容理解一篇文章的展开逻辑获得完整信息3. 为什么这两个角色一定要分开因为如果详情页还像列表页一样只讲摘要或者列表页像详情页一样塞满正文整个阅读结构就会混乱。当前阶段更稳的理解是列表页负责帮你选详情页负责帮你读十七、第一版详情页样式应该先追求什么这章的重点虽然不是样式但详情页一旦开始显示正文页面节奏就已经开始变重要了。当前阶段我建议第一版详情页样式先追求三件事文字容易读信息层级清楚内容宽度舒服1. 什么叫“文字容易读”例如段落不要太挤行高不要太小正文宽度不要过长2. 什么叫“层级清楚”例如标题最突出元信息更轻摘要和正文有明确区分3. 当前阶段不必急着做到什么程度不必一开始就追求杂志级排版很复杂的目录锚点丰富的富文本样式系统现在先让阅读舒服、主次分明就已经很有价值。十八、这一章先不急着做哪些细节虽然详情页现在已经开始真正成立但依然有很多内容可以先放到后面。例如Markdown / MDX 渲染代码高亮目录导航上一篇 / 下一篇相关文章推荐评论系统1. 为什么现在先不急因为这一章真正要解决的是详情页能不能根据slug稳定找到文章并把正文稳稳渲染出来。只要这条主线成立了后面的增强能力就都有了落点。2. 当前阶段最稳的节奏是什么先跑通动态路由参数单篇文章查找404 处理正文渲染再继续往上叠样式优化阅读体验增强内容管理升级十九、开发后怎么验证这一篇有没有搭好这一篇完成后非常适合做一轮小验证。你可以按下面顺序检查。1. 先检查一篇存在的文章例如访问/posts/html-semantic-basics确认页面能正常打开标题和摘要正确显示正文段落正确显示返回列表入口可点击2. 再检查另一篇存在的文章例如访问/posts/react-state-basics确认页面内容确实会随着slug变化。3. 再检查一个不存在的地址例如访问/posts/not-exist-post确认页面不是静默空白而是走到了 404 流程。4. 为什么这种验证特别重要因为它能直接帮你确认详情页最关键的“存在时可读、不存在时有反馈”两条主线是否都成立了。二十、这一章最容易踩的几个坑这一节建议你认真看。因为详情页阶段很容易出现一些“现在能跑、后面很难改”的问题。1. 坑一详情页不读slug而是直接写死某篇文章这样页面看起来能打开但其实动态路由根本没有真正成立。2. 坑二找不到文章时什么都不处理这样用户访问错误地址时页面可能是空白报错显示半截结构这都不够稳。3. 坑三正文和摘要混成一层这样详情页就会很像放大版列表卡片而不是一个真正的阅读页。4. 坑四正文数据结构还没稳就急着做复杂富文本系统这会让你过早把注意力从阅读主线转移到内容系统技术细节5. 坑五详情页没有返回路径这样用户看完一篇文章后路径会有点断。6. 坑六不同文章的slug命名不统一这样后面数据、图片、链接和路由很容易对不上。二十一、本章实践练习这一章的练习重点是把“动态路由详情页”这条主线真正跑顺。1. 练习 1完成详情页动态路由页面请你完成src/app/posts/[slug]/page.tsx并做到能读取params.slug能通过getPostBySlug找到对应文章能把文章标题渲染出来这个练习的重点是让路由参数和数据查找真正接上。2. 练习 2给不存在的文章加上 404 处理请你在详情页里补上notFound()并测试一个不存在的地址。这个练习会帮助你真正理解详情页找不到内容时更适合走 404 语义。3. 练习 3把正文段落真正渲染出来请你让详情页至少展示标题分类与日期摘要正文段落列表这个练习的重点是让详情页真正开始承担“阅读页”的角色。4. 练习 4加上返回列表入口请你为详情页补一个Link href/posts返回文章列表/Link这个练习会帮助你真正提升阅读路径的完整度。二十二、学习重点提示这一章请你重点记住下面这些话文章详情页是博客项目真正承载内容价值的关键页面之一。动态路由参数的核心价值是让一类内容共用一套详情页结构。slug是路由层和数据层之间最重要的连接点。列表页更像内容总览页详情页更像内容沉浸页两者职责不能混掉。详情页文章不存在时更适合走 404 语义而不是普通空状态。notFound()是当前阶段处理不存在详情内容的很稳方案。正文第一版先用string[]渲染是一个简单但有效的起点。当前阶段最重要的不是立刻做复杂内容系统而是先把详情页阅读主线跑通。如果你只记一句话请记住第五阶段综合实战真正开始具备“可阅读性”的那一刻往往不是你加了多少高级功能而是你先把slug、文章查找、404 处理和正文渲染这条详情页主线稳定地跑通了。二十三、本章小结这一章我们正式把第五阶段综合实战从“首页和列表页内容接入”推进到了“详情阅读主线真正成立”的阶段。你已经理解了为什么文章详情页在博客项目里这么关键动态路由参数到底在解决什么问题params.slug、文章数据和getPostBySlug之间怎样真正串起来为什么文章不存在时更适合用 404 处理notFound()在当前阶段可以怎样使用第一版详情页最值得先展示哪些内容为什么正文第一版先用string[]渲染就已经很有价值generateStaticParams在本地内容型项目里可以先怎样理解更重要的是你开始真正建立一种非常关键的阅读型项目意识内容网站真正成立不只是因为有内容入口还因为它能把用户顺利带进一篇文章、稳稳读完整篇文章。这一步非常关键。因为从这里开始你已经不只是在搭内容展示网站而是在开始真正进入个人博客项目第一版完整阅读路径的实现阶段。二十四、课后思考题请你认真思考下面这些问题为什么动态路由特别适合博客文章详情页为什么slug是详情页里最关键的连接点之一为什么详情页找不到文章时更适合走 404而不是普通空状态为什么正文第一版先用string[]渲染是一个适合当前阶段的折中方案为什么详情页和列表页的职责一定要分开为什么generateStaticParams对本地内容型项目尤其自然你觉得详情页里最容易在一开始被忽略的细节是什么为什么建议你把这些问题用自己的话写下来。只要你能把这些问题讲清楚说明你已经真正开始进入第五阶段综合实战的详情阅读主线了。二十五、下一篇预告下一篇我们会继续推进第五阶段综合实战进入第五阶段综合实战关于页、联系页与表单提交流程到那时你会开始真正把博客项目从“能读内容”继续推进到能更清楚地介绍作者和站点能让别人和你建立联系能跑通最基础的表单交互与提交反馈也就是说下一篇开始我们会从“文章详情页、动态路由参数与正文渲染”继续走到个人博客项目第一版站点完整度和联系交互主线的实现阶段。