type
Post
status
Published
date
Dec 22, 2025
slug
nextjs-005
summary
05 - Data Fetching - Deep Dive
tags
Next.js
category
编程学习
icon
password
😀
 

001 Module Introduction

  • 课程模块核心目标:在前一节构建的应用基础上进行扩展,为应用添加数据获取功能,不再在代码中硬编码假新闻或新闻数据,而是从其他数据源加载数据。
  • 课程学习内容:学习者将学习如何获取数据,以及如何在 Next.js 应用程序中获取数据,包括从外部 API 获取数据,还能学习如何直接从数据库或文件等其他数据源加载数据。

002 Adding a Backend - 项目添加后端

一、项目扩展与后端基础配置

  1. 项目扩展背景:基于前序构建的Next.js项目进行功能扩展,核心差异是新增独立的“后端文件夹”,用于模拟真实开发中“Next.js应用+独立REST API”的架构。
  1. 后端文件夹获取与集成:后端文件夹以ZIP压缩包形式提供,需手动添加到现有Next.js项目目录中,其内部包含假API代码(用于快速搭建演示环境)。
  1. 依赖安装关键步骤
      • 步骤1:下载后端文件夹ZIP包并解压到项目目录;
      • 步骤2:通过终端导航至后端文件夹路径;
      • 步骤3:执行npm install命令,安装后端所需的所有API依赖(因后端是独立项目,需单独配置依赖)。

二、数据库模拟与数据存储

  1. 数据库类型与作用:使用SQLite数据库(视频中展示对应数据库文件),核心目的是模拟“数据持久化存储”场景,替代代码中直接写死的假数据。
  1. 数据预填充逻辑
      • 项目代码中暂存的“假新闻数据”,仅用于初始化并预填充SQLite数据库;
      • 最终目标是实现“从数据库读取数据”,而非从代码硬编码中获取,贴近真实开发流程。
  1. 课程学习规划:本节聚焦“数据库数据获取”,后续课程将深入讲解“数据库数据修改”操作。

三、后端API核心功能

  1. 核心路由设计:后端API仅包含1个核心获取路由——/news(部署在独立后端服务器上)。
  1. 路由功能:发送请求至该路由时,后端会查询数据库中的所有新闻数据,并将数据返回给请求方(如Next.js应用或其他客户端)。

四、后端API的应用场景

  1. Next.js应用内使用:作为Next.js应用的数据源,通过调用/news路由获取新闻数据,支撑前端页面渲染;
  1. 多客户端兼容:API不仅限于Next.js应用,其他客户端(如移动端App、其他Web项目)也可通过该API获取数据;
  1. 后续学习方向:后续讲座将详细演示“如何在Next.js应用中调用该后端API”,完成数据交互流程。

003 Option 1 Client-side Data Fetching - 客户端数据获取

一、前置环境配置

1. 后端服务器启动

  • 核心原因:后端API是独立项目,仅集成在Next.js文件夹中,需单独启动
  • 操作步骤
      1. 终端中通过cd命令导航至后端文件夹
      1. 执行npm start启动服务器,保持运行状态
      1. 后端默认运行地址:localhost:8880

2. Next.js服务器启动

  • 操作步骤
      1. 打开新的独立终端窗口
      1. 导航至项目根文件夹
      1. 执行npm run dev启动Next.js服务
  • 关键要求:必须保证前后端两个服务器同时运行,否则数据获取失败

二、核心实现方案

1. 技术选型

方案
优势
适用场景
浏览器内置fetch+useEffect
无需额外依赖,原生支持
简单数据获取场景,保持代码轻量化
第三方库(如Tan Stack Query)
简化数据缓存、状态管理
复杂项目,需优化数据处理效率
  • 视频选择:为降低复杂度,选用fetch+useEffect方案

2. 代码规范与实现

  • 禁止直接用async装饰useEffect:会导致副作用执行异常
  • 正确写法:在useEffect内部创建独立async函数

    三、状态管理设计

    1. 三大核心状态

    状态名称
    初始值
    作用
    状态更新时机
    错误状态(error)
    null
    存储请求失败信息
    响应状态码异常时(如404、500)
    加载状态(isLoading)
    false
    标记数据获取中状态
    请求前设为true,请求成功/失败后设为false
    新闻数据状态(news)
    undefined
    存储解析后的新闻数据
    响应解析成功后,赋值为解析结果

    2. 状态更新逻辑

    • 错误处理:检查response.ok,若为false则设置错误消息(如“无法获取新闻”)
    • 数据解析:需通过await response.json()解析响应数据(返回Promise,必须用await)
    • 加载状态控制:请求开始时设为true,无论成功/失败,最终都需设为false

    四、UI状态联动

    1. 条件渲染逻辑

    • 关键校验:必须先检查news是否存在且有数据,避免初始undefined导致的渲染错误

    五、关键问题与解决方案

    1. 客户端组件标识问题

    • 报错原因useEffect是客户端钩子,无法在服务器组件中运行
    • 解决方案:在组件顶部添加'use client'指令,将组件标记为客户端组件
    • 注意事项:添加指令后组件不再是纯服务器组件,部分代码需在客户端执行

    2. 404错误修复

    • 错误原因:仅请求localhost:8880,未指定具体接口路径
    • 解决方案:查看后端代码,确认接口路径(如/news),调整请求地址为localhost:8880/news

    六、方案定位

    1. 通用性:该方案适用于所有React应用,Next.js中无特殊差异
    1. 局限性:并非Next.js最优数据获取方式(后续会学习服务器端获取等更优方案)
    1. 核心价值:作为基础客户端数据获取方案,是复杂方案的学习基础

    004 Option 2 Server-side Data Fetching - 服务端数据获取

    一、客户端数据获取的核心缺陷

    1. 渲染结果问题:通过useEffect在客户端获取数据时,服务器返回的页面源代码不包含实际业务数据(如新闻内容),仅能在客户端渲染后看到数据
    1. SEO 劣势:搜索引擎爬虫无法抓取客户端动态加载的数据(例:搜索新闻标题中的“海狸”无匹配结果)
    1. 技术局限性:属于通用 React 数据获取方式,未利用 Next.js 框架特性,非最优解

    二、Next.js 服务端数据获取的核心基础

    1. React 服务器组件(RSC)特性
        • Next.js 页面组件默认是 React 服务器组件,运行在服务端
        • 支持在组件内部直接发起数据请求,无需依赖客户端钩子
        • 可通过async/await语法处理异步请求(组件函数添加async修饰符),能返回 Promise(传统客户端组件仅能返回 JSX)
    1. 框架级支持:Next.js 对服务器组件提供原生支持,无需额外配置即可实现服务端数据获取

    三、代码改造关键步骤(从客户端到服务端)

    改造环节
    操作内容
    目的
    组件类型转换
    删除'use client'指令
    将组件从客户端组件转为服务器组件
    数据请求迁移
    删除useEffect,将fetchNews请求逻辑直接写入组件函数
    实现服务端发起数据请求
    状态清理
    删除useState相关代码(如加载态、数据存储状态)
    服务器组件无需客户端状态管理
    导入优化
    删除useEffectuseState导入
    精简代码,移除无用依赖
    数据渲染简化
    删除数据存在性检查(如if (!news) return null),直接渲染列表
    异步服务器组件会等待数据返回后再生成 JSX,避免数据未定义问题

    四、服务端数据获取关键细节

    1. fetch 函数特性
        • 服务器组件中可直接使用fetch,由 Node.js 环境支持
        • Next.js 扩展了原生fetch,增加缓存控制功能(后续课程详解)
    1. 错误处理:请求失败时通过throw new Error('无法获取新闻')抛出错误,后续可通过错误边界统一处理
    1. 数据可靠性:若后端稳定返回数据(如示例中假后端始终返回新闻数组),无需额外处理数据空值问题

    五、核心优势与效果

    1. 渲染效果:页面源代码包含完整业务数据,实现服务端预渲染(SSR)
    1. SEO 优化:搜索引擎可抓取完整页面内容,提升页面搜索排名
    1. 代码简化:减少客户端状态管理和数据检查逻辑,代码更简洁易维护
    1. 框架最佳实践:符合 Next.js 设计理念,是框架推荐的标准数据获取方式

    005 Why Use A Separate Backend Fetching Directly From The Source! - 直接数据源访问(React 服务器组件实践)

    一、核心架构疑问与优化方向

    1. 数据拉取方式对比:React 服务器组件内直接在服务器拉取数据,性能优于客户端拉取(减少客户端与服务器交互开销)
    1. 架构合理性质疑:若演示场景中无特殊需求(如多服务共享数据、复杂权限控制等),无需拆分“Next.js 应用服务器 + 单独后端服务器”,可直接从 Next.js 访问数据库,避免额外 HTTP 往返
    1. 优化目标:移除后端 fetch 调用,将数据库直接集成到 Next.js 项目,实现“数据源-应用”直连

    二、技术选型与依赖配置

    1. 数据库工具:选用 better-sqlite3 包(视频中口误为“batter dash c lite three”),支持与 SQLite 数据库快速交互,提供同步 API(无需处理 Promise,代码执行更简洁)
    1. 安装命令:在 Next.js 项目根目录执行依赖安装(如 npm install better-sqlite3
    1. 数据库文件路径:将原后端的 data.db 文件迁移到 Next.js 项目根目录,访问时需指定相对根目录的路径(确保服务器能正确定位文件)

    三、代码实现关键步骤

    1. 数据库初始化(lib/news.js)

    2. 数据获取逻辑改造

    • 替换假数据返回,通过 SQL 语句从数据库查询真实数据
    • 核心代码:

    3. 页面数据调用优化(page.js)

    • 删除原后端 fetch 代码,直接调用 getAllNews 函数
    • 核心代码:

    四、关键技术原理

    1. React 服务器组件特性
        • 代码仅在服务器执行,客户端无法获取数据库连接信息(保障安全)
        • 避免客户端直接访问数据库的风险(如敏感信息泄露、权限失控)
    1. 同步 API 优势better-sqlite3 的同步特性简化代码,无需处理异步加载状态(适合数据依赖明确、查询耗时短的场景)
    1. 数据结构兼容性:迁移前确保数据库表结构与原假数据一致(如字段名、数据类型),避免页面渲染报错

    五、效果验证与优势

    1. 功能验证:保存代码后刷新页面,新闻数据正常显示,且通过服务器渲染(查看页面源码可看到新闻内容,证明服务端数据注入成功)
    1. 性能提升:跳过“Next.js 服务器 → 后端服务器 → 数据库”的多环节交互,减少 HTTP 请求和数据传输延迟
    1. 架构简化:减少服务部署数量,降低运维成本(无需维护单独后端服务器)

    006 Showing a Loading Fallback - 数据获取与加载状态处理

    一、Next.js 数据获取核心方式

    1. 数据源适配方案
        • 外部API:需通过发送HTTP请求获取数据
        • 数据库(如SQLite):可直接集成到Next.js项目中访问,无需额外服务端中间层
    1. 同步包的异步改造(以Sequelize Lite为例)
        • 原生特性:Sequelize Lite相关包为同步执行,无需异步操作
        • 模拟慢请求:为演示加载状态,手动将同步函数改为异步
          • 添加async关键字声明异步函数
          • 嵌套Promise构造器,通过setTimeout设置2秒延迟(resolve触发时机)
          • 使用await等待Promise完成,模拟后端/数据库响应延迟

    二、异步数据获取的页面代码适配

    1. 数据返回形式变化
        • 同步场景:getAllNews()直接返回新闻数组
        • 异步场景:getAllNews()返回Promise(最终 resolved 为新闻数组)
    1. 服务器组件适配方案
        • 关键特性:Next.js服务器组件支持async装饰器
        • 实现方式:在页面组件函数前添加async,并通过await getAllNews()获取数据
        • 效果:保存后重新加载仍能正常渲染新闻页面,但存在缓存相关体验问题

    三、用户体验优化:加载状态处理

    1. 原始方案的问题
        • 缓存失效场景(开发环境常见):切换页面后重新加载,再点击进入新闻页时,初期无任何视觉反馈,仅数据加载完成后才跳转
        • 影响:用户易困惑,误以为操作未生效
    1. 加载Fallback实现(核心方案)
        • 原理:Next.js自动识别页面同级目录下的loading.js文件,在数据加载期间渲染该文件组件
        • 实现步骤
            1. 在目标页面(如新闻页)所在目录创建loading.js
            1. loading.js中导出组件,返回加载占位内容(支持任意JSX)
                • 基础:加载文本(如“Loading...”)
                • 进阶:加载动画(Spinner)、骨架屏(Skeleton Loader)
        • 效果:点击导航时即时跳转并显示加载状态,数据就绪后自动替换为真实内容

    四、关键注意点

    1. 缓存机制影响:Next.js在开发环境会缓存页面,需通过切换页面并重新加载触发缓存失效,以测试加载Fallback效果
    1. 组件类型限制async/await仅支持服务器组件(Server Components),客户端组件需通过其他方式处理异步数据
    1. 灵活性loading.js为页面级加载状态,若需更细粒度控制(如组件级),可结合Next.js的Suspense组件使用
    🗒️
    Promise、async 和 await 的概念和用法:

    1. Promise 是什么?

    Promise 是 JavaScript 中处理异步操作的一种机制。你可以把它想象成一个"承诺",表示一个将来才会完成的操作。

    通俗理解:

    • 想象你去餐厅点餐,服务员给你一个取餐器(这就是 Promise)
    • 取餐器会显示三种状态:
      • 等待中(pending):餐还没做好
      • 已完成(fulfilled):餐做好了,可以取餐
      • 已失败(rejected):厨房出问题了,无法做餐

    在代码中的体现:

    2. 你代码中的 Promise 分析

    这行代码的作用是:
    • 创建一个新的 Promise
    • 在 Promise 内部使用 setTimeout 延迟 2 秒(2000毫秒)
    • 2 秒后调用 resolve() 表示操作完成
    • await 等待这个 Promise 完成
    实际效果:让程序暂停 2 秒,模拟网络延迟或加载时间。

    3. async 和 await 是什么?

    async(异步函数):

    • async 是一个关键字,加在函数声明前
    • 表示这个函数是异步的,会返回一个 Promise
    • async 函数内部可以使用 await

    await(等待):

    • await 只能在 async 函数内部使用
    • 它会暂停函数执行,等待 Promise 完成
    • 如果 Promise 成功,返回结果值
    • 如果 Promise 失败,抛出错误

    4. Promise 与 async/await 的关系

    传统 Promise 写法:

    async/await 写法:

    async/await 本质上是 Promise 的语法糖,让异步代码看起来像同步代码,更容易理解和维护。

    5. 实际应用场景

    在你代码中的 getAllNews 函数:
    这个函数:
    1. 从数据库获取新闻(同步操作)
    1. 等待2秒(模拟网络延迟)
    1. 返回新闻数据

    6. 为什么要使用这些?

    1. 非阻塞:JavaScript 是单线程的,异步操作不会阻塞其他代码执行
    1. 用户体验:避免页面卡死,保持界面响应
    1. 处理耗时操作:如网络请求、文件读写、数据库查询等
    🗒️
    解释 Promise 中的 resolvereject 参数。

    1. resolve 和 reject 是什么?

    在创建 Promise 时,我们传入一个函数,这个函数接收两个参数:resolvereject
    • resolve:一个函数,调用它表示 Promise 成功完成
    • reject:一个函数,调用它表示 Promise 失败

    2. resolve - 成功回调

    当异步操作成功时,调用 resolve 并传入结果值:

    3. reject - 失败回调

    当异步操作失败时,调用 reject 并传入错误信息:

    4. 完整示例

    5. 在你的代码中的应用

    这里只使用了 resolve,因为:
    1. setTimeout 只是延迟执行,通常不会失败
    1. 2秒后调用 resolve() 表示"延迟完成"
    1. 没有使用 reject,因为不需要处理失败情况

    6. async/await 中的 resolve 和 reject

    使用 async/await 时:

    7. 通俗理解

    想象你点外卖:
    • resolve:外卖送到,你收到餐品(成功)
    • reject:外卖店关门了,你收不到餐品(失败)

    8. 重要特点

    1. 只能调用一次resolvereject 只能调用其中一个,且只能调用一次
    1. 状态不可变:一旦 Promise 状态确定(成功或失败),就无法改变
    1. 参数传递resolvereject 可以传递任意值作为结果或错误信息

    9. 实际应用场景

    10. 总结

    • resolve:Promise 成功时的回调函数,传递结果值
    • reject:Promise 失败时的回调函数,传递错误信息
    • 它们是 Promise 机制的核心,用于处理异步操作的成功和失败情况
    • 在 async/await 中,resolve 对应 await 的返回值,reject 对应 try/catch 中的 catch

    007 Migrating An Entire Application To A Local Data Source (Database) - 应用迁移至本地数据库

    一、核心目标

    将 Next.js 应用从“假新闻数据文件”完全迁移到“本地数据库”,同时优化数据加载时的用户体验(显示加载回退),解决客户端组件与服务器组件兼容问题。

    二、前置准备:数据库连接文件改造

    1. 文件替换
        • 删除存储假新闻数据的旧文件,替换为更新后的 news.js 文件。
        • 核心功能:news.js 内置连接数据库、从数据库获取数据的函数,部分函数添加延迟(模拟真实环境中较慢的数据库响应)。

    三、页面迁移关键步骤

    1. 新闻详情页(slug 页面)

    (1)数据获取改造

    • 问题:旧代码依赖已删除的假新闻文件,无法获取数据。
    • 解决方案:
        1. news.js 导入 获取新闻项函数(需传入 slug 作为参数,返回含新闻数据的 Promise)。
        1. 因函数返回 Promise,需在组件前添加 async 关键字,使用 await 等待数据(仅 React 服务器组件支持,需确保未添加 'use client' 指令)。

    (2)加载回退优化

    • 默认行为:因 slug 是新闻路由的嵌套路径,会自动使用新闻文件夹下的 loading.js 作为加载回退。
    • 自定义优化:可在 slug 页面同级目录添加嵌套 loading.js,导出专属加载组件(如显示“加载新闻详情...”,提升用户体验)。

    2. 嵌套图像路由

    • 改造逻辑与新闻详情页一致:
        1. 移除假新闻数据导入,给组件添加 async 装饰。
        1. 调用 获取新闻项函数 并传入 slug,实现数据库数据获取。
        1. 加载回退:可复用父级 loading.js,无需额外创建(若需更精准提示,也可添加嵌套加载文件)。

    3. 拦截器路由(难点:客户端组件转服务器组件)

    (1)核心问题

    拦截器路由因使用 useRouter(Next.js 导航钩子),需添加 'use client' 成为客户端组件,无法直接调用数据库函数(数据库操作需服务器组件)。

    (2)解决方案:拆分客户端依赖

    1. 提取客户端逻辑
        • components 文件夹创建 modal-dash-backdrop.js 组件,添加 'use client' 指令,内置 useRouter 实现“返回上一页”功能。
        • 将原拦截器页面中依赖客户端的 DOM 结构(如弹窗背景 div)移至该组件。
    1. 改造原页面
        • 原拦截器页面导入 modal-dash-backdrop 组件,移除 'use client' 指令和 useRouter 导入,恢复为 React 服务器组件。
        • 添加 async 关键字,调用 获取新闻项函数 传入 slug,实现数据库数据获取。
    1. bug 修复
        • 刷新拦截器页面可能出现“未找到错误”(因并行路由缺少默认组件)。
        • 解决:重命名 @modal 文件夹中的 page.js 文件为default.js,重启开发服务器。

    4. 存档与过滤路由

    (1)存档页面

    • 数据获取:调用 获取最新新闻函数(异步函数),需添加 async/await
    • 加载优化:在存档页面同级目录添加 loading.js,导出“存档加载中...”等专属回退内容。

    (2)过滤路由

    • 异步函数处理:多数数据函数为异步,需在组件前加 async,对需等待的函数用 await(如 获取年份新闻 获取月份新闻)。
    • 代码优化:将“获取可用年份”的逻辑外包到单独变量,保持 if 判断语句简洁。
    • 关键 bug 修复:
      • 问题:数据库返回的“可用年份/月份”是字符串类型,旧代码将选中值转为数字,导致匹配失败(显示“无效过滤器”)。
      • 解决:删除数值转换的 + 号,直接用字符串匹配。

    四、核心原则与避坑点

    1. 服务器组件 vs 客户端组件
        • 数据库操作、异步数据获取必须放在 服务器组件(无 'use client' 指令)。
        • 客户端依赖(如 useRouter useState)需拆分到独立客户端组件,再在服务器组件中引入(避免服务器组件被“降级”为客户端组件)。
    1. 数据类型兼容性
        • 数据库返回数据多为字符串类型,需与前端代码中的变量类型匹配(如本案例中“年份/月份”的字符串 vs 数字问题)。
    1. 加载回退设计
        • 嵌套路由默认继承父级 loading.js,可通过“同级嵌套 loading.js”实现更精准的加载提示,提升用户体验。

    008 Granular Data Fetching With Suspense - Suspense 实现精细数据获取

    一、核心问题背景

    在 Next.js 过滤页面中,切换月份加载新数据时,因已处于当前页面,Next.js 不会触发全局 loading.js 备用页面,导致数据加载期间无任何用户反馈,影响体验。

    二、关键解决方案:Suspense 组件

    1. 核心作用

    • 替代传统 loading.js 文件,实现精细化加载控制(仅针对特定数据获取组件,而非整个页面)。
    • 向 Next.js 声明:需等待的数据源、触发加载备用内容的场景。
    • 依赖 React 内置的 Suspense 组件,无需额外安装依赖。

    2. 核心原理

    通过将“数据获取逻辑”与“UI 渲染”拆分到独立组件,并用 Suspense 包裹该组件,Next.js 会自动检测组件数据获取状态:
    • 数据加载中:显示 fallback 配置的备用内容(如“加载中...”)。
    • 数据加载完成:渲染组件实际内容。

    三、实操步骤(核心流程)

    步骤 1:拆分独立异步组件(数据与 UI 分离)

    1.1 创建“数据获取+渲染”组件(如 FilteredNews

    • 性质:React 服务器组件(默认支持异步,无需额外声明 async 关键字)。
    • 接收 Propsyear(选中年份,可选)、month(选中月份,可选)。
    • 核心逻辑

      1.2 清理原页面代码

      • 原过滤页面(如 filter-news/page.js)仅保留:
        • 路由参数提取(yearmonth)。
        • 调用 FilteredNews 组件并传递 Props。
        • 移除原页面的 news 变量及 fetch 逻辑。

      步骤 2:用 Suspense 包裹组件(添加加载反馈)

      2.1 导入依赖

      2.2 包裹组件并配置备用内容

      步骤 3:多区域独立控制(可选,进阶)

      若页面有多个需独立加载的区域(如“标题导航区”+“新闻列表区”),可拆分多个组件并分别用 Suspense 包裹:

      3.1 拆分“标题组件”(如 FilterHeader

      • 功能:渲染年份/月份筛选导航、检查无效筛选条件(如不存在的年份)。
      • 逻辑:异步获取“可用年份列表”,并在组件内抛出无效筛选错误(传递到 error.js 处理)。

      3.2 多 Suspense 边界配置

      • 优势:各区域加载状态独立,无需相互等待(如标题加载完成先显示,列表继续加载)。

      步骤 4:错误处理适配

      • 将“无效筛选检查”逻辑移至独立组件(如 FilterHeader):
        • 错误会自动传递到最近的 error.js 文件(Next.js 错误边界机制)。

        四、关键对比:Suspense vs 传统 loading.js

        特性
        Suspense 方案
        传统 loading.js 方案
        控制粒度
        组件级(精细化)
        页面级(全局)
        加载反馈区域
        仅包裹的组件区域
        整个页面替换
        多区域加载支持
        支持(多 Suspense 边界)
        不支持(全局统一反馈)
        适用场景
        页面内局部数据刷新(如筛选)
        页面跳转时的全局数据加载

        五、注意事项

        1. 组件类型限制:仅 React 服务器组件(默认)支持直接在组件内异步获取数据,客户端组件需配合 useEffect 或数据请求库(如 SWR)。
        1. 加载延迟感知:Next.js 会有轻微延迟触发 fallback(避免频繁闪烁),若需即时反馈可配合 CSS 过渡效果。
        1. 代码组织建议:简单场景可将独立组件与页面写在同一文件;复杂场景建议拆分到单独文件(如 components/FilteredNews.js)。

        📎 参考文章

        • 一些引用
        • 引用文章
         
        💡
        欢迎您在底部评论区留言,一起交流~
        上一篇
        第一节 大脑:重新认识你自己
        下一篇
        Harness Engineering - 搭建Mini Harness
        Loading...