nextjs学习9: Next.js 渲染与缓存

从纯前端视角理解渲染

Part A:心智模型重建(纯前端 → Next.js)

A.1 你过去的纯前端世界

在纯前端(Vite / CRA + React Router)里,世界非常简单、干净:

sql 复制代码
浏览器加载一个几乎为空的 index.html
   + 下载一整包 JS(React 全家桶)
   → React 在浏览器里"画"出页面
   → 页面要数据?useEffect 里 fetch('/api/xxx')
   → 拿到数据 → setState → 页面局部重渲染

核心心智模型只有三句话:

  • 页面 = 浏览器里用 JS 画出来的,服务器不碰页面长什么样。
  • 数据 = 永远靠前端主动调后端接口拿(fetch / axios)。
  • 更新 = 我手动 setState 或重新 fetch,一切由我控制。

代码示例,纯前端拿列表数据:

tsx 复制代码
// 纯前端(Vite + React)------你熟悉的写法
function DocList() {
  const [docs, setDocs] = useState([]);

  useEffect(() => {
    fetch('/api/docs') // 前端主动发请求
      .then((res) => res.json())
      .then((data) => setDocs(data)); // 拿到数据 → setState → 页面更新
  }, []);

  const createDoc = async () => {
    await fetch('/api/docs', {
      method: 'POST',
      body: JSON.stringify({ title: '新文档' }),
    });
    // 写完后手动重新 fetch 一次,刷新列表
    const res = await fetch('/api/docs');
    const data = await res.json();
    setDocs(data);
  };

  return (
    <ul>
      {docs.map((doc) => (
        <li key={doc.id}>{doc.title}</li>
      ))}
      <button onClick={createDoc}>新建文档</button>
    </ul>
  );
}

这套模型里,服务器只提供"裸数据 API"(REST / GraphQL),从不碰"页面长什么样"

作为前端开发者,完全控制页面的渲染和数据更新。

A.2 Next.js 的颠覆:服务器也会画页面

Next.js App Router 最大的不同是:React 既可以跑在浏览器,也可以跑在服务器。同一个组件,服务器上先跑一遍生成 HTML,再传到浏览器。

现在"页面长什么样"有两个生产者

  1. 服务器:首屏 HTML 是它生成的(包含数据)。React Server Components(RSC)就在这层。
  2. 浏览器:后续交互、导航时,React 在浏览器里继续画和更新。Client Components 在这层。

你的代码变成了这样(对比纯前端):

tsx 复制代码
// Next.js App Router ------ 服务器组件直接在服务器端拿数据
// app/work/@directory/page.tsx
import { getDocList } from './action';

export default async function Directory() {
  // 注意:没有 useEffect、没有 fetch、没有 useState
  // list 是在服务器上直接查数据库拿到的!
  const list = await getDocList();

  return <DirectoryList list={list} />;
}
ts 复制代码
// app/work/@directory/action.ts
// 这段代码跑在服务器上,浏览器里完全看不到它
import { auth } from '@/auth';
import { prisma } from '@/lib/prisma';

export async function getDocList() {
  const session = await auth(); // 读 cookie,知道你是谁
  const docs = await prisma.doc.findMany({
    where: { userId: session!.user!.id }, // 直接从数据库查
    orderBy: { updatedAt: 'desc' },
  });
  return docs;
}

关键颠覆 :你的组件里没有 fetch、没有 useEffect、没有 setState,数据是在服务器画组件的时候顺手查的

前端习惯的"前端调接口 → setState 更新"这套流程,在服务器组件里根本不存在

比喻:

纯前端 ≈ 你去餐厅,服务员(服务器)只给你食材(数据),你自己在桌子上炒菜摆盘(渲染页面)。你想要新菜,得自己去厨房门口喊一声。

Next.js ≈ 厨房(服务器)直接把炒好的菜端到你桌上。你不用管菜是怎么炒的,甚至菜已经装在盘子里了(HTML)。你想换一盘菜?你不能自己炒,你得喊"厨师,重做一份"------这个"喊厨师重做"就是 revalidatePath

A.3 渲染页面的三种基本方式

忘掉"静态/动态"这种术语,先用**"这段代码在哪跑、什么时候跑"**来理解。

方式 1:服务器在"构建时"画好(Static / 预渲染 / SSG)

执行 pnpm build 时,服务器就把页面画好,存成静态 HTML 文件。

tsx 复制代码
// 1. 列出所有已知的 slug(构建时生成这些页面)
export function generateStaticParams() {
  return [
    { slug: 'hello-world' },
    { slug: 'nextjs-guide' },
  ]
}

// 2. fetch 加缓存
async function getPost(slug: string) {
  'use cache'
  cacheLife('hours')
  return fetch(`https://cms.example.com/posts/${slug}`).then(r => r.json())
}

export default async function BlogPost({
  params,
}: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)   // 用缓存版
  return <article>{post.content}</article>
}
  • 什么时候画pnpm build 的那一刻。
  • 用户访问时 :服务器直接把存好的 HTML 文件发给你,这次请求根本不跑你的 React 组件
  • 特点:极快(不用算),但所有人看到一模一样的内容,且内容停留在"构建那一刻"。
  • 适用:不随用户变化、不频繁更新的内容(官网首页、博客文章、产品介绍页)。
方式 2:服务器在"用户每次请求时"画(Dynamic / 动态 / SSR)

用户访问页面时,服务器现跑 你的 React 组件,包含里面的所有 await,画好 HTML 发出去。

tsx 复制代码
// DocAI 中的实际代码 ------ 动态渲染
// app/work/@directory/page.tsx
import { getDocList } from './action';

export default async function Directory() {
  // auth() 内部读了 cookies() → 强制动态渲染
  const list = await getDocList(); // 每次请求都在服务器上真查数据库
  return <DirectoryList list={list} />;
}
  • 什么时候画:用户每次访问的那一刻。服务器现算现发。
  • 特点 :可以根据当前用户、当前时间生成专属内容,数据永远最新。代价是每次都要跑代码、查数据库,比读静态文件慢一点(但配合缓存策略可大幅优化)。
  • 适用 :需要登录态、数据随用户变化、实时性要求高的页面(你的 /work 文档列表)。
方式 3:浏览器里画(Client Component / 跟 SPA 一样)

组件标了 'use client',里面的逻辑(useStateuseEffect、事件处理)只在浏览器跑

tsx 复制代码
// DocAI 中的实际代码 ------ 客户端组件
// app/work/[id]/content.tsx
'use client';

import { useEditor, EditorContent } from '@tiptap/react';

export default function EditorContent({ content, docId }: Props) {
  const editor = useEditor({
    content: JSON.parse(content), // 解析服务器传来的初始内容
    onUpdate: ({ editor }) => {
      // 用户在浏览器里打字 → 触发状态更新 → 局部重渲染
      debouncedSave(docId, editor.getJSON());
    },
  });

  return <EditorContent editor={editor} />;
}
  • 注意 :即使标了 'use client',它的初始 HTML 也是服务器先画了一版传过来的(为了首屏快、SEO 友好),然后浏览器接手后续交互。这叫"水合(Hydration)"。
  • 适用:需要交互、事件处理、浏览器 API 的部分(编辑器、表单、弹窗、动画)。
三种方式对照表
方式 在哪跑 什么时候跑 类比纯前端
静态(Static) 服务器 pnpm build 相当于你把页面+数据打成静态文件丢 CDN,谁来看都一样
动态(Dynamic) 服务器 用户每次请求时 纯前端里没有对应物------纯前端里服务器从不画页面
客户端组件 浏览器 用户交互时 等于纯前端------useState、useEffect、事件都在浏览器

你的 @directory/page.tsx服务器组件(没标 'use client')且因为 auth()cookies() 强制动态渲染,走的是方式 2

A.4 拿数据的两条路径

在纯前端你只有一条路:前端 fetch 接口 → setState 更新

Next.js 里有两条完全不同的路:

路 A:服务器组件里直接拿数据(本项目用法)
tsx 复制代码
// 服务器组件(无 'use client' 标记)
// app/work/@directory/page.tsx
export default async function Directory() {
  // 直接 await 数据库查询 ------ 这是在服务器上执行的!
  const list = await getDocList(); // 没有 fetch、没有 useEffect、没有 useState
  return <DirectoryList list={list} />;
}

数据获取的完整链路:

scss 复制代码
用户访问 /work/abc
  → 服务器收到请求
  → 执行 Directory() 组件
  → await getDocList()
      → await auth()  读取 cookies() 识别用户
      → await prisma.doc.findMany() 查数据库
      → 返回文档列表
  → 将数据注入组件,生成 HTML
  → 发送给浏览器

路 A 的更新不归前端管 ,归"服务器什么时候重新画这个组件"管。要让它更新,就得告诉服务器"你上一次画的结果作废了,重新画"------这就是 revalidatePath 干的事。

路 B:客户端组件里 fetch / 调用 API(跟纯前端一样)
tsx 复制代码
// 客户端组件
'use client';

function DocList() {
  const [docs, setDocs] = useState([]);

  useEffect(() => {
    fetch('/api/docs') // 浏览器里发请求
      .then((res) => res.json())
      .then(setDocs); // 前端 setState 更新
  }, []);

  const createDoc = async () => {
    await fetch('/api/docs', { method: 'POST' });
    // 手动重新 fetch 刷新列表
    const res = await fetch('/api/docs');
    setDocs(await res.json());
  };
}

这条路你完全懂,就是纯前端的老路子,不需要 revalidatePath

两条路的关键区别
路 A(服务器组件) 路 B(客户端组件)
代码在哪跑 服务器 浏览器
数据怎么拿 await getDocList() 直接查库 fetch('/api/...') 调接口
怎么更新数据 Server Action 写库 + revalidatePath fetch POST + 手动 refetch + setState
你在哪里写 setState 不需要 需要
是不是你看得见的数据流 ------浏览器里看不到查库过程 是------Network 面板里能看到请求

最重要的认知 :你的 @directory 走的是路 A 。所以当你点"新增文档",数据写进数据库了,但左侧列表没更新------不是数据库没存进去,而是服务器没被告知"重新画左侧那个组件" 。你以前在路 B 里习惯的"改完数据手动 refetch 列表",在路 A 里的等价动作就是 revalidatePath

A.5 客户端 Router Cache:动态页面也会被记住

这是最容易被误解、也最关键的机制。

先建立"两段式"认知:页面渲染涉及两个地方:

  1. 服务器:负责"算"出页面内容(查数据库、拼 HTML)。
  2. 浏览器里的 Next.js(Client Router) :负责"接管导航",让你点链接时不整页刷新,而是局部换内容。它自己有一个小仓库(Router Cache),会把服务器已经算过的结果存一份

通俗比喻------政务大厅与超级前台

想象你去一个政务大厅(服务器)办业务。大厅门口有个超级前台(客户端 Router Cache)。

  • 你第一次问前台:"我要看我的文档列表。"
  • 前台跑去大厅里面(服务器)查了一圈,拿回一份列表给你,并自己偷偷复印了一份贴在墙上
  • 后来你在楼里(应用内)从一个窗口走到另一个窗口(客户端软导航,比如 /work/A/work/B),没出大楼、没过安检
  • 这时候你回头看那面墙上的"文档列表"------前台不会主动去重新查,它给你看的是墙上那张复印版(缓存)。

核心矛盾

  • 即使政务大厅是"动态"的(每次有人进去查,都现查现给),但墙上那张复印纸是前台自己贴的,跟大厅里面是不是动态无关
  • 大厅"动态"只保证"有人进去问的时候给最新的"。
  • 但前台那张复印纸是浏览器端的记忆,它默认不会自己撕掉重贴。

这就是"即使路由是动态渲染的,Next.js 也会在客户端把已经渲染过的插槽临时记住"的意思。

代码层面的解释

tsx 复制代码
// app/work/layout.tsx ------ 静态段 layout,持久化侧边栏
export default function WorkLayout({ children, directory }: Props) {
  return (
    <div className="flex h-screen">
      {/* 左侧:@directory 插槽------首次渲染后被 Router Cache 记住 */}
      <aside className="w-64 border-r">{directory}</aside>
      {/* 右侧:children------切换文档时才重取 */}
      <main className="flex-1">{children}</main>
    </div>
  );
}
  • 用户打开 /work/abc → 服务器画左侧 @directory 插槽(查库拿到 [文档1, 文档2])→ 浏览器把结果缓存进 Router Cache
  • 用户在应用内点"新增文档" → create() 写库成功(数据库现在是 [文档1, 文档2, 文档3])→ 但左侧 @directory 插槽没有被重新请求 ,浏览器还在用缓存里的 [文档1, 文档2]

换一个角度理解------F5 整页刷新 vs 应用内操作的区别

操作 发生了什么 左侧列表是否更新
F5 整页刷新 浏览器向服务器发全新请求 → 服务器重新跑 Directory()getDocList() 重查库 → 返回新 HTML ✅ 更新
应用内点"新增文档" Server Action 写库 ✅,但左侧插槽用 Router Cache 旧值,没重查服务器 ❌ 不更新
应用内点"新增文档" + revalidatePath Server Action 写库 ✅ + 通知浏览器"左侧缓存作废" → 浏览器重查服务器 → 拿到新数据 ✅ 更新

A.6 更新页面的四种触发方式

把"页面内容什么时候会变"的所有可能来源列清楚:

# 触发 谁发起 发生什么 类比纯前端
1 整页刷新(F5 / 地址栏回车) 用户 浏览器向服务器发全新请求 → 服务器重新画整个页面 等于重新加载整个 SPA
2 应用内软导航(点 <Link> 用户 不刷新整页,Next.js 在浏览器里局部换路由。已渲染过的部分从 Router Cache 取,不会重新请求服务器 类似 React Router 切换路由,但多了一层缓存
3 客户端交互(路 B) 你的代码 'use client' 组件里 setState / fetch → 浏览器局部重渲染 完全等于纯前端
4 Server Action + revalidatePath(路 A 的数据变更) 你的代码 写库 → revalidatePath 标脏 → 浏览器立刻重取那块并重画 纯前端里"调接口后手动 refetch 那个列表"

重点:方式 4 就是你当前需要的方式。

在纯前端中,你改完数据后:

tsx 复制代码
// 纯前端写法
await fetch('/api/docs', { method: 'POST' });
const res = await fetch('/api/docs'); // 手动重新拿列表
setDocs(res);

在 Next.js 路 A 中,等价动作:

tsx 复制代码
// Next.js Server Action 写法
export async function create(data) {
  await prisma.doc.create({ data });
  revalidatePath('/work', 'layout'); // ← 等价于上面那两行
}

A.7 判断口诀

每次写页面或改数据时,问自己三句话:

1. 这个组件是服务器组件还是客户端组件?

  • 没标 'use client' → 服务器组件,数据在服务器拿(路 A)。
  • 标了 'use client' → 客户端组件,可以像纯前端一样 fetch + setState(路 B)。

2. 如果是服务器组件,它读 cookie / 查库 / 用了请求时数据吗?

  • 读了 → 动态渲染(每次请求现画)。
  • 没读且数据不变 → 可能被静态预渲染。

3. 我改了数据库后,想让某块服务器渲染的内容立刻更新,怎么办?

  • 在改数据的 Server Action 里加 revalidatePath(对应路径, 'layout')
  • 有并行路由插槽 → 必须用 'layout' 类型。
  • 这就等价于纯前端里"改完数据后 refetch 那个列表"。

Part B:revalidatePath 技术详解

B.1 revalidatePath 是什么

revalidatePath 是 Next.js 提供的一个函数,用于标记指定路径的缓存为"陈旧"

ts 复制代码
import { revalidatePath } from 'next/cache';

调用后,该路径在下一次访问时会重新渲染------重新执行服务器组件代码、重新查数据库、重新生成 HTML。

如果在 Server Action 中调用,还会立即刷新正在查看的 UI,不需要等用户下次导航。

它同时作用于三层缓存:

  • 全路由缓存(Full Route Cache):当页面被打包成 Static 时,Next.js 在服务器上把"这个页面渲染好的整段 HTML"存进磁盘。下次有人访问,直接从磁盘把 HTML 拿出来发回去,不用再跑一遍渲染逻辑。
  • 数据缓存(Data Cache)fetch 请求的响应缓存。调用后相关路径的缓存数据失效。
ts 复制代码
async function getPosts() {
  'use cache'; // ← 结果进 Data Cache
  cacheLife('hours');
  return db.post.findMany();
}
  • 客户端 Router Cache:浏览器访问过一个路由后,会把服务器返回的 RSC 结果(Server Component 渲染产物)暂存在内存里。下次再进这个路由(比如用户点"返回"),不用重新向服务器要,直接拿来用,所以切换飞快。

它和前两个的本质区别:前两个在服务器上,这个是浏览器里;前两个存的是"数据/HTML",这个存的是"渲染结果快照"。

注意 :即使你的页面是动态渲染(每次请求现算)、数据是直接查 Prisma(不走 Data Cache),revalidatePath 仍然有用------它主要起作用的是第三层(客户端 Router Cache),让浏览器不再复用已缓存的内容,而是去服务器重新取。

B.2 type: 'page' vs 'layout'

这是最容易踩的坑,也是最需要理解清楚的参数。

ts 复制代码
// 让 /work 这个 layout 及其下所有嵌套 page 都失效
revalidatePath('/work', 'layout');

// 只让 /work/[id] 这个 page 层失效
revalidatePath('/work/abc', 'page');

// 不传 type 时默认为 'page'
revalidatePath('/work/abc'); // 等同于 revalidatePath('/work/abc', 'page')
参数详解
调用 type 影响范围 示意图
revalidatePath('/blog/[slug]', 'page') 'page' 只让 page 层重执行,layout 层不动
revalidatePath('/blog/[slug]', 'layout') 'layout' layout + 所有嵌套 layout + page 全部重执行
revalidatePath('/blog/hello') 默认 'page' 让具体路径的 page 重执行
revalidatePath('/', 'layout') 'layout' 全局刷新,清空整个客户端路由缓存 最宽
为什么 'layout' 范围更大?
tsx 复制代码
// Next.js 路由嵌套示意
app/
├── layout.tsx              ← 根 layout
├── work/
│   ├── layout.tsx          ← /work 的 layout(@directory 挂载点)
│   ├── @directory/
│   │   └── page.tsx        ← 左侧目录插槽(layout 层插槽)
│   └── [id]/
│       └── page.tsx        ← 右侧编辑页(page 层)
  • revalidatePath('/work/abc', 'page'):只让 /work/abc 这个 page 层失效。@directory/work/layout.tsx 下的并行插槽(属于 layout 层 ),不会被标脏
  • revalidatePath('/work', 'layout'):让 /worklayout 层及下面所有东西失效,包括 @directory 插槽 + [id]/page.tsx

B.3 调用位置决定行为

revalidatePath效果差异取决于你在哪里调用它:

在 Server Action 中调用
ts 复制代码
// app/work/@directory/action.ts
'use server';

export async function create(formData: FormData) {
  // 1. 写数据库
  await prisma.doc.create({ data: { ... } });

  // 2. 标脏路径
  revalidatePath('/work', 'layout');

  // 3. Server Action 执行完后,Next.js 自动:
  //    a. 触发 router.refresh() 重新请求当前路由
  //    b. 因为 /work 的 layout 层被标脏,浏览器丢弃缓存
  //    c. 浏览器向服务器重新取 @directory 插槽
  //    d. 服务器重新执行 getDocList() → 拿到最新数据
  //    e. 左侧列表即时更新!
}

Server Action 执行完后 Next.js 默认会触发 router.refresh()(重新请求当前路由),加上你手动调了 revalidatePath,两个叠加的效果是:当前页面立即用新数据重新渲染,用户无需刷新。

⚠️ 重要router.refresh() 本身会请求整条路由(含并行插槽),但并行路由里"URL 段没变"的插槽(如 @directory)默认会被 Router Cache 复用,不真去服务器重查 。因此仅靠 Server Action 自带的 refresh 不足以可靠刷新并行插槽,必须配合 revalidatePath

在 Route Handler 中调用
ts 复制代码
// app/api/revalidate/route.ts
export async function POST(request: Request) {
  revalidatePath('/work', 'layout');
  return Response.json({ revalidated: true });
}

在 Route Handler 中调用时,仅标记缓存失效,不会立即触发 UI 更新 。因为 Route Handler 不在浏览器交互上下文中,Next.js 无法知道"当前显示的是哪条路由"。缓存在下次用户访问该路径时才会被重新计算。

两种调用位置的对比
调用位置 是否立即刷新 UI 适用场景
Server Action ✅ 是(官方:Updates the UI immediately) 用户操作后需要即时看到变化(新建、编辑、删除)
Route Handler ❌ 否,仅标记(下次访问才生效) 外部系统回调、Webhook、定时任务触发的内容更新

B.8 完整链路图

下面用一张 Mermaid 图,把整个 DocAI 中"新建文档 → 左侧列表即时更新"的完整链路串起来:

sequenceDiagram actor U as 用户 participant B as 浏览器<br/>(Client Router Cache) participant SA as Server Action<br/>(create) participant DB as PostgreSQL participant RSC as RSC 服务器<br/>(@directory/page.tsx) Note over B: 当前显示 /work/abc<br/>左侧列表: [文档A, 文档B] U->>B: 点击&#34;新建文档&#34;按钮 B->>SA: 调用 create() Server Action activate SA SA->>DB: INSERT INTO doc ... DB-->>SA: 新文档 id = xyz Note over DB: 数据库有 3 条了: [A, B, C] ✅ SA->>SA: revalidatePath('/work', 'layout') Note over SA: 标脏 /work layout 层<br/>(含 @directory 插槽) SA-->>B: 返回结果 deactivate SA Note over B: Server Action 执行完<br/>Next.js 自动 router.refresh() B->>B: 检查 Router Cache<br/>@directory 插槽 → 已标脏 → 丢弃旧值! B->>RSC: 重新请求 @directory 插槽 activate RSC RSC->>DB: SELECT * FROM doc WHERE userId = ... DB-->>RSC: [文档A, 文档B, 文档C] RSC-->>B: 新的 RSC Payload (含 3 条文档) deactivate RSC B->>B: 左侧列表更新为<br/>[文档A, 文档B, 文档C] ✅

关键步骤标注

  1. 🔴 revalidatePath('/work', 'layout') ------ 如果不做这一步,下面第 4 步中 Router Cache 不会主动丢弃 @directory 插槽的旧值。
  2. 🔵 router.refresh() ------ Server Action 自带的自动刷新。但仅靠它不够(见误区 3)。
  3. 🟢 SELECT * FROM doc ------ 服务器重新查库,拿到的数据永远是最新的。
  4. 🟡 浏览器丢弃缓存旧值,用服务器返回的新 RSC Payload 更新 UI。

总结:一页纸速记

sql 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Next.js 数据更新决策树                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  我要更新页面数据了                                               │
│      │                                                          │
│      ├─ 数据是"客户端组件里 fetch"拿的?                           │
│      │   └─ 走纯前端老路:useEffect + fetch + setState            │
│      │      不需要 revalidatePath                                │
│      │                                                          │
│      ├─ 数据是"服务器组件里直接 await 拿的"?                      │
│      │   └─ 需要 revalidatePath                                 │
│      │       │                                                  │
│      │       ├─ 有并行路由插槽(@directory 等)?                  │
│      │       │   └─ 必须用 revalidatePath('/path', 'layout')     │
│      │       │                                                  │
│      │       └─ 没有并行路由?                                    │
│      │           └─ revalidatePath('/path') 通常够用              │
│      │                                                          │
│      └─ F5 整页刷新能拿到新数据吗?                                │
│          ├─ 能 → 说明服务器端每次查库没问题,问题是客户端缓存        │
│          ├─ 不能 → 先排查服务器端数据获取逻辑                       │
│          └─ 都是需要 revalidatePath 的信号                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
相关推荐
乘风gg2 小时前
AI 时代,你的编程能力在第几层?我敢说,大多数人卡在第一层
前端·ai编程·claude
东风破_2 小时前
React Router v7 实战:用路由配置、懒加载与嵌套路由搭建一个完整的 SPA
前端
引山洪082 小时前
Babylon.js 8.x 中文文档整理——Node篇 (上)
前端·webgl
柒和远方2 小时前
V058:前端路由的第一性原理:从 hashchange 手写路由,到 React Router 的嵌套与懒加载
前端·javascript·react.js
计算机魔术师2 小时前
阿里云上线 One Key MCP 服务:兼容 Qoder、Codex 等,可一键调用多家 MCP 服务
前端
Darling噜啦啦2 小时前
React Router 全家桶实战:从路由懒加载到嵌套路由的 6 大核心玩法
前端·react.js
宋哥转AI2 小时前
深入理解 AI Agent 03|RAG评估体系:量化检索增强效果,精准定位系统短板
人工智能·后端·agent
Goodbye2 小时前
组件详解:从起源到未来的全方位解读
前端
无糖可可果2 小时前
从前端路由的起源到 React Router 实战
前端