从零开始学前端 | 第四十六章:第五阶段综合实战:博客首页、文章列表与本地文章数据接入

📅 2026/7/23 14:56:25
从零开始学前端 | 第四十六章:第五阶段综合实战:博客首页、文章列表与本地文章数据接入
本章定位上一章我们已经把第五阶段综合实战从“需求分析与技术选型”推进到了“项目骨架真正落地”的阶段。你已经完成了这些非常关键的前置工作项目已经初始化完成。app/路由目录已经搭出了第一版骨架。首页、文章列表页、文章详情页、关于页、联系页都已经有了基础位置。layout.tsx已经把页头、页脚和页面主体连接起来了。博客项目已经不再是抽象想法而是已经有了一个可以继续填充内容的网站外壳。也就是说到现在为止你已经不是“准备做一个博客”而是已经真正走到了可以开始把真实内容接进页面。接下来我们要进入一个非常关键的节点让博客项目第一版的内容展示主线真正跑起来。这一章非常重要。因为博客项目和很多纯工具类项目不一样它的核心价值之一就是内容如果没有内容数据博客网站就只会停留在有路由有布局有占位但还没有真正开始表达自己。所以这一篇我们要解决的核心问题是本地文章数据应该怎么组织为什么第一版更适合先用本地数据而不是一上来就接复杂内容系统首页应该先展示哪些内容文章列表页应该怎样把一组文章渲染出来类型、数据、工具函数、页面和组件之间应该怎样连接你可以把这一章理解成第五阶段综合实战里第一次让“内容真的出现在博客页面上”的起步篇。本章学习目标学完这一章后你应该能做到理解为什么博客项目第一版更适合先从本地文章数据开始。学会为博客项目设计第一版文章类型。知道本地文章数据文件应该放在哪里更清楚。理解为什么数据访问函数值得从页面里单独抽出来。学会为首页设计“推荐文章 最近文章”的第一版结构。理解文章列表页在博客项目中的职责。学会把本地文章数组渲染成可复用的文章卡片列表。知道slug、文章卡片、推荐内容、列表页之间是怎样连接起来的。理解为什么空状态现在就值得一起考虑。掌握一个适合当前阶段的本地数据接入流程。知道这一章先不急着做哪些细节。为下一篇继续进入文章详情页、动态路由参数与正文渲染做好准备。一、这一篇到底要把哪条主线跑起来上一章我们解决的是网站结构和公共布局怎么成立。这一章要解决的是网站里最核心的内容怎样真正进到页面里。换句话说这一篇要跑通的是这条主线先定义文章类型。再准备本地文章数据。再通过工具函数组织数据。再把数据接入首页。再把数据接入文章列表页。最后让页面开始像一个真正的内容网站。这一条主线非常关键。因为从现在开始博客项目会第一次从“站点骨架已经搭好”真正走向“站点已经开始有内容、有入口、有阅读路径”。二、为什么先从首页和文章列表页开始很多人一做博客项目时会很想先冲进详情页。因为详情页看起来更像“真正的文章页面”。但当前阶段更稳的顺序通常是先首页再列表页最后再接详情页。为什么1. 首页是整个站点的第一印象它负责回答这个网站是做什么的这里主要有什么内容用户下一步应该去哪里2. 列表页是内容聚合入口它负责把多篇文章组织起来让用户能快速浏览内容全貌。3. 详情页依赖前面两层主线因为只有当前面已经有文章数据文章列表slug入口详情页的路径和内容来源才会自然成立。4. 当前阶段最稳的开发顺序是什么你可以先记住首页负责“先看见”列表页负责“集中看”详情页负责“深入看”。这三层最好一层一层接。三、首页和文章列表页第一版分别要负责什么这一节很重要。因为只要职责一乱后面的页面就很容易越写越散。1. 首页第一版的职责当前阶段首页第一版建议先做这三件事清楚介绍这个博客是什么。给出主要内容入口。展示少量推荐文章或最近文章。也就是说首页不是要把所有文章都塞进去而是用更轻、更清楚的方式带用户进入站点。2. 文章列表页第一版的职责文章列表页第一版建议先做这三件事展示全部文章。让用户快速浏览标题、摘要、分类和日期。给每篇文章提供进入详情页的入口。3. 为什么要把这两个页面分工分开因为它们解决的问题不一样首页更像“欢迎页 内容入口页”列表页更像“内容总览页”如果首页也试图承担完整列表职责它很容易变重。四、为什么第一版文章数据先用本地文件最稳上一章我们已经明确过第一版更适合先用本地 TypeScript 数据文件这一章要把这个选择真正落到项目里。先看结论当前阶段先用本地文章数据文件是一个非常稳、非常实用的起点。为什么1. 它最直接你不需要先处理数据库后台管理接口协议Markdown 解析CMS 配置2. 它更容易把重点放在主线当前阶段博客项目最重要的是页面结构内容入口列表展示动态路由而不是先陷在内容管理系统本身。3. 它和 TypeScript 类型天然衔接这意味着你可以很自然地把文章类型文章数组文章卡片 props详情页查找逻辑统一到一条类型主线上。4. 这不是“偷懒”它只是先用更轻的方式把内容主线跑通。后面完全可以继续升级到MarkdownMDXHeadless CMS数据库五、先定义博客项目最核心的文章类型博客项目里最重要的数据当然是文章所以这一章第一步最值得先做的事就是把文章类型定义清楚。例如我们可以在src/types/blog.ts先写出这样的类型exportinterfaceBlogPostItem{slug:string;title:string;summary:string;category:string;publishedAt:string;coverImage:string;content:string[];isFeatured?:boolean;}1. 为什么这几个字段已经很够用了因为它们已经能支撑当前阶段最关键的页面首页文章列表页文章详情页2. 每个字段分别在解决什么问题slug决定详情页地址title文章标题summary列表摘要category分类标签publishedAt发布日期coverImage封面图content正文内容isFeatured首页是否作为推荐文章展示3. 为什么isFeatured很适合现在就加上因为首页和列表页职责不同。首页往往更适合展示全部文章里的少量重点内容有了这个字段后面筛首页推荐会更自然。六、本地文章数据文件应该怎么组织定义好类型以后下一步就是把真实文章数据先放进去。当前阶段更适合先在src/data/posts.ts维护一组文章数组。例如importtype{BlogPostItem}from/types/blog;exportconstblogPostList:BlogPostItem[][{slug:html-semantic-basics,title:语义化 HTML 入门为什么标签不只是长得不一样,summary:从标题、段落、列表到 section、article重新理解 HTML 标签的职责。,category:HTML,publishedAt:2026-07-20,coverImage:/images/posts/html-semantic-basics.jpg,content:[正文第一段,正文第二段],isFeatured:true}];1. 这一版建议至少准备几篇文章当前阶段建议至少先准备3 到 5 篇文章这样首页和列表页都会更有“内容站”的感觉。2. 为什么不要只放一篇因为只放一篇时你很难真正练到列表页的意义推荐内容的筛选数据排序多条内容的入口组织3. 文章内容现在一定要写很多吗不一定。当前阶段更重要的是结构和内容来源先成立。你完全可以先准备少量真实内容再逐步丰富。七、为什么正文先用string[]很合适很多人一看到文章正文就会马上想要不要直接接 Markdown这个方向当然可以。但对当前阶段来说正文第一版先用string[]其实是一个非常稳的选择。1. 为什么它适合当前阶段因为它简单、直接而且足够支撑详情页正文渲染段落列表展示内容结构建立2. 它怎么理解你可以先把它理解成每个数组项先代表正文里的一个段落。例如content:[HTML 标签不只是长得不同它们本身就在表达内容结构。,当你用更合适的标签描述内容时页面结构会更清楚。]3. 后面还能升级吗当然可以。后面完全可以继续升级成Markdown 字符串MDX 文件富文本结构但现在先让正文主线可用最重要。八、文章封面资源第一版适合放哪里既然文章类型里已经有coverImage那现在也要顺手把封面图路径规范一下。当前阶段更适合先把文章图片统一放在public/images/posts/例如public/images/posts/html-semantic-basics.jpg public/images/posts/react-state-basics.jpg public/images/posts/nextjs-routing-introduction.jpg1. 为什么这个位置很合适因为这些图片本质上是会被页面直接引用的静态资源。2. 为什么图片命名也值得先规范因为后面文章一多如果图片名很随意就很容易找不到对应资源命名和slug对不上当前阶段更稳的习惯是封面图命名尽量和文章主题或slug保持接近。九、为什么数据访问函数值得先抽出来很多初学者刚开始时会直接在页面里写filterfindsortslice这当然可以运行。但只要首页、列表页、详情页都开始用同一组数据很快就会出现一个问题数据处理逻辑开始在多个页面里重复。所以当前阶段非常值得先建立一个更稳的做法把数据访问函数单独抽到lib/里。十、先建立lib/posts.ts当前阶段可以在src/lib/posts.ts先放一些最基础、最常用的文章数据函数。例如第一批非常值得先有的函数是获取全部文章获取推荐文章获取最新文章通过slug获取单篇文章1. 获取全部文章列表import{blogPostList}from/data/posts;exportfunctiongetSortedPostList(){return[...blogPostList].sort(function(prevPost,nextPost){// 让日期新的文章排在前面return(newDate(nextPost.publishedAt).getTime()-newDate(prevPost.publishedAt).getTime());});}2. 获取推荐文章exportfunctiongetFeaturedPostList(){returngetSortedPostList().filter(function(postItem){returnpostItem.isFeatured;});}3. 获取最近文章exportfunctiongetLatestPostList(limit:number3){returngetSortedPostList().slice(0,limit);}4. 通过slug获取文章exportfunctiongetPostBySlug(slug:string){returnblogPostList.find(function(postItem){returnpostItem.slugslug;});}5. 为什么这些函数已经很有价值因为它们已经能支撑后面三条主线首页推荐文章首页最近文章列表页全部文章详情页查找单篇文章十一、为什么这些函数放在lib/里更清楚因为页面更适合表达的是当前页面拿什么数据然后怎么展示。而不是一边渲染页面一边写很多零散的数据处理逻辑。1. 这样做的好处是什么很直接页面更干净数据逻辑更集中后面更容易复用2. 后面如果数据源变化怎么办这也是把函数抽出去的价值。例如未来如果你把本地 TypeScript 数据换成Markdown 解析结果CMS 返回结果页面层通常不必全部重写只需要优先调整数据访问层十二、首页第一版最值得先展示什么有了文章数据以后首页很容易出现一个诱惑要不要把所有内容都塞到首页当前阶段更稳的做法是首页展示重点内容而不是展示全部内容。第一版首页我建议至少先放下面三块1. 顶部欢迎区负责回答这个网站是什么作者在记录什么这里主要能看到哪些内容2. 推荐文章区负责展示少量最值得先看到的内容。3. 最近文章区负责让用户感受到这个网站是持续更新、有内容流动的。4. 为什么这三块就已经很够用了因为它们已经能把首页最核心的价值撑起来建立第一印象给内容入口显示内容存在感十三、为什么推荐文章和最近文章要分开想这一点很值得现在就建立。很多初学者会默认推荐文章 最近文章它们有时可能重合但含义其实不一样。1. 推荐文章更像什么更像是你希望别人先看到的内容它强调的是代表性重要性首页优先级2. 最近文章更像什么更像是最近新发布或最近更新的内容它强调的是时间顺序内容更新感3. 为什么现在就把这两者分开很有价值因为这会让首页结构更清楚也让数据组织更自然。十四、先抽一个SectionTitle组件首页和后面的很多页面里都会反复出现一种结构一个区块标题一句区块说明这类内容非常适合先抽成一个很轻的公共组件。例如可以在src/components/section-title.tsx先写成这样interface SectionTitleProps { title: string; description: string; } export function SectionTitle(props: SectionTitleProps) { return ( div h2{props.title}/h2 p{props.description}/p /div ); }1. 为什么这种小组件值得先抽因为它职责清楚、重复概率高而且能让页面结构更容易读。2. 当前阶段不需要把它做得多复杂只要先把标题描述这两个入口统一起来就已经很有价值。十五、再抽一个PostCard组件博客项目里最值得先抽的组件之一几乎一定是文章卡片因为首页和文章列表页都会用到它。一个适合当前阶段的src/components/post-card.tsx可以先这样写import Link from next/link; import type { BlogPostItem } from /types/blog; interface PostCardProps { post: BlogPostItem; } export function PostCard({ post }: PostCardProps) { return ( article p{post.category} · {post.publishedAt}/p h3{post.title}/h3 p{post.summary}/p {/* 这里用 slug 拼出详情页地址 */} Link href{/posts/${post.slug}}阅读全文/Link /article ); }1. 为什么这里先不急着把卡片做得很复杂因为当前阶段最重要的是数据能不能稳定地变成一张卡片结构。2. 后面它还能继续加什么后面完全可以再补封面图分类标签样式悬停效果摘要截断但现在先让结构成立更重要。十六、博客首页怎么把数据真正接起来有了文章类型本地数据数据函数区块标题组件文章卡片组件现在就可以把首页真正接起来了。当前阶段首页可以先用getFeaturedPostList()getLatestPostList(3)这两个函数。例如首页文件src/app/page.tsx可以先这样组织数据import { PostCard } from /components/post-card; import { SectionTitle } from /components/section-title; import { getFeaturedPostList, getLatestPostList } from /lib/posts; export default function HomePage() { const featuredPostList getFeaturedPostList(); const latestPostList getLatestPostList(3); return ( section classNamesite-shell h1你好这里是我的个人博客/h1 p记录前端学习、项目实践与成长思考。/p /section ); }然后再继续往下补推荐文章区SectionTitle title推荐文章 description这里会优先展示最值得先阅读的几篇内容。 / div classNamepost-grid {featuredPostList.map(function (postItem) { return PostCard key{postItem.slug} post{postItem} /; })} /div最近文章区也是类似的SectionTitle title最近更新 description这里会展示最近发布或更新的内容。 / div classNamepost-grid {latestPostList.map(function (postItem) { return PostCard key{postItem.slug} post{postItem} /; })} /div1. 这一组代码最关键的意义是什么它真正把数据层 - 页面层 - 组件层连接起来了。2. 为什么首页现在已经开始“像博客”了因为它不再只是一个空标题页而是已经开始具备站点介绍内容入口推荐内容更新感十七、为什么首页不要一上来就把所有文章都展示完这一点很值得现在就建立。首页的职责不是把整个网站所有内容一次倒出来。而是带用户进入网站。1. 如果首页塞太满会怎样很容易出现首屏重点不明确页面很长列表页存在感下降2. 当前阶段首页更像什么更像是站点门口 内容导览页这也是为什么首页更适合少量推荐文章少量最近文章清楚的入口按钮或链接十八、文章列表页第一版最重要的职责是什么和首页相比列表页的职责就更集中。它第一版最重要的是把所有文章清楚地组织起来。也就是说列表页现在先不用承担搜索标签筛选分页排序切换这些更复杂的能力。第一版只要先做到把全部文章渲染出来显示每篇文章最基础的信息给出进入详情页的入口就已经非常有价值了。十九、文章列表页怎么接入全部文章数据列表页只需要先接getSortedPostList()即可。例如src/app/posts/page.tsx可以先写成这样import { PostCard } from /components/post-card; import { SectionTitle } from /components/section-title; import { getSortedPostList } from /lib/posts; export default function PostsPage() { const postList getSortedPostList(); return ( section classNamesite-shell SectionTitle title全部文章 description这里收集了当前博客已经发布的全部内容。 / /section ); }然后继续把列表渲染补上div classNamepost-grid {postList.map(function (postItem) { return PostCard key{postItem.slug} post{postItem} /; })} /div1. 为什么列表页优先用getSortedPostList因为大多数博客列表页最自然的展示顺序就是新的内容排在前面。2. 现在为什么还不急着加筛选因为当前阶段更重要的是先把“全部文章总览”这条主线跑通。二十、空状态为什么现在就要一起考虑虽然这一章的重点是“让数据接进页面”但这并不意味着页面只需要考虑有数据时长什么样。列表页非常值得现在就一起考虑空状态。因为你迟早会遇到这些情况文章数组暂时为空正在搭建时还没准备好内容某个筛选条件下没有结果1. 先做一个轻量EmptyState例如interface EmptyStateProps { title: string; description: string; } export function EmptyState(props: EmptyStateProps) { return ( section classNamesite-shell h2{props.title}/h2 p{props.description}/p /section ); }2. 在列表页里怎么用if (postList.length 0) { return ( EmptyState title暂时还没有文章 description后面我们会继续补充真实内容。 / ); }3. 为什么这一步现在就很值得做因为它会让你从一开始就建立页面不是只有成功状态。二十一、首页和列表页的数据流现在是怎样的做到这里你的博客项目已经有了一条非常清楚的数据流。你可以先把它理解成类型定义 - 本地数据 - 数据函数 - 页面 - 组件更具体一点可以理解成这样src/types/blog.ts先定义BlogPostItemsrc/data/posts.ts维护文章数组src/lib/posts.ts提供数据访问函数src/app/page.tsx和src/app/posts/page.tsx使用这些函数PostCard、SectionTitle、EmptyState把数据渲染成界面1. 为什么这条链路很重要因为从这里开始你已经不是在“页面里手写一些内容”而是在用一种可维护的结构把数据稳定地送到界面上。2. 后面扩展时它为什么很有价值因为无论你后面接入详情页分类页Markdown搜索这条链路都还能继续复用。二十二、这一章先不急着做哪些细节虽然首页和列表页已经开始跑通内容主线了但现在依然有很多细节先不用急着做。例如文章卡片完整样式卡片封面图显示细节列表页分类筛选搜索功能分页功能文章详情正文完整排版1. 为什么现在先不急因为这一章真正要解决的是内容数据能不能稳定地接进首页和列表页。只要这一步成立了后面继续做详情、筛选、样式和优化都会更顺。2. 当前阶段最稳的节奏是什么先跑通类型数据首页列表页再继续进入详情页正文渲染页面状态细节二十三、开发后怎么验证这一篇有没有搭好这一篇完成后其实很适合做一轮非常朴素、但非常有效的验证。你可以逐个检查这些点1. 检查首页确认首页能正常打开推荐文章区有内容最近文章区有内容每张卡片都有进入详情页的链接2. 检查文章列表页确认/posts能正常打开列表里能看到全部文章顺序是否按日期从新到旧3. 检查空状态可以临时把blogPostList改为空数组确认列表页是否出现空状态页面是否还保持基本结构4. 为什么这种小验证很重要因为它能帮你立刻确认本地数据接入这条主线是不是真的已经成立。二十四、这一章最容易踩的几个坑这一节建议你认真看。因为内容接入阶段很容易出现“能跑但后面很难扩”的写法。1. 坑一把文章数据直接写死在页面里这样短期看方便但后面首页要用列表页要用详情页也要用就会越来越乱。2. 坑二页面里直接写很多filter、find、sort这样会让页面越来越像“数据脚本”而不是“页面结构”。3. 坑三只准备一篇文章这样很难真正建立列表页和推荐内容的感觉。4. 坑四数据字段还没想清楚就急着做样式这样后面字段一变页面结构和样式都要跟着返工。5. 坑五以为首页就是“把所有文章展示一遍”这样会让首页和列表页职责变得非常模糊。6. 坑六现在就急着做复杂内容系统这样很容易把注意力从内容展示主线转移到内容管理技术细节二十五、本章实践练习这一章的练习重点是把“本地文章数据 - 首页和列表页”这条主线真正跑顺。1. 练习 1补出你的文章类型和本地数据请你根据这一章内容完成BlogPostItem类型定义blogPostList本地文章数组至少 3 篇真实文章数据这个练习的重点是让博客项目从“有骨架”真正走向“有内容”。2. 练习 2整理文章数据访问函数请你至少写出下面这些函数获取全部文章获取推荐文章获取最近文章通过slug查找单篇文章这个练习会帮助你真正建立页面不应该承担所有数据处理细节。3. 练习 3让首页显示推荐文章和最近文章请你让首页至少展示一段站点说明一组推荐文章一组最近文章这个练习的重点是首页要开始承担“站点入口和内容导览”的职责。4. 练习 4给文章列表页补齐空状态请你在列表页中补上正常列表显示空数组时的空状态提示这个练习会帮助你真正提升页面完成度而不只是内容渲染本身。二十六、学习重点提示这一章请你重点记住下面这些话博客项目第一版更适合先用本地 TypeScript 数据文件把内容展示主线跑通。首页负责“先看见”和“先进入”列表页负责“集中浏览全部内容”两者职责不同。数据模型越早想清楚页面和组件后面就越稳。数据访问函数尽早从页面里抽出来会让页面结构更清楚。推荐文章和最近文章不是同一个概念最好从一开始就分开理解。页面不能只考虑有数据时长什么样空状态也值得尽早补上。当前阶段最重要的不是内容系统多高级而是内容主线先成立。先数据再页面先首页和列表页再详情页和更复杂能力。如果你只记一句话请记住第五阶段综合实战真正开始有“内容感”的那一刻往往不是你写了多复杂的后台而是你先把本地文章数据、首页入口和列表页主线稳定地接起来了。二十七、本章小结这一章我们正式把第五阶段综合实战从“项目骨架搭建”推进到了“内容主线真正开始跑起来”的阶段。你已经理解了为什么博客项目第一版更适合先从本地文章数据开始文章类型和本地数据文件应该怎样组织为什么正文第一版先用string[]是一个很稳的选择为什么数据访问函数值得先抽到lib/首页第一版应该怎样承担内容入口职责文章列表页第一版应该怎样承担内容总览职责推荐文章、最近文章和全部文章之间分别在解决什么问题为什么空状态值得现在就一起考虑更重要的是你开始真正建立一种非常关键的内容型项目意识博客项目不是先有复杂系统才有内容恰恰相反它常常是先把内容主线跑通项目才真正开始成立。这一步非常关键。因为从这里开始你已经不只是在搭网站外壳而是在开始真正进入个人博客项目第一版内容展示主线的实现阶段。二十八、课后思考题请你认真思考下面这些问题为什么博客项目第一版更适合先用本地 TypeScript 数据而不是一开始就接复杂内容系统为什么首页和文章列表页应该分工而不是都去承担全部内容展示为什么文章数据访问函数尽早从页面中抽出来会更稳为什么推荐文章和最近文章最好从一开始就分开理解为什么正文第一版先用string[]是一个适合当前阶段的折中方案为什么空状态现在就值得补上而不是等“项目差不多做完”再说你觉得当前这条“类型 - 数据 - 数据函数 - 页面 - 组件”的链路里最容易被忽略的是哪一层为什么建议你把这些问题用自己的话写下来。只要你能把这些问题讲清楚说明你已经真正开始进入第五阶段综合实战的内容主线了。二十九、下一篇预告下一篇我们会继续推进第五阶段综合实战进入第五阶段综合实战文章详情页、动态路由参数与正文渲染到那时你会开始真正把这一篇准备好的slug文章数据getPostBySlug动态路由目录继续往前推进真正完成一条完整的阅读路径。也就是说下一篇开始我们会从“博客首页、文章列表与本地文章数据接入”继续走到个人博客项目第一版详情阅读主线的真正实现。