<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>blog</id>
    <title>Colin3191 Blog</title>
    <updated>2026-02-07T03:12:26.567Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://colin3191.me"/>
    <subtitle>A personal blog with thoughts and insights</subtitle>
    <icon>https://colin3191.me/colin3191.jpg</icon>
    <entry>
        <title type="html"><![CDATA[Next.js 入门（二）：导航与链接]]></title>
        <id>/blog/2026/2026-02-07-nextjs-getting-started-part2</id>
        <link href="https://colin3191.me/blog/2026/2026-02-07-nextjs-getting-started-part2"/>
        <updated>2026-02-07T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<!--$--><h1 class="rp-toc-include" id="nextjs-入门二导航与链接"><a href="#nextjs-入门二导航与链接" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Next.js 入门（二）：导航与链接<!-- --> </h1>
<p>上一篇讲了怎么用文件夹定义路由，这篇讲怎么在路由之间跳转。</p>
<p>用过 react-router 的话，导航无非就是 <code>&lt;Link&gt;</code> 和 <code>useNavigate</code>。Next.js 也差不多，但因为页面默认跑在服务端，所以多了 prefetch、流式渲染这些东西。听着复杂，写起来其实没多少新东西。</p>
<h2 class="rp-toc-include" id="导航背后发生了什么"><a href="#导航背后发生了什么" class="rp-header-anchor rp-link" aria-hidden="true">#</a>导航背后发生了什么</h2>
<p>先花一分钟搞清楚原理，不然后面看 API 会觉得莫名其妙。</p>
<p>Next.js 页面默认是服务端组件，内容在服务器上生成再发给浏览器。那每次跳页面都要等服务器？那不是很慢？</p>
<p>还好，框架在背后做了三件事：</p>
<h3 class="rp-toc-include" id="prefetch"><a href="#prefetch" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Prefetch</h3>
<p><code>&lt;Link&gt;</code> 出现在屏幕上的时候，Next.js 就偷偷在后台把这个链接的页面数据加载好了。等你真点的时候，数据早就到了，所以感觉是秒开。</p>
<p>不过 prefetch 也分情况：</p>
<ul>
<li><strong>静态路由</strong>：整个页面都 prefetch</li>
<li><strong>动态路由</strong>：不 prefetch，或者只 prefetch 到最近的 <code>loading.tsx</code> 那一层</li>
</ul>
<h3 class="rp-toc-include" id="流式渲染"><a href="#流式渲染" class="rp-header-anchor rp-link" aria-hidden="true">#</a>流式渲染</h3>
<p>动态路由没法提前 prefetch，那就换个思路——服务器渲染好一块就先发一块，不用等整个页面都好了才发。配合 <code>loading.tsx</code> 的骨架屏，用户至少能先看到个加载状态，不会一直看着白屏。</p>
<h3 class="rp-toc-include" id="客户端过渡"><a href="#客户端过渡" class="rp-header-anchor rp-link" aria-hidden="true">#</a>客户端过渡</h3>
<p>传统 SSR 每次跳页面都是整页刷新，白屏闪一下。Next.js 的 <code>&lt;Link&gt;</code> 不走这套，它在客户端直接替换变化的部分，共享布局和状态都保留着，体验跟 SPA 没区别。</p>
<p>这三个东西配合起来，虽然页面是服务端渲染的，但用起来跟客户端渲染一样流畅。</p>
<h2 class="rp-toc-include" id="link-组件"><a href="#link-组件" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>&lt;Link&gt;</code> 组件</h2>
<p>日常写得最多的就是它，react-router 里也有同名组件，用法差不多。</p>
<h3 class="rp-toc-include" id="基本用法"><a href="#基本用法" class="rp-header-anchor rp-link" aria-hidden="true">#</a>基本用法</h3>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> Link </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/link&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> Navigation</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">nav</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/&quot;</span><span style="color:var(--shiki-foreground)">&gt;首页&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/blog&quot;</span><span style="color:var(--shiki-foreground)">&gt;博客&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/about&quot;</span><span style="color:var(--shiki-foreground)">&gt;关于&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">nav</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>跟 react-router 的区别：<code>href</code> 代替了 <code>to</code>，不需要额外的 <code>&lt;a&gt;</code> 标签（v13 之前需要）。</p>
<h3 class="rp-toc-include" id="动态路由链接"><a href="#动态路由链接" class="rp-header-anchor rp-link" aria-hidden="true">#</a>动态路由链接</h3>
<p>结合上一篇讲的动态路由，用模板字符串生成链接：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> Link </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/link&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">interface</span><span style="color:var(--shiki-token-function)"> Post</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  slug</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  title</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> PostList</span><span style="color:var(--shiki-foreground)">({ posts }</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> { posts</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Post</span><span style="color:var(--shiki-foreground)">[] }) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">ul</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      {</span><span style="color:var(--shiki-token-constant)">posts</span><span style="color:var(--shiki-token-function)">.map</span><span style="color:var(--shiki-foreground)">((post) </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-token-function)"> key</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-constant)">post</span><span style="color:var(--shiki-foreground)">.slug}&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">          &lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-string-expression)">`/blog/</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">post</span><span style="color:var(--shiki-foreground)">.slug</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">}&gt;{</span><span style="color:var(--shiki-token-constant)">post</span><span style="color:var(--shiki-foreground)">.title}&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;/</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      ))}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">ul</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><code>href</code> 也可以传对象：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">Link</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">  href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{{</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    pathname</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;/blog/[slug]&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    query</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> { slug</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> post</span><span style="color:var(--shiki-foreground)">.slug }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  {</span><span style="color:var(--shiki-token-constant)">post</span><span style="color:var(--shiki-foreground)">.title}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>两种写法效果一样，我个人更喜欢模板字符串，直观。对象写法参数多的时候可能更清晰一点。</p>
<h3 class="rp-toc-include" id="常用-props"><a href="#常用-props" class="rp-header-anchor rp-link" aria-hidden="true">#</a>常用 Props</h3>









































<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>Prop</th><th>类型</th><th>默认值</th><th>说明</th></tr></thead><tbody><tr><td><code>href</code></td><td><code>string | object</code></td><td>—</td><td>目标路径，必填</td></tr><tr><td><code>replace</code></td><td><code>boolean</code></td><td><code>false</code></td><td>替换当前历史记录，而不是新增一条</td></tr><tr><td><code>scroll</code></td><td><code>boolean</code></td><td><code>true</code></td><td>导航后是否滚动到顶部</td></tr><tr><td><code>prefetch</code></td><td><code>boolean | null</code></td><td><code>null</code>（自动）</td><td>是否 prefetch，<code>false</code> 可以关闭</td></tr><tr><td><code>onNavigate</code></td><td><code>function</code></td><td>—</td><td>客户端导航时的回调，可以 <code>preventDefault()</code> 取消导航</td></tr></tbody></table></div>
<h3 class="rp-toc-include" id="替换历史记录"><a href="#替换历史记录" class="rp-header-anchor rp-link" aria-hidden="true">#</a>替换历史记录</h3>
<p>默认每次导航都会往浏览器历史栈里 push 一条。不想让用户点返回回到当前页？加个 <code>replace</code>：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/dashboard&quot;</span><span style="color:var(--shiki-token-function)"> replace</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  进入仪表盘</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>比如登录成功后跳仪表盘，用户不该再回到登录页。</p>
<h3 class="rp-toc-include" id="控制滚动行为"><a href="#控制滚动行为" class="rp-header-anchor rp-link" aria-hidden="true">#</a>控制滚动行为</h3>
<p>默认跳转后会滚到页面顶部（如果目标页面不在视口内的话）。不想要这个行为：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/blog&quot;</span><span style="color:var(--shiki-token-function)"> scroll</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-constant)">false</span><span style="color:var(--shiki-foreground)">}&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  博客</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>也可以用 <code>#</code> 锚点滚动到指定位置：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/blog/intro#section-2&quot;</span><span style="color:var(--shiki-foreground)">&gt;跳到第二节&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="控制-prefetch"><a href="#控制-prefetch" class="rp-header-anchor rp-link" aria-hidden="true">#</a>控制 Prefetch</h3>
<p>一般不用管，框架自己处理。但如果页面上链接特别多（比如无限滚动列表），全 prefetch 太浪费了，可以关掉：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-token-function)"> href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-string-expression)">`/products/</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-foreground)">id</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">} </span><span style="color:var(--shiki-token-function)">prefetch</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-constant)">false</span><span style="color:var(--shiki-foreground)">}&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  {name}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="拦截导航"><a href="#拦截导航" class="rp-header-anchor rp-link" aria-hidden="true">#</a>拦截导航</h3>
<p><code>onNavigate</code> 可以在跳转前拦一下，比如表单还没保存，用户就要走：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">Link</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">  href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;/other-page&quot;</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">  onNavigate</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{(e) </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    if</span><span style="color:var(--shiki-foreground)"> (hasUnsavedChanges) {</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">      e</span><span style="color:var(--shiki-token-function)">.preventDefault</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-comment)">      // 弹窗确认</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    }</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  离开</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>注意这个回调只在客户端导航时触发，Ctrl/Cmd + 点击开新标签页不会走这里。</p>
<h2 class="rp-toc-include" id="userouter-hook"><a href="#userouter-hook" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>useRouter</code> Hook</h2>
<p><code>&lt;Link&gt;</code> 是写在 JSX 里的，<code>useRouter</code> 是写在逻辑里的——表单提交完跳转、判断条件后跳转，这种场景用它。</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-string-expression)">&quot;use client&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { useRouter } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/navigation&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> LoginForm</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> router</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> useRouter</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  async</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> handleSubmit</span><span style="color:var(--shiki-foreground)">(e</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> React</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-function)">FormEvent</span><span style="color:var(--shiki-foreground)">) {</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    e</span><span style="color:var(--shiki-token-function)">.preventDefault</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    const</span><span style="color:var(--shiki-token-constant)"> success</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-function)"> login</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    if</span><span style="color:var(--shiki-foreground)"> (success) {</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">      router</span><span style="color:var(--shiki-token-function)">.push</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;/dashboard&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    }</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">form</span><span style="color:var(--shiki-token-function)"> onSubmit</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{handleSubmit}&gt;{</span><span style="color:var(--shiki-token-comment)">/* ... */</span><span style="color:var(--shiki-foreground)">}&lt;/</span><span style="color:var(--shiki-token-string-expression)">form</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>两个容易踩的坑：</p>
<ol>
<li>必须加 <code>&quot;use client&quot;</code>，hook 只能在客户端组件里用</li>
<li>从 <code>next/navigation</code> 导入，别写成 <code>next/router</code>，那是老版 Pages Router 的</li>
</ol>
<h3 class="rp-toc-include" id="常用方法"><a href="#常用方法" class="rp-header-anchor rp-link" aria-hidden="true">#</a>常用方法</h3>

































<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td><code>router.push(url)</code></td><td>跳转到新页面，新增历史记录</td></tr><tr><td><code>router.replace(url)</code></td><td>跳转到新页面，替换当前历史记录</td></tr><tr><td><code>router.back()</code></td><td>返回上一页</td></tr><tr><td><code>router.forward()</code></td><td>前进到下一页</td></tr><tr><td><code>router.refresh()</code></td><td>刷新当前路由（重新请求服务器，不丢失客户端状态）</td></tr><tr><td><code>router.prefetch(url)</code></td><td>手动 prefetch 某个路由</td></tr></tbody></table></div>
<h3 class="rp-toc-include" id="push-vs-replace"><a href="#push-vs-replace" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>push</code> vs <code>replace</code></h3>
<p>跟 <code>&lt;Link&gt;</code> 的 <code>replace</code> prop 一样的逻辑：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// 用户可以点返回回到当前页</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">router</span><span style="color:var(--shiki-token-function)">.push</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;/dashboard&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 用户点返回会跳过当前页</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">router</span><span style="color:var(--shiki-token-function)">.replace</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;/dashboard&quot;</span><span style="color:var(--shiki-foreground)">);</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="refresh-挺好用的"><a href="#refresh-挺好用的" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>refresh</code> 挺好用的</h3>
<p><code>router.refresh()</code> 会让服务端组件重新从服务器拿数据、重新渲染，但客户端组件的状态不会丢（<code>useState</code> 的值、滚动位置都还在）。</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-keyword)">async</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> handleDelete</span><span style="color:var(--shiki-foreground)">(id</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  await</span><span style="color:var(--shiki-token-function)"> deletePost</span><span style="color:var(--shiki-foreground)">(id);</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">  router</span><span style="color:var(--shiki-token-function)">.refresh</span><span style="color:var(--shiki-foreground)">(); </span><span style="color:var(--shiki-token-comment)">// 重新获取列表数据，UI 自动更新</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>react-router 里没有对应的东西，这算是服务端组件带来的新能力。</p>
<h3 class="rp-toc-include" id="link-还是-userouter"><a href="#link-还是-userouter" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>&lt;Link&gt;</code> 还是 <code>useRouter</code></h3>





























<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>场景</th><th>用什么</th></tr></thead><tbody><tr><td>导航栏、列表里的链接</td><td><code>&lt;Link&gt;</code></td></tr><tr><td>表单提交后跳转</td><td><code>useRouter</code></td></tr><tr><td>条件判断后跳转</td><td><code>useRouter</code></td></tr><tr><td>需要 prefetch 和 SEO</td><td><code>&lt;Link&gt;</code></td></tr><tr><td>返回上一页</td><td><code>useRouter</code></td></tr></tbody></table></div>
<p>原则很简单：能用 <code>&lt;Link&gt;</code> 就用 <code>&lt;Link&gt;</code>，自动 prefetch、语义化都有了。<code>useRouter</code> 留给那些非得用代码控制的场景。</p>
<h2 class="rp-toc-include" id="usepathname拿当前路径"><a href="#usepathname拿当前路径" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>usePathname</code>：拿当前路径</h2>
<p>返回当前 URL 的路径部分，不含查询参数和 hash。用得最多的地方就是导航高亮：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-string-expression)">&quot;use client&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> Link </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/link&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { usePathname } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/navigation&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> links</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-foreground)"> [</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  { href</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;/&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> label</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;首页&quot;</span><span style="color:var(--shiki-foreground)"> }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  { href</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;/blog&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> label</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;博客&quot;</span><span style="color:var(--shiki-foreground)"> }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  { href</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;/about&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> label</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;关于&quot;</span><span style="color:var(--shiki-foreground)"> }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">];</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> Navigation</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> pathname</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> usePathname</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">nav</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      {</span><span style="color:var(--shiki-token-constant)">links</span><span style="color:var(--shiki-token-function)">.map</span><span style="color:var(--shiki-foreground)">((link) </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;</span><span style="color:var(--shiki-token-constant)">Link</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">          key</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-constant)">link</span><span style="color:var(--shiki-foreground)">.href}</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">          href</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{</span><span style="color:var(--shiki-token-constant)">link</span><span style="color:var(--shiki-foreground)">.href}</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">          style</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{{</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">            fontWeight</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> pathname </span><span style="color:var(--shiki-token-keyword)">===</span><span style="color:var(--shiki-token-constant)"> link</span><span style="color:var(--shiki-foreground)">.href </span><span style="color:var(--shiki-token-keyword)">?</span><span style="color:var(--shiki-token-string-expression)"> &quot;bold&quot;</span><span style="color:var(--shiki-token-keyword)"> :</span><span style="color:var(--shiki-token-string-expression)"> &quot;normal&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">          }}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">          {</span><span style="color:var(--shiki-token-constant)">link</span><span style="color:var(--shiki-foreground)">.label}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;/</span><span style="color:var(--shiki-token-constant)">Link</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      ))}</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">nav</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>

























<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>URL</th><th><code>usePathname()</code> 返回值</th></tr></thead><tbody><tr><td><code>/</code></td><td><code>&quot;/&quot;</code></td></tr><tr><td><code>/dashboard</code></td><td><code>&quot;/dashboard&quot;</code></td></tr><tr><td><code>/dashboard?v=2</code></td><td><code>&quot;/dashboard&quot;</code></td></tr><tr><td><code>/blog/hello-world</code></td><td><code>&quot;/blog/hello-world&quot;</code></td></tr></tbody></table></div>
<p>只返回路径，不带查询参数。查询参数要用下面的 <code>useSearchParams</code>。</p>
<h2 class="rp-toc-include" id="usesearchparams读查询参数"><a href="#usesearchparams读查询参数" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>useSearchParams</code>：读查询参数</h2>
<p>返回一个只读的 <code>URLSearchParams</code> 对象，就是 URL 里 <code>?</code> 后面那部分。</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-string-expression)">&quot;use client&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { useSearchParams } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/navigation&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> SearchResults</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> searchParams</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> useSearchParams</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> query</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> searchParams</span><span style="color:var(--shiki-token-function)">.get</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;q&quot;</span><span style="color:var(--shiki-foreground)">); </span><span style="color:var(--shiki-token-comment)">// /search?q=nextjs → &quot;nextjs&quot;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> page</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> searchParams</span><span style="color:var(--shiki-token-function)">.get</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;page&quot;</span><span style="color:var(--shiki-foreground)">); </span><span style="color:var(--shiki-token-comment)">// /search?q=nextjs&amp;page=2 → &quot;2&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">p</span><span style="color:var(--shiki-foreground)">&gt;搜索：{query}&lt;/</span><span style="color:var(--shiki-token-string-expression)">p</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">p</span><span style="color:var(--shiki-foreground)">&gt;第 {page </span><span style="color:var(--shiki-token-keyword)">??</span><span style="color:var(--shiki-token-constant)"> 1</span><span style="color:var(--shiki-foreground)">} 页&lt;/</span><span style="color:var(--shiki-token-string-expression)">p</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="常用方法-1"><a href="#常用方法-1" class="rp-header-anchor rp-link" aria-hidden="true">#</a>常用方法</h3>






























<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>方法</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td><code>get(key)</code></td><td>获取参数值，不存在返回 <code>null</code></td><td><code>searchParams.get(&quot;q&quot;)</code></td></tr><tr><td><code>has(key)</code></td><td>判断参数是否存在</td><td><code>searchParams.has(&quot;page&quot;)</code></td></tr><tr><td><code>getAll(key)</code></td><td>获取同名参数的所有值</td><td><code>?tag=a&amp;tag=b</code> → <code>[&quot;a&quot;, &quot;b&quot;]</code></td></tr><tr><td><code>toString()</code></td><td>转成字符串</td><td><code>&quot;q=nextjs&amp;page=2&quot;</code></td></tr></tbody></table></div>
<h3 class="rp-toc-include" id="更新查询参数"><a href="#更新查询参数" class="rp-header-anchor rp-link" aria-hidden="true">#</a>更新查询参数</h3>
<p><code>useSearchParams</code> 本身是只读的，改参数得配合 <code>useRouter</code> 或 <code>&lt;Link&gt;</code>：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-string-expression)">&quot;use client&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { useRouter</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> useSearchParams</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> usePathname } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/navigation&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> Pagination</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> router</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> useRouter</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> pathname</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> usePathname</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> searchParams</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> useSearchParams</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  function</span><span style="color:var(--shiki-token-function)"> goToPage</span><span style="color:var(--shiki-foreground)">(page</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> number</span><span style="color:var(--shiki-foreground)">) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    const</span><span style="color:var(--shiki-token-constant)"> params</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> URLSearchParams</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">searchParams</span><span style="color:var(--shiki-token-function)">.toString</span><span style="color:var(--shiki-foreground)">());</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    params</span><span style="color:var(--shiki-token-function)">.set</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;page&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-constant)"> page</span><span style="color:var(--shiki-token-function)">.toString</span><span style="color:var(--shiki-foreground)">());</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    router</span><span style="color:var(--shiki-token-function)">.push</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-foreground)">pathname</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">?</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">params</span><span style="color:var(--shiki-token-function)">.toString</span><span style="color:var(--shiki-foreground)">()</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-token-function)"> onClick</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{() </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-function)"> goToPage</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">1</span><span style="color:var(--shiki-foreground)">)}&gt;第 1 页&lt;/</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-token-function)"> onClick</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{() </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-function)"> goToPage</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">2</span><span style="color:var(--shiki-foreground)">)}&gt;第 2 页&lt;/</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="服务端组件里怎么读"><a href="#服务端组件里怎么读" class="rp-header-anchor rp-link" aria-hidden="true">#</a>服务端组件里怎么读</h3>
<p>服务端组件用不了 hook，但 <code>page.tsx</code> 有个 <code>searchParams</code> prop：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// src/app/search/page.tsx（服务端组件）</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> async</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> SearchPage</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  searchParams</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  searchParams</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Promise</span><span style="color:var(--shiki-foreground)">&lt;{ q</span><span style="color:var(--shiki-token-keyword)">?:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">; page</span><span style="color:var(--shiki-token-keyword)">?:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)"> }&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-foreground)"> { </span><span style="color:var(--shiki-token-constant)">q</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-constant)"> page</span><span style="color:var(--shiki-foreground)"> } </span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-foreground)"> searchParams;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> results</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-function)"> fetchResults</span><span style="color:var(--shiki-foreground)">(q</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-function)"> Number</span><span style="color:var(--shiki-foreground)">(page) </span><span style="color:var(--shiki-token-keyword)">||</span><span style="color:var(--shiki-token-constant)"> 1</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;{</span><span style="color:var(--shiki-token-comment)">/* 渲染搜索结果 */</span><span style="color:var(--shiki-foreground)">}&lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="用哪个"><a href="#用哪个" class="rp-header-anchor rp-link" aria-hidden="true">#</a>用哪个</h3>





















<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>场景</th><th>用什么</th></tr></thead><tbody><tr><td>服务端根据参数加载数据（分页、筛选）</td><td><code>searchParams</code> prop</td></tr><tr><td>客户端读取/操作参数（筛选已有数据）</td><td><code>useSearchParams</code></td></tr><tr><td>事件回调里读参数，不想触发重渲染</td><td><code>new URLSearchParams(window.location.search)</code></td></tr></tbody></table></div>
<h3 class="rp-toc-include" id="别忘了包-suspense"><a href="#别忘了包-suspense" class="rp-header-anchor rp-link" aria-hidden="true">#</a>别忘了包 <code>&lt;Suspense&gt;</code></h3>
<p>这里有个坑：静态渲染的路由里用了 <code>useSearchParams</code>，会导致从这个组件往上到最近的 <code>&lt;Suspense&gt;</code> 边界全部变成客户端渲染。所以记得包一层 <code>&lt;Suspense&gt;</code>：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { Suspense } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;react&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> SearchResults </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;./SearchResults&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> SearchPage</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-constant)">Suspense</span><span style="color:var(--shiki-token-function)"> fallback</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{&lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;加载中...&lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;}&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-constant)">SearchResults</span><span style="color:var(--shiki-foreground)"> /&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-constant)">Suspense</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>这样 <code>&lt;Suspense&gt;</code> 外面的部分还能正常静态渲染，不会被影响到。</p>
<h2 class="rp-toc-include" id="redirect服务端重定向"><a href="#redirect服务端重定向" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>redirect</code>：服务端重定向</h2>
<p>用在服务端组件、Server Action、Route Handler 里，把用户重定向到另一个 URL。</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { redirect } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/navigation&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">async</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> checkAuth</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> session</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-function)"> getSession</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  if</span><span style="color:var(--shiki-foreground)"> (</span><span style="color:var(--shiki-token-keyword)">!</span><span style="color:var(--shiki-foreground)">session) {</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">    redirect</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;/login&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> async</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> DashboardPage</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  await</span><span style="color:var(--shiki-token-function)"> checkAuth</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;仪表盘内容&lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="redirect-vs-permanentredirect"><a href="#redirect-vs-permanentredirect" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>redirect</code> vs <code>permanentRedirect</code></h3>




















<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>函数</th><th>HTTP 状态码</th><th>用途</th></tr></thead><tbody><tr><td><code>redirect(url)</code></td><td>307</td><td>临时重定向（未登录跳登录页）</td></tr><tr><td><code>permanentRedirect(url)</code></td><td>308</td><td>永久重定向（URL 改了，告诉搜索引擎）</td></tr></tbody></table></div>
<p>为什么是 307/308 而不是 302/301？因为 302 会把 POST 请求变成 GET，307 会保留原始请求方法。这在表单提交场景下很重要。</p>
<h3 class="rp-toc-include" id="注意事项"><a href="#注意事项" class="rp-header-anchor rp-link" aria-hidden="true">#</a>注意事项</h3>
<ul>
<li><code>redirect</code> 内部是靠抛异常来中断渲染的，别放在 <code>try/catch</code> 里，会被吞掉</li>
<li>不用写 <code>return redirect(...)</code>，TypeScript 类型是 <code>never</code></li>
<li>客户端组件里只能在渲染过程中调 <code>redirect</code>，事件处理器里要用 <code>useRouter</code></li>
</ul>
<h2 class="rp-toc-include" id="原生-history-api"><a href="#原生-history-api" class="rp-header-anchor rp-link" aria-hidden="true">#</a>原生 History API</h2>
<p>浏览器自带的 <code>window.history.pushState</code> 和 <code>replaceState</code> 也能用，Next.js 会自动跟路由器同步。</p>
<h3 class="rp-toc-include" id="pushstate加一条历史记录"><a href="#pushstate加一条历史记录" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>pushState</code>：加一条历史记录</h3>
<p>只想改 URL 不想触发页面导航的时候用，比如排序：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-string-expression)">&quot;use client&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { useSearchParams } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next/navigation&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> SortProducts</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> searchParams</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> useSearchParams</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  function</span><span style="color:var(--shiki-token-function)"> updateSorting</span><span style="color:var(--shiki-foreground)">(sortOrder</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    const</span><span style="color:var(--shiki-token-constant)"> params</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> URLSearchParams</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">searchParams</span><span style="color:var(--shiki-token-function)">.toString</span><span style="color:var(--shiki-foreground)">());</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    params</span><span style="color:var(--shiki-token-function)">.set</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;sort&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> sortOrder);</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    window</span><span style="color:var(--shiki-token-function)">.</span><span style="color:var(--shiki-token-constant)">history</span><span style="color:var(--shiki-token-function)">.pushState</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">null</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> &quot;&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> `?</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">params</span><span style="color:var(--shiki-token-function)">.toString</span><span style="color:var(--shiki-foreground)">()</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-token-function)"> onClick</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{() </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-function)"> updateSorting</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;asc&quot;</span><span style="color:var(--shiki-foreground)">)}&gt;升序&lt;/</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-token-function)"> onClick</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{() </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-function)"> updateSorting</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;desc&quot;</span><span style="color:var(--shiki-foreground)">)}&gt;降序&lt;/</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="replacestate替换当前那条"><a href="#replacestate替换当前那条" class="rp-header-anchor rp-link" aria-hidden="true">#</a><code>replaceState</code>：替换当前那条</h3>
<p>不需要用户能回退的场景，比如切语言：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-string-expression)">&quot;use client&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> LocaleSwitcher</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  function</span><span style="color:var(--shiki-token-function)"> changeLocale</span><span style="color:var(--shiki-foreground)">(locale</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">) {</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    window</span><span style="color:var(--shiki-token-function)">.</span><span style="color:var(--shiki-token-constant)">history</span><span style="color:var(--shiki-token-function)">.replaceState</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">null</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> &quot;&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> `/</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-foreground)">locale</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-token-function)"> onClick</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{() </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-function)"> changeLocale</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;zh&quot;</span><span style="color:var(--shiki-foreground)">)}&gt;中文&lt;/</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-token-function)"> onClick</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{() </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-function)"> changeLocale</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;en&quot;</span><span style="color:var(--shiki-foreground)">)}&gt;English&lt;/</span><span style="color:var(--shiki-token-string-expression)">button</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>跟 <code>useRouter</code> 的区别：这俩只改 URL，不会触发导航、不会重新拿数据。就是想让 URL 反映当前状态，但页面不用动。</p>
<h2 class="rp-toc-include" id="导航-api-一览"><a href="#导航-api-一览" class="rp-header-anchor rp-link" aria-hidden="true">#</a>导航 API 一览</h2>





















































<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>API</th><th>类型</th><th>用在哪</th><th>典型场景</th></tr></thead><tbody><tr><td><code>&lt;Link&gt;</code></td><td>声明式</td><td>服务端/客户端组件</td><td>导航栏、列表链接</td></tr><tr><td><code>useRouter</code></td><td>编程式</td><td>客户端组件</td><td>表单提交后跳转、条件跳转</td></tr><tr><td><code>redirect</code></td><td>服务端重定向</td><td>服务端组件/Server Action</td><td>权限校验、未登录跳转</td></tr><tr><td><code>usePathname</code></td><td>读取路径</td><td>客户端组件</td><td>导航高亮</td></tr><tr><td><code>useSearchParams</code></td><td>读取查询参数</td><td>客户端组件</td><td>筛选、分页</td></tr><tr><td><code>searchParams</code> prop</td><td>读取查询参数</td><td>服务端 page.tsx</td><td>服务端数据加载</td></tr><tr><td><code>history.pushState</code></td><td>原生 API</td><td>客户端组件</td><td>只改 URL 不导航</td></tr></tbody></table></div>
<h2 class="rp-toc-include" id="导航慢查查这几个原因"><a href="#导航慢查查这几个原因" class="rp-header-anchor rp-link" aria-hidden="true">#</a>导航慢？查查这几个原因</h2>
<p>实际开发中如果觉得页面跳转卡，大概率是这几个问题：</p>
<ol>
<li>
<p><strong>动态路由没写 <code>loading.tsx</code></strong> — 浏览器干等服务器渲染完，用户看到的就是卡住了。加个 <code>loading.tsx</code> 马上就有加载状态，体感完全不一样。</p>
</li>
<li>
<p><strong>该静态的路由变成了动态</strong> — 动态路由段忘了加 <code>generateStaticParams</code>，本来构建时就能生成的页面，变成每次请求都要现算。</p>
</li>
<li>
<p><strong>网络不好</strong> — prefetch 还没完成用户就点了，数据没到位。可以用 <code>useLinkStatus</code> hook 加个加载指示器，至少让用户知道在转。</p>
</li>
<li>
<p><strong>JS bundle 太大</strong> — <code>&lt;Link&gt;</code> 要水合完才能开始 prefetch，bundle 大了水合就慢，prefetch 也跟着延迟。</p>
</li>
</ol>
<hr/>
<h3 class="rp-toc-include" id="本文源码示例代码已上传至-githubhttpsgithubcomcolin3191nextjs-demo"><a href="#本文源码示例代码已上传至-githubhttpsgithubcomcolin3191nextjs-demo" class="rp-header-anchor rp-link" aria-hidden="true">#</a>本文源码示例代码已上传至 GitHub：<a href="https://github.com/Colin3191/nextjs-demo" target="_blank" rel="noopener noreferrer" class="rp-link">https://github.com/Colin3191/nextjs-demo</a></h3><!--/$-->]]></content>
        <author>
            <name>Colin</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Next.js 入门（一）：App Router 项目结构与路由]]></title>
        <id>/blog/2026/2026-02-06-nextjs-getting-started-part1</id>
        <link href="https://colin3191.me/blog/2026/2026-02-06-nextjs-getting-started-part1"/>
        <updated>2026-02-06T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<!--$--><h1 class="rp-toc-include" id="nextjs-入门一app-router-项目结构与路由"><a href="#nextjs-入门一app-router-项目结构与路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Next.js 入门（一）：App Router 项目结构与路由<!-- --> </h1>
<h2 class="rp-toc-include" id="nextjs-是什么"><a href="#nextjs-是什么" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Next.js 是什么</h2>
<p>Next.js 是 Vercel 维护的 React 全栈框架，开箱就有文件路由、SSR/SSG、API 路由、自动代码分割这些能力。用过 React 的话可以这么理解：React 管 UI，剩下的事 Next.js 都包了。</p>
<p>我之前写 React 项目，路由要装 react-router，SSR 要自己配，代码分割要折腾 lazy loading。换到 Next.js 之后发现这些都不用操心了，框架通过文件约定帮你处理好了。所以学 Next.js 的重点其实就是搞懂这些约定。</p>
<h2 class="rp-toc-include" id="创建项目"><a href="#创建项目" class="rp-header-anchor rp-link" aria-hidden="true">#</a>创建项目</h2>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> create</span><span style="color:var(--shiki-token-string)"> next-app@latest</span><span style="color:var(--shiki-token-string)"> nextjs-demo</span><span style="color:var(--shiki-token-string)"> --yes</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">cd</span><span style="color:var(--shiki-token-string)"> nextjs-demo</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> dev</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><code>--yes</code> 跳过所有交互提示，直接用默认配置（TypeScript、ESLint、Tailwind CSS、<code>src/</code> 目录、App Router）。跑起来之后打开 <code>http://localhost:3000</code> 能看到欢迎页就行。</p>
<h2 class="rp-toc-include" id="项目结构"><a href="#项目结构" class="rp-header-anchor rp-link" aria-hidden="true">#</a>项目结构</h2>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>nextjs-demo/</span></span>
<span class="line"><span>├── src/</span></span>
<span class="line"><span>│   └── app/                  # ⭐ 路由核心目录</span></span>
<span class="line"><span>│       ├── layout.tsx         # 根布局（必须有）</span></span>
<span class="line"><span>│       ├── page.tsx           # 首页 /</span></span>
<span class="line"><span>│       └── globals.css        # 全局样式</span></span>
<span class="line"><span>├── public/                    # 静态资源，/文件名 直接访问</span></span>
<span class="line"><span>├── next.config.ts             # Next.js 配置</span></span>
<span class="line"><span>├── tsconfig.json              # TypeScript 配置</span></span>
<span class="line"><span>├── package.json               # 依赖和脚本</span></span>
<span class="line"><span>└── postcss.config.mjs         # PostCSS 配置</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>刚开始只需要关注三个地方：</p>
<ul>
<li><code>src/app/</code> — 写页面的地方，这篇文章基本都在讲它</li>
<li><code>public/</code> — 放图片、字体之类的静态文件</li>
<li><code>.next/</code> — 构建产物，别提交到 git 就行</li>
</ul>
<p>根目录那一堆配置文件先不用管，默认的就够用。</p>
<h2 class="rp-toc-include" id="路由特殊文件"><a href="#路由特殊文件" class="rp-header-anchor rp-link" aria-hidden="true">#</a>路由特殊文件</h2>
<p>App Router 有一套文件命名约定，放在路由文件夹下的这些文件名会被框架识别：</p>













































<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>文件</th><th>干嘛的</th></tr></thead><tbody><tr><td><code>page.tsx</code></td><td>页面本体，有它这个文件夹才算一个路由</td></tr><tr><td><code>layout.tsx</code></td><td>布局，包裹当前和所有子路由</td></tr><tr><td><code>loading.tsx</code></td><td>加载中的 UI</td></tr><tr><td><code>error.tsx</code></td><td>出错时的 UI</td></tr><tr><td><code>not-found.tsx</code></td><td>404</td></tr><tr><td><code>route.ts</code></td><td>API 接口（不能和 page.tsx 放一起）</td></tr><tr><td><code>template.tsx</code></td><td>跟 layout 类似，但每次导航都重新渲染</td></tr><tr><td><code>default.tsx</code></td><td>并行路由的 fallback</td></tr><tr><td><code>global-error.tsx</code></td><td>全局错误兜底</td></tr></tbody></table></div>
<p>渲染的时候它们是一层套一层的：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>layout → template → error → loading → not-found → page</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>如果你写过 React，<code>loading.tsx</code> 其实就是 <code>&lt;Suspense fallback={...}&gt;</code>，<code>error.tsx</code> 就是 ErrorBoundary。只不过 Next.js 把它们变成了文件，不用你手动写了。</p>
<h2 class="rp-toc-include" id="文件即路由"><a href="#文件即路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>文件即路由</h2>
<p>这是 App Router 最核心的东西：<strong>文件夹结构就是 URL 结构</strong>。</p>
<p>用过 react-router 的话需要转变一下思路——没有集中式的路由配置了，你建什么文件夹，URL 就长什么样。</p>
<h3 class="rp-toc-include" id="基本路由"><a href="#基本路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>基本路由</h3>
<p>每个有 <code>page.tsx</code> 的文件夹就是一个路由：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// src/app/page.tsx → /</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> Home</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">h1</span><span style="color:var(--shiki-foreground)">&gt;首页&lt;/</span><span style="color:var(--shiki-token-string-expression)">h1</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// src/app/blog/page.tsx → /blog</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> Blog</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;博客列表&lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// src/app/about/page.tsx → /about</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> About</span><span style="color:var(--shiki-foreground)">() {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;关于我们&lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>没有 <code>page.tsx</code> 的文件夹不会变成路由，所以你可以放心在 <code>app/</code> 下面放组件、工具函数什么的，不会被意外暴露出去。官方管这个叫 colocation（就近放置）。</p>
<h3 class="rp-toc-include" id="嵌套路由"><a href="#嵌套路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>嵌套路由</h3>
<p>文件夹套文件夹，URL 就跟着嵌套：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>src/app/</span></span>
<span class="line"><span>├── page.tsx                        → /</span></span>
<span class="line"><span>├── blog/</span></span>
<span class="line"><span>│   ├── page.tsx                    → /blog</span></span>
<span class="line"><span>│   └── first-post/</span></span>
<span class="line"><span>│       └── page.tsx                → /blog/first-post</span></span>
<span class="line"><span>└── dashboard/</span></span>
<span class="line"><span>    ├── page.tsx                    → /dashboard</span></span>
<span class="line"><span>    └── settings/</span></span>
<span class="line"><span>        └── page.tsx                → /dashboard/settings</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="动态路由"><a href="#动态路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>动态路由</h3>
<p>文件夹名用方括号包起来就能匹配动态参数：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// src/app/blog/[slug]/page.tsx</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> async</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> Post</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  params</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  params</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Promise</span><span style="color:var(--shiki-foreground)">&lt;{ slug</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)"> }&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-foreground)"> { </span><span style="color:var(--shiki-token-constant)">slug</span><span style="color:var(--shiki-foreground)"> } </span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-foreground)"> params;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> &lt;</span><span style="color:var(--shiki-token-string-expression)">h1</span><span style="color:var(--shiki-foreground)">&gt;文章：{slug}&lt;/</span><span style="color:var(--shiki-token-string-expression)">h1</span><span style="color:var(--shiki-foreground)">&gt;;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>访问 <code>/blog/hello-world</code> 的时候，<code>slug</code> 就是 <code>&quot;hello-world&quot;</code>。</p>
<p>动态路由有三种写法：</p>

























<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>写法</th><th>例子</th><th>能匹配什么</th></tr></thead><tbody><tr><td><code>[slug]</code></td><td><code>/blog/[slug]</code></td><td><code>/blog/hello</code>，只匹配一段</td></tr><tr><td><code>[...slug]</code></td><td><code>/docs/[...slug]</code></td><td><code>/docs/a</code>、<code>/docs/a/b/c</code>，一段或多段</td></tr><tr><td><code>[[...slug]]</code></td><td><code>/help/[[...slug]]</code></td><td><code>/help</code>、<code>/help/a/b</code>，零段或多段</td></tr></tbody></table></div>
<p>react-router 里 <code>[slug]</code> 相当于 <code>:slug</code>，<code>[...slug]</code> 相当于 <code>*</code>，只是定义方式从路由配置变成了文件夹名。</p>
<h2 class="rp-toc-include" id="layouttsx"><a href="#layouttsx" class="rp-header-anchor rp-link" aria-hidden="true">#</a>layout.tsx</h2>
<p>Layout 是另一个核心概念。</p>
<h3 class="rp-toc-include" id="根布局"><a href="#根布局" class="rp-header-anchor rp-link" aria-hidden="true">#</a>根布局</h3>
<p><code>src/app/layout.tsx</code> 是整个应用的根布局，必须有，而且必须包含 <code>&lt;html&gt;</code> 和 <code>&lt;body&gt;</code>：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-token-keyword)"> type</span><span style="color:var(--shiki-foreground)"> { Metadata } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;next&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-token-string-expression)"> &quot;./globals.css&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> const</span><span style="color:var(--shiki-token-constant)"> metadata</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Metadata</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  title</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;My App&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  description</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;我的第一个 Next.js 应用&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> RootLayout</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  children</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  children</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> React</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-function)">ReactNode</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">html</span><span style="color:var(--shiki-token-function)"> lang</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">&quot;zh-CN&quot;</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">body</span><span style="color:var(--shiki-foreground)">&gt;{children}&lt;/</span><span style="color:var(--shiki-token-string-expression)">body</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">html</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><code>children</code> 就是当前路由的页面内容，所有页面都会被这个布局包着。</p>
<h3 class="rp-toc-include" id="嵌套布局"><a href="#嵌套布局" class="rp-header-anchor rp-link" aria-hidden="true">#</a>嵌套布局</h3>
<p>在子路由文件夹里也可以放 <code>layout.tsx</code>，只影响这个路由和它下面的子路由：</p>
<div class="rp-codeblock language-tsx"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="tsx"><code><span class="line"><span style="color:var(--shiki-token-comment)">// src/app/blog/layout.tsx</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">export</span><span style="color:var(--shiki-token-keyword)"> default</span><span style="color:var(--shiki-token-keyword)"> function</span><span style="color:var(--shiki-token-function)"> BlogLayout</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  children</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  children</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> React</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-function)">ReactNode</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}) {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  return</span><span style="color:var(--shiki-foreground)"> (</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-token-function)"> style</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{{ display</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;flex&quot;</span><span style="color:var(--shiki-foreground)"> }}&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">nav</span><span style="color:var(--shiki-token-function)"> style</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{{ width</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> 200</span><span style="color:var(--shiki-foreground)"> }}&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;</span><span style="color:var(--shiki-token-string-expression)">h2</span><span style="color:var(--shiki-foreground)">&gt;博客导航&lt;/</span><span style="color:var(--shiki-token-string-expression)">h2</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;</span><span style="color:var(--shiki-token-string-expression)">ul</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">          &lt;</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;最新文章&lt;/</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">          &lt;</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;分类&lt;/</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">          &lt;</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;标签&lt;/</span><span style="color:var(--shiki-token-string-expression)">li</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">        &lt;/</span><span style="color:var(--shiki-token-string-expression)">ul</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;/</span><span style="color:var(--shiki-token-string-expression)">nav</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">      &lt;</span><span style="color:var(--shiki-token-string-expression)">main</span><span style="color:var(--shiki-token-function)"> style</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)">{{ flex</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> 1</span><span style="color:var(--shiki-foreground)"> }}&gt;{children}&lt;/</span><span style="color:var(--shiki-token-string-expression)">main</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    &lt;/</span><span style="color:var(--shiki-token-string-expression)">div</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  );</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>这样 <code>/blog</code> 和 <code>/blog/first-post</code> 都有侧边栏，<code>/about</code> 不受影响。</p>
<p>有个细节值得注意：layout 在页面切换时不会重新渲染。从 <code>/blog</code> 跳到 <code>/blog/first-post</code>，<code>BlogLayout</code> 不会卸载重建，里面的状态会保留。这个行为在 react-router 里需要额外处理，Next.js 默认就是这样。如果你就是想每次都重新渲染，用 <code>template.tsx</code> 替代就行。</p>
<h2 class="rp-toc-include" id="路由分组和私有文件夹"><a href="#路由分组和私有文件夹" class="rp-header-anchor rp-link" aria-hidden="true">#</a>路由分组和私有文件夹</h2>
<h3 class="rp-toc-include" id="路由分组"><a href="#路由分组" class="rp-header-anchor rp-link" aria-hidden="true">#</a>路由分组</h3>
<p>文件夹名用圆括号包起来，不会出现在 URL 里，纯粹用来组织代码：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>src/app/</span></span>
<span class="line"><span>├── (marketing)/</span></span>
<span class="line"><span>│   ├── layout.tsx          # marketing 专用布局</span></span>
<span class="line"><span>│   ├── page.tsx            → /</span></span>
<span class="line"><span>│   └── about/</span></span>
<span class="line"><span>│       └── page.tsx        → /about</span></span>
<span class="line"><span>├── (shop)/</span></span>
<span class="line"><span>│   ├── layout.tsx          # shop 专用布局</span></span>
<span class="line"><span>│   ├── cart/</span></span>
<span class="line"><span>│   │   └── page.tsx        → /cart</span></span>
<span class="line"><span>│   └── products/</span></span>
<span class="line"><span>│       └── page.tsx        → /products</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><code>(marketing)</code> 和 <code>(shop)</code> 不影响 URL，但可以各自有独立的布局。适合按业务模块拆分代码，或者给不同模块套不同的布局。甚至可以搞多个根布局——把顶层的 <code>layout.tsx</code> 删了，每个分组里各写一个。</p>
<h3 class="rp-toc-include" id="私有文件夹"><a href="#私有文件夹" class="rp-header-anchor rp-link" aria-hidden="true">#</a>私有文件夹</h3>
<p>下划线 <code>_</code> 开头的文件夹会被路由系统忽略：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>src/app/blog/</span></span>
<span class="line"><span>├── _components/</span></span>
<span class="line"><span>│   └── PostCard.tsx        # 不会变成路由</span></span>
<span class="line"><span>├── _lib/</span></span>
<span class="line"><span>│   └── api.ts              # 不会变成路由</span></span>
<span class="line"><span>└── page.tsx                → /blog</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>其实没有 <code>page.tsx</code> 的文件夹本来就不会生成路由，但加个 <code>_</code> 前缀意图更明确，也能避免跟 Next.js 以后可能新增的特殊文件名撞车。</p>
<h2 class="rp-toc-include" id="并行路由和拦截路由"><a href="#并行路由和拦截路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>并行路由和拦截路由</h2>
<p>这两个是进阶玩法，刚入门知道有这回事就行，用到再学。</p>
<h3 class="rp-toc-include" id="并行路由"><a href="#并行路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>并行路由</h3>
<p><code>@slot</code> 命名的文件夹可以在同一个布局里同时渲染多个页面：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>src/app/dashboard/</span></span>
<span class="line"><span>├── @analytics/</span></span>
<span class="line"><span>│   └── page.tsx</span></span>
<span class="line"><span>├── @team/</span></span>
<span class="line"><span>│   └── page.tsx</span></span>
<span class="line"><span>├── layout.tsx              # 同时接收 analytics 和 team</span></span>
<span class="line"><span>└── page.tsx</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>典型场景是仪表盘，多个面板各自独立加载、独立处理错误。</p>
<h3 class="rp-toc-include" id="拦截路由"><a href="#拦截路由" class="rp-header-anchor rp-link" aria-hidden="true">#</a>拦截路由</h3>
<p>用特殊的括号语法可以在当前布局里渲染另一个路由的内容：</p>

























<div class="rp-table-scroll-container rp-scrollbar"><table><thead><tr><th>写法</th><th>意思</th></tr></thead><tbody><tr><td><code>(.)folder</code></td><td>拦截同级</td></tr><tr><td><code>(..)folder</code></td><td>拦截上一级</td></tr><tr><td><code>(..)(..)folder</code></td><td>拦截上两级</td></tr><tr><td><code>(...)folder</code></td><td>拦截根路由</td></tr></tbody></table></div>
<p>最常见的用法：列表页点击某一项，弹个 Modal 显示详情，URL 变了但没离开当前页面。</p>
<hr/>
<h3 class="rp-toc-include" id="本文源码示例代码已上传至-githubhttpsgithubcomcolin3191nextjs-demo"><a href="#本文源码示例代码已上传至-githubhttpsgithubcomcolin3191nextjs-demo" class="rp-header-anchor rp-link" aria-hidden="true">#</a>本文源码示例代码已上传至 GitHub：<a href="https://github.com/Colin3191/nextjs-demo" target="_blank" rel="noopener noreferrer" class="rp-link">https://github.com/Colin3191/nextjs-demo</a></h3><!--/$-->]]></content>
        <author>
            <name>Colin</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[LangChain.js RAG 实战（上）：环境搭建与文档处理]]></title>
        <id>/blog/2025/2025-12-31-RAG1</id>
        <link href="https://colin3191.me/blog/2025/2025-12-31-RAG1"/>
        <updated>2025-12-31T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<!--$--><h1 class="rp-toc-include" id="langchainjs-rag-实战上环境搭建与文档处理"><a href="#langchainjs-rag-实战上环境搭建与文档处理" class="rp-header-anchor rp-link" aria-hidden="true">#</a>LangChain.js RAG 实战（上）：环境搭建与文档处理<!-- --> </h1>
<blockquote>
<p>随着大模型技术的普及，RAG（检索增强生成）已成为企业级应用的标配。作为前端开发者，我们可以直接利用熟悉的 JavaScript/TypeScript 技术栈来构建强大的 AI 应用。本文是我学习 LangChain.js 的学习笔记，记录了从零开发 RAG 应用的过程。</p>
</blockquote>
<h2 class="rp-toc-include" id="目录"><a href="#目录" class="rp-header-anchor rp-link" aria-hidden="true">#</a>目录</h2>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF-rag" class="rp-link">什么是 RAG</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-rag" class="rp-link">为什么需要 RAG</a></li>
<li><a href="#rag-%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF" class="rp-link">RAG 的应用场景</a></li>
<li><a href="#rag-%E7%9A%84%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5" class="rp-link">RAG 的核心概念</a></li>
<li><a href="#%E6%8A%80%E6%9C%AF%E6%A0%88%E9%80%89%E6%8B%A9" class="rp-link">技术栈选择</a></li>
<li><a href="#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87" class="rp-link">环境准备</a></li>
<li><a href="#document-%E6%8A%BD%E8%B1%A1%E8%AF%A6%E8%A7%A3" class="rp-link">Document 抽象详解</a></li>
<li><a href="#%E6%96%87%E6%A1%A3%E5%8A%A0%E8%BD%BD%E5%99%A8" class="rp-link">文档加载器</a></li>
<li><a href="#%E7%A4%BA%E4%BE%8B%E5%8A%A0%E8%BD%BD-pdf-%E6%96%87%E6%A1%A3" class="rp-link">示例：加载 PDF 文档</a></li>
<li><a href="#%E6%96%87%E6%9C%AC%E5%88%86%E5%89%B2%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7" class="rp-link">文本分割的重要性</a></li>
<li><a href="#recursivecharactertextsplitter-%E8%AF%A6%E8%A7%A3" class="rp-link">RecursiveCharacterTextSplitter 详解</a></li>
<li><a href="#%E5%AE%9E%E6%88%98%E5%8A%A0%E8%BD%BD%E4%B8%8E%E5%88%86%E5%89%B2" class="rp-link">实战：加载与分割</a></li>
<li><a href="#%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" class="rp-link">最佳实践</a></li>
<li><a href="#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98" class="rp-link">常见问题</a></li>
</ul>
<hr/>
<h2 class="rp-toc-include" id="什么是-rag"><a href="#什么是-rag" class="rp-header-anchor rp-link" aria-hidden="true">#</a>什么是 RAG</h2>
<p><strong>RAG（Retrieval-Augmented Generation，检索增强生成）</strong> 听起来很高大上，其实原理特别简单。</p>
<p>你可以把它想象成<strong>开卷考试</strong>。</p>
<ul>
<li><strong>传统的 LLM（闭卷考试）</strong>：模型全靠训练时“背”下来的知识回答。如果问它最新的新闻，或者公司内部的私密数据，它要么说不知道，要么就开始一本正经地胡说八道（幻觉）。</li>
<li><strong>RAG（开卷考试）</strong>：当用户提问时，系统先去翻阅你提供的“教科书”（知识库），找到相关的段落，然后把这些段落连同问题一起扔给 LLM。LLM 看着参考资料回答，自然就准确多了。</li>
</ul>
<p>技术上的流程是这样的：</p>
<ol>
<li><strong>检索</strong>：根据用户的问题，从知识库中找到相关的文档</li>
<li><strong>增强</strong>：将这些相关文档作为上下文，与用户的问题一起提供给大语言模型</li>
<li><strong>生成</strong>：大模型基于检索到的上下文生成准确的答案</li>
</ol>
<h3 class="rp-toc-include" id="一个简单的例子"><a href="#一个简单的例子" class="rp-header-anchor rp-link" aria-hidden="true">#</a>一个简单的例子</h3>
<p>想象你在向一个助手提问：</p>
<p><strong>用户</strong>：&quot;Nike 2023 年的收入是多少？&quot;</p>
<p><strong>没有 RAG 的 LLM</strong>：</p>
<blockquote>
<p>可能会回答不知道，或者给出过时/错误的信息（因为模型的训练数据有限）</p>
</blockquote>
<p><strong>有 RAG 的 LLM</strong>：</p>
<blockquote>
<ol>
<li>先从 Nike 2023 年的财务报告中检索到相关段落</li>
<li>将这些段落作为上下文提供给模型</li>
<li>模型基于准确的信息回答：Nike 2023 年的收入是 512 亿美元</li>
</ol>
</blockquote>
<hr/>
<h2 class="rp-toc-include" id="为什么需要-rag"><a href="#为什么需要-rag" class="rp-header-anchor rp-link" aria-hidden="true">#</a>为什么需要 RAG</h2>
<ul>
<li><strong>解决幻觉问题</strong>：强制模型参考真实文档，减少编造信息</li>
<li><strong>突破知识截止</strong>：实时更新知识库，获取最新信息</li>
<li><strong>可验证的来源</strong>：提供答案出处，增加可信度</li>
<li><strong>保护隐私安全</strong>：基于企业内部文档构建私有知识库</li>
</ul>
<hr/>
<h2 class="rp-toc-include" id="rag-常见的应用场景"><a href="#rag-常见的应用场景" class="rp-header-anchor rp-link" aria-hidden="true">#</a>RAG 常见的应用场景</h2>
<ul>
<li><strong>企业知识库问答</strong>：员工快速查找公司政策、技术文档、流程规范</li>
<li><strong>智能客服系统</strong>：24*7 自动回答客户常见问题</li>
<li><strong>文档分析与理解</strong>：快速从长篇报告中提取关键信息</li>
</ul>
<hr/>
<h2 class="rp-toc-include" id="rag-的核心概念"><a href="#rag-的核心概念" class="rp-header-anchor rp-link" aria-hidden="true">#</a>RAG 的核心概念</h2>
<p>理解 RAG 需要掌握几个核心概念。</p>
<h3 class="rp-toc-include" id="1-文档向量化embedding"><a href="#1-文档向量化embedding" class="rp-header-anchor rp-link" aria-hidden="true">#</a>1. 文档向量化（Embedding）</h3>
<p><strong>问题</strong>：计算机如何理解&quot;苹果手机&quot;和&quot;iPhone&quot;是相似的概念？</p>
<p><strong>解决</strong>：将文本转换为数字向量（一组数字），语义相近的文本在向量空间中距离更近。</p>
<p><strong>示例</strong>：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>&quot;苹果手机&quot;  →  [0.23, -0.45, 0.78, ...]</span></span>
<span class="line"><span>&quot;iPhone&quot;    →  [0.25, -0.43, 0.76, ...]</span></span>
<span class="line"><span>&quot;香蕉&quot;      →  [-0.67, 0.34, -0.12, ...]</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>前两个向量的距离很近，表示它们语义相似。</p>
<h3 class="rp-toc-include" id="2-向量相似度"><a href="#2-向量相似度" class="rp-header-anchor rp-link" aria-hidden="true">#</a>2. 向量相似度</h3>
<p><strong>余弦相似度</strong>（Cosine Similarity）是最常用的计算方法：</p>
<ul>
<li>1 = 完全相同</li>
<li>0 = 完全不相关</li>
<li>-1 = 完全相反</li>
</ul>
<h3 class="rp-toc-include" id="3-rag-的基本流程"><a href="#3-rag-的基本流程" class="rp-header-anchor rp-link" aria-hidden="true">#</a>3. RAG 的基本流程</h3>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>┌─────────────────────────────────────────────────────────────┐</span></span>
<span class="line"><span>│                        RAG 完整流程                          │</span></span>
<span class="line"><span>└─────────────────────────────────────────────────────────────┘</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  用户问题</span></span>
<span class="line"><span>     │</span></span>
<span class="line"><span>     ▼</span></span>
<span class="line"><span>┌─────────────┐</span></span>
<span class="line"><span>│  向量化查询  │  将问题转换为向量</span></span>
<span class="line"><span>└─────────────┘</span></span>
<span class="line"><span>     │</span></span>
<span class="line"><span>     ▼</span></span>
<span class="line"><span>┌─────────────────────────────────────┐</span></span>
<span class="line"><span>│        向量搜索（检索阶段）          │</span></span>
<span class="line"><span>│  • 在向量库中找到最相似的文档块      │</span></span>
<span class="line"><span>│  • 通常返回 top-k 个结果            │</span></span>
<span class="line"><span>└─────────────────────────────────────┘</span></span>
<span class="line"><span>     │</span></span>
<span class="line"><span>     ▼</span></span>
<span class="line"><span>┌─────────────────────────────────────┐</span></span>
<span class="line"><span>│        构建提示词（增强阶段）        │</span></span>
<span class="line"><span>│  • 将检索到的文档作为上下文          │</span></span>
<span class="line"><span>│  • 与用户问题组合成完整的 prompt     │</span></span>
<span class="line"><span>└─────────────────────────────────────┘</span></span>
<span class="line"><span>     │</span></span>
<span class="line"><span>     ▼</span></span>
<span class="line"><span>┌─────────────────────────────────────┐</span></span>
<span class="line"><span>│        LLM 生成答案（生成阶段）      │</span></span>
<span class="line"><span>│  • 基于上下文生成准确的回答          │</span></span>
<span class="line"><span>│  • 可以引用来源                      │</span></span>
<span class="line"><span>└─────────────────────────────────────┘</span></span>
<span class="line"><span>     │</span></span>
<span class="line"><span>     ▼</span></span>
<span class="line"><span>   返回答案</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>简单来说，就是先把知识切碎（向量化），存起来（索引）；等用户问的时候，搜出相关的碎片（检索），拼在一起给 AI 看（增强），最后让 AI 回答（生成）。</p>
<hr/>
<h2 class="rp-toc-include" id="技术栈选择"><a href="#技术栈选择" class="rp-header-anchor rp-link" aria-hidden="true">#</a>技术栈选择</h2>
<h3 class="rp-toc-include" id="为什么选择-langchainjs"><a href="#为什么选择-langchainjs" class="rp-header-anchor rp-link" aria-hidden="true">#</a>为什么选择 LangChain.js？</h3>
<p>说实话，市面上关于 LangChain 的教程 90% 都是 Python 的。那我为什么还要头铁选 JS 版？</p>
<ol>
<li><strong>前端友好</strong>：作为前端出身，实在不想为了写个 Demo 再去折腾 Python 的虚拟环境、包管理那些东西。能用 TypeScript 搞定全栈，何乐而不为？</li>
<li><strong>全栈整合</strong>：现在的 Next.js / NestJS 应用直接集成 LangChain.js 非常顺滑，不用再单独起一个 Python 服务做中间层。</li>
<li><strong>类型安全</strong>：TypeScript 的类型提示在处理复杂的数据流时真的救命，比 Python 猜类型舒服多了。</li>
</ol>
<p>虽然 JS 版的文档有时候更新比 Python 版慢半拍，但社区活跃度很高，坑基本都能填上。</p>
<hr/>
<h2 class="rp-toc-include" id="环境准备"><a href="#环境准备" class="rp-header-anchor rp-link" aria-hidden="true">#</a>环境准备</h2>
<p>开始搭建 RAG 应用的开发环境。</p>
<h3 class="rp-toc-include" id="1-前置要求"><a href="#1-前置要求" class="rp-header-anchor rp-link" aria-hidden="true">#</a>1. 前置要求</h3>
<ul>
<li><strong>Node.js</strong>：建议 v18 或更高版本</li>
<li><strong>包管理器</strong>：pnpm、npm 或 yarn</li>
<li><strong>代码编辑器</strong>：VS Code</li>
</ul>
<h3 class="rp-toc-include" id="2-创建项目"><a href="#2-创建项目" class="rp-header-anchor rp-link" aria-hidden="true">#</a>2. 创建项目</h3>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># 创建项目目录</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">mkdir</span><span style="color:var(--shiki-token-string)"> langchain-rag-demo</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">cd</span><span style="color:var(--shiki-token-string)"> langchain-rag-demo</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 初始化 npm 项目</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">npm</span><span style="color:var(--shiki-token-string)"> init</span><span style="color:var(--shiki-token-string)"> -y</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 或者使用 pnpm</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> init</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="3-安装依赖"><a href="#3-安装依赖" class="rp-header-anchor rp-link" aria-hidden="true">#</a>3. 安装依赖</h3>
<h4 class="rp-toc-include" id="基础依赖"><a href="#基础依赖" class="rp-header-anchor rp-link" aria-hidden="true">#</a>基础依赖</h4>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># 核心包</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> add</span><span style="color:var(--shiki-token-string)"> @langchain/core</span><span style="color:var(--shiki-token-string)"> @langchain/community</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 文档加载器（PDF、文本等）</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> add</span><span style="color:var(--shiki-token-string)"> pdf-parse@1</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 文本分割器</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> add</span><span style="color:var(--shiki-token-string)"> @langchain/textsplitters</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 环境变量管理</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> add</span><span style="color:var(--shiki-token-string)"> dotenv</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h4 class="rp-toc-include" id="embedding-服务本文使用-ollama"><a href="#embedding-服务本文使用-ollama" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Embedding 服务（本文使用 Ollama）</h4>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># Ollama（本地模型，无需 API key）</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> add</span><span style="color:var(--shiki-token-string)"> @langchain/ollama</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h4 class="rp-toc-include" id="向量存储"><a href="#向量存储" class="rp-header-anchor rp-link" aria-hidden="true">#</a>向量存储</h4>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># 开发环境：使用内存存储（用于学习，无需额外安装）</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="4-配置环境变量"><a href="#4-配置环境变量" class="rp-header-anchor rp-link" aria-hidden="true">#</a>4. 配置环境变量</h3>
<p>创建 <code>.env</code> 文件（Ollama 无需 API key）：</p>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># Ollama（本文使用）</span></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 无需 API key，确保 Ollama 服务运行即可</span></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 默认地址：http://localhost:11434</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># 阿里云百炼（可选，作为备选方案）</span></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># DASHSCOPE_API_KEY=sk-your-dashscope-key-here</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>⚠️ <strong>注意</strong>：不要将 <code>.env</code> 文件提交到 Git，添加到 <code>.gitignore</code>：</p>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-function)">echo</span><span style="color:var(--shiki-token-string-expression)"> &quot;.env&quot;</span><span style="color:var(--shiki-token-keyword)"> &gt;&gt;</span><span style="color:var(--shiki-token-string)"> .gitignore</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">echo</span><span style="color:var(--shiki-token-string-expression)"> &quot;node_modules&quot;</span><span style="color:var(--shiki-token-keyword)"> &gt;&gt;</span><span style="color:var(--shiki-token-string)"> .gitignore</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="5-配置-typescript"><a href="#5-配置-typescript" class="rp-header-anchor rp-link" aria-hidden="true">#</a>5. 配置 TypeScript</h3>
<p>直接复制即可：</p>
<div class="rp-codeblock language-json"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="json"><code><span class="line"><span style="color:var(--shiki-foreground)">{</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  &quot;compilerOptions&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;moduleDetection&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;force&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;module&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;nodenext&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;target&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;es2022&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;moduleResolution&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;nodenext&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;allowJs&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> true</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;esModuleInterop&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> true</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;isolatedModules&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> true</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>添加运行脚本（<code>package.json</code>）：</p>
<div class="rp-codeblock language-json"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="json"><code><span class="line"><span style="color:var(--shiki-foreground)">{</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  &quot;scripts&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    &quot;rag&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;tsx src/rag/index.ts&quot;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  &quot;type&quot;</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;module&quot;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>安装开发依赖：</p>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> add</span><span style="color:var(--shiki-token-string)"> -D</span><span style="color:var(--shiki-token-string)"> typescript</span><span style="color:var(--shiki-token-string)"> tsx</span><span style="color:var(--shiki-token-string)"> @types/node</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<hr/>
<h2 class="rp-toc-include" id="document-对象万物皆可-document"><a href="#document-对象万物皆可-document" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Document 对象：万物皆可 Document</h2>
<p>在 LangChain 的世界里，不管你读的是 PDF、网页、Markdown 还是 Word，最终都会被统一封装成 <strong>Document</strong> 对象。</p>
<p>理解它的结构非常简单，它就两个核心属性：</p>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-keyword)">interface</span><span style="color:var(--shiki-token-function)"> Document</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  pageContent</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> string</span><span style="color:var(--shiki-foreground)">;            </span><span style="color:var(--shiki-token-comment)">// 文本内容</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  metadata</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Record</span><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-constant)">string</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-constant)"> any</span><span style="color:var(--shiki-foreground)">&gt;;  </span><span style="color:var(--shiki-token-comment)">// 元数据（来源、页码、作者等）</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="为什么-metadata-很重要"><a href="#为什么-metadata-很重要" class="rp-header-anchor rp-link" aria-hidden="true">#</a>为什么 Metadata 很重要？</h3>
<p>很多新手容易忽略 <code>metadata</code>，只关注文本内容。但在实际的 RAG 应用中，元数据是<strong>精准检索</strong>的关键。</p>
<p>比如，当用户问“Nike 2023 年的营收是多少？”时，如果你在检索时能通过元数据过滤掉 <code>year: 2022</code> 的文档，准确率直接翻倍。</p>
<h3 class="rp-toc-include" id="document-对象示例"><a href="#document-对象示例" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Document 对象示例</h3>
<p>虽然我们通常从文件加载文档，但也可以手动创建 Document 对象来理解其结构：</p>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { Document } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;@langchain/core/documents&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> doc</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> Document</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  pageContent</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;Nike 是一家全球知名的运动品牌，成立于 1967 年。&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  metadata</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    source</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;nike-info&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    category</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;company&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  }</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">doc</span><span style="color:var(--shiki-foreground)">.pageContent);</span></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 输出: Nike 是一家全球知名的运动品牌，成立于 1967 年。</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="embedding-服务示例"><a href="#embedding-服务示例" class="rp-header-anchor rp-link" aria-hidden="true">#</a>Embedding 服务示例</h3>
<p>本文使用 <strong>Ollama 本地模型</strong>（完全免费、无需 API key）。</p>
<h4 class="rp-toc-include" id="什么是向量维度"><a href="#什么是向量维度" class="rp-header-anchor rp-link" aria-hidden="true">#</a>什么是向量维度？</h4>
<p><strong>向量维度</strong>（Vector Dimension）指的是 Embedding 模型生成的数字向量的长度。</p>
<p>简单来说，维度越高，能表达的语义越丰富，但计算和存储成本也越高。本文使用的 <strong>nomic-embed-text</strong> 模型生成的向量维度是 <strong>768</strong>。</p>
<h4 class="rp-toc-include" id="使用-ollama-embeddings"><a href="#使用-ollama-embeddings" class="rp-header-anchor rp-link" aria-hidden="true">#</a>使用 Ollama Embeddings</h4>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { OllamaEmbeddings } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;@langchain/ollama&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> embeddings</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> OllamaEmbeddings</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  model</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;nomic-embed-text&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  baseUrl</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-string-expression)"> &quot;http://localhost:11434&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 生成向量</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> vector</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-constant)"> embeddings</span><span style="color:var(--shiki-token-function)">.embedQuery</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;Nike 是一家运动品牌&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">`向量维度: </span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">vector</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-constant)">length</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">);  </span><span style="color:var(--shiki-token-comment)">// 输出：768</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">`前 10 个值: </span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">vector</span><span style="color:var(--shiki-token-function)">.slice</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">0</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-constant)"> 10</span><span style="color:var(--shiki-foreground)">)</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">`</span><span style="color:var(--shiki-foreground)">);  </span><span style="color:var(--shiki-token-comment)">// 示例：[-0.23, 0.45, ...]</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><strong>使用 Ollama 前的准备</strong>：</p>
<ol>
<li><strong>安装 Ollama</strong>：</li>
</ol>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># macOS</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">brew</span><span style="color:var(--shiki-token-string)"> install</span><span style="color:var(--shiki-token-string)"> ollama</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)"># Linux</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">curl</span><span style="color:var(--shiki-token-string)"> -fsSL</span><span style="color:var(--shiki-token-string)"> https://ollama.com/install.sh</span><span style="color:var(--shiki-token-keyword)"> |</span><span style="color:var(--shiki-token-function)"> sh</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<ol start="2">
<li><strong>拉取 embedding 模型</strong>：</li>
</ol>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-comment)"># 推荐模型（性能好、体积小）</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">ollama</span><span style="color:var(--shiki-token-string)"> pull</span><span style="color:var(--shiki-token-string)"> nomic-embed-text</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<ol start="3">
<li><strong>启动 Ollama 服务</strong>：</li>
</ol>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-function)">ollama</span><span style="color:var(--shiki-token-string)"> serve</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<ol start="4">
<li><strong>验证安装</strong>：</li>
</ol>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-function)">curl</span><span style="color:var(--shiki-token-string)"> http://localhost:11434/api/tags</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><strong>说明</strong>：Ollama 完全免费，数据在本地运行。如果硬件配置较低，可以使用阿里云百炼等云服务。</p>
<hr/>
<h2 class="rp-toc-include" id="文档加载器把数据喂给-llm"><a href="#文档加载器把数据喂给-llm" class="rp-header-anchor rp-link" aria-hidden="true">#</a>文档加载器：把数据喂给 LLM</h2>
<p>LangChain 提供了极其丰富的加载器（Loaders），几乎支持所有你能想到的数据源。</p>
<h3 class="rp-toc-include" id="常用加载器一览"><a href="#常用加载器一览" class="rp-header-anchor rp-link" aria-hidden="true">#</a>常用加载器一览</h3>
<ul>
<li><strong>PDFLoader</strong>: 处理 PDF 文件（本文重点）。</li>
<li><strong>TextLoader</strong>: 处理 <code>.txt</code>、<code>.md</code> 等纯文本。</li>
<li><strong>CSVLoader / JSONLoader</strong>: 处理结构化数据。</li>
<li><strong>CheerioWebBaseLoader</strong>: 爬取网页内容。</li>
<li><strong>NotionLoader / GitHubLoader</strong>: 对接第三方工具。</li>
</ul>
<p>你可以在 <a href="https://js.langchain.com/docs/integrations/document_loaders/" target="_blank" rel="noopener noreferrer" class="rp-link">LangChain 官方文档</a> 中找到几百种集成。但在实际开发中，<strong>PDF 和 Markdown</strong> 是最常见的两种格式。</p>
<h3 class="rp-toc-include" id="示例加载-pdf-文档"><a href="#示例加载-pdf-文档" class="rp-header-anchor rp-link" aria-hidden="true">#</a>示例：加载 PDF 文档</h3>
<p>我们直接进入实战，使用 Nike 2023 年财务报告（10-K 文件）作为案例。</p>
<h3 class="rp-toc-include" id="准备工作"><a href="#准备工作" class="rp-header-anchor rp-link" aria-hidden="true">#</a>准备工作</h3>
<p>项目中的 PDF 文件路径：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>src/rag/data/nke-10k-2023.pdf</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>如需下载，可以从以下位置获取：</p>
<ul>
<li><a href="https://github.com/langchain-ai/langchain/blob/v0.3/docs/docs/example_data/nke-10k-2023.pdf" target="_blank" rel="noopener noreferrer" class="rp-link">LangChain GitHub</a></li>
</ul>
<h3 class="rp-toc-include" id="示例-1基础-pdf-加载"><a href="#示例-1基础-pdf-加载" class="rp-header-anchor rp-link" aria-hidden="true">#</a>示例 1：基础 PDF 加载</h3>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { PDFLoader } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;@langchain/community/document_loaders/fs/pdf&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> path </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;path&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { fileURLToPath } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;url&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> __filename</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> fileURLToPath</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-constant)">meta</span><span style="color:var(--shiki-foreground)">.url);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> __dirname</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> path</span><span style="color:var(--shiki-token-function)">.dirname</span><span style="color:var(--shiki-foreground)">(__filename);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> pdfPath</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> path</span><span style="color:var(--shiki-token-function)">.join</span><span style="color:var(--shiki-foreground)">(__dirname</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> &#x27;data&#x27;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> &#x27;nke-10k-2023.pdf&#x27;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> loader</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> PDFLoader</span><span style="color:var(--shiki-foreground)">(pdfPath);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> docs</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-constant)"> loader</span><span style="color:var(--shiki-token-function)">.load</span><span style="color:var(--shiki-foreground)">();</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<hr/>
<h2 class="rp-toc-include" id="文本分割的重要性"><a href="#文本分割的重要性" class="rp-header-anchor rp-link" aria-hidden="true">#</a>文本分割的重要性</h2>
<h3 class="rp-toc-include" id="为什么需要文本分割"><a href="#为什么需要文本分割" class="rp-header-anchor rp-link" aria-hidden="true">#</a>为什么需要文本分割？</h3>
<p>加载完文档后，下一步是将长文档分割成更小的文本块（chunks）。这是因为：</p>
<h4 class="rp-toc-include" id="1-llm-的上下文窗口限制"><a href="#1-llm-的上下文窗口限制" class="rp-header-anchor rp-link" aria-hidden="true">#</a>1. LLM 的上下文窗口限制</h4>
<ul>
<li><strong>GPT-3.5</strong>：4K tokens（约 3000 个单词）</li>
<li><strong>GPT-4</strong>：8K-32K tokens</li>
<li><strong>Claude 2</strong>：100K tokens</li>
<li><strong>本地模型</strong>：通常更小</li>
</ul>
<p>如果文档太长，LLM 无法一次性处理。</p>
<h4 class="rp-toc-include" id="2-提高检索精度"><a href="#2-提高检索精度" class="rp-header-anchor rp-link" aria-hidden="true">#</a>2. 提高检索精度</h4>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>❌ 不分割的情况：</span></span>
<span class="line"><span>整个 100 页的 PDF → 一个向量</span></span>
<span class="line"><span>→ 检索时只能返回整篇文档</span></span>
<span class="line"><span>→ 精确度低</span></span>
<span class="line"><span></span></span>
<span class="line"><span>✅ 分割后：</span></span>
<span class="line"><span>每 500-1000 字符 → 一个向量</span></span>
<span class="line"><span>→ 检索时返回相关段落</span></span>
<span class="line"><span>→ 精确度高</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h4 class="rp-toc-include" id="3-避免信息冲淡"><a href="#3-避免信息冲淡" class="rp-header-anchor rp-link" aria-hidden="true">#</a>3. 避免信息&quot;冲淡&quot;</h4>
<p>假设你在查询 &quot;Nike 的毛利率&quot;：</p>
<ul>
<li><strong>大文档</strong>：包含大量无关信息，相关的毛利率信息被&quot;冲淡&quot;</li>
<li><strong>小文档</strong>：集中在毛利率段落，更容易被检索到</li>
</ul>
<h4 class="rp-toc-include" id="4-节省成本"><a href="#4-节省成本" class="rp-header-anchor rp-link" aria-hidden="true">#</a>4. 节省成本</h4>
<ul>
<li>向量化更小的文本块更便宜</li>
<li>检索和生成的 token 数更少</li>
<li>减少 LLM 的处理时间</li>
</ul>
<h3 class="rp-toc-include" id="分割的挑战"><a href="#分割的挑战" class="rp-header-anchor rp-link" aria-hidden="true">#</a>分割的挑战</h3>
<p>文本分割不是简单的&quot;按字符数切分&quot;，需要考虑：</p>
<ol>
<li><strong>语义完整性</strong>：不要在句子中间切断</li>
<li><strong>上下文连贯性</strong>：保留足够的上下文</li>
<li><strong>信息不丢失</strong>：重要信息不能被分割到两个块</li>
<li><strong>大小适中</strong>：每个块不能太大或太小</li>
</ol>
<hr/>
<h2 class="rp-toc-include" id="recursivecharactertextsplitter-详解"><a href="#recursivecharactertextsplitter-详解" class="rp-header-anchor rp-link" aria-hidden="true">#</a>RecursiveCharacterTextSplitter 详解</h2>
<p>LangChain 提供了多种文本分割器，其中 <strong>RecursiveCharacterTextSplitter</strong> 是最推荐的通用选择。</p>
<h3 class="rp-toc-include" id="工作原理"><a href="#工作原理" class="rp-header-anchor rp-link" aria-hidden="true">#</a>工作原理</h3>
<p>它按优先级<strong>递归地</strong>尝试不同的分隔符。如果一段文本用第一个分隔符分割后仍然超过 <code>chunkSize</code>，它会拿那一段继续用下一个分隔符分割，而不是只分割一次。</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>1. 段落分隔符  - 优先级最高</span></span>
<span class="line"><span>2. 句子分隔符 . 或 ! 或 ?</span></span>
<span class="line"><span>3. 单词分隔符 (空格)</span></span>
<span class="line"><span>4. 字符级分割 - 最后手段</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="核心参数"><a href="#核心参数" class="rp-header-anchor rp-link" aria-hidden="true">#</a>核心参数</h3>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { RecursiveCharacterTextSplitter } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &quot;@langchain/textsplitters&quot;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> textSplitter</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> RecursiveCharacterTextSplitter</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  keepSeparator</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> false</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">      // 是否在输出中保留分隔符（可选）</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">  lengthFunction</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> (text) </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-token-constant)"> text</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-constant)">length</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">  // 计算长度的函数（可选）</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">});</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<blockquote>
<p>💡 <strong>避坑指南：字符 vs Token</strong></p>
<p>在 LangChain.js 中，默认的 <code>chunkSize</code> 是基于<strong>字符数</strong>（Characters）计算的。而大模型的上下文窗口（Context Window）是基于 <strong>Tokens</strong> 计算的。</p>
<ul>
<li>对于英文：1 个 Token $\approx$ 4 个字符或 0.75 个单词。</li>
<li>对于中文：1 个汉字通常占用 1~2 个 Tokens。</li>
</ul>
<p>如果你的文档主要是中文，建议 <code>chunkSize</code> 设置得稍微小一点（如 500-800），以防单个块的 Token 数超出模型限制。</p>
</blockquote>
<h3 class="rp-toc-include" id="参数详解"><a href="#参数详解" class="rp-header-anchor rp-link" aria-hidden="true">#</a>参数详解</h3>
<h4 class="rp-toc-include" id="1-chunksize块大小"><a href="#1-chunksize块大小" class="rp-header-anchor rp-link" aria-hidden="true">#</a>1. chunkSize（块大小）</h4>
<p>定义每个文本块的最大字符数。</p>
<p><strong>参考值</strong>：从 1000 开始，根据实际效果调整。</p>
<h4 class="rp-toc-include" id="2-chunkoverlap重叠大小"><a href="#2-chunkoverlap重叠大小" class="rp-header-anchor rp-link" aria-hidden="true">#</a>2. chunkOverlap（重叠大小）</h4>
<p>相邻文本块之间共享的字符数。</p>
<p><strong>作用</strong>：</p>
<ul>
<li>保持上下文连贯性</li>
<li>避免重要信息在边界被切断</li>
<li>确保同一个句子不会分散到多个块</li>
</ul>
<p><strong>参考值</strong>：chunkSize 的 10-20%</p>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-comment)">// 示例</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">chunkSize</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> 1000</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">chunkOverlap</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> 200</span><span style="color:var(--shiki-token-comment)">  // 20%</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 结果：</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">Chunk </span><span style="color:var(--shiki-token-constant)">1</span><span style="color:var(--shiki-foreground)">: [</span><span style="color:var(--shiki-token-constant)">0</span><span style="color:var(--shiki-foreground)">:</span><span style="color:var(--shiki-token-constant)">1000</span><span style="color:var(--shiki-foreground)">]</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">Chunk </span><span style="color:var(--shiki-token-constant)">2</span><span style="color:var(--shiki-foreground)">: [</span><span style="color:var(--shiki-token-constant)">800</span><span style="color:var(--shiki-foreground)">:</span><span style="color:var(--shiki-token-constant)">1800</span><span style="color:var(--shiki-foreground)">]   </span><span style="color:var(--shiki-token-comment)">// 800-1000 重叠</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">Chunk </span><span style="color:var(--shiki-token-constant)">3</span><span style="color:var(--shiki-foreground)">: [</span><span style="color:var(--shiki-token-constant)">1600</span><span style="color:var(--shiki-foreground)">:</span><span style="color:var(--shiki-token-constant)">2600</span><span style="color:var(--shiki-foreground)">]  </span><span style="color:var(--shiki-token-comment)">// 1600-1800 重叠</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><strong>为什么需要重叠</strong>？</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>❌ 无重叠：</span></span>
<span class="line"><span>Chunk 1: &quot;...公司的毛利率为&quot;</span></span>
<span class="line"><span>Chunk 2: &quot;43.5%，相比去年下降了...&quot;</span></span>
<span class="line"><span></span></span>
<span class="line"><span>→ 查询&quot;毛利率是多少？&quot;时，Chunk 1 信息不完整</span></span>
<span class="line"><span></span></span>
<span class="line"><span>✅ 有重叠：</span></span>
<span class="line"><span>Chunk 1: &quot;...公司的毛利率为 43.5%，相比去年...&quot;</span></span>
<span class="line"><span>Chunk 2: &quot;毛利率为 43.5%，相比去年下降了 250 个基点...&quot;</span></span>
<span class="line"><span></span></span>
<span class="line"><span>→ 两个块都包含完整信息</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h4 class="rp-toc-include" id="3-separators分隔符列表"><a href="#3-separators分隔符列表" class="rp-header-anchor rp-link" aria-hidden="true">#</a>3. separators（分隔符列表）</h4>
<p>控制文本如何被分割。默认值：<code>[&quot;\n\n&quot;, &quot;\n&quot;, &quot; &quot;, &quot;&quot;]</code></p>
<p><strong>自定义示例</strong>：</p>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-comment)">// 针对 Markdown 的分割器</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> markdownSplitter</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> RecursiveCharacterTextSplitter</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  chunkSize</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> 1000</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  chunkOverlap</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> 200</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  separators</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-foreground)"> [</span></span>
<span class="line"><span style="color:var(--shiki-token-string-expression)">    &quot;\n## &quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">      // 二级标题</span></span>
<span class="line"><span style="color:var(--shiki-token-string-expression)">    &quot;\n### &quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">     // 三级标题</span></span>
<span class="line"><span style="color:var(--shiki-token-string-expression)">    &quot;\n\n&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">       // 段落</span></span>
<span class="line"><span style="color:var(--shiki-token-string-expression)">    &quot;\n&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">         // 行</span></span>
<span class="line"><span style="color:var(--shiki-token-string-expression)">    &quot; &quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-comment)">          // 词</span></span>
<span class="line"><span style="color:var(--shiki-token-string-expression)">    &quot;&quot;</span><span style="color:var(--shiki-token-comment)">            // 字符</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  ]</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">});</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="递归分割的优势"><a href="#递归分割的优势" class="rp-header-anchor rp-link" aria-hidden="true">#</a>递归分割的优势</h3>
<p>为什么叫“递归”？简单说就是<strong>不死板</strong>。</p>
<p>它不会一上来就暴力地按 1000 字切一刀。它会先试着用“双换行符”（段落）来切。如果切完的一段还是太长，它会再进到这一段里，用“单换行符”（句子）继续切。</p>
<p>这种方式最大程度地保留了文本的语义结构，不会莫名其妙地把一句话切成两半。</p>
<p>让我们看一个具体的例子：</p>
<p><strong>原始文本</strong>：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>第一段。</span></span>
<span class="line"><span></span></span>
<span class="line"><span>第二段。</span></span>
<span class="line"><span></span></span>
<span class="line"><span>第三段。第四段。第五段。</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><strong>使用 RecursiveCharacterTextSplitter（chunkSize=20）</strong>：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>尝试 &quot;\n\n&quot; 分割：</span></span>
<span class="line"><span>✅ [&quot;第一段。\n\n第二段。\n\n第三段。第四段。第五段。&quot;]</span></span>
<span class="line"><span></span></span>
<span class="line"><span>&quot;第一段。&quot; 太短，继续分割</span></span>
<span class="line"><span></span></span>
<span class="line"><span>尝试 &quot;\n&quot; 分割：</span></span>
<span class="line"><span>✅ [&quot;第一段。\n\n第二段。\n\n第三段。&quot;, &quot;第四段。&quot;, &quot;第五段。&quot;]</span></span>
<span class="line"><span></span></span>
<span class="line"><span>&quot;第一段。\n\n第二段。\n\n第三段。&quot; 仍然太长</span></span>
<span class="line"><span></span></span>
<span class="line"><span>尝试 &quot; &quot; 分割：</span></span>
<span class="line"><span>✅ [&quot;第一段。\n\n第二段。&quot;, &quot;第三段。&quot;, &quot;第四段。&quot;, &quot;第五段。&quot;]</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p><strong>其他分割器</strong>（如简单的字符分割）：</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>❌ [&quot;第一段。\n\n第二段。\n\n第&quot;, &quot;三段。第四段。第五段。&quot;]</span></span>
<span class="line"><span>→ 在&quot;第&quot;字处截断，破坏了语义</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<blockquote>
<p><strong>结论</strong>：除非你有非常特殊的格式需求（比如代码文件用 <code>CodeTextSplitter</code>，Markdown 用 <code>MarkdownTextSplitter</code>），否则<strong>无脑选 <code>RecursiveCharacterTextSplitter</code></strong> 就对了。</p>
</blockquote>
<hr/>
<h2 class="rp-toc-include" id="实战加载与分割"><a href="#实战加载与分割" class="rp-header-anchor rp-link" aria-hidden="true">#</a>实战：加载与分割</h2>
<p>现在，我们将把学到的知识应用到代码中。我们将创建一个完整的 RAG 入口文件 <code>src/rag/index.ts</code>，并在其中实现文档的加载和分割。</p>
<h3 class="rp-toc-include" id="完整代码实现"><a href="#完整代码实现" class="rp-header-anchor rp-link" aria-hidden="true">#</a>完整代码实现</h3>
<p>编辑 <code>src/rag/index.ts</code>：</p>
<div class="rp-codeblock language-typescript"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="typescript"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { PDFLoader } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;@langchain/community/document_loaders/fs/pdf&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { RecursiveCharacterTextSplitter } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;@langchain/textsplitters&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> path </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;path&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> { fileURLToPath } </span><span style="color:var(--shiki-token-keyword)">from</span><span style="color:var(--shiki-token-string-expression)"> &#x27;url&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-token-string-expression)"> &#x27;dotenv/config&#x27;</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 1. 设置路径</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> __filename</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-function)"> fileURLToPath</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-constant)">meta</span><span style="color:var(--shiki-foreground)">.url);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> __dirname</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> path</span><span style="color:var(--shiki-token-function)">.dirname</span><span style="color:var(--shiki-foreground)">(__filename);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> pdfPath</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> path</span><span style="color:var(--shiki-token-function)">.join</span><span style="color:var(--shiki-foreground)">(__dirname</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> &#x27;data&#x27;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-string-expression)"> &#x27;nke-10k-2023.pdf&#x27;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 2. 加载 PDF</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;📄 正在加载文档...&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> loader</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> PDFLoader</span><span style="color:var(--shiki-foreground)">(pdfPath);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> docs</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-constant)"> loader</span><span style="color:var(--shiki-token-function)">.load</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">`✅ 加载完成，共 </span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">docs</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-constant)">length</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)"> 页`</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 3. 分割文本</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;✂️  正在分割文档...&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> textSplitter</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> RecursiveCharacterTextSplitter</span><span style="color:var(--shiki-foreground)">({</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  chunkSize</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> 1000</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  chunkOverlap</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-constant)"> 200</span><span style="color:var(--shiki-token-punctuation)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> allSplits</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> await</span><span style="color:var(--shiki-token-constant)"> textSplitter</span><span style="color:var(--shiki-token-function)">.splitDocuments</span><span style="color:var(--shiki-foreground)">(docs);</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">`✅ 分割完成，共生成 </span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-token-constant)">allSplits</span><span style="color:var(--shiki-foreground)">.</span><span style="color:var(--shiki-token-constant)">length</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)"> 个文本块`</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-comment)">// 打印第一个文本块看看效果</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;\n🔍 第一个文本块示例：&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(allSplits[</span><span style="color:var(--shiki-token-constant)">0</span><span style="color:var(--shiki-foreground)">].</span><span style="color:var(--shiki-token-constant)">pageContent</span><span style="color:var(--shiki-token-function)">.slice</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">0</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-constant)"> 200</span><span style="color:var(--shiki-foreground)">) </span><span style="color:var(--shiki-token-keyword)">+</span><span style="color:var(--shiki-token-string-expression)"> &quot;...&quot;</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">console</span><span style="color:var(--shiki-token-function)">.log</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">&quot;\n元数据：&quot;</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> allSplits[</span><span style="color:var(--shiki-token-constant)">0</span><span style="color:var(--shiki-foreground)">].metadata);</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<h3 class="rp-toc-include" id="运行代码"><a href="#运行代码" class="rp-header-anchor rp-link" aria-hidden="true">#</a>运行代码</h3>
<div class="rp-codeblock language-bash"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="bash"><code><span class="line"><span style="color:var(--shiki-token-function)">pnpm</span><span style="color:var(--shiki-token-string)"> rag</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<p>你将看到类似以下的输出，证明文档已经成功加载并被分割成了更小的块，为后续的向量化做好了准备。</p>
<div class="rp-codeblock language-txt"><div class="rp-codeblock__content"><div class="rp-codeblock__content__scroll-container rp-scrollbar rp-scrollbar--always"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0" data-lang="txt"><code><span class="line"><span>📄 正在加载文档...</span></span>
<span class="line"><span>✅ 加载完成，共 107 页</span></span>
<span class="line"><span>✂️  正在分割文档...</span></span>
<span class="line"><span>✅ 分割完成，共生成 514 个文本块</span></span>
<span class="line"><span></span></span>
<span class="line"><span>🔍 第一个文本块示例：</span></span>
<span class="line"><span>Table of Contents</span></span>
<span class="line"><span>UNITED STATES</span></span>
<span class="line"><span>SECURITIES AND EXCHANGE COMMISSION...</span></span>
<span class="line"><span></span></span>
<span class="line"><span>元数据： { source: &#x27;.../src/rag/data/nke-10k-2023.pdf&#x27;, pdf: { ... }, loc: { pageNumber: 1 } }</span></span></code></pre></div><div class="rp-code-button-group"><button class="rp-code-button-group__button rp-code-wrap-button" title="Toggle code wrap"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrapped"><path fill="#22a041" d="M21 5H3v2h18zM3 19h7v-2H3zm0-6h15c1 0 2 .43 2 2s-1 2-2 2h-2v-2l-4 3 4 3v-2h2c2.95 0 4-1.27 4-4 0-2.72-1-4-4-4H3z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="rp-code-button-group__icon rp-code-button-group__icon--wrap"><path fill="currentColor" d="M16 7H3V5h13zM3 19h13v-2H3zm19-7-4-3v2H3v2h15v2z"></path></svg></button><button class="rp-code-button-group__button rp-code-copy-button" title="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--copy"><path fill="currentColor" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"></path><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"></path></svg><svg width="32" height="32" viewBox="0 0 30 30" class="rp-code-button-group__icon rp-code-copy-button__icon rp-code-copy-button__icon--success"><path fill="#49cd37" d="m13 24-9-9 1.414-1.414L13 21.171 26.586 7.586 28 9z"></path></svg></button></div></div></div>
<hr/>
<h2 class="rp-toc-include" id="经验之谈参数怎么调"><a href="#经验之谈参数怎么调" class="rp-header-anchor rp-link" aria-hidden="true">#</a>经验之谈：参数怎么调？</h2>
<p>很多同学问 <code>chunkSize</code> 和 <code>chunkOverlap</code> 到底该设多少？这里分享一些我的实战经验：</p>
<ol>
<li><strong>默认起手式</strong>：<code>chunkSize: 1000, chunkOverlap: 200</code>。这个配置在大多数通用文档（如 PDF 报告、文章）上表现都很稳。</li>
<li><strong>中文环境</strong>：如果你的文档全是中文，建议适当调小 <code>chunkSize</code>（比如 500-800）。因为中文的信息密度比英文大，同样的字符数，中文包含的语义更多，太长了容易让 Embedding 模型“消化不良”。</li>
<li><strong>重叠的重要性</strong>：<code>chunkOverlap</code> 千万别设为 0。保持 10%-20% 的重叠能有效防止一句话被切成两半，导致语义丢失。</li>
<li><strong>特殊场景：问答对拆分</strong>：如果你处理的是 FAQ 文档（问答对），普通的按长度切分可能会把问题和答案切开。这时候可以考虑预处理文档，把每个“问题+答案”合并成一行，或者使用自定义分隔符（如 <code>Q:</code>）来确保它们始终在同一个 chunk 里。</li>
<li><strong>调试技巧</strong>：不要迷信理论值。最好的办法是像上面代码那样，打印出前几个 chunk 看看。如果发现经常有半截句子，就调大 overlap；如果发现包含太多无关废话，就调小 chunkSize。</li>
</ol>
<hr/>
<h3 class="rp-toc-include" id="本文源码"><a href="#本文源码" class="rp-header-anchor rp-link" aria-hidden="true">#</a>本文源码</h3>
<p>示例代码已上传至github：
<a href="https://github.com/Colin3191/langchain-demo" target="_blank" rel="noopener noreferrer" class="rp-link">https://github.com/Colin3191/langchain-demo</a></p><!--/$-->]]></content>
        <author>
            <name>Colin</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[欢迎来到我的博客]]></title>
        <id>/blog/2025/2025-09-23-welcome</id>
        <link href="https://colin3191.me/blog/2025/2025-09-23-welcome"/>
        <updated>2025-09-23T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<!--$--><h1 class="rp-toc-include" id="欢迎来到我的博客"><a href="#欢迎来到我的博客" class="rp-header-anchor rp-link" aria-hidden="true">#</a>欢迎来到我的博客<!-- --> </h1>
<p>大家好！欢迎来到我的个人博客。</p>
<h2 class="rp-toc-include" id="关于这个博客"><a href="#关于这个博客" class="rp-header-anchor rp-link" aria-hidden="true">#</a>关于这个博客</h2>
<p>这个博客使用 Rspress 构建，是一个基于 Rust 的静态站点生成器。我选择它的原因包括：</p>
<ul>
<li><strong>快速构建</strong>：基于 Rust 的构建工具链，提供极快的编译速度</li>
<li><strong>MDX 支持</strong>：可以在 Markdown 中使用 React 组件</li>
<li><strong>全文搜索</strong>：内置全文搜索功能</li>
<li><strong>现代化设计</strong>：支持暗色主题，响应式设计</li>
</ul>
<h2 class="rp-toc-include" id="博客内容"><a href="#博客内容" class="rp-header-anchor rp-link" aria-hidden="true">#</a>博客内容</h2>
<p>在这里你可以找到：</p>
<ul>
<li>技术文章和教程</li>
<li>个人学习笔记</li>
<li>项目经验分享</li>
<li>生活感悟</li>
</ul><!--/$-->]]></content>
        <author>
            <name>Colin</name>
        </author>
    </entry>
</feed>